From 2824fd85434856fa7d0bf8b42737dcf11e9de1d8 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Mon, 8 Jun 2026 08:13:44 +0000 Subject: [PATCH] architect(ET): auto-commit from architect run_id=354 --- docs/architecture/README.md | 38 ++++ .../adr/adr-0013-merge-verify-gate.md | 63 ++++++ .../06-adr/ADR-001-merge-verify-gate.md | 186 ++++++++++++++++++ .../ORCH-071/07-infra-requirements.md | 47 +++++ docs/work-items/ORCH-071/10-tech-risks.md | 23 +++ 5 files changed, 357 insertions(+) create mode 100644 docs/architecture/adr/adr-0013-merge-verify-gate.md create mode 100644 docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md create mode 100644 docs/work-items/ORCH-071/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-071/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 149199a..abd7160 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -121,6 +121,44 @@ sentinel-файлы (`/.deploy-state-//`), без мигр Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` (уточняет/триггер Фазы B относительно adr-0007). +#### Merge-в-main + пост-деплой верификация как условие `done` (ORCH-071 — фикс фантомного merge) +**Фантомный merge** (CRITICAL, постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`): +на self-hosting пути `deploy` агент `deployer` НЕ запускается, а фактический merge PR в `main` +исторически делал ТОЛЬКО он → детерминированный путь +(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`) **не содержал шага +merge-в-main вообще**. Detached host-деплой лишь retag'ал образ + рестартил 8500; `done` +достигался по `deploy_status: SUCCESS` без верификации `main`. Зелёный деплой (образ из рабочей +ветки) маскировал отсутствие merge → следующая задача срезала ветку от устаревшего `main` и +теряла код предшественника (накопительно потеряны ORCH-022/059/066/068). ORCH-071 вводит +**детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра `deploy → done`** +(симметрично edge-под-гейтам `deploy-staging → deploy`), только для self-hosting: +- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и + `next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). Гейтит + **ВСЕ** пути к `done` единообразно (`run_deploy_finalizer` Phase C, reconciler F-1, job-reaper — + все идут через `advance_stage`), закрывая дыру обхода merge. +- **Merge в Phase C (после рестарта), НЕ в Phase B** — finalizer restart-surviving (claim воркером + нового контейнера, re-drive reaper'ом), merge физически строго ПОСЛЕ рестарта прода → рестарт его + не убивает (G3 «шаг, переживающий рестарт»; постмортем-урок №3). +- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) → иначе + Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в `main`. +- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ + `git merge-base --is-ancestor origin/main` (`validated_revision` — тот же якорь, + что у ORCH-058). never-raise → `False`. +- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD** + (`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged есть + инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → штатный `deploy → + done` + `merged_to_main: true` во frontmatter `14-deploy-log.md` (`deploy_status:` нетронут). +- **Условность как ORCH-35/43/58:** `merge_verify_enabled` (kill-switch, дефолт `true`) + + `merge_verify_repos` (пусто → только self-hosting); non-self — no-op, merge остаётся за `deployer`. + never-raise; идемпотентность (`pr_already_merged`, INV-5); ручной approve сохранён (`Confirm Deploy`). +- **Инварианты:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, реестр + `QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG), схема БД, + БАГ-8, terminal-sync, merge-gate, image-freshness, exit-коды хука — **без изменений**. + Диагностика фантома — runbook `docs/operations/PHANTOM_MERGE_RUNBOOK.md` (4 проверки постмортема). + +Подробнее: [adr-0013](adr/adr-0013-merge-verify-gate.md), детально — +`docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md`. + ### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано) Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 — diff --git a/docs/architecture/adr/adr-0013-merge-verify-gate.md b/docs/architecture/adr/adr-0013-merge-verify-gate.md new file mode 100644 index 0000000..d03fe87 --- /dev/null +++ b/docs/architecture/adr/adr-0013-merge-verify-gate.md @@ -0,0 +1,63 @@ +# adr-0013: Merge-в-main + пост-деплой верификация как условие `done` (фикс фантомного merge) + +- **Статус:** accepted +- **Дата:** 2026-06-08 +- **Задача:** ORCH-071 (CRITICAL bug) +- **Детальный ADR:** `docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md` +- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md` + +## Контекст +Для self-hosting репо `orchestrator` стадия `deploy` идёт детерминированным путём +(`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`), а LLM-агент +`deployer` НЕ запускается. Фактический merge PR в `main` исторически делал **только** +агент `deployer` → на self-hosting пути **нет шага merge-в-main вообще**. Detached +host-деплой лишь retag'ает образ + рестартит 8500; `done` достигается по +`deploy_status: SUCCESS` без верификации `main`. «Зелёный» деплой (образ из рабочей +ветки) маскирует отсутствие merge → следующая задача срезает ветку от устаревшего `main` +и теряет код предшественника. Накопительно потеряны ORCH-022/059/066/068. Вторичный +фактор: Phase B рестартит прод → merge внутри живого процесса гонялся бы с рестартом +(урок №3). + +## Решение +Детерминированный **merge-актор + пост-merge верификация** как **под-гейт ребра +`deploy → done`**, врезанный в единственную функцию перехода `advance_stage` (симметрично +edge-под-гейтам security/merge-gate/image-freshness). `STAGE_TRANSITIONS`, +`check_deploy_status`/`_parse_deploy_status`, реестр `QG_CHECKS`, схема БД — **не меняются**. + +- **Врезка `_handle_merge_verify` в `advance_stage`** (`current_stage=="deploy"` и + `next_stage=="done"`, ПОСЛЕ зелёного `check_deploy_status`, ДО `update_task_stage`). + Гейтит **ВСЕ** пути к `done` единообразно: `run_deploy_finalizer` (Phase C), reconciler + F-1, job-reaper — все идут через `advance_stage`. Закрывает дыру: reconciler F-1 иначе + протолкнул бы `done` в обход merge. +- **Merge в Phase C (после рестарта), НЕ в Phase B.** Phase C finalizer — + restart-surviving (reserved-job `deploy-finalizer`, claim воркером нового контейнера, + re-drive reaper'ом). Merge физически строго ПОСЛЕ рестарта → рестарт его не убивает + (G3 вторым вариантом — «шаг, переживающий рестарт»). +- **Merge-актор `merge_gate.merge_pr`** — `pr_already_merged` (no-op повтор, ORCH-065) → + иначе Gitea `POST /repos/{owner}/{repo}/pulls/{index}/merge`. Никогда push/force-push в + `main`. never-raise. +- **Верификатор `merge_gate.verify_merged_to_main`** — `PR.merged==true` ИЛИ + `git merge-base --is-ancestor origin/main`. never-raise → `False` + («не подтверждено»). +- **Не подтверждено → alert «deploy succeeded but not merged» (Telegram+Plane) + HOLD** + (`set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на `development` — not-merged + есть инфра-дефект, реакция ALERT-only как ORCH-021 self-hosting). Подтверждено → + штатный `deploy → done` (терминал-sync / post-deploy monitor как сегодня) + + `merged_to_main: true` во frontmatter `14-deploy-log.md` (наблюдаемость, `deploy_status:` + нетронут). +- **Идемпотентность (INV-5):** `pr_already_merged` перед merge; verify зелёный для + уже-слитого PR; повтор без дубль-merge/ложного отката. +- **Условность (как ORCH-35/43/58):** `merge_verify_enabled` (kill-switch, дефолт `true`) + + `merge_verify_repos` (пусто → только self-hosting). Non-self репо — no-op, merge остаётся + за агентом `deployer`. + +## Инварианты +never-raise на verify/merge (ошибка → alert, не падение конвейера); не рестартить/не ронять +прод 8500; ручной approve прод-деплоя сохранён (`Confirm Deploy`, ORCH-059); только PR-merge +API Gitea; restart-safe (sentinel + jobs, без миграции БД). + +## Последствия +Невозможно «`done` + прод задеплоен, а PR `open`». Минусы: при недоступной Gitea verify +консервативно `False` → возможен ложный HOLD+alert (снимается повтором; fail-closed для +`done` приоритетен); HOLD требует ручного вмешательства. Диагностика фантома — runbook +`docs/operations/PHANTOM_MERGE_RUNBOOK.md` (G4). diff --git a/docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md b/docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md new file mode 100644 index 0000000..2c4968d --- /dev/null +++ b/docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md @@ -0,0 +1,186 @@ +# ADR-001 (ORCH-071): Детерминированный merge-в-main + пост-деплой верификация как условие `done` + +## Статус +Accepted + +## Контекст + +### Подтверждённый root cause (постмортем `docs/history/LESSONS_2026-06-08_phantom-merge.md`) +Для self-hosting репо `orchestrator` стадия `deploy` идёт **детерминированным** путём +`stage_engine._handle_self_deploy_phase_b → self_deploy.initiate_deploy → +run_deploy_finalizer`, а LLM-агент `deployer` **не запускается** (так предписывает +`.openclaw/agents/deployer.md`). Фактический merge PR в `main` исторически выполнял +**только** агент `deployer` через Bash/curl. Следствие: на self-hosting пути **нет ни +одного шага, выполняющего git-merge ветки в `main`** (аудит: `grep` по +`pulls/.../merge` в `src/` — 0 совпадений). + +Detached host-процесс (Phase B) лишь **retag staging-образа на прод-тег + рестарт 8500**. +`run_deploy_finalizer` маппит exit-code хука `0 → SUCCESS`, пишет `14-deploy-log.md`, +вызывает `advance_stage(..., finished_agent="deployer")`; гейт `check_deploy_status` +читает только `deploy_status:` → `SUCCESS → done`. **Состояние `main` нигде не +верифицируется.** «Зелёный» деплой (прод-образ собран из рабочей ветки) маскирует +отсутствие merge — сигнала нет, пока следующая задача не срежет ветку от устаревшего +`main` и не потеряет код предшественника. Накопительно потеряны ORCH-022/059/066/068. + +Вторичный фактор (урок №3): Phase B **рестартит прод-контейнер**, поэтому любой +держатель merge-lease / незавершённый git-шаг ВНУТРИ живого процесса умирает до +завершения merge. Значит наивно «добавить merge в Phase B» (живой старый контейнер, +который вот-вот рестартует) — снова гонка с рестартом. + +### Требования (из ТЗ/BRD) +- **G1/FR-2** — пост-деплой верификация: deployed SHA — предок `origin/main` ИЛИ `PR.merged==true`. +- **G2/FR-3** — `done` ТОЛЬКО при подтверждённом merge; `deploy_status: SUCCESS` + post-deploy `HEALTHY` — недостаточно. +- **G3/FR-1** — merge детерминированным кодом (агент не запускается), через Gitea PR-merge API; завершён ДО рестарта ЛИБО вынесен в шаг, переживающий рестарт. +- **INV-1** never-raise; **INV-2** не рестартить/не ронять прод; **INV-3** ручной approve сохранён; **INV-4** только PR-merge API, никогда push/force-push в `main`; **INV-5** идемпотентность (`pr_already_merged`). +- **НЕ менять:** `STAGE_TRANSITIONS`, `check_deploy_status`/`_parse_deploy_status`, схему БД, source-of-truth. + +## Решение + +Вводим **детерминированный merge-актор + пост-merge верификацию** как **под-гейт ребра +`deploy → done`**, врезанный в `advance_stage`. Это симметрично существующим edge-под-гейтам +(security/merge-gate/image-freshness на ребре `deploy-staging → deploy`): `STAGE_TRANSITIONS` +не меняется, новый под-гейт — условие финализации, а не новая стадия. + +### D1. Точка врезки — `advance_stage`, ребро `deploy → done` (единая для ВСЕХ путей) +Врезка `_handle_merge_verify(...)` в `src/stage_engine.py::advance_stage` **после** успешного +прохождения QG (`check_deploy_status == SUCCESS`, т.е. `next_stage == "done"`) и **до** +`update_task_stage(task_id, next_stage)`: + +```python +# --- ORCH-071 merge-verify under-gate (deploy -> done edge) --- +if current_stage == "deploy" and next_stage == "done": + if _handle_merge_verify(task_id, repo, work_item_id, branch, result): + return result # HOLD: merge не подтверждён -> alert, НЕ done, НЕ rollback +``` + +`advance_stage` — **единственная** функция перехода стадий. Её вызывают `run_deploy_finalizer` +(Phase C), reconciler F-1 (`finished_agent=None`), job-reaper (re-drive). Врезка именно здесь +**гейтит ВСЕ пути единообразно**: ни один из них не сможет довести `deploy → done` без +подтверждённого merge. Это закрывает скрытую дыру: reconciler F-1 предоценивает +`check_deploy_status` read-only и при зелёном вызывает `advance_stage` — без врезки он бы +протолкнул `done` в обход merge. + +### D2. Когда выполняется merge — в Phase C (после рестарта), а НЕ в Phase B +Merge выполняется внутри `_handle_merge_verify`, т.е. на ребре `deploy → done`, которое +достигается **из `run_deploy_finalizer` уже в НОВОМ контейнере после рестарта прода**. Это +осознанный выбор в пользу второго варианта G3 («шаг, переживающий рестарт»): + +- Phase B лишь **диспетчеризует** detached-деплой (`ssh` возвращается мгновенно), рестарт прода + происходит асинхронно на хосте. Merge в Phase B (живой старый контейнер) **гонялся бы** с + рестартом и мог быть убит на полушаге — ровно постмортем-урок №3. Поэтому merge в Phase B + **отвергнут**. +- Phase C finalizer уже **restart-surviving**: это reserved-agent job `deploy-finalizer`, + переставляемый с defer и **claim'ится воркером нового контейнера** после рестарта; если новый + контейнер умрёт на полушаге merge — job re-drive'ится (reaper/requeue), а `pr_already_merged` + делает повтор идемпотентным. Merge физически происходит **строго ПОСЛЕ** рестарта → рестарт + его не убивает. G3 удовлетворён. + +### D3. Merge-актор — `src/merge_gate.py::merge_pr(repo, branch) -> (bool, str)` +Новый детерминированный merge-актор (рядом с `pr_already_merged`/`pid_alive`/`reclaim_stale_lease`): +1. `pr_already_merged(repo, branch)` → `True` → **no-op** `(True, "already-merged")` (INV-5/AC-9). +2. Иначе `GET /repos/{owner}/{repo}/pulls?state=open&head=` → индекс открытого PR. +3. `POST /repos/{owner}/{repo}/pulls/{index}/merge` (Do: `merge`) через существующий httpx-клиент + и `settings.gitea_*`. Никогда не push/force-push в `main` (INV-4/AC-8). +4. **never-raise** (INV-1): любая HTTP/parse-ошибка → `(False, reason)`; нет открытого PR при + `pr_already_merged==False` → `(False, "no open PR")`. + +Работает под merge-lease, который уже **удерживается** этой задачей с merge-gate ребра +`deploy-staging → deploy` (Phase A held-across-wait) и освобождается на `done`/откате +(существующий `release_merge_lease`, ORCH-043) либо реклеймится по смерти держателя (ORCH-065). +Сериализация слияний сохранена без новой блокировки. + +### D4. Верификатор — `src/merge_gate.py::verify_merged_to_main(repo, branch, sha) -> bool` +Возвращает `True`, если merge подтверждён (FR-2): +- `pr_already_merged(repo, branch) is True` **ИЛИ** +- `git merge-base --is-ancestor origin/main` в worktree задачи (после `git fetch origin main`), + где `` — validated commit = `git rev-parse HEAD` worktree (тот же якорь, что + `image_freshness.validated_revision`). + +**never-raise** (INV-1/AC-7): любая git/HTTP-ошибка → `False` (= «не подтверждено» → alert + HOLD, +fail-closed для `done`). Исключение НИКОГДА не пробрасывается в `advance_stage`. + +### D5. `_handle_merge_verify` (оркестрация под-гейта, `src/stage_engine.py`) +Возвращает `True` (вмешался → HOLD, не advance) / `False` (merge подтверждён → штатный advance в `done`): +1. Условность: `merge_verify_applies(repo)` (см. D7) `False` → вернуть `False` (поведение 1:1 как раньше). +2. `sha = validated_revision(...)`; `merge_gate.merge_pr(repo, branch)` (no-op если уже слит). +3. `ok = merge_gate.verify_merged_to_main(repo, branch, sha)`. +4. `ok==True`: + - дописать `merged_to_main: true` во frontmatter `14-deploy-log.md` (машиночитаемая + наблюдаемость; `deploy_status:` НЕ трогаем — контракт парсинга `check_deploy_status` + неизменен), вернуть `False` → `advance_stage` штатно ведёт `deploy → done` + (терминал-sync/post-deploy-monitor как сегодня; AC-4). +5. `ok==False`: + - **alert** «deploy succeeded but not merged» — Telegram + Plane-коммент; + - `set_issue_blocked(work_item_id)` (Plane не-терминальный; согласовано с ORCH-066 + DEGRADED→Blocked и deploy-finalize-exhausted); + - дописать `merged_to_main: false`; **НЕ** `update_task_stage` (задача остаётся на `deploy`), + **НЕ** откат на `development` (not-merged — инфра-дефект, не код; FR-3 → ALERT-only, как + ORCH-021 self-hosting); + - вернуть `True`. + Повтор (re-drive/reaper) переоценит: после ручного устранения merge подтвердится → `done`. + +Вся функция обёрнута never-raise: внутренняя ошибка → трактуется как «не подтверждено» (HOLD+alert), +не падение конвейера. + +### D6. Идемпотентность (INV-5/AC-9) +- Перед merge — `pr_already_merged` (no-op повтор). +- `verify` зелёный для уже-слитого PR (ветвь `pr_already_merged is True`). +- Повторный прогон ребра `deploy → done` (двойной webhook / reaper / reconciler): merge no-op, + verify зелёный, нет дубль-merge, нет ложного БАГ-8 отката. + +### D7. Условность раската (FR-5/AC-10) — `src/config.py` +Новые флаги (паттерн `merge_gate_*`/`image_freshness_*`): +- `merge_verify_enabled: bool = True` — глобальный kill-switch; `False` → строго прежнее + поведение (`_handle_merge_verify` сразу `False`, 1:1 до фикса). +- `merge_verify_repos: str = ""` — CSV; пусто → реально ТОЛЬКО для self-hosting + (`is_self_hosting_repo`); непусто → только перечисленные. +- (опц.) `merge_pr_timeout_s` / `merge_verify_timeout_s` — таймауты Gitea/git. + +`merge_verify_applies(repo)` — never-raise, зеркало `self_deploy_applies` / `image_freshness`. +Non-self репо (enduro-trails): под-гейт — **no-op**, merge остаётся за агентом `deployer` (AC-4b). + +### D8. Наблюдаемость (опц., FR §2/§3) +Блок `merge_verify` в `GET /queue` (по образцу `reaper`/`post_deploy`): `enabled`, +`merge_verified_total`, `not_merged_alerts_total`, `last_alert_wi`. Каждый alert → `logger.warning` ++ Telegram. + +### D9. Диагностический runbook (G4/FR-4) +`docs/operations/PHANTOM_MERGE_RUNBOOK.md` — 4 проверки постмортема с copy-paste командами: +(1) Gitea API список PR + `merged`-флаги; (2) md5 прод-файлов vs `git show origin/main:`; +(3) `git merge-base` ветки vs `main`; (4) таймлайн деплой-логов. + критерий «фантом подтверждён». + +## Что НЕ меняется (контракты) +`STAGE_TRANSITIONS`; `check_deploy_status`/`_parse_deploy_status` (читают только `deploy_status:`); +реестр `QG_CHECKS` (под-гейт — врезка в `advance_stage`, НЕ новый зарегистрированный QG, как +`_handle_merge_gate`); схема БД (restart-safe состояние — существующие sentinel'ы +`.deploy-state-//` + очередь `jobs`); БАГ-8; terminal-sync; merge-gate (ORCH-043); +image-freshness (ORCH-058); `Confirm Deploy` (ORCH-059); post-deploy monitor (ORCH-021); +exit-коды хука (0/1/2); ручной approve прод-деплоя (INV-3). Non-self merge — за агентом `deployer`. + +## Последствия + +**Плюсы** +- Невозможно состояние «`done` + прод задеплоен, а PR `open`»: либо merge подтверждён → `done`, + либо HOLD + alert (G2/критерий успеха BRD §8). +- Единая врезка в `advance_stage` гейтит ВСЕ пути (finalizer/reconciler/reaper) — нет обходных + дверей к `done`. +- Merge в restart-surviving Phase C структурно не убивается рестартом прода (G3, урок №3). +- Минимальный blast-radius: `STAGE_TRANSITIONS`/`check_deploy_status`/схема БД/реестр QG — нетронуты; + раскат за kill-switch. + +**Минусы / ограничения** +- При недоступной Gitea verify консервативно даёт `False` → возможен ложный not-merged alert и + HOLD; снимается повтором после восстановления Gitea (приемлемо: fail-closed для `done` важнее). +- HOLD при not-merged требует ручного вмешательства (ALERT-only) — осознанно (not-merged — + инфра-дефект, авто-откат на `development` запрещён FR-3). +- Появляется реальный исходящий merge-вызов из кода — должно покрываться mock-тестами Gitea + (AC-2) и smoke рестарта (AC-3). + +## Альтернативы (отвергнуто) +- **Merge в Phase B (до рестарта).** Гонка с асинхронным рестартом прода → merge может быть убит + на полушаге (постмортем-урок №3). Отвергнуто в пользу restart-surviving Phase C. +- **Новый зарегистрированный QG `check_merged_to_main` на стадии `deploy`.** У стадии один QG + (`check_deploy_status`); второй потребовал бы менять `STAGE_TRANSITIONS`/контракт. Врезка + под-гейта в `advance_stage` (как merge-gate) даёт тот же охват без изменения реестра. +- **Авто-откат на `development` при not-merged.** Запрещено FR-3: not-merged — инфра-дефект, + не код; реакция = alert + ручное вмешательство. diff --git a/docs/work-items/ORCH-071/07-infra-requirements.md b/docs/work-items/ORCH-071/07-infra-requirements.md new file mode 100644 index 0000000..5aeada1 --- /dev/null +++ b/docs/work-items/ORCH-071/07-infra-requirements.md @@ -0,0 +1,47 @@ +# 07 — Требования к инфраструктуре (ORCH-071) + +## Топология — без изменений +Новой топологии не вводится. Прод `orchestrator` (8500) и staging (8501) — как есть. +Merge выполняется детерминированным кодом в уже существующем restart-surviving Phase C +finalizer (новый контейнер после рестарта), без новых сервисов/портов/контейнеров. + +## I-1. Gitea токен с правом merge PR (предусловие) +Merge-актор `merge_gate.merge_pr` вызывает `POST /repos/{owner}/{repo}/pulls/{index}/merge` +через существующий клиент и `settings.gitea_token` / `settings.gitea_url` / `settings.gitea_owner`. +- Требование: тот же `gitea_token`, которым агент `deployer` сегодня мержит PR в `main`, + ДОЛЖЕН иметь право write/merge на репо `orchestrator`. Так как deployer уже мержит этим + токеном — **новых прав, как правило, не требуется** (тот же токен, тот же путь API). +- Действие при раскате: убедиться, что бот-токен — член/коллаборатор репо `orchestrator` + с правом merge (иначе merge_pr вернёт HTTP-ошибку → never-raise → HOLD+alert, не падение). + +## I-2. Сетевой доступ контейнера к Gitea +Контейнер прода уже ходит в Gitea API (`pr_already_merged`, webhooks). Дополнительного +сетевого доступа не нужно. При недоступности Gitea verify консервативно даёт «не +подтверждено» → HOLD+alert (fail-closed для `done`). + +## I-3. Доступ к `origin/main` из worktree задачи +Верификатор делает `git fetch origin main` + `git merge-base --is-ancestor origin/main` +в worktree задачи (как `image_freshness`/merge-gate уже делают `git fetch`/`rebase`). +Предусловие — рабочий git-remote `origin` в worktree (есть сегодня). Ошибка fetch → +never-raise → `False` → HOLD+alert. + +## I-4. Конфигурация (env, дефолты безопасны) +| Флаг | Дефолт | Назначение | +|------|--------|------------| +| `ORCH_MERGE_VERIFY_ENABLED` | `true` | kill-switch; `false` → строго прежнее поведение (1:1 до фикса) | +| `ORCH_MERGE_VERIFY_REPOS` | `""` | CSV; пусто → только self-hosting (`orchestrator`) | +| `ORCH_MERGE_PR_TIMEOUT_S` (опц.) | напр. 30 | таймаут merge-вызова Gitea | +| `ORCH_MERGE_VERIFY_TIMEOUT_S` (опц.) | напр. 60 | таймаут git fetch/merge-base | + +Дефолты не требуют изменения `.env` для штатного раската (область = self-hosting). +Откатить фикс мгновенно можно `ORCH_MERGE_VERIFY_ENABLED=false`. + +## I-5. Раскат через staging-гейт (self-hosting safety) +Изменение касается self-deploy пути орка → раскат ОБЯЗАН пройти стадию `deploy-staging` +(8501) перед прод-деплоем (CLAUDE.md §self-hosting). Прод-деплой — только переводом задачи +в статус `Confirm Deploy` (ORCH-059), ручной approve сохранён (INV-3). Никаких рестартов +прода в рамках разработки/ревью. + +## I-6. Без миграции БД +Schema-changes запрещены. Restart-safe состояние нового шага — существующие sentinel'ы +`.deploy-state-//` + очередь `jobs` (колонка `jobs.pid`, ORCH-065, уже есть). diff --git a/docs/work-items/ORCH-071/10-tech-risks.md b/docs/work-items/ORCH-071/10-tech-risks.md new file mode 100644 index 0000000..75c8078 --- /dev/null +++ b/docs/work-items/ORCH-071/10-tech-risks.md @@ -0,0 +1,23 @@ +# 10 — Технические риски (ORCH-071) + +| ID | Риск | Вероятность / Влияние | Митигация | +|----|------|----------------------|-----------| +| R-1 | **Гонка merge с рестартом прода** (постмортем-урок №3): merge в Phase B убивается рестартом → снова фантом. | Средняя / Критич. | Merge вынесен в **Phase C finalizer** (restart-surviving, новый контейнер ПОСЛЕ рестарта). Merge физически строго после рестарта. Smoke-тест AC-3. | +| R-2 | **Обходной путь к `done`** мимо merge-шага (reconciler F-1 / reaper протолкнут `deploy → done` по зелёному `check_deploy_status`). | Средняя / Критич. | Врезка `_handle_merge_verify` в **`advance_stage`** (единственная функция перехода) → гейтит ВСЕ вызывающие пути единообразно. | +| R-3 | **Ложный not-merged alert при недоступной Gitea** (verify→`False`) → лишний HOLD. | Средняя / Низкое | Осознанный fail-closed для `done`; снимается повтором (re-drive/reconciler) после восстановления Gitea. Alert информативен, не роняет конвейер. | +| R-4 | **Дубль-merge / merge-error** при re-drive (двойной webhook, reaper-requeue). | Средняя / Среднее | `pr_already_merged` ПЕРЕД merge → no-op повтор (INV-5/AC-9). Ложного БАГ-8 отката нет (merge-verify не откатывает). | +| R-5 | **Прямой/force push в `main`** случайно. | Низкая / Критич. | Merge ТОЛЬКО через Gitea PR-merge API (`merge_pr`); код не делает `git push origin main`. INV-4/AC-8, ревью. | +| R-6 | **Verify/merge роняет прод-контейнер** (self-hosting). | Низкая / Критич. | merge_pr/verify — только API + read-only git в worktree; никаких `docker`/restart 8500. INV-2/AC-8. | +| R-7 | **Регрессия non-self деплоя** (enduro-trails). | Низкая / Среднее | Условность `merge_verify_applies` (пусто→self-hosting); non-self — no-op, merge остаётся за `deployer`. AC-4b. | +| R-8 | **HOLD-залипание**: not-merged → Blocked, никто не вмешался → задача вечно не `done`. | Средняя / Среднее | Alert (Telegram+Plane) + Plane `Blocked` (видимый сигнал). Реакция ALERT-only осознанна (not-merged — инфра-дефект, авто-откат запрещён FR-3). Runbook G4 для быстрой локализации. | +| R-9 | **Validated SHA рассинхронизирован** (verify проверяет не тот коммит). | Низкая / Среднее | Единый якорь `validated_revision` (`git rev-parse HEAD` worktree) — тот же, что у image-freshness ORCH-058. | +| R-10 | **Exception из verify валит finalizer/advance_stage**. | Низкая / Высокое | never-raise контракт на всех публичных хелперах + обёртка `_handle_merge_verify`. AC-7. | +| R-11 | **Merge ветки, чей deploy FAILED** (если бы merge был до verify статуса). | — / — | Merge выполняется на ребре `deploy → done`, достигаемом ТОЛЬКО при `deploy_status: SUCCESS`. FAILED → БАГ-8 откат ДО merge-шага (merge не вызывается). | + +## Открытые вопросы / follow-up +- **Merge-style** (`merge` / `rebase` / `squash`) в Gitea API — зафиксировать тот же стиль, + что использовал агент `deployer` (по умолчанию `merge`), чтобы не менять историю `main`. +- **Восстановление текущего `main`** (долив 022/059/066/068) — ОТДЕЛЬНАЯ ветка + `integ/restore-main-2026-06-08`, вне scope ORCH-071. +- **Полный авто-деплой** (ORCH-54) — merge-verify совместим, но INV-3 (ручной approve) на + старте сохраняется.