211 lines
19 KiB
Markdown
211 lines
19 KiB
Markdown
# 02 — ТЗ (TRZ): ORCH-088 — Serial gate (Этап 1: пакетный автономный режим, serial e2e)
|
||
|
||
Work Item: **ORCH-088** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> Документ описывает **что** должно измениться и **где** (модули/контракты/артефакты). **Как**
|
||
> (конкретная схема реализации, выбор «таблица vs sentinel», точки врезки) — решает архитектор в
|
||
> `06-adr/`. ТЗ фиксирует требования и границы, не предлагает архитектурное решение.
|
||
|
||
> ⚠️ Скоп — только FR-1…FR-5 (serial e2e). Merge-очередь / pre-merge rebase / фазы A/B/C / ORCH-83 —
|
||
> вне скопа.
|
||
|
||
---
|
||
|
||
## 1. Сводка изменения
|
||
Ввести **per-repo serial gate**: новая задача репо не входит в стадию `analysis` (не режет ветку, не
|
||
запускает analyst-агент), пока в том же репо есть незавершённая задача (`stage != 'done'`). Открытие
|
||
gate — по достижении предшественником `stage = 'done'` (после прод-деплоя). Дополнительно — **per-repo
|
||
freeze** при деградации/rollback прода (post-deploy), снимаемый вручную. Всё — аддитивно, под
|
||
kill-switch, с областью репо, never-raise, restart-safe. Машина стадий и реестр QG **не меняются**.
|
||
|
||
---
|
||
|
||
## 2. Задействованные модули `src/`
|
||
|
||
| Модуль | Роль в задаче | Характер изменения |
|
||
|--------|---------------|--------------------|
|
||
| `src/db.py` | `claim_next_job` (горячий claim), схема `tasks`/`jobs`, helper'ы выборки активной задачи репо; (возможно) аддитивная таблица/колонка для freeze | gate-условие в claim + новые read-only helper'ы + аддитивная миграция (идемпотентная, `_ensure_column`/`CREATE TABLE IF NOT EXISTS`) |
|
||
| `src/queue_worker.py` | вызывает `claim_next_job` в `_drain_once` | без изменения контракта; gate работает внутри claim |
|
||
| `src/webhooks/plane.py` | `start_pipeline` / `handle_status_start` / `_create_gitea_branch` | **отсрочка создания ветки** до момента, когда репо свободен (ключевое для AC-6); постановка задачи в очередь ожидания вместо немедленного среза ветки |
|
||
| `src/git_worktree.py` | `ensure_worktree` — срез ветки от `origin/main` | гарантия: для новой задачи база = свежий `origin/main` после `git fetch` (см. §6) |
|
||
| `src/agents/launcher.py` | `_spawn` — ленивое создание worktree на claim | согласование с отсрочкой среза ветки (не материализовать stale-ветку) |
|
||
| `src/stage_engine.py` | `run_post_deploy_monitor` / блок `next_stage == "done"` | при вердикте деградации/rollback — выставить per-repo freeze (FR-5) |
|
||
| `src/post_deploy.py` | `decide_action` / реакция | сигнал для freeze (`ALERT_ONLY` self / `ROLLBACK*` non-self) → выставление freeze |
|
||
| `src/config.py` | флаги фичи | новые: `serial_gate_enabled`, `serial_gate_repos` (CSV), при необходимости — флаги freeze |
|
||
| `src/main.py` | `GET /queue` | новый read-only блок наблюдаемости `serial_gate` |
|
||
| `src/notifications.py` / `src/plane_sync.py` | алерты freeze | переиспользовать `send_telegram` / `set_issue_blocked` / `notify_*` (never-raise) |
|
||
|
||
> Чистую логику gate/freeze желательно вынести в **leaf-модуль** (например `src/serial_gate.py`,
|
||
> never-raise, по образцу `src/task_deps.py` / `src/post_deploy.py`) — окончательно решает архитектор.
|
||
|
||
---
|
||
|
||
## 3. Функциональные изменения (требования к поведению)
|
||
|
||
### 3.1. FR-1 — Serial gate на входе в анализ
|
||
- **Условие закрытия gate (per-repo):** для репо `R` gate **закрыт**, если существует задача `A` репо
|
||
`R` со `stage != 'done'` (любая стадия `created…deploy`), **отличная** от рассматриваемой новой
|
||
задачи `B`.
|
||
- **Что блокируется при закрытом gate:** запуск analyst-агента новой задачи `B` **и** создание её
|
||
ветки (Gitea-ветка + worktree). Branch у `B` не должен быть срезан, пока gate закрыт (иначе stale-base,
|
||
AC-6).
|
||
- **Где гейтить:** в горячем пути выбора работы — `db.claim_next_job` (по образцу `task_deps` NOT EXISTS
|
||
gate), читая ТОЛЬКО локальную БД (NFR-2). Дополнительно — на входе `start_pipeline`, чтобы **не резать
|
||
ветку** до открытия gate (см. §3.3).
|
||
- **Применимость:** gate работает только для analyst-job новой задачи (вход в анализ). Job'ы уже
|
||
активной задачи (architect/developer/…/deployer) проходят свободно — иначе единственная активная
|
||
задача не сможет двигаться по конвейеру.
|
||
|
||
### 3.2. FR-2 — Очередь e2e
|
||
- Накиданные задачи репо встают в очередь; обрабатывается строго одна end-to-end. Реализуется
|
||
естественно: gate держит остальных, активная идёт по стадиям до `done`, затем gate открывается и
|
||
выбирается следующая (FIFO по существующему порядку очереди `jobs.id`).
|
||
|
||
### 3.3. FR-1/AC-6 — Отсрочка среза ветки (анти-stale-base)
|
||
- **Проблема (проверено):** ветка создаётся в Gitea в `start_pipeline._create_gitea_branch` от `main`
|
||
в момент перевода issue в «To Analyse» (T0) — **до** того, как предшественник влит. `ensure_worktree`
|
||
затем **присоединяет уже существующую** Gitea-ветку (а не режет свежую от `origin/main`), т.е. свежий
|
||
`git fetch` не спасает — база остаётся stale.
|
||
- **Требование:** создание ветки (Gitea-ветка и/или worktree) для новой задачи должно происходить
|
||
**после** того, как gate открылся (предшественник `done`), чтобы базой был `origin/main`, уже
|
||
содержащий код предшественника. Конкретный механизм отсрочки (отложить `_create_gitea_branch`;
|
||
материализовать ветку лениво при claim'е analyst-job из свежего `origin/main`; и т.п.) — выбирает
|
||
архитектор. Инвариант результата: **ветка `B` имеет в предках merge-commit/код всех ранее
|
||
завершённых задач репо** (проверяемо `git merge-base --is-ancestor`).
|
||
- Если архитектура решит резать ветку при claim'е analyst-job (а не в `start_pipeline`), это
|
||
автоматически даёт AC-6 (claim происходит только при открытом gate).
|
||
|
||
### 3.4. FR-3 — Per-repo
|
||
- Все выборки gate фильтруются по `tasks.repo` (и `jobs.repo`). Состояние gate/freeze репо `R` не
|
||
влияет на claim/старт задач другого репо. Cross-repo параллелизм сохранён.
|
||
|
||
### 3.5. FR-4 — Restart-safe
|
||
- «Активная задача репо» вычисляется запросом к БД (`tasks` по `repo` + `stage != 'done'`), не из
|
||
in-memory. Freeze хранится в БД (аддитивная таблица/колонка). После рестарта поведение идентично.
|
||
|
||
### 3.6. FR-5 — Rollback-freeze
|
||
- При вердикте post-deploy `DEGRADED` (для self — реакция `ALERT_ONLY`; для non-self с
|
||
`post_deploy_auto_rollback` — `ROLLBACK`) для репо выставляется **durable freeze** (в БД).
|
||
- При активном freeze репо gate **закрыт безусловно**, независимо от наличия задач `stage<done`
|
||
(важно: деградировавшая задача к этому моменту уже `stage='done'` — BR-7 — поэтому обычный gate её
|
||
не удержит; нужен отдельный сигнал).
|
||
- Снятие freeze — **ручное** (оператор). Способ снятия (эндпоинт/админ-команда/ручная правка БД/
|
||
Plane-жест) определяет архитектор; требование — снятие должно быть простым, явным и наблюдаемым.
|
||
- Алерт: Telegram (`send_telegram`/`notify_*`) + Plane `Blocked` для деградировавшей задачи (как
|
||
ORCH-021), плюс явное сообщение «пакет заморожен, следующая задача не стартует до ручного снятия».
|
||
|
||
---
|
||
|
||
## 4. Изменения API
|
||
|
||
### 4.1. Новые публичные endpoint'ы
|
||
- **Нет обязательных новых endpoint'ов.** (Снятие freeze может быть реализовано как админ-эндпоинт —
|
||
на усмотрение архитектора; если вводится, описать в ADR и обновить таблицу API в README.)
|
||
|
||
### 4.2. Изменяемые endpoint'ы
|
||
- `GET /queue` — **аддитивно** добавляется блок `serial_gate` (read-only снимок), по образцу блоков
|
||
`task_deps` / `reconcile` / `post_deploy`:
|
||
- `enabled` (флаг), `repos` (область),
|
||
- per-repo: `active_task` (`{work_item_id, stage}` или `null`), `waiting` (список ожидающих
|
||
задач/job'ов репо), `frozen` (bool) + причина/таймстамп freeze.
|
||
- never-raise: при ошибке — минимальный словарь с флагами и пустыми данными.
|
||
- Контракт `GET /queue` — **расширяется аддитивно**, существующие ключи не меняются.
|
||
|
||
### 4.3. Webhook-обработчики
|
||
- `start_pipeline` / `handle_status_start` (`webhooks/plane.py`): добавляется ветвление «репо занят/
|
||
заморожен → отложить старт/срез ветки, поставить в очередь ожидания» вместо немедленного
|
||
`_create_gitea_branch` + enqueue. Внешний контракт вебхука Plane не меняется.
|
||
|
||
---
|
||
|
||
## 5. Изменения схемы БД
|
||
|
||
> Только **аддитивные, идемпотентные** миграции (общая прод-БД, enduro не трогать). Без изменения
|
||
> существующих таблиц-контрактов.
|
||
|
||
- **Freeze-состояние (FR-5):** требуется durable per-repo признак заморозки. Варианты (выбор —
|
||
архитектор): новая таблица `repo_freeze(repo TEXT, frozen_at TEXT, reason TEXT, work_item_id TEXT,
|
||
cleared_at TEXT)` **или** аддитивная колонка в существующей таблице. Требования к выбранному варианту:
|
||
идемпотентная миграция (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`), restart-safe, per-repo.
|
||
- **Активная задача репо:** **новых колонок НЕ требуется** — вычисляется из существующих
|
||
`tasks(repo, stage)`.
|
||
- **Очередь ожидания:** переиспользовать существующую `jobs` (status='queued' + gate в claim) — новой
|
||
таблицы очереди **не вводить** (FR-2 решается gate'ом, не отдельной структурой).
|
||
- `STAGE_TRANSITIONS`, `QG_CHECKS`, `tasks`-контракт, `job_deps`, `agent_runs` — **без изменений**.
|
||
|
||
---
|
||
|
||
## 6. Требования к срезу ветки (`git_worktree` / launcher)
|
||
- Для новой задачи, чья ветка создаётся после открытия gate: перед срезом — `git fetch origin`
|
||
(уже есть в `ensure_worktree`), база — `origin/main` HEAD.
|
||
- Гарантировать, что ветка НЕ присоединяется к stale Gitea-ветке, созданной раньше времени: либо не
|
||
создавать Gitea-ветку преждевременно (отсрочка §3.3), либо при материализации worktree база
|
||
безусловно = свежий `origin/main` (включающий предшественника).
|
||
- Никогда не push/force-push в `main`. Существующие merge-lease / auto_rebase (ORCH-026/043) не
|
||
трогаются.
|
||
|
||
---
|
||
|
||
## 7. Требования к новым QG checks
|
||
- **Новых QG-проверок не вводить.** Gate — это условие планировщика (claim / старт), а **не**
|
||
Quality Gate стадии. Реестр `QG_CHECKS` и `check_*` не меняются (как `task_deps` ORCH-026 —
|
||
gate в claim, не новый QG).
|
||
|
||
## 8. Конфигурация (`src/config.py`)
|
||
По образцу `task_deps_enabled` / `merge_gate_*` / `post_deploy_*`:
|
||
- `serial_gate_enabled: bool = True` (env `ORCH_SERIAL_GATE_ENABLED`) — kill-switch; `False` → claim и
|
||
старт ведут себя строго как сейчас (нулевая регрессия, NFR-4).
|
||
- `serial_gate_repos: str = ""` (env `ORCH_SERIAL_GATE_REPOS`, CSV) — область; пусто → применять как
|
||
по умолчанию (см. ниже).
|
||
- Helper `serial_gate_applies(repo) -> bool` (leaf-модуль, never-raise) по образцу `post_deploy_applies`:
|
||
`enabled` + (если CSV непуст — членство репо; иначе — область по умолчанию).
|
||
- **Область по умолчанию (решение для ADR):** serial gate осмыслен для ВСЕХ репо (FR-3 — и orchestrator,
|
||
и enduro выигрывают от serial e2e), в отличие от self-hosting-only гейтов (ORCH-35/43/58). Рекомендация:
|
||
пустой CSV → применять ко всем зарегистрированным репо. Архитектор фиксирует и обосновывает в ADR.
|
||
- При необходимости — отдельные флаги для freeze (FR-5), например `serial_gate_freeze_enabled`.
|
||
|
||
---
|
||
|
||
## 9. Наблюдаемость и алерты
|
||
- `GET /queue` блок `serial_gate` (см. §4.2).
|
||
- Лог: каждое решение «gate закрыт, задача отложена» и «freeze выставлен/снят» → `logger.info/warning`.
|
||
- Telegram: freeze (выставление) → алерт (`send_telegram`/`notify_*`); карточка задачи (ORCH-042/087)
|
||
может отражать «⏳ ждёт завершения <work_item_id>» (по образцу строки `task_deps` «⏳ ждёт ORCH-NNN»),
|
||
never-raise.
|
||
|
||
---
|
||
|
||
## 10. Артефакты pipeline (создать/обновить в ТОМ ЖЕ PR)
|
||
Документация — golden source (CLAUDE.md §2). По итогам разработки обновить:
|
||
- `docs/work-items/ORCH-088/06-adr/ADR-001-serial-gate.md` — решение (механизм отсрочки ветки, freeze-
|
||
хранилище, область по умолчанию, точки врезки).
|
||
- `docs/architecture/README.md` — новый раздел «Serial gate (ORCH-088)» + строка статуса доработок;
|
||
обновить описание `GET /queue` (блок `serial_gate`) и раздел «База данных», если добавлена таблица.
|
||
- `CLAUDE.md` — краткий абзац о serial-режиме (если уместно в паспорте).
|
||
- `CHANGELOG.md` — запись `feat:`.
|
||
- При новой таблице freeze — `docs/work-items/ORCH-088/08-data-requirements.md`.
|
||
- При новом админ-эндпоинте снятия freeze — обновить таблицу API в README.
|
||
|
||
---
|
||
|
||
## 11. Инварианты (не нарушать)
|
||
- `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, `check_*`, exit-коды deploy-хука, merge-gate (ORCH-043),
|
||
merge-verify (ORCH-071/073), image-freshness (ORCH-058), post-deploy контракт (ORCH-021),
|
||
`max_concurrency` — **без изменений**.
|
||
- never-raise на единицу работы; claim fail-**open** на ошибке БД (NFR-1); freeze fail-**closed**.
|
||
- Offline в горячем claim (NFR-2): без сетевых вызовов Plane/Gitea.
|
||
- Не рестартить/не ронять прод-контейнер (CLAUDE.md self-hosting).
|
||
- Миграции аддитивны и идемпотентны; enduro при выключенном/неприменимом флаге не затрагивается.
|
||
|
||
---
|
||
|
||
## 12. Открытые вопросы для архитектора (не блокируют анализ)
|
||
- OQ-1: Механизм отсрочки среза ветки — отложить `_create_gitea_branch` в `start_pipeline` ИЛИ
|
||
перенести материализацию ветки на claim analyst-job? (Влияет на AC-6 и на то, где живёт «ожидающая»
|
||
задача — в Plane-статусе vs как `queued` job без ветки.)
|
||
- OQ-2: Хранилище freeze — отдельная таблица `repo_freeze` vs колонка.
|
||
- OQ-3: Способ ручного снятия freeze (эндпоинт / Plane-жест / админ-команда).
|
||
- OQ-4: Поведение при задаче в Blocked/Needs-Input, держащей gate закрытым (Этап 1 — держит; нужен ли
|
||
отдельный «вывод из учёта активных» — вероятно нет, фиксируем как осознанное).
|
||
- OQ-5: Область по умолчанию (все репо vs только self-hosting) — рекомендация §8.
|