Files
orchestrator/docs/work-items/ORCH-093/02-trz.md

13 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-093 analysis analyst ready-for-review 2026-06-09 claude-opus-4-8

02 — ТЗ (TRZ): ORCH-093 — merge-актор ретраит транзиентные ошибки Gitea + гард «ветка уже в main»

Work Item: ORCH-093 · Repo: orchestrator · Стадия: analysis

ТЗ описывает конкретные изменения к реализации, выведенные из BRD и фактического кода (src/merge_gate.py, src/config.py, src/stage_engine.py). Архитектурное обоснование (точный алгоритм классификации, формат хелпера, выбор дефолтов) — задача архитектора (06-adr).

1. Сводка изменения

Две точечные доработки src/merge_gate.py:

  1. merge_pr (~700) — обернуть POST /pulls/{index}/merge в retry-loop на транзиентных кодах (405/«try again», 408, 5xx, таймаут/сетевые, плюс 409/422 при mergeable==True) с ограниченным числом попыток и backoff; терминальные исходы (404 нет PR, реальный конфликт / mergeable==False, 403) → быстрый (False, …) без ретрая. По образцу check_ci_green (attempts + interval) и transient-breaker агентов.
  2. ensure_open_pr (~605) — добавить гард «ветка уже полностью в main» (нет коммитов origin/main..branch) → новый исход "already-in-main" до создания PR; в _handle_merge_verify этот исход трактуется как «мерж уже состоялся» → SHA-in-main подтверждает → done без мусорного PR.

Новые флаги ретрая в src/config.py (ORCH_MERGE_RETRY_*) + дескрипторы в .env.example. Контракт never-raise и INV-4 (никогда не push/force-push main) — сохраняются. STAGE_TRANSITIONS, QG_CHECKS, схема БД — не трогаются.

2. Задействованные модули / пути

Путь Действие
src/merge_gate.py изменить — merge_pr (retry-loop + классификатор транзиент/терминал); ensure_open_pr (гард already-in-main); при необходимости leaf-хелперы _is_transient_merge_error() / _branch_fully_in_main()
src/config.py изменить — добавить флаги ретрая мержа (merge_retry_enabled, merge_retry_max_attempts, merge_retry_backoff_base_s, merge_retry_backoff_max_s) по образцу ci_poll_* / merge_pr_timeout_s
src/stage_engine.py изменить (точечно) — _handle_merge_verify (~1447): обработать новый исход ensure_open_pr == "already-in-main" как «мерж уже состоялся» (пропустить merge_pr, дать verify_merged_to_main подтвердить → done)
.env.example изменить — новые дескрипторы ORCH_MERGE_RETRY_*
tests/test_merge_gate.py изменить — мок httpx-последовательностей (405×2→200; конфликт; already-in-main; исчерпание; kill-switch off)
CHANGELOG.md изменить — запись ORCH-093
docs/architecture/README.md (merge-gate раздел) / CLAUDE.md изменить — описать ретрай и гард already-in-main

3. Функциональные требования

FR-1 — retry-loop транзиентных ошибок мержа в merge_pr (BR-1, BR-4, BR-6, BR-7)

  • Шаги merge_pr до POST (idempotency-guard pr_already_merged; GET …/pulls?state=open поиск code-PR head==branch AND base==main; index is None → (False, "no open PR")) — без изменений.
  • POST /pulls/{index}/merge выполняется в цикле до merge_retry_max_attempts попыток (дефолт 3):
    • 200/201(True, "merged PR #<n>") (немедленный выход).
    • транзиентный исход (см. FR-2) И остались попытки → sleep(backoff) и повтор POST; backoff экспоненциальный от merge_retry_backoff_base_s (дефолт 2) с потолком merge_retry_backoff_max_s (дефолт 5).
    • терминальный исход (см. FR-2) → немедленно (False, "merge failed: HTTP <code>") без дальнейших попыток.
    • исчерпание попыток на транзиенте → (False, "merge failed after <N> attempts: HTTP <code>").
  • Kill-switch merge_retry_enabled=False → ровно одна попытка POST (текущее one-shot поведение, BR-7).
  • Каждая попытка логируется (attempt i/N, код, transient/terminal) — образец check_ci_green.

FR-2 — классификация транзиент vs терминал (BR-2, BR-3)

  • Транзиентные (ретраить): 405 («Please try again later»), 408 (timeout), любой 5xx, httpx-таймаут / сетевая ошибка, и 409/422 когда PR всё ещё mergeable.
  • Терминальные (НЕ ретраить, быстрый False): 403 (нет прав), 404 (PR исчез), и 409/422 при реальном конфликте (mergeable==False).
  • Различение неоднозначного 409/422: дополнительный GET /pulls/{index} → поле mergeable:
    • mergeable==True → транзиент (Gitea ещё не пересчитал) → ретрай.
    • mergeable==False → реальный конфликт → терминал.
    • mergeable отсутствует/None → консервативная дефолт-политика (рекомендация аналитика: трактовать как транзиент с тем же ограниченным бюджетом ретраев, т.к. сетевая икота Gitea — наблюдаемый кейс; финальное решение — архитектор в 06-adr).
  • Сетевые/таймаут-исключения httpx внутри попытки ловятся (never-raise) и классифицируются как транзиент в рамках того же бюджета.

FR-3 — гард «ветка уже полностью в main» в ensure_open_pr (BR-5)

  • Перед шагом «создать PR» (после того как открытый code-PR не найден) ensure_open_pr проверяет, что в ветке нет коммитов сверх origin/main: в per-branch worktree git fetch origin main + git rev-list --count origin/main..<branch> (или git merge-base --is-ancestor <branch> origin/main).
    • count == 0 (ветка целиком в main) → ("already-in-main", "<reason>")PR не создаётся.
    • count > 0 (есть невлитые коммиты) → текущий путь POST …/pulls (создать code-PR).
    • git/OS ошибка проверки → не блокировать (never-raise); деградировать на текущее поведение (попытаться создать PR) ИЛИ вернуть failed — точную fail-политику фиксирует архитектор. Гард не должен превратить инфра-икоту git в ложный no-op мержа.
  • Сигнатура возврата ensure_open_pr расширяется новым статусом "already-in-main" дополнительно к "existed"|"created"|"failed" (обратносовместимо для существующих веток вызова).

FR-4 — обработка already-in-main в _handle_merge_verify (BR-5)

  • В stage_engine._handle_merge_verify (~1487): при pr_status == "already-in-main" — логировать, пропустить merge_gate.merge_pr (мержить нечего) и перейти сразу к verify_merged_to_main (SHA-in-main подтвердит факт мержа → done). Это НЕ failed-ветка (не HOLD): ветка уже в main, цель достигнута.
  • SHA-in-main (verify_merged_to_main) остаётся авторитетным доказательством мержа; гард только избегает мусорного PR и лишнего merge_pr.

FR-5 — конфигурация и обратная совместимость (BR-6, BR-7)

  • Новые поля settings (см. §2) с дефолтами; читаются из env (ORCH_MERGE_RETRY_*).
  • При merge_retry_enabled=False — поведение merge_pr байт-в-байт как сейчас (one-shot).
  • Гард already-in-main также под флагом ИЛИ всегда-вкл (рекомендация: всегда-вкл, т.к. он лишь предотвращает создание заведомо пустого PR; решение — архитектор).

4. Изменения API

Нет (внешних HTTP-эндпоинтов оркестратора не добавляется/не меняется). Меняется только клиентское обращение к Gitea API внутри merge_gate (дополнительный GET /pulls/{index} для чтения mergeable при неоднозначном 409/422; ретрай POST …/merge). Read-only блок merge-verify в GET /queue (merge_verify_status()) опционально может получить счётчик ретраев (необязательно).

5. Изменения схемы БД

Нет. (Merge-lease — файловый, не БД; счётчики _MERGE_VERIFY_COUNTERS — in-process. Новые поля — только в config.Settings, не в схеме.)

6. Требования к новым/изменённым QG checks

Нет. STAGE_TRANSITIONS, состав QG_CHECKS, exit-гейты рёбер и под-гейты ребра deploy-staging → deployне трогаются. Изменение целиком внутри детерминированного merge-актора merge_pr/ensure_open_pr (под-гейт-врезка _handle_merge_verify ребра deploy → done), который НЕ зарегистрирован в QG_CHECKS.

7. Совместимость / регресс

  • Kill-switch merge_retry_enabled=False → one-shot merge_pr (текущее поведение) — нулевая регрессия.
  • Защита ORCH-071/081 «deploy succeeded but not merged» сохраняется 1:1: после исчерпания ретраев / на терминальном конфликте merge_pr возвращает False, и при неподтверждённом SHA-in-main срабатывает прежний HOLD + алерт.
  • INV-4 / self-hosting safety: никаких push/force-push в main; мерж только через Gitea PR-merge API; прод-контейнер не перезапускается.
  • never-raise: merge_pr / ensure_open_pr ловят все исключения и возвращают безопасный кортеж — контракт сохранён (тесты на never-raise остаются зелёными).
  • Идемпотентность: pr_already_merged (idempotency-guard) и гард already-in-main делают повторный прогон финализатора бесследным (нет дублей PR/мержей).
  • Область раската: реально задействуется на merge-verify under-gate (self-hosting, merge_verify_applies); на прочих репо merge делает LLM-deployer — изменение нейтрально.
  • Артефакты pipeline: создаётся/обновляется только аналитический пакет (0104); в development-стадии обновятся CHANGELOG.md, .env.example, merge-gate-раздел доки. ADR (06-adr/) — пишет архитектор.
  • Полный регресс pytest tests/ -q должен оставаться зелёным.