143 lines
13 KiB
Markdown
143 lines
13 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
|
||
---
|
||
|
||
# 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**: создаётся/обновляется только аналитический пакет (`01`–`04`); в
|
||
development-стадии обновятся `CHANGELOG.md`, `.env.example`, merge-gate-раздел доки. ADR
|
||
(`06-adr/`) — пишет архитектор.
|
||
- Полный регресс `pytest tests/ -q` должен оставаться зелёным.
|