7.8 KiB
Lessons Learned — 2026-06-08: «Фантомный merge» — прод деплоится, но код не сливается в main
Severity: CRITICAL (потеря целостности main, накопительная потеря кода между задачами)
Резюме
Self-deploy (Phase B) собирал прод-образ из ВЕТКИ задачи и рапортовал finalize SUCCESS + post-deploy HEALTHY, но git-merge ветки в main НЕ происходил. PR оставался open. Следующая задача срезала свою ветку от устаревшего main → теряла код незалитых предшественников. Накопительно потеряны в main: ORCH-022, ORCH-059, ORCH-066, ORCH-068 (PR#67/68/69/70 — все open, merged=False). Последний реально слитый — ORCH-065 (PR#66).
Как обнаружено
Симптом: ORCH-067 переведён в To Analyse, но конвейер не стартовал (no pipeline action). Причина — прод слушал старый триггер in_progress, а не to_analyse (ORCH-066). При разборе выяснилось: код ORCH-066 не в проде, хотя он «деплоился».
Решающее наблюдение оператора (Слава): «спам ET-002 начался СРАЗУ после деплоя 66 → значит код деплоился». Это вскрыло механизм: код 66 БЫЛ в проде 22:17–05:32, потом стёрт деплоем 068 (срезан от старого main без 66).
Доказательная база (как подтверждали — воспроизводимый метод)
- PR-статус (Gitea API): PR#67(022)/68(059)/69(066)/70(068) = open, merged=False. PR#66(065) = merged=True (последний честный).
- md5-сверка файлов прод vs origin/main vs ветка:
src/reconciler.py,src/plane_sync.py: prod md5 == ветка ORCH-068 != main → прод = снимок ветки 068, НЕ main.src/webhooks/plane.py: prod == main == ветка-068 (ветка 068 этот файл не трогала → видна старая база без to_analyse).
- git merge-base: ветка ORCH-068 срезана от
bb03350(ORCH-065), не от кода 066. История ветки-068 по 066 содержит толькоdocs staging, кода (to_analyse) нет. - Таймлайн логов: деплой 22:17 (ветка-066, сломанный reconciler) → спам ET-002 начался; деплой 05:32 (ветка-068, база 065 без 66) → спам прекратился (0 после 05:33). Подтверждает: прод-образ = снимок ВЕТКИ, меняется при каждом деплое, теряет незалитое.
Root cause (гипотеза → нужен код-аудит self_deploy/merge_gate)
Self-deploy Phase B инициирует прод-деплой из worktree ветки (BUILD-ONCE из validated commit). Шаг git-merge ветки в main:
- ЛИБО не вызывается на self-hosting пути (Phase B уходит в detached host-процесс, finalizer пишет SUCCESS-маркеры, но merge отдельно и молча скипается/падает),
- ЛИБО регресс фикса ORCH-065 (idempotent merge / merge-lease reclaim): guard
pr_already_mergedили lease-reclaim ошибочно считает PR уже слитым / не докатывает merge после рестарта контейнера (а Phase B ИМЕННО рестартит контейнер → процесс, державший merge-lease, умирает до завершения merge). Симптоматически ORCH-065 был последним успешным merge — деградация началась СРАЗУ после него или из-за взаимодействия его механики с self-deploy-рестартом.
Почему конвейер не заметил
finalize SUCCESSиpost-deploy HEALTHYмаркеры пишутся НЕЗАВИСИМО от факта merge. Пайплайн считает задачу done по этим маркерам, git-состояние main не верифицируется.- Прод здоров (образ из ветки рабочий) → health-check зелёный → нет сигнала о проблеме.
- Дыра видна только при сравнении main с прод ИЛИ когда следующая задача теряет код предыдущей (что и случилось с 67).
Уроки
- Деплой ОБЯЗАН верифицировать, что код реально в main ПОСЛЕ деплоя. finalize SUCCESS без проверки
git merge-base origin/main == deployed_commit(или PR.merged==true) — фальшивый зелёный. Добавить post-merge верификацию: deployed SHA должен быть предком origin/main. - Маркер «deployed» != «merged». Нельзя считать задачу завершённой по staging/post-deploy-маркерам, если PR не закрыт merge. Гейт: задача → done ТОЛЬКО при PR.merged==true.
- Self-deploy рестартит контейнер → любой держатель merge-lease/незавершённый git-шаг умирает. Merge ДОЛЖЕН завершиться и быть подтверждён ДО рестарта прод-контейнера, либо merge выносится в шаг, переживающий рестарт (как requeue_running_jobs, но для merge-в-main).
- Срез ветки от main делает целостность main критичной. Если main отстаёт — каждая новая задача наследует дыру. main = единственный источник для новых веток, его рассинхрон с прод = накопительная потеря.
- Метод диагностики (сохранить как runbook): при подозрении на рассинхрон — (a) Gitea API PR list merged-флаги, (b) md5 prod-файлов vs
git show origin/main:<file>, (c) merge-base ветки vs main, (d) таймлайн деплой-логов. Эти 4 проверки однозначно локализуют фантом.
Действия
- Восстановление main: интеграционная ветка
integ/restore-main-2026-06-08— последовательный merge 022→059→066→068 (docs union-resolved, reconciler-конфликт 066⊕068 разрешён: каркас 068 livelock-fix + триггер to_analyse 066), полный pytest, затем merge в main + передеплой. - Заведён критбаг ORCH-071: «фантомный merge — self-deploy без верификации merge в main» (root-fix: post-deploy verify + done-гейт по PR.merged + merge до рестарта).
- ORCH-070 (Confirm Deploy trigger) частично ДУБЛИРУЕТ ORCH-059 (handle_confirm_deploy уже написан в 059) — после долива 059 пересмотреть scope 070 (остаётся только display-слой статусов Monitoring after Deploy).
Связанные
- ORCH-065 (последний честный merge; подозрение на регресс его merge-механики)
- ORCH-066/068 (потерянный код), ORCH-059 (Confirm Deploy trigger, тоже потерян)
- Урок 2026-06-08 confirm-deploy-deadtrigger (симптом того же корня)