architect(ET): auto-commit from architect run_id=354
This commit is contained in:
@@ -121,6 +121,44 @@ sentinel-файлы (`<repos_dir>/.deploy-state-<repo>/<wi>/`), без мигр
|
||||
Детально — `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 <validated_sha> 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 —
|
||||
|
||||
63
docs/architecture/adr/adr-0013-merge-verify-gate.md
Normal file
63
docs/architecture/adr/adr-0013-merge-verify-gate.md
Normal file
@@ -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 <validated_sha> 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).
|
||||
186
docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md
Normal file
186
docs/work-items/ORCH-071/06-adr/ADR-001-merge-verify-gate.md
Normal file
@@ -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=<branch>` → индекс открытого 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 <sha> origin/main` в worktree задачи (после `git fetch origin main`),
|
||||
где `<sha>` — 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:<file>`;
|
||||
(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-<repo>/<wi>/` + очередь `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 + ручное вмешательство.
|
||||
47
docs/work-items/ORCH-071/07-infra-requirements.md
Normal file
47
docs/work-items/ORCH-071/07-infra-requirements.md
Normal file
@@ -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 <sha> 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-<repo>/<wi>/` + очередь `jobs` (колонка `jobs.pid`, ORCH-065, уже есть).
|
||||
23
docs/work-items/ORCH-071/10-tech-risks.md
Normal file
23
docs/work-items/ORCH-071/10-tech-risks.md
Normal file
@@ -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) на
|
||||
старте сохраняется.
|
||||
Reference in New Issue
Block a user