146 lines
12 KiB
Markdown
146 lines
12 KiB
Markdown
---
|
||
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` (заполняет архитектор).
|