13 KiB
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:
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 агентов.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-guardpr_already_merged;GET …/pulls?state=openпоиск code-PRhead==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 worktreegit 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 мержа.
- count
- Сигнатура возврата
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-shotmerge_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: создаётся/обновляется только аналитический пакет (
01–04); в development-стадии обновятсяCHANGELOG.md,.env.example, merge-gate-раздел доки. ADR (06-adr/) — пишет архитектор. - Полный регресс
pytest tests/ -qдолжен оставаться зелёным.