# Runbook — диагностика «фантомного merge» (ORCH-071) > **Когда применять.** Задача дошла до `done` (или прод задеплоен «зелёным»), но есть > подозрение, что её ветка **не влита в `main`** — следующая задача срежет ветку от > устаревшего `main` и потеряет код предшественника (постмортем > `docs/history/LESSONS_2026-06-08_phantom-merge.md`). Этот runbook даёт 4 проверки > для **однозначной локализации** фантома. С ORCH-071 такой исход блокируется автоматически: под-гейт `deploy → done` (`stage_engine._handle_merge_verify`) сначала **детерминированно вливает PR** (`merge_gate.merge_pr`, Gitea PR-merge API), затем **верифицирует merge** (`merge_gate.verify_merged_to_main`) и НЕ пускает задачу в `done`, пока merge не подтверждён (alert + HOLD). Этот runbook — для ручной перепроверки/инцидентов (в т.ч. при выключенном kill-switch `ORCH_MERGE_VERIFY_ENABLED=false`). Подставьте значения: ```bash OWNER=admin # settings.gitea_owner REPO=orchestrator # репозиторий BRANCH=feature/ORCH-071-slug # ветка задачи GITEA=http://localhost:3000 # settings.gitea_url TOKEN= # settings.gitea_token FILE=src/stage_engine.py # любой файл, гарантированно изменённый задачей ``` --- ## Проверка 1 — Gitea API: список PR + флаги `merged` Показывает, считает ли сам Gitea PR влитым. ```bash curl -s -H "Authorization: token $TOKEN" \ "$GITEA/api/v1/repos/$OWNER/$REPO/pulls?state=all" \ | python3 -c 'import sys,json; \ [print(p["number"], p["state"], "merged="+str(p.get("merged")), p["head"]["ref"]) \ for p in json.load(sys.stdin)]' ``` * **Фантом НЕ подтверждён (всё хорошо):** строка ветки `$BRANCH` имеет `merged=True`. * **Фантом подтверждён (по этому критерию):** PR ветки `state=open` / `merged=False` (или PR отсутствует), при том что задача в `done` / прод задеплоен. --- ## Проверка 2 — md5 прод-файлов vs `git show origin/main:` Сверяет содержимое файла на проде с тем, что лежит в `origin/main`. ```bash # в прод-контейнере (или через docker exec orchestrator): md5sum "/app/$FILE" # содержимое того же файла из origin/main (на хосте, в клоне репо): git -C /home/slin/repos/$REPO fetch origin main -q git -C /home/slin/repos/$REPO show "origin/main:$FILE" | md5sum ``` * **Совпало:** прод соответствует `main` (фантома нет ИЛИ задача не меняла этот файл — возьмите файл из проверки 3/diff'а ветки). * **Разошлось:** прод собран из ветки, а `main` его не получил → косвенный признак фантома. --- ## Проверка 3 — `git merge-base` ветки vs `main` Главный детерминированный критерий: является ли HEAD ветки предком `origin/main`. ```bash git -C /home/slin/repos/$REPO fetch origin -q SHA=$(git -C /home/slin/repos/$REPO rev-parse "origin/$BRANCH") git -C /home/slin/repos/$REPO merge-base --is-ancestor "$SHA" origin/main \ && echo "MERGED: ветка влита в main" \ || echo "NOT MERGED: ветка НЕ предок origin/main (ФАНТОМ)" ``` Это ровно та проверка, что выполняет `merge_gate.verify_merged_to_main` (rc=0 → влито). * **`MERGED`:** фантома нет. * **`NOT MERGED`:** фантом подтверждён — `main` не содержит коммитов задачи. --- ## Проверка 4 — таймлайн деплой-логов Восстанавливает порядок событий: был ли merge до/после деплоя, и был ли он вообще. ```bash # Вердикт деплоя + новое поле merge-верификации (ORCH-071): git -C /home/slin/repos/$REPO show "origin/$BRANCH:docs/work-items//14-deploy-log.md" \ | sed -n '1,12p' # frontmatter: deploy_status:, merged_to_main: # Наблюдаемость под-гейта в живом сервисе: curl -s "$GITEA_HEALTH/queue" | python3 -c \ 'import sys,json; print(json.load(sys.stdin)["merge_verify"])' # -> {"enabled":..., "merge_verified_total":..., "not_merged_alerts_total":..., "last_alert_wi":...} # Журнал хоста по деплою (sentinel-каталог задачи): ls -la /home/slin/repos/.deploy-state-$REPO// cat /home/slin/repos/.deploy-state-$REPO//hook.log ``` * `deploy_status: SUCCESS` + `merged_to_main: false` → деплой прошёл, merge — нет (это и есть класс ORCH-071; задача должна быть удержана на `deploy`, не `done`). * `not_merged_alerts_total` растёт / `last_alert_wi == ` → под-гейт уже поднял alert. --- ## Критерий «фантом подтверждён» Фантомный merge считается **подтверждённым**, если выполняется ХОТЯ БЫ ОДНО из: 1. Проверка 1: PR ветки `state=open` / `merged=False` (или PR нет), а задача в `done`. 2. Проверка 3: `merge-base --is-ancestor` вернул **NOT MERGED** (HEAD ветки не предок `origin/main`). 3. Проверка 4: `14-deploy-log.md` имеет `deploy_status: SUCCESS` при `merged_to_main: false`. Проверка 2 — вспомогательная (зависит от того, менял ли файл задачей), используется для подтверждения проверок 1/3. ### Что делать при подтверждённом фантоме 1. **Влить PR вручную** через Gitea (PR-merge API / UI) — НИКОГДА не `git push`/`--force` в `main` (INV-4). 2. Повторить approve задачи (re-drive) — под-гейт переоценит: merge подтвердится → задача уйдёт в `done`. 3. Если фантом случился при выключенном kill-switch — включить `ORCH_MERGE_VERIFY_ENABLED=true`.