--- work_item: ORCH-093 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-09 model_used: claude-opus-4-8 --- # 01 — BRD (бизнес-требования): ORCH-093 — merge-актор не ретраит транзиентные ошибки Gitea (405/5xx) → ложный HOLD + мусорные PR Work Item: **ORCH-093** · Repo: **orchestrator** · Стадия: analysis ## 1. Бизнес-контекст и проблема Тип: **BUG** (надёжность self-deploy merge-фазы). Найдено по инциденту **ORCH-063 (09.06)**. **Инцидент-первоисточник (ORCH-063, 09.06).** Прод-деплой self-hosting прошёл, staging OK, но при мерже PR в `main` Gitea вернул `HTTP 405 {"message":"Please try again later"}` — транзиентная икота (Gitea пересчитывал `mergeable` сразу после пуша). PR #98 был `open` + `mergeable=True`, конфликтов **не было**. Однако merge-актор `merge_gate.merge_pr()` — **one-shot**: на любой не-200/201 он сразу вернул `(False, "merge failed: HTTP 405")`. Сработала корректная защита ORCH-071/081 «deploy succeeded but not merged» → задача удержана на `deploy` (НЕ `done`), алерт, потребовался **ручной домерж** (повтор `merge_pr` вручную → смержилось с первого раза). Защита отработала верно, но **транзиент не должен был требовать человека**. **Два дефекта, оба верифицированы по коду прода `src/merge_gate.py`:** - **ДЕФЕКТ 1 — `merge_pr` не ретраит транзиентные HTTP-ошибки.** `merge_gate.merge_pr()` (`src/merge_gate.py` ~700) делает **один** `POST /pulls/{index}/merge`; на любой не-200/201 (включая `405 "try again later"`, `5xx`, `409/422` «ещё считается mergeable») сразу `return False, "merge failed: HTTP {code}"` — без ретрая. Сравни: у Claude-агентов есть transient-breaker (`429/overload` ретраится), у merge-актора такого механизма нет → инфра-икота Gitea = ложный HOLD. - **ДЕФЕКТ 2 — `ensure_open_pr` плодит мусорные PR на уже влитой ветке.** При повторном прогоне финализатора **после** ручного мержа: PR #98 уже `merged+closed` → `ensure_open_pr` (`src/merge_gate.py` ~605) не находит открытого code-PR → **создаёт новый пустой PR #99** (ветка уже в `main`, diff пустой). Пришлось закрывать вручную. **Боль:** ложные HOLD при инфра-икоте Gitea требуют ручного вмешательства в автономный конвейер (эпик ORCH-088 — пакетный автономный прогон) и оставляют мусорные пустые PR. ## 2. Объём (scope) ### В объёме - `merge_pr` ретраит **транзиентные** ошибки мержа (405/«try again», 408, `5xx`, таймаут/сетевые, а также `409/422` когда PR **всё ещё mergeable**) с ограниченным числом попыток и backoff — **перед** тем как вернуть `False`. - Различение «mergeable, но Gitea временно отказал» (ретраить) vs «реальный конфликт / не-mergeable» (НЕ ретраить, честный быстрый HOLD). - `ensure_open_pr` / merge-verify **не создаёт** новый PR, если ветка уже полностью в `main` (нет коммитов `origin/main..branch`) — возвращает исход «already-in-main»; финализатор сразу доводит до `done` без мусорного PR. - Конфигурируемость (число ретраев, backoff, kill-switch на ретрай-поведение); разумные дефолты. - Обновление `.env.example`, `CHANGELOG.md`, merge-gate-раздела документации. ### Вне объёма - ❌ Снятие/ослабление защиты ORCH-071/081 «deploy succeeded but not merged» — она корректна; задача лишь снижает **ложные** срабатывания на транзиентах. - ❌ Ретрай **реального** конфликта / не-mergeable — это законный HOLD, нужен человек. - ❌ Любые прямые `push`/`force-push` в `main` (инвариант INV-4 ORCH-071/073 — мерж только через Gitea PR-merge API). - ❌ Изменение `STAGE_TRANSITIONS`, состава `QG_CHECKS`, схемы БД. - ❌ Изменение SHA-in-main-доказательства мержа (`verify_merged_to_main`) как источника истины. ## 3. Заинтересованные стороны - **Заказчик / оператор автономного конвейера (Owner, Стрим)** — меньше ручных домержей, чище список PR в Gitea. - **Self-hosting репо `orchestrator`** — основной потребитель merge-verify under-gate (ORCH-071); изменение в первую очередь касается self-deploy merge-фазы. - **Все проекты на общем инстансе** — косвенно: меньше зависших на `deploy` задач, держащих merge-lease и клинящих serial-gate репо (ORCH-088). - **Reviewer / tester** — принимают результат по AC и зелёному `pytest`. ## 4. Бизнес-требования (BR) - **BR-1** — При транзиентной ошибке мержа (`405`/«Please try again later», `408`, `5xx`, таймаут/сетевая ошибка) `merge_pr` повторяет `POST …/merge` до `N` раз с backoff, прежде чем вернуть `(False, …)`; успешный повтор внутри бюджета → `(True, …)`, мерж выполнен. - **BR-2** — `merge_pr` различает «PR mergeable, Gitea временно отказал» (ретраить) и «реальный конфликт / PR не mergeable» (НЕ ретраить). Различение опирается на код ответа **и** поле `mergeable` PR (`GET /pulls/{n}`). Неоднозначный `409/422` классифицируется по `mergeable`. - **BR-3** — Терминальные ошибки (`404` нет PR / реальный конфликт / `403`) НЕ ретраятся — `merge_pr` возвращает `(False, …)` быстро; честный HOLD (защита ORCH-071/081) сохраняется. - **BR-4** — При исчерпании ретраев `merge_pr` возвращает `(False, …)` с понятным reason; защита «deploy succeeded but not merged» срабатывает как прежде (HOLD + алерт). - **BR-5** — Если ветка уже полностью в `main` (нет коммитов `origin/main..branch`), `ensure_open_pr` НЕ создаёт PR — возвращает исход «already-in-main»; merge-verify доводит задачу до `done` без мусорного пустого PR. - **BR-6** — Поведение ретрая конфигурируемо: число попыток, backoff и kill-switch; дефолты разумны (≈3 попытки, backoff 2–5 с) и задокументированы в `.env.example`. - **BR-7** — При выключенном ретрай-kill-switch поведение `merge_pr` идентично текущему (one-shot) — нулевая регрессия. ## 5. Нефункциональные требования (NFR) - **NFR-1 (never-raise)** — Контракт never-raise `merge_pr` / `ensure_open_pr` сохранён: любая HTTP/parse/сетевая ошибка → `(False, …)` / `("failed"|"already-in-main", …)`, исключение никогда не пробрасывается в `_handle_merge_verify` / `advance_stage`. - **NFR-2 (self-hosting safety / INV-4)** — Никаких прямых `push`/`force-push` в `main`; мерж только через Gitea PR-merge API. Прод-контейнер `orchestrator` не перезапускается этой задачей. - **NFR-3 (обратимость / kill-switch)** — Ретрай-поведение полностью отключаемо одним флагом → откат к нынешнему one-shot без изменения кода. - **NFR-4 (ограниченность)** — Суммарное время ретраев ограничено (`N` × backoff_max) и не может «подвесить» monitor-поток, исполняющий merge-verify; backoff с верхним потолком. - **NFR-5 (идемпотентность)** — Повторный прогон финализатора на уже влитой ветке безопасен и бесследен (нет дублей PR, нет дублей мержа — переиспользуется `pr_already_merged`). - **NFR-6 (наблюдаемость)** — Каждый ретрай и его причина логируются (по образцу `check_ci_green`: `attempt i/N`); исход (успех/исчерпание/терминал) различим в логе. ## 6. Допущения и ограничения - Gitea-код `405 {"message":"Please try again later"}` — **транзиент** (Gitea пересчитывает `mergeable` сразу после пуша); `5xx`/таймаут/сетевая — транзиент. - `409` (conflict) и `422` (unprocessable) **двойственны**: либо реальный конфликт, либо «ещё не пересчитан mergeable». Источник различения — поле `mergeable` из `GET /pulls/{n}` (а не только код): `mergeable==True` → транзиент (ретраить), `mergeable==False` → реальный конфликт (НЕ ретраить). - `404` (нет PR) обрабатывается раньше шагом «no open PR» и/или трактуется как терминал. - Образец паттерна ретрая уже есть в репо: `check_ci_green` (`src/qg/checks.py`, attempts + interval + backoff) и transient-breaker агентов (`backoff_base_seconds`/`backoff_max_seconds`/ `transient_max_attempts` в `config.py`). - Merge-verify under-gate (ORCH-071) реален только для self-hosting (`merge_verify_applies`); на прочих репо мерж делает LLM-deployer — там изменение `merge_pr` не задействуется. - Изменение **точечное** в `src/merge_gate.py` + флаги в `src/config.py`; `STAGE_TRANSITIONS`, `QG_CHECKS`, схема БД не трогаются. ## 7. Критерии успеха `merge_pr` переживает транзиентную икоту Gitea (405/5xx/таймаут/«not mergeable yet») за счёт ограниченного ретрая с backoff и больше не даёт ложного HOLD; реальный конфликт по-прежнему даёт быстрый честный HOLD; `ensure_open_pr` не создаёт мусорных PR на уже влитой ветке; поведение конфигурируемо и отключаемо; never-raise сохранён; `pytest tests/ -q` зелёный; доки и `.env.example` обновлены. Детальные PASS/FAIL — `03-acceptance-criteria.md`. ## 8. Риски - Слишком агрессивный ретрай реального конфликта → задержка честного HOLD (митигируется BR-2/BR-3: классификация по `mergeable`). - Ошибочная классификация транзиента как терминала (или наоборот) при неполном ответе Gitea (`mergeable=None`) — нужна осторожная дефолт-политика. - Гонка `ensure_open_pr` already-in-main vs параллельный мерж. Детали и оценка — `10-tech-risks.md` (заполняет архитектор).