From ad1589084be5ef796371b77d109ec7118a4512a2 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sat, 6 Jun 2026 17:16:00 +0000 Subject: [PATCH] architect(ET): auto-commit from architect run_id=183 --- docs/architecture/README.md | 16 +- docs/architecture/adr/README.md | 1 + docs/architecture/adr/adr-0006-merge-gate.md | 53 ++++ .../ORCH-043/06-adr/ADR-001-merge-gate.md | 235 ++++++++++++++++++ .../ORCH-043/07-infra-requirements.md | 25 ++ .../ORCH-043/08-data-requirements.md | 27 ++ docs/work-items/ORCH-043/10-tech-risks.md | 24 ++ 7 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 docs/architecture/adr/adr-0006-merge-gate.md create mode 100644 docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md create mode 100644 docs/work-items/ORCH-043/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-043/08-data-requirements.md create mode 100644 docs/work-items/ORCH-043/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9fdfe85..27710b4 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -34,17 +34,29 @@ created → analysis → architecture → development → review → testing → | deploy | — | `check_deploy_status` | 14-deploy-log.md (`deploy_status:`) | | done | — | — | — | -**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status. +**Реестр QG** (`QG_CHECKS`): check_analysis_approved, check_analysis_complete, check_architecture_done, check_ci_green, check_review_approved, check_tests_passed, check_reviewer_verdict, check_tests_local, check_deploy_status, check_staging_status, check_branch_mergeable (ORCH-043). **Канон гейтов:** машинные вердикты читаются ТОЛЬКО из YAML-frontmatter, никогда из прозы. Лог-файлы мержатся в `origin/main` отдельным PR; гейт читает из `origin/main`. ### Условный staging-гейт (ORCH-35) `check_staging_status` реален только для self-hosting (`is_self_hosting_repo(repo)` → `orchestrator`); для остальных проектов → no-op `(True, "Staging gate N/A")`. Для orchestrator парсит `staging_status:` из `15-staging-log.md`; FAILED → откат на `development`. Подробнее: [ADR-0003](adr/adr-0003-staging-gate.md). +### Merge-gate: догон `main` + re-test + сериализация слияний (ORCH-043) +Детерминированный под-гейт (`check_branch_mergeable`, без LLM) на ребре **`deploy-staging → deploy`**: исполняется ПОСЛЕ `check_staging_status` и ДО запуска deployer'а, который вливает PR в `main` (deployer мержит в начале стадии `deploy`). Стадии (`STAGE_TRANSITIONS`) НЕ меняются — это «под-гейт» ребра, а не отдельная стадия (триггер — то же событие «staging-deployer завершился»). + +Назначение: ветка валидируется относительно того `main`, из которого создана; параллельная задача могла уйти вперёд → семантический конфликт слияния (зелёная ветка ломает обновлённый `main`). Merge-gate гарантирует проверку против **актуального** `origin/main` перед слиянием: +- **Догон:** ветка отстаёт (⇔ `origin/main` не предок HEAD) → `rebase origin/main` в worktree + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → `rebase --abort` → откат на `development`. +- **Re-test:** `python -m pytest` (`merge_retest_target`, дефолт `tests/`) в worktree догнанной ветки, тайм-аут `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`. +- **Сериализация (merge-lock):** файловый **merge-lease** на репо (`/.merge-lease-.json`), живёт от гейта до фактического merge. Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** (повторная постановка deployer'а на `deploy-staging` с задержкой через `available_at`), а не откат. Release — на PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe; без изменения схемы БД. +- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги `merge_gate_enabled` / `merge_gate_repos` — поэтапный раскат. Контракт **never-raise**. + +Подробнее: [adr-0006](adr/adr-0006-merge-gate.md), детально — `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md`. + ## Откаты - Reviewer REQUEST_CHANGES → откат на `development` + retry (`MAX_DEVELOPER_RETRIES = 3`). - Tester `check_tests_passed` FAIL → откат на `development` + retry. - Deploy / deploy-staging FAILED → откат на `development`. +- Merge-gate FAIL (конфликт rebase / красный re-test, ORCH-043) → откат на `development` + retry; `merge-lock busy` → **defer** (не откат, dev-retry не тратится). - `get_previous_stage` использует порядок ключей `STAGE_TRANSITIONS`. ### Обогащение `task_desc` при заворотах (ORCH-046) @@ -97,4 +109,4 @@ created → analysis → architecture → development → review → testing → Схема БД, потоки данных, resilience-слой, детали Dockerfile — [internals.md](internals.md). --- -*Актуально на 2026-06-05 (main `f1b3146`). Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py.* +*Актуально на 2026-06-06. Обновлять при изменении src/stages.py, src/qg/checks.py, src/main.py. ORCH-043: merge-gate — design (см. adr-0006), реализация в ветке feature/ORCH-043.* diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index f4874c4..0a4c74a 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -10,6 +10,7 @@ Per-work-item решения живут в `docs/work-items//06-adr/ADR-NNN- | adr-0003 | Условный staging-гейт перед прод-деплоем | accepted | 2026-06-05 | ORCH-35 | | adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 | | adr-0005 | Контейнеры бегут под uid:gid хоста (1000:1000) | accepted | 2026-06-06 | ORCH-040 | +| adr-0006 | Merge-gate (догон main + re-test + сериализация слияний) | proposed | 2026-06-06 | ORCH-043 | ## Формат **Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded. diff --git a/docs/architecture/adr/adr-0006-merge-gate.md b/docs/architecture/adr/adr-0006-merge-gate.md new file mode 100644 index 0000000..c977446 --- /dev/null +++ b/docs/architecture/adr/adr-0006-merge-gate.md @@ -0,0 +1,53 @@ +# adr-0006: Merge-gate — догон `main` + re-test + сериализация слияний + +- **Статус:** proposed +- **Дата:** 2026-06-06 +- **Задача:** ORCH-043 +- **Детальный ADR:** `docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md` + +## Контекст +Ветка валидируется относительно того `main`, из которого создана, а не относительно `main` +на момент слияния. Параллельная задача могла влиться раньше → **семантический конфликт +слияния** (git мержит без текстового конфликта, но `main` сломан). Для self-hosting это +красный `main` инструмента, обслуживающего все проекты. Слияние в `main` делает +deployer-агент в начале стадии `deploy`; замена механизма PR-merge — вне объёма. + +## Решение +Детерминированный merge-gate (`check_branch_mergeable`, без LLM) на ребре +`deploy-staging → deploy`, ДО запуска deployer'а, который мержит. `STAGE_TRANSITIONS` не +меняется (минимальный blast-radius); в `QG_CHECKS` добавлен `check_branch_mergeable`. + +- **Догон:** ветка отстаёт ⇔ `origin/main` не предок HEAD → `rebase origin/main` в worktree + + `push --force-with-lease` (ТОЛЬКО ветка задачи; `main` — никогда). Текстовый конфликт → + `rebase --abort` → откат на `development`. +- **Re-test:** `python -m pytest tests/` в worktree догнанной ветки, тайм-аут + `merge_retest_timeout_s`. Красный/тайм-аут → откат на `development`. +- **Сериализация (BR-5):** файловый **merge-lease** на репо + (`/.merge-lease-.json`), живёт от гейта до фактического merge. + Acquire **неблокирующий** (anti-deadlock при `max_concurrency=1`): busy → **defer** + (re-enqueue deployer с задержкой через `available_at`), не rollback. Release — на + PR-merged вебхуке / `deploy→done` / откате / по возрасту (crash-реклейм). Restart-safe. +- **Условность (как ORCH-35):** реален для `orchestrator`; прочие репо — no-op. Флаги + `merge_gate_enabled` / `merge_gate_repos` для поэтапного раската. + +## Альтернативы +- **Новая стадия `merge-gate`** (кандидат B) — «пустая» стадия без агента не имеет триггера + (`advance_stage` срабатывает только на завершении агента/вебхуке); потребовала бы chaining + в движке (не restart-safe) или синтетический job-тип. Отклонено. +- **Перенос merge в детерминированный шаг оркестратора** (кандидат C) — запрещён объёмом + (замена механизма PR-merge вне scope). Отклонено. +- **Блокирующий lock** — дедлок при одном worker-слоте. Отклонено в пользу defer. + +## Последствия +- Сценарий «две зелёные ветки ломают `main`» закрыт: re-test против актуального `main` + + сериализация слияний. +- Плата: merge-gate — «скрытый» под-гейт ребра (нет в `STAGE_TRANSITIONS`); сериализация + опирается на PR-merged вебхук со страховкой реклеймом по возрасту; defer перепрогоняет + staging; длинный re-test держит worker-слот. +- Сквозное изменение конвейера → `arch:major-change`; прод-деплой ORCH-043 строго через + staging-гейт (8501). + +## Связи +adr-0001 (`is_self_hosting_repo`), adr-0003 (условный staging-гейт — образец условности), +adr-0002 (очередь / `available_at` для defer), ORCH-2 (worktree-изоляция), ORCH-046 +(дословный reason в `task_desc` при откате). diff --git a/docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md b/docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md new file mode 100644 index 0000000..ac1e49b --- /dev/null +++ b/docs/work-items/ORCH-043/06-adr/ADR-001-merge-gate.md @@ -0,0 +1,235 @@ +# ADR-001: Merge-gate + auto-rebase + re-test (безопасная параллель в одном репо) + +## Статус +Proposed + +> Решение архитектора по ТЗ ORCH-043 (`02-trz.md`). Реализует BR-1..BR-8, удовлетворяет +> AC-1..AC-15. Глобальный сквозной аналог — `docs/architecture/adr/adr-0006-merge-gate.md`. + +--- + +## Контекст + +Конвейер валидирует ветку относительно того `main`, из которого она была создана, а не +относительно `main` на момент слияния. Между «ветка проверена» и «ветка влита» `main` мог +уйти вперёд из-за слияния другой параллельной задачи → **семантический конфликт слияния**: +git сливает без текстового конфликта, но объединённый код `main` сломан. Для self-hosting +(`orchestrator`) это = красный `main` инструмента, обслуживающего ВСЕ проекты из одного +инстанса с общей БД/очередью. + +Ключевые факты текущей архитектуры, влияющие на решение (проверено по коду): + +1. **Где происходит слияние в `main`.** Ветку в `main` вливает **deployer-агент в начале + своего запуска на стадии `deploy`** (см. `src/webhooks/gitea.py:336-353` — комментарий + «deployer merges the PR at the START of its run»). Замена самого механизма слияния PR + в Gitea — **вне объёма** (BRD §4). Значит, merge остаётся PR-merge через deployer. +2. **Как запускается deployer стадии `deploy`.** При прохождении `check_staging_status` + на стадии `deploy-staging` движок (`stage_engine.advance_stage`) переводит задачу + `deploy-staging → deploy` и запускает `get_agent_for_stage("deploy-staging") = deployer`. + Этот deployer и делает merge. Значит **merge-gate обязан отработать на ребре + `deploy-staging → deploy`, ДО запуска этого deployer'а**. +3. **Чем триггерится QG.** `advance_stage` вызывается ТОЛЬКО при (а) завершении + LLM-агента (`launcher._try_advance_stage`) или (б) приходе вебхука. **Стадия без агента + не имеет собственного триггера** (стадия `deploy` оценивается, когда заканчивает + deployer, исполняющийся ВО ВРЕМЯ неё). Поэтому новая «пустая» стадия `merge-gate` + между `deploy-staging` и `deploy` зависла бы без триггера (нужен был бы chaining в + движке либо синтетический job — лишняя и не-restart-safe поверхность). +4. **Concurrency.** `max_concurrency` по умолчанию `1`; QG исполняется в monitor-thread + агента. Блокирующее ожидание lock внутри `advance_stage` при одном worker-слоте даёт + **дедлок** (задача B держит слот, ожидая merge задачи A, которой нужен тот же слот). + Сериализация обязана быть **неблокирующей**. + +--- + +## Решение + +### 1. Место встройки — ребро `deploy-staging → deploy` (кандидат A ТЗ §6), без новой стадии + +Merge-gate — детерминированный шаг в `advance_stage`, исполняемый **после** прохождения +`check_staging_status` и **до** `update_task_stage(deploy)` / запуска deployer'а, который +мержит. `STAGE_TRANSITIONS` **не меняется** (минимальный blast-radius; `get_previous_stage` +не затрагивается; snapshot `_EXPECTED_TRANSITIONS` без изменений). В реестр `QG_CHECKS` +добавляется один ключ `check_branch_mergeable` (snapshot `_EXPECTED_QGS` обновляется +осознанно, AC-10). + +Отвергнутые варианты: +- **(B) Новая стадия `merge-gate`** — концептуально честнее, но «пустая» стадия без агента + не имеет триггера (см. Контекст §3). Потребовала бы chaining в `advance_stage` + (не restart-safe для безагентного перехода) или синтетический job-тип в очереди + (поверхность в `launcher`/`queue_worker`, который сейчас умеет только LLM-агентов). +- **(C) Перенос merge в детерминированный шаг оркестратора** — прямо запрещён объёмом + (BRD §4: «Замена механизма слияния PR в Gitea — вне объёма»). + +Триггер гейта — **существующее** событие «staging-deployer завершился» → отдельного +механизма триггера не вводим. + +### 2. Догон ветки — `rebase` onto `origin/main` + `push --force-with-lease` + +Выбор `rebase` (а не merge-commit) обусловлен критериями приёмки AC-2/AC-7, которые прямо +требуют `push --force-with-lease` догнанной ветки. Алгоритм `auto_rebase_onto_main`: + +1. `git fetch origin main` в worktree ветки (`ensure_worktree`, AC-8 — изоляция). +2. `branch_is_behind_main`: ветка отстаёт ⇔ `git merge-base --is-ancestor origin/main ` + вернул ненулевой код. Не удалось определить (git/сеть) → трактуем как «не пропускаем + вслепую» (never-raise → `(False, reason)`), НЕ как «up-to-date». +3. Не отстаёт → `(True, "branch up-to-date with main")`, rebase/push **не выполняются** (AC-1). +4. Отстаёт → `git rebase origin/main`: + - **текстовый конфликт** → `git rebase --abort`, worktree чист → `(False, "rebase conflict: <файлы>")` (AC-3); + - **чистый rebase** → `git push --force-with-lease origin ` (**ТОЛЬКО ветка задачи; НИКОГДА `main`**, AC-7) → далее re-test. +5. Контракт **never-raise**: любая git/OS-ошибка → `(False, "")` (AC-9). + +`main` гейтом не пушится и не форс-пушится никогда. Единственная force-операция — +`--force-with-lease` по ветке задачи. + +### 3. Re-test — `python -m pytest` в worktree догнанной ветки + +`retest_branch(repo, branch)`: +- Команда `python -m pytest ` (`merge_retest_target` по умолчанию + `tests/`) из корня worktree ветки — согласовано с CI orchestrator + (`pytest tests/ -q`, CLAUDE.md) и паттерном `check_tests_local`. +- Тайм-аут `settings.merge_retest_timeout_s` (дефолт 600); превышение → + `(False, "re-test timeout (s)")` (AC-6), процесс убивается, задача не виснет. +- `returncode == 0` → `(True, "re-test green")`; иначе `(False, "re-test failed after rebase: ")` (AC-4). + +> Гейт по умолчанию реален для self-hosting репо `orchestrator` (BR-7). Для других репо +> применять только при совпадающей тест-команде/раскладке — через `merge_gate_repos` +> (см. §6). Команда re-test параметризуется `merge_retest_target` для портируемости. + +### 4. Сериализация слияний — файловый merge-lease на репозиторий (BR-5, AC-5) + +Цель: «догон + re-test + **слияние**» одного репо выполняет одновременно только одна +задача. Слияние делает deployer ПОЗЖЕ и в ОТДЕЛЬНОМ запуске, поэтому простой +context-manager-lock внутри гейта окно гонки не закрывает — нужен **lease, живущий от +гейта до фактического merge**. + +**Механизм — файловый lease**, БЕЗ изменения схемы БД (ТЗ §4 предпочитает no-schema-change): +- Файл `/.merge-lease-.json`, содержимое `{task_id, work_item_id, branch, + acquired_at, pid}`. +- **Acquire — атомарный, НЕблокирующий** (`open(..., O_CREAT|O_EXCL)`): + - файла нет → захват, запись метаданных; + - файл есть, holder == self → идемпотентно «уже наш» (restart/повтор); + - файл есть, holder != self, возраст `< merge_lock_timeout_s` → **busy**; + - файл есть, возраст `>= merge_lock_timeout_s` → **stale, перезахват** с `logger.warning` + (crash-recovery: процесс-холдер умер, не освободив lease). +- **Release — идемпотентный** (`os.remove`, ignore-missing). +- **Restart-safe**: lease на диске; зависший lease реклеймится по возрасту. + +**Поведение `check_branch_mergeable(repo, work_item_id, branch)`** (детерминированно, без LLM): +1. Попытка acquire (неблокирующая). Busy → `(False, "merge-lock busy")` — **сигнальный + reason** (НЕ провал кода, см. §5: defer, а не rollback). +2. **Double-check под lease**: повторно `branch_is_behind_main` (пока ждали/между тиками + `main` мог уйти — например, другая задача только что влилась). +3. Не отстаёт → `(True, "branch up-to-date with main")`. +4. Отстаёт → `auto_rebase_onto_main`: + - конфликт → `(False, "rebase conflict: ...")`; + - успех → `retest_branch`: зелёный → `(True, "rebased onto main, re-test green")`; + красный/тайм-аут → `(False, "re-test failed after rebase: ...")`. +5. **При успехе lease НЕ освобождается** — он удерживается до фактического merge. + **При любом провале (конфликт/красный re-test) lease освобождается** (откат на + development, слияния не будет). +6. Регистрация в `QG_CHECKS["check_branch_mergeable"]`; сигнатура `(repo, work_item_id, + branch)` совпадает с дефолтной артефактной → `_run_qg` диспетчеризует без спец-кейса. + +**Жизненный цикл lease (точки release):** +- **PR-merged вебхук** ветки (`gitea.handle_pr`, `action=closed & merged`) → release; +- **`deploy → done`** в `advance_stage` (страховочный release); +- **любой откат на development** из merge-gate / `check_deploy_status` → release; +- **возраст `>= merge_lock_timeout_s`** → авто-реклейм (backstop при краше). + +### 5. Откаты и defer (интеграция в `stage_engine`, BR-4/BR-8, AC-11) + +`check_branch_mergeable` различает два негативных исхода: + +- **`reason == "merge-lock busy"` → DEFER, не rollback.** Код задачи исправен — нельзя + слать на development и нельзя тратить `MAX_DEVELOPER_RETRIES`. Движок **повторно + ставит deployer на `deploy-staging` с задержкой** `settings.merge_defer_delay_s` + (через `available_at`-гейт очереди, ORCH-1; задача остаётся на `deploy-staging`). + Неблокирующий defer освобождает worker-слот → задача-холдер успевает влиться (нет + дедлока при `max_concurrency=1`). Повторов defer — ограниченное число + (`merge_defer_max_attempts`), исчерпание → Telegram-алерт + блокировка. +- **`reason` = конфликт rebase ИЛИ красный re-test → rollback на `development`** по образцу + `check_staging_status`/`check_deploy_status` в `_handle_qg_failure_rollbacks`: + `update_task_stage(development)`, `set_issue_blocked`, дословный `reason` в Plane + (`plane_add_comment`, author="deployer"), `send_telegram`, учёт `MAX_DEVELOPER_RETRIES`, + **release lease**. Дословный `reason` встраивается в `task_desc` developer'а (по образцу + ORCH-046), чтобы агент видел суть. + +### 6. Конфигурация (`src/config.py`, env-префикс `ORCH_`) + +| Setting | Назначение | Дефолт | +|---------|-----------|--------| +| `merge_gate_enabled: bool` | Глобальный вкл/выкл (no-op `(True, "merge-gate disabled")` при False, AC-12) | `True` | +| `merge_gate_repos: str` | CSV-список репо, где гейт реален; пусто = только self-hosting (`orchestrator`) | `""` | +| `merge_retest_timeout_s: int` | Тайм-аут re-test | `600` | +| `merge_retest_target: str` | pytest-цель для re-test (портируемость) | `tests/` | +| `merge_lock_timeout_s: int` | Макс. возраст lease (ожидание/реклейм) | `300` | +| `merge_defer_delay_s: int` | Задержка перед повтором гейта при busy | `60` | +| `merge_defer_max_attempts: int` | Лимит defer-повторов до эскалации | `5` | + +Семантика `merge_gate_repos`: пусто → гейт реален ТОЛЬКО для `orchestrator` +(`is_self_hosting_repo`), для прочих — no-op `(True, "merge-gate N/A for ")` +(по образцу условного staging-гейта ORCH-35). Это безопасный поэтапный раскат. + +### 7. API +Новых HTTP-эндпоинтов нет. Допустимо (необязательно) добавить в `GET /status`/`GET /queue` +индикатор состояния merge-lease для наблюдаемости — без изменения существующих контрактов. + +--- + +## Последствия + +### Плюсы +- Закрывает воспроизводимый сценарий «две зелёные ветки ломают `main`»: перед слиянием + ветка догоняется до актуального `origin/main` и повторно тестируется; слияния + сериализуются lease'ом. +- Минимальный blast-radius: `STAGE_TRANSITIONS` не тронут, snapshot-переходы не меняются, + +1 ключ в `QG_CHECKS`. Триггер — существующее событие, без chaining/новых job-типов. +- Restart-safe и deadlock-safe: файловый lease с реклеймом по возрасту; неблокирующий + acquire + defer вместо блокирующего ожидания. +- Соответствует self-hosting-инвариантам: никогда не пуш/форс-пуш `main`; force только + `--force-with-lease` по ветке задачи; прод-контейнер не рестартится; страховка + `deploy-staging` сохранена. +- Поэтапный раскат через `merge_gate_enabled` / `merge_gate_repos`. + +### Минусы / ограничения +- **Merge-gate как «скрытый» под-гейт** ребра `deploy-staging → deploy` не отражён в + `STAGE_TRANSITIONS` (плата за отказ от новой стадии). Смягчение: явно описан в + `docs/architecture/README.md` и этом ADR. +- **Сериализация зависит от вебхука PR-merged** для своевременного release. Деградация + предусмотрена (реклейм по возрасту `merge_lock_timeout_s`), но при «потерянном» + вебхуке возможна задержка следующей задачи до тайм-аута lease. +- **Defer перезапускает staging-deployer** (повторно прогоняет staging-проверку и + перезаписывает `15-staging-log.md`) — переиспользует существующий механизм очереди + ценой лишнего прогона staging. Допустимо; альтернатива (отдельный «retry-gate» job-тип) + дороже по поверхности. +- **Длинный re-test (до 600s)** исполняется синхронно в monitor-thread staging-deployer'а + и удерживает worker-слот на это время (при `max_concurrency=1` приостанавливает прочие + задачи). Это неотъемлемая стоимость «re-test перед слиянием». +- **`rebase --force-with-lease`** переписывает историю ветки и обновляет head открытого PR; + прежний approve ревьюера может пометиться stale в Gitea. На стадии `deploy` ревью + повторно не проверяется — функционально безопасно. + +### Влияние на масштаб изменения +Вводится новый модуль (`src/merge_gate.py`), новый QG, lease-подсистема и изменение +поведения ребра `deploy-staging → deploy` + откаты/вебхук. Это **сквозное изменение +конвейера** → рекомендуется лейбл `arch:major-change` и обязательная страховка стадией +`deploy-staging` (8501) перед прод-деплоем самого ORCH-043. Глобальный ADR — +`docs/architecture/adr/adr-0006-merge-gate.md`. + +--- + +## Точки изменения кода (для developer; имена функций — финальные) +- `src/merge_gate.py` (**новый**): `branch_is_behind_main`, `auto_rebase_onto_main`, + `retest_branch`, lease (`acquire_merge_lease`/`release_merge_lease`/реклейм). +- `src/qg/checks.py`: `check_branch_mergeable(repo, work_item_id, branch)` + регистрация в `QG_CHECKS`. +- `src/stage_engine.py`: вызов merge-gate на ребре `deploy-staging → deploy` (после + `check_staging_status`, до advance); ветка rollback merge-gate в + `_handle_qg_failure_rollbacks`; defer-ветка для `"merge-lock busy"`; release lease в + `deploy → done` и в откатах. +- `src/webhooks/gitea.py`: release lease в `handle_pr` (closed & merged). +- `src/db.py` (опц.): `enqueue_job(..., available_at_delay_s=...)` для defer, либо переиспользовать `available_at`. +- `src/config.py`: настройки §6. +- `tests/`: тесты по `04-test-plan.yaml` + обновить `tests/test_qg_registry_snapshot.py` + (`_EXPECTED_QGS` += `check_branch_mergeable`; `_EXPECTED_TRANSITIONS` — **без изменений**). +- Документация: `docs/architecture/README.md` (обновлена в этом PR), `CHANGELOG.md`, + `.env.example` (новые `ORCH_*`). diff --git a/docs/work-items/ORCH-043/07-infra-requirements.md b/docs/work-items/ORCH-043/07-infra-requirements.md new file mode 100644 index 0000000..f88f4ae --- /dev/null +++ b/docs/work-items/ORCH-043/07-infra-requirements.md @@ -0,0 +1,25 @@ +# 07 — Требования к инфраструктуре (ORCH-043) + +## Вывод: топология не меняется. Новых контейнеров/портов/сервисов нет. + +| Аспект | Требование | +|--------|-----------| +| Контейнеры | Без изменений. Прод `orchestrator` (8500) и `orchestrator-staging` (8501) — как есть. | +| Порты | Без изменений. | +| Сеть/внешние сервисы | Без новых зависимостей. Используются существующие git/Gitea (fetch/push) и pytest. | +| Файловая система | Новый артефакт времени выполнения — lease-файл `/.merge-lease-.json` (см. `08-data-requirements.md`). Лежит в уже примонтированном `repos_dir` (`/repos`). Дополнительного volume не требуется. | +| Worktree | Переиспользуется существующая изоляция (`/repos/_wt//`, ORCH-2). Все git-операции merge-gate — в worktree. | +| `.env` / compose / прод-инфра | **НЕ изменяются** (AC-14). Новые `ORCH_*` настройки имеют безопасные дефолты (см. ADR-001 §6) и документируются в `.env.example`. | + +## Эксплуатационные требования +- **git push прав** для оркестратора достаточно существующих (он уже пушит ветки/PR-артефакты). + Merge-gate пушит ТОЛЬКО ветку задачи (`--force-with-lease`), `main` — никогда. +- **Раскат поэтапно**: `merge_gate_enabled=False` или пустой `merge_gate_repos` (реален + только для `orchestrator`) позволяют включать гейт постепенно без риска для чужих репо. +- **Self-hosting-страховка сохранена**: изменения ORCH-043 проходят обязательную стадию + `deploy-staging` (8501) до прод-деплоя самого оркестратора; прод-контейнер не рестартится + в рамках задачи. + +## Рекомендация по процессу +Изменение сквозное (новый QG + поведение ребра `deploy-staging → deploy`) → +рекомендуется лейбл `arch:major-change`. Прод-деплой ORCH-043 — строго через staging-гейт. diff --git a/docs/work-items/ORCH-043/08-data-requirements.md b/docs/work-items/ORCH-043/08-data-requirements.md new file mode 100644 index 0000000..0fedd03 --- /dev/null +++ b/docs/work-items/ORCH-043/08-data-requirements.md @@ -0,0 +1,27 @@ +# 08 — Требования к данным / схеме БД (ORCH-043) + +## Вывод: изменение схемы SQLite НЕ требуется. + +Merge-lease (сериализация слияний, BR-5) реализуется **файлом**, а не таблицей: + +- Путь: `/.merge-lease-.json` (`settings.repos_dir`, по умолчанию `/repos`). +- Содержимое: `{ "task_id": int, "work_item_id": str, "branch": str, + "acquired_at": "", "pid": int }`. +- Жизненный цикл — см. ADR-001 §4 (acquire неблокирующий / release идемпотентный / + реклейм по возрасту `merge_lock_timeout_s`). + +### Почему файл, а не таблица БД +- ТЗ §4 прямо предпочитает реализацию без миграции схемы. +- Файловый lease проще делается **restart-safe** (реклейм по mtime/возрасту + `pid`) и не + трогает инициализацию `src/db.py` (никаких изменений `tasks`/`agent_runs`/`jobs`/`events`). +- Атомарность захвата обеспечивается `open(O_CREAT|O_EXCL)` на одном хосте (mva154, + один инстанс) — достаточно для сериализации в пределах одного процесса-оркестратора. + +### Существующие таблицы — без изменений +`tasks`, `agent_runs`, `jobs`, `events` не модифицируются. Defer-механизм переиспользует +существующий столбец `jobs.available_at` (ORCH-1) для отложенного повторного запуска +deployer'а — **новых столбцов не нужно**. + +> Если в будущем потребуется кросс-процессная/мульти-хостовая сериализация — lease можно +> мигрировать в таблицу (или advisory-lock). Это будет отдельным ADR; в рамках ORCH-043 +> файловый lease достаточен (один хост, один инстанс). diff --git a/docs/work-items/ORCH-043/10-tech-risks.md b/docs/work-items/ORCH-043/10-tech-risks.md new file mode 100644 index 0000000..35b3c39 --- /dev/null +++ b/docs/work-items/ORCH-043/10-tech-risks.md @@ -0,0 +1,24 @@ +# 10 — Технические риски (ORCH-043) + +Merge-gate + auto-rebase + re-test. Риски, их влияние и меры снижения. Привязка к AC. + +| # | Риск | Влияние | Снижение | AC | +|---|------|---------|----------|----| +| R-1 | **Дедлок при `max_concurrency=1`**: блокирующее ожидание merge-lock в `advance_stage` держит единственный worker-слот, а задаче-холдеру тот же слот нужен для merge. | Полная остановка конвейера (self-hosting = все проекты). | Acquire **неблокирующий**; busy → **defer** (re-enqueue с задержкой, слот освобождается), НЕ блокирующее ожидание. | AC-5 | +| R-2 | **Потерянный PR-merged вебхук** → lease не освобождается вовремя. | Следующая задача ждёт до тайм-аута. | Реклейм lease по возрасту `merge_lock_timeout_s`; release продублирован в `deploy→done` и в откатах. | AC-5 | +| R-3 | **Краш сервиса под lease** (зависший lease-файл после рестарта). | Блокировка merge репо. | Файловый lease с реклеймом по возрасту + `pid`; идемпотентный re-acquire холдером. Restart-safe. | AC-5, AC-9 | +| R-4 | **Долгий re-test (до 600s)** держит worker-слот и блокирует прочие задачи. | Замедление конвейера. | Жёсткий тайм-аут `merge_retest_timeout_s` + kill; осознанная стоимость re-test-перед-merge. | AC-6 | +| R-5 | **Случайный push/force-push в `main`** из логики гейта. | Прямая порча `main` прод-инструмента. | Код гейта НИКОГДА не пушит `main`; единственная force — `--force-with-lease` по ветке задачи; покрыто тестом-стражем. | AC-7 | +| R-6 | **Необработанное исключение** из гейта всплывает в `advance_stage`. | Падение авто-advance, зависшая задача. | Контракт **never-raise** во всех функциях `merge_gate.py` и `check_branch_mergeable`: исключение → `(False, reason)`. | AC-9 | +| R-7 | **Git-операции в общем clone** `/repos/` вместо worktree → S-4-гонка параллельных задач. | Порча рабочих копий, ложные конфликты. | Все операции — в worktree ветки (`ensure_worktree`/`get_worktree_path`). | AC-8 | +| R-8 | **Defer-петля** (lease вечно busy из-за залипшего холдера) → бесконечные перепрогоны staging. | Зацикливание, расход токенов/CPU. | `merge_defer_max_attempts` + Telegram-эскалация + блокировка; реклейм lease (R-2/R-3) снимает первопричину. | AC-5, AC-11 | +| R-9 | **rebase --force-with-lease** помечает прежний approve ревьюера stale и пересоздаёт head PR. | Теоретическая потеря «зелёного» статуса PR. | На стадии `deploy` ревью повторно не проверяется; re-test в гейте — авторитетная проверка. Документировано в ADR. | AC-2 | +| R-10 | **Re-test-команда не подходит чужому репо** (раскладка enduro-trails ≠ orchestrator). | Ложный красный re-test на не-self-hosting репо. | Гейт по умолчанию реален ТОЛЬКО для `orchestrator`; прочие — no-op; `merge_retest_target` параметризует цель. | AC-12, BR-7 | +| R-11 | **Дрейф snapshot-реестра** при добавлении QG. | Красные тесты / расхождение контракта. | Обновить `_EXPECTED_QGS` (+`check_branch_mergeable`) осознанно; `_EXPECTED_TRANSITIONS` НЕ менять (стадии не трогаем). | AC-10 | +| R-12 | **Рестарт/падение прод-контейнера** `orchestrator` в рамках задачи. | Остановка конвейера всех проектов. | Не трогаем `.env*`/`docker-compose.yml`/инфру; обязательная страховка `deploy-staging` (8501). | AC-14 | +| R-13 | **Регресс существующих тестов** от изменения `advance_stage`/`gitea.handle_pr`. | Поломка конвейера. | `pytest tests/ -q` целиком зелёный; изменения аддитивны (новая ветвь на ребре, существующие пути не меняются). | AC-15 | + +## Остаточные риски (принимаются) +- **Скрытый под-гейт** (merge-gate не отражён в `STAGE_TRANSITIONS`) — плата за минимальный + blast-radius; смягчён документацией (README + ADR). +- **Лишний прогон staging** при defer — переиспользование очереди вместо нового job-типа.