diff --git a/docs/work-items/ORCH-082/01-brd.md b/docs/work-items/ORCH-082/01-brd.md new file mode 100644 index 0000000..3cf4288 --- /dev/null +++ b/docs/work-items/ORCH-082/01-brd.md @@ -0,0 +1,119 @@ +# 01 — BRD: ORCH-082 (ORCH-81) + +**Конвейер не создаёт PR для ветки → деплой стопорится на merge-verify (HOLD)** + +- Work Item: **ORCH-082** (Plane-заголовок «ORCH-81») +- Repo: `orchestrator` (self-hosting) +- Тип: **Багфикс / надёжность конвейера** +- Приоритет: **HIGH** — блокирует автономный деплой +- Зона: создание PR (reviewer/developer/deployer пути), `src/merge_gate.py`, `src/stage_engine.py` (`_handle_merge_verify`), `src/agents/launcher.py` (`_ensure_pr`) + +--- + +## 1. Контекст и проблема + +При деплое **ORCH-074** (08.06, статус «Confirm Deploy») детерминированный finalizer +(`run_deploy_finalizer` → под-гейт `_handle_merge_verify`) вызвал +`merge_gate.merge_pr(repo, branch)` и получил **`ok=False` («no open PR»)**: в Gitea для +ветки `feature/ORCH-074-…` **не существовало открытого PR** с `head.ref==branch` и +`base.ref=="main"`. + +Защита **ORCH-073** (fail-closed по «SHA-в-main») отработала **корректно**: задача удержана +на стадии `deploy` (НЕ `done`), Plane → Blocked, Telegram-alert, ложно-зелёного `done` не +произошло. Это **правильное** поведение для случая «merge реально невозможен». + +**Дефект не в защите, а в инварианте до неё:** автономный конвейер **не гарантировал**, что к +моменту merge у ветки существует открытый PR. PR на сегодня создаётся ровно в одном месте — +`launcher._ensure_pr`, вызываемом **только** на пути `agent == "developer"` и **только** когда +в этом конкретном run был непустой git-diff, успешный commit и успешный push (см. root-cause +ниже). Любой сценарий, где developer-run не произвёл свежий коммит, оставляет ветку **без PR**, +и задача неминуемо застревает на merge-verify. + +### Workaround, применённый вручную (НЕ фикс) +PR #79 создан вручную через Gitea API (`mergeable=True`) → штатно перезапущен +`run_deploy_finalizer` → `merge_pr` честно влил код в `main` → задача `done`. Это разовое ручное +вмешательство, **не** устранение причины. + +### Почему это системный пробел, а не разовый сбой +Так как создание PR **не гарантировано конвейером**, любая следующая задача с тем же стечением +обстоятельств (developer-run без нового коммита; тихо упавший вызов создания PR; ветка +восстановлена/пересоздана вручную) застрянет на merge-verify тем же образом. Автономность +деплоя (цель ORCH-54) этим заблокирована. + +--- + +## 2. Root cause (предварительный аудит кода — подтвердить логами G1) + +PR создаётся **исключительно** функцией `AgentLauncher._ensure_pr` (`src/agents/launcher.py`), +которая вызывается из `_monitor_agent` по цепочке условий: + +``` +exit_code == 0 + → есть worktree-изменения (git status --porcelain непусто) + → git commit succeeded + → git push succeeded + → agent == "developer" ←── ТОЛЬКО здесь вызывается self._ensure_pr(...) +``` + +Отсюда минимум три структурных способа остаться без PR: + +- **R-A (условное создание).** Если developer-run завершился без изменений (`git status` + пустой) — ветка уже была закоммичена/запушена в прошлый run, бойнс REQUEST_CHANGES без новых + правок, повторный прогон, или ручное восстановление ветки — `_ensure_pr` **не вызывается + вовсе**. PR не появится никогда. (Соответствует гипотезе ТЗ №2.) +- **R-B (тихий сбой создания).** `_ensure_pr` ловит любое исключение + (`except Exception → logger.error → return None`): транзиентная ошибка Gitea на шаге + `POST …/pulls` теряется без ретрая и без эскалации. Конвейер «думает», что developer + отработал, и едет дальше. (Гипотеза ТЗ №1 — silent fail.) +- **R-C (разъехавшееся состояние ветки/PR).** ORCH-074 — первая задача после серии ручных + восстановлений `main` 08.06. PR мог быть закрыт/пересоздан, либо у ветки остался только + авто-docs-PR (`base != main`), который `merge_pr`/`pr_already_merged` корректно НЕ считают + кодовым PR. (Гипотеза ТЗ №4.) + +Идемпотентность (гипотеза №3): сам `_ensure_pr` идемпотентен на чтении (сначала `GET …open&head`, +создаёт только если пусто), но он не запускается вне «свежий developer-коммит», поэтому +идемпотентность не достигает merge-стадии — никакой флаг «PR создан» в БД не хранится. + +**Вывод:** гарантия «к моменту merge у ветки есть открытый код-PR» в конвейере **отсутствует**. + +--- + +## 3. Бизнес-цели + +| ID | Цель | +|----|------| +| **G1** | Установить и задокументировать точную причину отсутствия PR на ORCH-074 (код-аудит + логи run_id 396/398). | +| **G2** | Гарантировать инвариант: к моменту merge-verify у ветки **есть** открытый код-PR; если его нет — finalizer/deployer создаёт его сам, **идемпотентно**, ПЕРЕД `merge_pr`, вместо HOLD на ручное вмешательство. | +| **G3** | Явно логировать факт PR: **PR-created / PR-existed / PR-create-failed** (наблюдаемость). | + +## 4. Не-цели (явные границы) + +- НЕ ослаблять защиту ORCH-073: fail-closed по «SHA-в-main» остаётся. Реальная невозможность + merge → по-прежнему HOLD + alert. +- НЕ авто-мержить без PR (PR — обязательный артефакт ревью/слияния). +- НЕ создавать PR в неподходящий момент — только на ребре `deploy → done`, ПОСЛЕ прохождения + всех гейтов (security/merge-gate/staging/image-freshness уже пройдены). +- НЕ менять `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, схему БД, контракты `check_deploy_status`, + exit-коды хука. + +## 5. Заинтересованные стороны +- **Owner** (homenet542) — автономность деплоя орка. +- Все проекты на инстансе (enduro-trails) — общий прод/очередь: ложный HOLD self-задачи не + должен требовать ручного вмешательства, а реальный дефект merge — обязан удерживаться. + +## 6. Бизнес-риски и допущения +- **Грабли (из ORCH-073):** у ветки может быть несколько PR (код-PR + авто docs-PR). Создание/ + выбор PR обязан фильтровать `head.ref==branch` И `base.ref=="main"`, иначе слияние/верификация + схватят не тот PR. +- **Допущение:** merge-verify исполняется ПОСЛЕ всех гейтов, поэтому создание PR именно здесь не + обходит ревью и безопасно по времени. +- **Контракт надёжности:** весь новый путь — **never-raise**; ошибка создания PR (Gitea + недоступна) → честный HOLD + alert, а не исключение в `advance_stage`. + +## 7. Definition of Done (бизнес-уровень) +1. Root cause задокументирован (`06-adr/` архитектором, ссылка из ADR на этот BRD). +2. После фикса задача с веткой без PR не зависает: конвейер создаёт PR идемпотентно и доводит до + `done` (при честном merge). +3. Защита ORCH-073 цела (регресс-тест на «код не в main» → HOLD). +4. Логи различают created/existed/failed. +5. `pytest` зелёный; never-raise соблюдён. diff --git a/docs/work-items/ORCH-082/02-trz.md b/docs/work-items/ORCH-082/02-trz.md new file mode 100644 index 0000000..cf6c9c9 --- /dev/null +++ b/docs/work-items/ORCH-082/02-trz.md @@ -0,0 +1,108 @@ +# 02 — ТЗ: ORCH-082 (ORCH-81) + +**Гарантированный идемпотентный код-PR перед merge-verify + наблюдаемость** + +> Машина стадий, реестр `QG_CHECKS`, схема БД, exit-коды хука, контракты +> `check_deploy_status`/`_parse_deploy_status`, защита ORCH-073 (SHA-в-main) — **НЕ меняются**. +> Изменение — точечная врезка «ensure PR» в под-гейт merge-verify + новый идемпотентный +> PR-актор в `merge_gate` + структурное логирование. + +--- + +## 1. Задействованные модули `src/` + +| Модуль | Роль в задаче | Характер изменения | +|--------|---------------|--------------------| +| `src/merge_gate.py` | leaf-логика merge-актора (`merge_pr`, `verify_merged_to_main`, `pr_already_merged`) | **+ новый идемпотентный актор** `ensure_open_pr(repo, branch) -> (status, detail)` (never-raise). | +| `src/stage_engine.py` | под-гейт `_handle_merge_verify` на ребре `deploy → done` | **врезка:** вызвать `ensure_open_pr` ПЕРЕД `merge_pr`; на `failed` → честный HOLD+alert; логировать исход. | +| `src/agents/launcher.py` | `_ensure_pr` (текущий единственный создатель PR) | **усилить наблюдаемость** (различать created/existed/failed) — опционально переиспользовать новый актор `merge_gate.ensure_open_pr`, чтобы создание PR было единым кодом. Поведение «создавать только у developer» НЕ ужесточать без необходимости. | +| `src/config.py` | флаги | **+ kill-switch** `merge_verify_autocreate_pr_enabled` (дефолт `True`), область — та же `merge_verify_applies` (self-hosting / `merge_verify_repos`). | +| `docs/architecture/README.md`, `CHANGELOG.md` | golden source | обновить (раздел ORCH-071/073 merge-verify — дописать про авто-создание PR). | + +> Точная сигнатура `ensure_open_pr`, имя/дефолт kill-switch и место врезки — за архитектором +> (ADR). Ниже — функциональные требования к поведению, не финальный дизайн. + +## 2. Функциональные требования + +### FR-1 — Идемпотентный PR-актор `merge_gate.ensure_open_pr(repo, branch)` +Возвращает структурированный исход (например `("existed"|"created"|"failed", detail)`): +1. `GET …/pulls?state=open` → если есть PR с **`head.ref==branch` И `base.ref=="main"`** → + `("existed", )`. **Фильтр идентичен `merge_pr`/ORCH-073 FR-3** — авто-docs-PR + (`base != main`) НЕ считается код-PR. +2. Иначе `POST …/pulls` (`head=branch`, `base=main`, заголовок/тело — авто) → `201` → + `("created", )`. +3. Идемпотентность: если параллельно PR уже создан и Gitea вернёт ошибку «PR exists» — + повторный `GET` подтверждает существующий PR и возвращает `("existed", …)`, **дубль не + плодится** (AC-2). +4. Любая иная ошибка HTTP/parse/сети → `("failed", )`. **Never-raise.** + +### FR-2 — Врезка в `_handle_merge_verify` (ребро `deploy → done`) +Внутри существующего `_handle_merge_verify`, ПОСЛЕ `merge_verify_applies(repo)`-гейта и +резолва `validated_revision`, но **ПЕРЕД** `merge_pr`: +- если `merge_verify_autocreate_pr_enabled` → вызвать `ensure_open_pr(repo, branch)`; +- `status == "created"|"existed"` → продолжить штатно к `merge_pr` → `verify_merged_to_main`; +- `status == "failed"` → **честный HOLD + alert** (как сегодняшний not-merged путь: + `note_not_merged_alert` + `set_issue_blocked` + Plane-коммент + Telegram; задача остаётся на + `deploy`, НЕ `done`, БЕЗ отката на development) с сообщением, отражающим «PR создать не + удалось» (а не «PR не влит»). +- kill-switch off → текущее поведение 1:1 (никакого создания PR). + +### FR-3 — Защита ORCH-073 цела (регресс-инвариант) +Создание PR **не подменяет** проверку слияния. После `ensure_open_pr` + `merge_pr` верификация +остаётся **только** `verify_merged_to_main` (SHA-в-main, ORCH-073 FR-1) + регресс-гард +(`check_main_regression`). Если код реально не оказался в `main` — HOLD сохраняется. Создание PR +лишь устраняет **ложный** HOLD «no open PR», который конвейер обязан был предотвратить. + +### FR-4 — Наблюдаемость (G3) +В лог писать однозначный исход на каждом из мест работы с PR: +- `merge-verify ensure_open_pr -> created PR #N` / +- `… -> existed PR #N` / +- `… -> failed: `. +Сообщение HOLD при `failed` обязано отличаться текстом от HOLD «not merged» (оператор должен +видеть, что причина — невозможность создать PR, а не невозможность слить уже созданный). +Желательно — пометка исхода в `14-deploy-log.md` (best-effort, frontmatter `deploy_status:` +нетронут). + +### FR-5 — Идемпотентность повторного прохода +Повторный заход в merge-verify (reaper / reconciler / повторный approve) при уже существующем +PR → `ensure_open_pr` возвращает `("existed", …)`, `merge_pr` → `already-merged`/штатно — **без +дублей PR и без побочных эффектов** (INV-5/AC-9 ORCH-073 сохранены). + +## 3. Изменения API (HTTP / внутренние) +- **Внешний HTTP API сервиса — без изменений** (новых endpoint нет). +- **Исходящие вызовы Gitea:** новый `POST /api/v1/repos/{owner}/{repo}/pulls` из контекста + merge-verify (тот же вызов, что уже делает `_ensure_pr`); чтение — существующий + `GET …/pulls?state=open`. +- **Внутренний контракт `merge_gate`:** новая публичная функция `ensure_open_pr` (leaf, + never-raise), вызывается из `stage_engine._handle_merge_verify` (и опционально из + `launcher._ensure_pr`). + +## 4. Изменения схемы БД +**Нет.** Состояние идемпотентности выводится из самого Gitea (наличие открытого PR), миграции +не требуются. (Согласуется с restart-safe-моделью merge-verify.) + +## 5. Требования к новым QG checks +**Новых зарегистрированных QG-checks нет.** Это под-гейт-врезка в `advance_stage` +(`_handle_merge_verify`), как и сам ORCH-071 merge-verify — не отдельный `QG_CHECKS`-элемент. +Реестр `QG_CHECKS` не трогается. + +## 6. Конфигурация / kill-switch +- `merge_verify_autocreate_pr_enabled: bool = True` (env `ORCH_MERGE_VERIFY_AUTOCREATE_PR_ENABLED`). + `False` → ровно прежнее поведение (нет авто-создания PR; «no open PR» → HOLD как раньше). +- Область действия — `merge_gate.merge_verify_applies(repo)`: реально только для self-hosting / + `merge_verify_repos`; прочие репо — no-op. + +## 7. Артефакты pipeline (создать/обновить) +- `docs/work-items/ORCH-082/06-adr/ADR-001-*.md` — архитектор (root cause G1 + дизайн ensure-PR). +- `12-review.md`, `13-test-report.md`, `14/15/16-*` — последующие стадии. +- Обновить `docs/architecture/README.md` (блок ORCH-071/073) и `CHANGELOG.md` — в ТОМ ЖЕ PR + (правило агентов №2/№6). + +## 8. Инварианты (не нарушать) +- `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`/`_parse_deploy_status`, + exit-коды хука, terminal-sync, merge-gate (ORCH-043), image-freshness (ORCH-058) — **без + изменений**. +- Контракт **never-raise** на всём пути merge-verify (INV-1 ORCH-073). +- Слияние только через PR (`POST /pulls/{index}/merge`); `main` никогда не push/force-push. +- Защита ORCH-073 (SHA-в-main + регресс-гард) приоритетна: при конфликте «создать PR» проигрывает + «не дать ложно-зелёный done». diff --git a/docs/work-items/ORCH-082/03-acceptance-criteria.md b/docs/work-items/ORCH-082/03-acceptance-criteria.md new file mode 100644 index 0000000..909f50b --- /dev/null +++ b/docs/work-items/ORCH-082/03-acceptance-criteria.md @@ -0,0 +1,69 @@ +# 03 — Критерии приёмки: ORCH-082 (ORCH-81) + +Каждый критерий — однозначное условие PASS/FAIL. Машинные вердикты гейтов — только из +YAML-frontmatter. + +--- + +### AC-1 — Root cause задокументирован +- **PASS:** в `06-adr/ADR-001-*.md` зафиксировано, **почему** PR не создался на ORCH-074 + (со ссылкой на код-путь `launcher._ensure_pr` и/или логи run_id 396/398), и какая из гипотез + R-A/R-B/R-C подтвердилась. +- **FAIL:** причина не названа / только догадка без привязки к коду или логам. + +### AC-2 — Гарантированный идемпотентный код-PR к merge-verify +- **PASS:** к моменту merge-verify у ветки гарантированно существует открытый PR с + `head.ref==branch` И `base.ref=="main"`; повторный вызов авто-создания при уже существующем PR + **не плодит дубль** (возвращает existed). +- **FAIL:** при отсутствии PR задача сразу уходит в HOLD; ИЛИ повторный проход создаёт второй PR. + +### AC-3 — Авто-создание PR ПЕРЕД merge_pr (вместо немедленного HOLD) +- **PASS:** при физическом отсутствии открытого код-PR `_handle_merge_verify` сначала создаёт PR + (`ensure_open_pr → created`), затем выполняет `merge_pr` → `verify_merged_to_main`; ложного + HOLD «no open PR» не возникает. +- **FAIL:** «no open PR» по-прежнему приводит к HOLD без попытки создать PR (при включённом + kill-switch). + +### AC-4 — Защита ORCH-073 цела (регресс) +- **PASS:** при реальном «код не в `main`» (`verify_merged_to_main → False`) — по-прежнему HOLD + + alert + `set_issue_blocked`, задача НЕ `done`, БЕЗ авто-отката на development. Регресс-гард + `check_main_regression` не ослаблен. +- **FAIL:** создание PR маскирует невлитый код и пропускает задачу в `done`; ИЛИ ослаблен + SHA-в-main / регресс-гард. + +### AC-5 — Логи различают исход PR +- **PASS:** в логах присутствует ровно один однозначный исход на проход: **PR-created** / + **PR-existed** / **PR-create-failed**; HOLD по «create-failed» текстуально отличим от HOLD + «not merged». +- **FAIL:** исход не логируется или created/existed/failed неразличимы. + +### AC-6 — Грабли мультиPR: фильтр base==main +- **PASS:** при наличии у ветки авто-docs-PR (`base != main`) актор НЕ принимает его за код-PR и + создаёт/выбирает именно PR на `main`. +- **FAIL:** docs-PR трактуется как код-PR (слияние/верификация работают не с тем PR). + +### AC-7 — Never-raise + честный HOLD при недоступности Gitea +- **PASS:** при ошибке создания PR (Gitea недоступна/HTTP-ошибка) `ensure_open_pr` возвращает + `failed`, путь merge-verify даёт честный HOLD+alert, исключение НЕ всплывает в `advance_stage`. +- **FAIL:** исключение пробрасывается / процесс падает / задача молча уходит в `done`. + +### AC-8 — Kill-switch off → прежнее поведение 1:1 +- **PASS:** при `merge_verify_autocreate_pr_enabled=False` авто-создание не выполняется; «no open + PR» → HOLD как до фикса (поведение ORCH-074 воспроизводится). +- **FAIL:** при выключенном флаге PR всё равно создаётся. + +### AC-9 — Условность (область self-hosting) +- **PASS:** для не-self репозиториев (`merge_verify_applies → False`) врезка — no-op; создание PR + остаётся за прежним механизмом. +- **FAIL:** авто-создание срабатывает для чужих репо. + +### AC-10 — Инварианты не нарушены +- **PASS:** `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД, `check_deploy_status`, exit-коды хука, + merge-gate/image-freshness — без изменений; `main` не push/force-push; документация + (`README.md`, `CHANGELOG.md`) обновлена в этом же PR. +- **FAIL:** затронут любой из перечисленных инвариантов / документация не обновлена. + +### AC-11 — pytest зелёный +- **PASS:** `pytest tests/ -q` зелёный, включая новые тесты из `04-test-plan.yaml` и + существующие `test_merge_verify*.py` / `test_orch073_*` / `test_merge_actor.py`. +- **FAIL:** любой тест падает. diff --git a/docs/work-items/ORCH-082/04-test-plan.yaml b/docs/work-items/ORCH-082/04-test-plan.yaml new file mode 100644 index 0000000..8c6e315 --- /dev/null +++ b/docs/work-items/ORCH-082/04-test-plan.yaml @@ -0,0 +1,90 @@ +work_item: ORCH-082 +title: "Гарантированный идемпотентный код-PR перед merge-verify (фикс ложного HOLD)" +strategy: > + Юнит-тесты на новый идемпотентный актор merge_gate.ensure_open_pr (мок Gitea HTTP) + и интеграционные тесты на врезку в stage_engine._handle_merge_verify (мок merge_gate + + verify), включая регресс ORCH-073. Все пути — never-raise. Gitea и git мокаются, + сеть не дёргается. + +tests: + # ---- ensure_open_pr: идемпотентный PR-актор (FR-1) ---- + - id: TC-01 + type: unit + description: "ensure_open_pr: открытого код-PR нет -> POST создаёт PR -> ('created', N); фильтр base==main применён" + module: tests/test_orch082_ensure_pr.py + expected: PASS + + - id: TC-02 + type: unit + description: "ensure_open_pr: открытый PR head==branch И base==main уже есть -> ('existed', N), POST не вызывается (нет дубля)" + module: tests/test_orch082_ensure_pr.py + expected: PASS + + - id: TC-03 + type: unit + description: "Грабли мультиPR: у ветки только docs-PR (base!=main) -> он НЕ считается код-PR -> создаётся PR на main (AC-6)" + module: tests/test_orch082_ensure_pr.py + expected: PASS + + - id: TC-04 + type: unit + description: "ensure_open_pr never-raise: Gitea POST/GET кидает HTTP/timeout -> ('failed', reason), исключение не всплывает (AC-7)" + module: tests/test_orch082_ensure_pr.py + expected: PASS + + - id: TC-05 + type: unit + description: "Идемпотентность гонки: POST вернул 'PR exists' -> повторный GET подтверждает существующий -> ('existed', N), дубль не создан" + module: tests/test_orch082_ensure_pr.py + expected: PASS + + # ---- _handle_merge_verify: врезка ensure-PR (FR-2/FR-3) ---- + - id: TC-06 + type: integration + description: "merge-verify: PR отсутствовал -> ensure_open_pr создаёт -> merge_pr -> verify True -> deploy->done БЕЗ ложного HOLD (AC-3)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + - id: TC-07 + type: integration + description: "Регресс ORCH-073: PR создан/влит, но verify_merged_to_main=False (код не в main) -> HOLD + set_issue_blocked, НЕ done, без отката (AC-4)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + - id: TC-08 + type: integration + description: "ensure_open_pr -> 'failed' (Gitea down) -> честный HOLD+alert, текст отличается от 'not merged', advance_stage не падает (AC-7)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + - id: TC-09 + type: integration + description: "Kill-switch merge_verify_autocreate_pr_enabled=False -> ensure_open_pr не вызывается, 'no open PR' -> прежний HOLD 1:1 (AC-8)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + - id: TC-10 + type: integration + description: "Условность: non-self репо (merge_verify_applies=False) -> врезка no-op, авто-создание не выполняется (AC-9)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + - id: TC-11 + type: integration + description: "Идемпотентный повторный проход (reaper/reconciler): PR уже existed, merge_pr=already-merged -> verify True -> done, без дублей PR (AC-2/FR-5)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + # ---- Наблюдаемость (G3 / AC-5) ---- + - id: TC-12 + type: unit + description: "Логи различают created/existed/failed; HOLD-сообщение create-failed != HOLD-сообщение not-merged (caplog, AC-5)" + module: tests/test_orch082_merge_verify_autocreate.py + expected: PASS + + # ---- Регресс существующего merge-verify контракта ---- + - id: TC-13 + type: integration + description: "Happy-path ORCH-071/073 не изменён: merge_pr ok + verify True + регресс-гард ok -> done, merged_to_main: true во frontmatter" + module: tests/test_merge_verify.py + expected: PASS