Files
orchestrator/docs/work-items/ORCH-093/01-brd.md

12 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

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+closedensure_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-2merge_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 25 с) и задокументированы в .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 (заполняет архитектор).