analyst(ET): auto-commit from analyst run_id=353

This commit is contained in:
2026-06-08 07:59:49 +00:00
committed by stream
parent dd5fe619d5
commit c26a6b637c
4 changed files with 295 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# BRD — ORCH-071: Фантомный merge — деплой без слияния в main
## 1. Контекст и тип
- **Тип:** BUG CRITICAL (целостность `main` / надёжность деплоя, self-hosting).
- **Обнаружено:** Слава + Стрим, 2026-06-08, при разборе «ORCH-067 не подхватился».
- **Постмортем:** `docs/history/LESSONS_2026-06-08_phantom-merge.md`.
- **Подозрение на регресс:** ORCH-065 (idempotent merge / lease-reclaim) — последний честный merge (PR#66).
- **Связано:** восстановление текущего `main` ведётся ОТДЕЛЬНО (ветка `integ/restore-main-2026-06-08`); эта задача — ROOT-FIX, чтобы фантом не повторялся.
## 2. Проблема (бизнес-формулировка)
Self-deploy (Phase B) для self-hosting репо `orchestrator` собирает прод-образ из ВЕТКИ задачи и рапортует `finalize SUCCESS` + post-deploy `HEALTHY`, **но git-merge ветки в `main` НЕ происходит**. PR остаётся `open`. Следующая задача срезает свою ветку от устаревшего `main` → теряет код незалитых предшественников.
Накопительно потеряны в `main`: **ORCH-022, 059, 066, 068** (PR#67/68/69/70 — open). Последний реально слитый — ORCH-065 (PR#66).
## 3. Подтверждённый root cause (по результатам код-аудита)
Гипотеза A постмортема подтверждена аудитом кода ветки:
1. **В `src/` НЕТ кода, выполняющего merge PR в `main`** (`grep` по `pulls/.../merge`, `/merge`, `merge_pr` — 0 совпадений). Фактический merge выполняет ТОЛЬКО LLM-агент `deployer` через Bash в начале стадии `deploy` (см. `.openclaw/agents/deployer.md`).
2. Для self-hosting (`orchestrator`) стадия `deploy` оркеструется **детерминированным кодом** (`stage_engine._handle_self_deploy_phase_b``self_deploy.initiate_deploy` → finalizer `run_deploy_finalizer`), и агент `deployer` **НЕ запускается** (так предписывает `deployer.md`). Detached host-процесс делает retag staging-образа на прод-тег + рестарт 8500. **Ни одна фаза A/B/C не вызывает merge ветки в `main`.**
3. `run_deploy_finalizer` маппит exit-code хука `0→SUCCESS`, пишет `14-deploy-log.md` и вызывает `advance_stage(..., finished_agent="deployer")`. Гейт `check_deploy_status` читает только `deploy_status:` из артефакта → `SUCCESS → done`. **Состояние `main` нигде не верифицируется.**
Итог: для self-hosting путь `deploy` структурно НЕ содержит шага merge-в-main, а `done` достигается исключительно по deploy-маркеру. «Зелёный» деплой + здоровый прод (образ из рабочей ветки) маскируют отсутствие merge — сигнала о проблеме нет, пока следующая задача не потеряет код предшественника.
Вторичный фактор (усиливает риск даже если merge добавить наивно): Phase B **рестартит прод-контейнер**, поэтому любой держатель merge-lease / незавершённый git-шаг внутри процесса умирает до завершения merge (урок №3 постмортема).
## 4. Бизнес-цели
| ID | Цель |
|----|------|
| **G1** | Деплой ВЕРИФИЦИРУЕТ, что задеплоенный commit реально влит в `main` ПОСЛЕ деплоя (deployed SHA — предок `origin/main` ИЛИ `PR.merged==true`). Иначе — alert, задача НЕ `done`. |
| **G2** | Задача → `done` ТОЛЬКО при подтверждённом merge (`PR.merged==true`); маркеров `finalize`/`post-deploy` недостаточно. |
| **G3** | Merge в `main` завершается и подтверждается ДО рестарта прод-контейнера, ЛИБО merge вынесен в шаг, переживающий рестарт (паттерн `requeue_running_jobs` для merge-в-main). |
| **G4** | Диагностический runbook (4 проверки из постмортема) — в `docs/operations`. |
## 5. Не-цели
- Не менять source-of-truth (Plane), схему БД.
- Не отменять self-hosting safety (no auto-rollback / no-restart-others) — наоборот, усилить верификацией.
- Восстановление текущего `main` (долив 022/059/066/068) — ОТДЕЛЬНАЯ ветка `integ/restore-main-2026-06-08`, вне scope.
## 6. Инварианты (обязательны к соблюдению)
| ID | Инвариант |
|----|-----------|
| **INV-1** | **never-raise** на шаге верификации — при ошибке шлётся alert, не падение процесса/конвейера. |
| **INV-2** | self-hosting safety: верификация НЕ рестартит и НЕ роняет прод-контейнер `orchestrator` (8500), не трогает другие проекты. |
| **INV-3** | Ручной approve прод-деплоя (триггер «Confirm Deploy», ORCH-059) сохранён — новая логика не вводит авто-деплой. |
| **INV-4** | Никогда не делать force-push / прямой push в `main`; merge только через PR-merge API Gitea (как у deployer-агента сегодня). |
| **INV-5** | Идемпотентность: повторный прогон (re-drive/reaper/двойной webhook) не делает второй merge и не ломает контракты (опора на `pr_already_merged`, ORCH-065). |
## 7. Заинтересованные стороны
- **Owner** — одобряет прод-деплой («Confirm Deploy»), получает alert при «deployed but not merged».
- **Все проекты на инстансе** (enduro-trails) — косвенно: целостность `main` орка влияет на инструмент, обслуживающий их из общей БД/очереди.
## 8. Критерий успеха (бизнес-уровень)
После доработки невозможно состояние «задача `done` + прод задеплоен, а PR `open` / commit не в `main`»: либо merge подтверждён и задача `done`, либо задача НЕ `done` и поднят alert «deploy succeeded but not merged». Воспроизведение исходного сценария на staging показывает, что `main` реально получает commit.

View File

@@ -0,0 +1,78 @@
# ТЗ — ORCH-071: Верификация merge-в-main как условие done
> Документ фиксирует ТРЕБОВАНИЯ к изменениям (WHAT). Конкретный дизайн (HOW: новый
> leaf-модуль vs расширение существующего, где разместить шаг merge, формат
> sentinel'ов) — за архитектором (ADR `06-adr/`). ТЗ задаёт инварианты, точки
> врезки и контракты, которые дизайн обязан удовлетворить.
## 0. Резюме root cause (вход для дизайна)
Для self-hosting (`orchestrator`) стадия `deploy` идёт детерминированным путём
`_handle_self_deploy_phase_b → initiate_deploy → run_deploy_finalizer`, который
**не содержит шага merge PR в `main`** (merge делает только LLM-`deployer`, не
запускаемый на self-hosting). `done` достигается по `deploy_status: SUCCESS` без
верификации `main`. Требуется: (A) выполнить/докатить merge в `main` детерминированно
до перехода в `done`; (B) верифицировать факт merge ПОСЛЕ деплоя; (C) запретить
`done` без подтверждённого merge.
## 1. Задействованные модули `src/`
| Модуль | Роль в фиксе | Характер изменения |
|--------|--------------|--------------------|
| `src/stage_engine.py` | `run_deploy_finalizer` (Phase C), терминал-блок `next_stage == "done"`, `_handle_self_deploy_phase_b` | Врезка шага merge-в-main + пост-merge верификация; блокировка перехода в `done` при неподтверждённом merge. |
| `src/merge_gate.py` | Уже содержит `pr_already_merged` (ORCH-065, read-only guard) | Добавить детерминированный **merge-актор** для self-hosting (выполнить merge PR через Gitea API) + helper верификации «SHA предок `origin/main`». Опора на существующие `pid_alive`/`reclaim_stale_lease`. |
| `src/self_deploy.py` | Sentinel-state Phase A/B/C | Возможный новый sentinel-маркер `merged` (restart-safe), если дизайн выносит merge в отдельный переживающий рестарт шаг (G3). |
| `src/qg/checks.py` | Реестр `QG_CHECKS`, `check_deploy_status` | Возможный новый под-чек верификации merge (например `check_merged_to_main`) ЛИБО усиление условия перехода `deploy→done`. `check_deploy_status` НЕ менять по контракту парсинга. |
| `src/config.py` | Флаги | Новый kill-switch (напр. `merge_verify_enabled` / `merge_verify_repos`), таймауты merge/verify. Дефолт — область self-hosting (как ORCH-35/43/58). |
| `.openclaw/agents/deployer.md` | Промпт deployer'а (non-self merge) | Уточнить: для self-hosting merge выполняет детерминированный код; non-self путь без изменений. |
| `src/main.py` (`/queue`) | Наблюдаемость | Опционально: блок/счётчики верификации merge (`merge_verified_total`, `not_merged_alerts_total`). |
## 2. Функциональные требования
### FR-1 (G3) — Детерминированный merge-в-main для self-hosting
- Для self-hosting репо merge PR ветки в `main` ДОЛЖЕН выполняться **детерминированным кодом** (не LLM-агентом), т.к. `deployer`-агент на self-hosting `deploy` не запускается.
- Merge выполняется через **Gitea PR-merge API** (как сегодня делает агент), НИКОГДА не force-push / не прямой push в `main` (INV-4).
- ПЕРЕД merge консультироваться `merge_gate.pr_already_merged(repo, branch)` — уже слит → no-op (INV-5, переиспользовать ORCH-065).
- **G3 — порядок относительно рестарта:** merge ДОЛЖЕН быть завершён и подтверждён ДО рестарта прод-контейнера, ЛИБО вынесен в шаг, переживающий рестарт (паттерн `requeue_running_jobs`/finalizer-defer): если процесс умер во время Phase B, шаг merge докатывается после рестарта (re-drive finalizer'а или отдельный merge-job). Дизайн выбирает один из двух вариантов; выбранный обязан быть restart-safe (sentinel/jobs, без миграции БД — §4).
### FR-2 (G1) — Пост-деплой верификация merge
- ПОСЛЕ деплоя (в Phase C / финализации, ДО фиксации `done`) выполнить детерминированную верификацию: задеплоенный commit (validated SHA) — **предок `origin/main`** (`git merge-base --is-ancestor <sha> origin/main`) **ИЛИ** `PR.merged == true` (Gitea API).
- Верификация **never-raise** (INV-1): любая ошибка git/HTTP → трактуется как «не подтверждено» → alert, НЕ падение.
- При неподтверждённой верификации — **alert** «deploy succeeded but not merged» (Telegram + Plane-коммент) и задача **НЕ переходит в `done`** (FR-3).
### FR-3 (G2) — `done` только при подтверждённом merge
- Переход `deploy → done` для self-hosting ДОЛЖЕН быть обусловлен подтверждённым merge (verify из FR-2 зелёный). Наличие `deploy_status: SUCCESS` + post-deploy `HEALTHY`**недостаточно**.
- При `SUCCESS`-маркере деплоя, но неподтверждённом merge: задача удерживается (не `done`), Plane-статус — не терминальный (например текущий `Deploying`/`Awaiting` или `Blocked` по решению дизайна), шлётся alert. Конвейер НЕ откатывается на `development` автоматически из-за not-merged (это инфраструктурный, не код-дефект) — реакция = alert + ручное вмешательство (согласовать с дизайном; по умолчанию ALERT-only, как ORCH-021 self-hosting).
### FR-4 (G4) — Диагностический runbook
- В `docs/operations/` добавить runbook с 4 проверками из постмортема (метод однозначной локализации фантома):
1. Gitea API: список PR + флаги `merged`.
2. md5 прод-файлов vs `git show origin/main:<file>`.
3. `git merge-base` ветки vs `main`.
4. Таймлайн деплой-логов.
- Включить готовые команды (copy-paste) и критерий «фантом подтверждён».
### FR-5 — Условность раската (как ORCH-35/43/58)
- Новая логика merge+verify реальна для self-hosting (`is_self_hosting_repo` / `merge_verify_repos`); прочие репо — поведение БЕЗ изменений (non-self merge остаётся за агентом `deployer`).
- Kill-switch (env, дефолт `true`) → `false` восстанавливает строго прежнее поведение.
## 3. Изменения API
- **Внешний HTTP API сервиса (`/health`, `/status`, `/queue`, `/webhook/*`) — без новых endpoint'ов.** Допустимо обогащение ответа `GET /queue` блоком наблюдаемости merge-verify (счётчики), по образцу блоков `reaper`/`post_deploy`.
- **Gitea API (исходящие вызовы):** новый детерминированный вызов `POST /repos/{owner}/{repo}/pulls/{index}/merge` (merge-актор, FR-1) + чтение `GET /repos/{owner}/{repo}/pulls?...` (уже используется в `pr_already_merged`). Через существующий httpx-клиент и `settings.gitea_*`.
## 4. Изменения схемы БД
- **НЕТ.** Schema-changes запрещены (не-цель). Restart-safe состояние нового шага merge — через sentinel-файлы (`.deploy-state-<repo>/<wi>/`, как ORCH-036) и/или существующую очередь `jobs` (finalizer-defer). Колонка `jobs.pid` (ORCH-065) уже есть, при необходимости переиспользуется.
## 5. Требования к новым QG checks
- Допускается ввести детерминированный под-чек верификации merge (напр. `check_merged_to_main`), регистрируемый в `QG_CHECKS`, ЛИБО встроить верификацию как условие в логику перехода `deploy→done` без нового чека — на усмотрение дизайна. В любом случае:
- Контракт `check_deploy_status` / `_parse_deploy_status` (читает только `deploy_status:` frontmatter) **НЕ меняется**.
- `STAGE_TRANSITIONS` **НЕ меняется** (verify — это условие/под-гейт ребра/финализации, не новая стадия).
- Вердикт (если артефакт) — строго YAML-frontmatter (канон гейтов), never проза.
## 6. Артефакты, создаваемые/обновляемые по pipeline
- `14-deploy-log.md` — существующий; дизайн может добавить поле статуса merge (напр. `merged_to_main: true|false`) во frontmatter (машиночитаемо), не ломая `deploy_status:`.
- Новый runbook в `docs/operations/` (FR-4).
- **Обязательно (CLAUDE.md §2):** обновить `docs/architecture/README.md` (раздел Phase B / merge-gate / executable self-deploy — описать новый merge+verify шаг), `CHANGELOG.md`, при сквозном решении — ADR (`docs/work-items/ORCH-071/06-adr/ADR-001-*.md` и/или global `docs/architecture/adr/`).
## 7. Совместимость / регресс
- Happy-path не-self репо (enduro-trails): merge остаётся за агентом `deployer` → поведение без изменений.
- Happy-path self-hosting: при штатном merge задача `done` ставится как раньше (после добавления verify, который зелёный).
- Все существующие контракты неизменны: `STAGE_TRANSITIONS`, реестр `QG_CHECKS` (кроме возможного нового under-чека), `check_deploy_status`, БАГ-8, terminal-sync, merge-gate (ORCH-043), `Confirm Deploy` (ORCH-059), exit-коды хука (0/1/2), схема БД.

View File

@@ -0,0 +1,61 @@
# Критерии приёмки — ORCH-071
Формат: каждый критерий имеет явное условие PASS/FAIL. Машинные вердикты — из артефактов/состояния, не из прозы.
## AC-1 (G1) — Пост-деплой верификация: not-merged ⇒ не done + alert
- **Условие:** после Phase B/финализации, если задеплоенный commit НЕ влит в `origin/main` (не предок `origin/main` И `PR.merged != true`).
- **PASS:** задача НЕ переходит в `done`; шлётся alert «deploy succeeded but not merged» (Telegram + Plane-коммент).
- **FAIL:** задача стала `done` при неслитом PR ИЛИ alert не отправлен.
## AC-2 (G2) — done только при PR.merged==true (mock-тест)
- **Условие:** SUCCESS-маркеры деплоя присутствуют (`deploy_status: SUCCESS`), но PR `open` (`merged=false`).
- **PASS:** переход в `done` НЕ выполняется (тест на mock Gitea: PR open → done не ставится).
- **FAIL:** задача переведена в `done`.
## AC-3 (G3) — Merge подтверждён до/независимо от рестарта (smoke)
- **Условие:** симулирован рестарт контейнера во время Phase B (процесс/держатель merge умер до завершения merge).
- **PASS:** после рестарта merge докатывается (re-drive finalizer / merge-job, как `requeue_running_jobs`), `main` получает commit, верификация зелёная → задача `done`.
- **FAIL:** после рестарта merge не докатился, задача `done` без merge ИЛИ навсегда зависла без alert.
## AC-4 (регресс) — Happy-path
- **Условие:** merge прошёл штатно, `PR.merged==true`, deploy `SUCCESS`, верификация зелёная.
- **PASS:** `done` ставится как раньше (терминал-sync/Plane-статус как сегодня для self-hosting), без лишних alert.
- **FAIL:** регрессия — happy-path не доходит до `done` или шлёт ложный not-merged alert.
## AC-4b (регресс) — non-self репо без изменений
- **Условие:** деплой репо enduro-trails (не self-hosting).
- **PASS:** merge выполняет агент `deployer` (прежний путь), новая детерминированная merge/verify-логика — no-op для не-self.
- **FAIL:** изменилось поведение non-self деплоя.
## AC-5 — Зелёный pytest + документация
- **PASS:** `pytest tests/ -q` зелёный; обновлены `CHANGELOG.md`, `docs/architecture/README.md` (раздел Phase B / merge-verify) и runbook (`docs/operations/`).
- **FAIL:** красные тесты ИЛИ документация/CHANGELOG/runbook не обновлены (reviewer → REQUEST_CHANGES, CLAUDE.md §6).
## AC-6 — Воспроизведение исходного сценария на staging
- **Условие:** на staging провести задачу до деплоя.
- **PASS:** проверить (методом runbook), что `main` реально получил commit задачи (PR merged / SHA предок `origin/main`).
- **FAIL:** прод/«done» достигнуты, а `main` не получил commit.
## AC-7 (INV-1) — never-raise на верификации
- **Условие:** verify сталкивается с ошибкой git/HTTP (Gitea недоступна, битый ref).
- **PASS:** функция возвращает «не подтверждено» → alert, процесс/конвейер НЕ падает (исключение не пробрасывается).
- **FAIL:** исключение из verify валит finalizer/advance_stage.
## AC-8 (INV-2) — self-hosting safety
- **Условие:** шаг верификации/merge исполняется для `orchestrator`.
- **PASS:** verify/merge НЕ рестартят и НЕ роняют прод-контейнер 8500, не трогают другие проекты; merge — только PR-merge API, без push в `main`.
- **FAIL:** verify/merge перезапускает прод ИЛИ делает прямой/force push в `main`.
## AC-9 (INV-5) — идемпотентность повторного прогона
- **Условие:** re-drive стадии `deploy` / повторный webhook / reaper-requeue при уже слитом PR.
- **PASS:** `pr_already_merged` → merge не повторяется (no-op), верификация зелёная, нет дубль-merge/ошибки Gitea, нет ложного БАГ-8 отката.
- **FAIL:** второй merge / merge-error / ложный откат.
## AC-10 (FR-5) — kill-switch
- **Условие:** kill-switch новой merge/verify-логики выключен (`false`).
- **PASS:** строго прежнее поведение (1:1 до фикса).
- **FAIL:** при выключенном флаге логика всё равно срабатывает.
## AC-11 (INV-3) — ручной approve сохранён
- **PASS:** прод-деплой по-прежнему запускается только статусом «Confirm Deploy» (ORCH-059); merge/verify не вводят авто-деплой.
- **FAIL:** деплой/merge запускается без человеческого триггера.

View File

@@ -0,0 +1,103 @@
work_item: ORCH-071
title: "Верификация merge-в-main как условие done (фантомный merge)"
notes: >
Тесты детерминированные, без LLM. Gitea/PR-состояние и git-операции мокаются
(monkeypatch httpx / subprocess / merge_gate helpers). Цель — закрыть AC-1..AC-11.
Все новые функции верификации/merge соблюдают never-raise.
tests:
# --- FR-2 / G1 / AC-1: пост-деплой верификация merge ---
- id: TC-01
type: unit
description: "verify_merged_to_main возвращает True, когда deployed SHA — предок origin/main (git merge-base --is-ancestor rc=0)"
module: tests/test_merge_verify.py
expected: PASS
- id: TC-02
type: unit
description: "verify_merged_to_main возвращает True, когда PR.merged==true (Gitea mock), даже если git-проверка недоступна"
module: tests/test_merge_verify.py
expected: PASS
- id: TC-03
type: unit
description: "verify_merged_to_main возвращает False, когда SHA не предок origin/main И PR.merged==false (фантом)"
module: tests/test_merge_verify.py
expected: PASS
- id: TC-04
type: unit
description: "never-raise (AC-7): ошибка git/HTTP в verify -> False (не подтверждено), исключение не пробрасывается"
module: tests/test_merge_verify.py
expected: PASS
# --- FR-3 / G2 / AC-2: done только при подтверждённом merge ---
- id: TC-05
type: integration
description: "Phase C finalizer: deploy_status=SUCCESS но PR open -> задача НЕ переходит в done, шлётся alert 'deploy succeeded but not merged'"
module: tests/test_deploy_finalizer_merge_gate.py
expected: PASS
- id: TC-06
type: integration
description: "Phase C finalizer: deploy_status=SUCCESS и merge подтверждён -> задача переходит в done (happy-path, AC-4)"
module: tests/test_deploy_finalizer_merge_gate.py
expected: PASS
# --- FR-1 / AC-9: детерминированный merge-актор + идемпотентность ---
- id: TC-07
type: unit
description: "merge-актор self-hosting вызывает Gitea POST /pulls/{index}/merge, когда PR не слит; никакого push/force-push в main"
module: tests/test_merge_actor.py
expected: PASS
- id: TC-08
type: unit
description: "идемпотентность (AC-9): pr_already_merged==True -> merge-актор no-op (нет второго merge, нет ошибки Gitea)"
module: tests/test_merge_actor.py
expected: PASS
- id: TC-09
type: unit
description: "merge-актор never-raise: ошибка Gitea API -> (False, reason), исключение не пробрасывается"
module: tests/test_merge_actor.py
expected: PASS
# --- FR-1 G3 / AC-3: merge переживает рестарт ---
- id: TC-10
type: integration
description: "smoke (AC-3): симуляция смерти процесса во время Phase B -> re-drive finalizer/merge-job докатывает merge после 'рестарта', main получает commit, verify зелёная -> done"
module: tests/test_deploy_restart_merge_recovery.py
expected: PASS
# --- FR-5 / AC-10: условность раската ---
- id: TC-11
type: unit
description: "AC-4b: для non-self репо (enduro-trails) новая merge/verify-логика = no-op (merge остаётся за агентом deployer)"
module: tests/test_merge_verify.py
expected: PASS
- id: TC-12
type: unit
description: "AC-10: kill-switch выключен -> строго прежнее поведение (verify/merge не выполняются)"
module: tests/test_merge_verify.py
expected: PASS
# --- INV-2 / AC-8: self-hosting safety ---
- id: TC-13
type: unit
description: "AC-8: путь merge/verify не вызывает рестарт прод-контейнера и не делает прямой/force push в main (проверка отсутствия соответствующих вызовов)"
module: tests/test_merge_actor.py
expected: PASS
# --- INV-3 / AC-11: ручной approve сохранён ---
- id: TC-14
type: integration
description: "AC-11: Phase B запускается только при confirm_deploy=True ('Confirm Deploy'); merge/verify не вводят авто-деплой (обычный Approved -> no-op)"
module: tests/test_deploy_finalizer_merge_gate.py
expected: PASS
# --- Регресс существующих контрактов ---
- id: TC-15
type: unit
description: "регресс: check_deploy_status / _parse_deploy_status неизменны (читают только deploy_status: frontmatter)"
module: tests/test_qg_checks.py
expected: PASS
- id: TC-16
type: unit
description: "регресс: STAGE_TRANSITIONS и реестр QG_CHECKS не сломаны (deploy->done ребро на месте)"
module: tests/test_stages.py
expected: PASS