architect(ET): auto-commit from architect run_id=334
All checks were successful
CI / test (push) Successful in 19s
All checks were successful
CI / test (push) Successful in 19s
This commit is contained in:
156
docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md
Normal file
156
docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# ADR-001 (ORCH-059): Выделенный статус «Confirm Deploy» как триггер прод-деплоя
|
||||
|
||||
## Статус
|
||||
Accepted (design) — реализация в ветке `feature/ORCH-059-approve-confirm-deploy-approve`.
|
||||
|
||||
## Контекст
|
||||
ORCH-036 (исполняемый самодеплой стадии `deploy`) запускает прод-деплой
|
||||
self-hosting инстанса **Фазой B**: человек переводит issue в Plane-статус
|
||||
`Approved` → webhook `work_item.updated` → `handle_issue_updated` →
|
||||
`handle_verdict(approved=True)` → `_try_advance_stage` →
|
||||
`advance_stage(finished_agent=None)`; в `stage_engine.advance_stage` блок
|
||||
`current_stage == "deploy" and finished_agent is None` →
|
||||
`_handle_self_deploy_phase_b` → detached host-деплой прода (8500).
|
||||
|
||||
Тот же UUID `Approved` (`a519a341-…`, `_DEFAULT_STATES["approved"]`) — это
|
||||
**человеческий гейт одобрения** на стадии `analysis`
|
||||
(`check_analysis_approved`, путь `approved-via-status`) и общий verdict-роутинг
|
||||
в `handle_verdict`. Один визуальный «Approved» на доске значит две принципиально
|
||||
разные вещи: «принять BRD» (дёшево, обратимо) и «**ВЫКАТИТЬ В ПРОД** инструмент,
|
||||
обслуживающий все проекты из одного инстанса с общей БД» (дорого, групповой
|
||||
риск). Привычный жест approve на стадии `deploy` молча триггерит прод-рестарт —
|
||||
цена случайного клика высока (см. self-hosting в `CLAUDE.md`).
|
||||
|
||||
Ограничения, формирующие дизайн (см. `02-trz.md`, `03-acceptance-criteria.md`):
|
||||
1. **Нулевая регрессия** гейта `Approved` на `analysis` и прочих стадиях (TRZ-4).
|
||||
2. **Fail-closed**: среды без статуса (enduro, fallback `_DEFAULT_STATES`,
|
||||
недоступный API) не должны падать и не должны «вслепую» деплоить (TRZ-1, R-1).
|
||||
3. **`Approved` на `deploy` не должен** запускать Фазу B И не должен вызывать
|
||||
ложный откат (БАГ-8) или ложный advance по `check_deploy_status` — вердикта
|
||||
ещё нет (TRZ-3, R-2).
|
||||
4. **Без правки контрактов**: `STAGE_TRANSITIONS`, `QG_CHECKS`,
|
||||
`check_deploy_status`, Фазы A/C, merge-gate, exit-коды хука, схема БД (TRZ-8).
|
||||
5. **Self-hosting safety**: правка — чистая маршрутизация, не требует внепланового
|
||||
рестарта прода; выкат через штатный `deploy-staging` (8501) → `deploy` (R-3).
|
||||
|
||||
## Решение
|
||||
Ввести отдельный логический статус `confirm_deploy` («Confirm Deploy»), который
|
||||
триггерит **ТОЛЬКО** Фазу B на стадии `deploy`. `Approved` теряет смысл «запусти
|
||||
прод-деплой» и остаётся исключительно человеческим гейтом конвейера.
|
||||
|
||||
Четыре точечные правки в трёх модулях:
|
||||
|
||||
### 1. Резолвер состояний — `src/plane_sync.py`
|
||||
- В `_PLANE_NAME_TO_KEY` добавить маппинг `"Confirm Deploy" → "confirm_deploy"`.
|
||||
- В `_DEFAULT_STATES` ключ `confirm_deploy` **НЕ добавлять** (реального UUID для
|
||||
enduro/fallback нет; отсутствие ключа = fail-closed). Для проекта ORCH ключ
|
||||
резолвится `get_project_states` из живого Plane API; для проектов без статуса и
|
||||
на fallback-пути ключ просто отсутствует в результирующем словаре.
|
||||
- Следствие: `get_project_states(orch)["confirm_deploy"]` → реальный UUID;
|
||||
`get_project_states(enduro).get("confirm_deploy")` → `None`.
|
||||
|
||||
### 2. Маршрутизация webhook — `src/webhooks/plane.py`
|
||||
В `handle_issue_updated`, **до** ветки `approved`, добавить fail-closed-ветку:
|
||||
```python
|
||||
confirm_state = proj_states.get("confirm_deploy") # .get -> AC-7/R-1
|
||||
if confirm_state and new_state == confirm_state:
|
||||
await handle_confirm_deploy(data, project_id)
|
||||
elif new_state == proj_states["in_progress"]:
|
||||
...
|
||||
elif new_state == proj_states["approved"]:
|
||||
await handle_verdict(data, project_id, approved=True)
|
||||
```
|
||||
Новый `handle_confirm_deploy(data, project_id)`:
|
||||
- резолвит задачу по `plane_id`;
|
||||
- если `stage != "deploy"` → **no-op с логом** (Confirm Deploy осмыслен только на
|
||||
approval-pending стадии `deploy`; защищает прочие гейты от случайного approve);
|
||||
- иначе → `_try_advance_stage(..., confirm_deploy=True)`.
|
||||
|
||||
`handle_verdict(approved=True)` не меняется — продолжает звать `_try_advance_stage`
|
||||
с `confirm_deploy=False` (дефолт).
|
||||
|
||||
### 3. Сигнал в движок — `src/stage_engine.advance_stage(...)`
|
||||
Добавить keyword-only параметр `confirm_deploy: bool = False` (back-compat: все
|
||||
существующие вызовы из launcher/reconciler/finalizer/webhook передают
|
||||
`finished_agent`, новый kwarg дефолтный). Блок Фазы B переписать так, чтобы он
|
||||
**всегда возвращался рано** для `deploy + finished_agent is None` self-hosting,
|
||||
но деплоил только по сигналу:
|
||||
```python
|
||||
if (current_stage == "deploy" and finished_agent is None
|
||||
and settings.deploy_require_manual_approve
|
||||
and self_deploy.self_deploy_applies(repo)):
|
||||
if confirm_deploy:
|
||||
_handle_self_deploy_phase_b(task_id, repo, work_item_id, branch, result)
|
||||
else:
|
||||
# TRZ-3/R-2: обычный Approved на deploy — no-op; НЕ запускаем
|
||||
# check_deploy_status (вердикта ещё нет -> ложный откат БАГ-8).
|
||||
result.note = "approved-on-deploy-noop"
|
||||
return result
|
||||
```
|
||||
Ключевое: возврат **до** блока Quality Gate в обоих случаях → `check_deploy_status`
|
||||
по `Approved` на `deploy` не исполняется. Фаза C (finalizer,
|
||||
`finished_agent="deployer"`) не затронута — условие требует `finished_agent is
|
||||
None`.
|
||||
|
||||
### 4. CTA Фазы A — `src/stage_engine._handle_self_deploy_phase_a`
|
||||
Текст Plane-комментария и Telegram изменить: вместо «смените статус на Approved»
|
||||
инструктировать перевести задачу в статус **«Confirm Deploy»** для запуска
|
||||
прод-деплоя (TRZ-5/AC-6).
|
||||
|
||||
### Условность (как ORCH-35/36)
|
||||
Вся ветка реальна только для `self_deploy.self_deploy_applies(repo)` →
|
||||
`orchestrator`. Прочие репо — прежний синхронный ssh-деплой агентом; статус
|
||||
`Confirm Deploy` им не нужен и на них не влияет (AC-8).
|
||||
|
||||
## Альтернативы
|
||||
- **A. Telegram inline-кнопка подтверждения** вместо нового статуса — отклонено:
|
||||
кнопочная инфраструктура в коде отсутствует, заявлено вне scope (ORCH-036 п.
|
||||
«inline-кнопка» не реализован); управление остаётся статусом Plane.
|
||||
- **B. Добавить `confirm_deploy` в `_DEFAULT_STATES`** — отклонено: реального UUID
|
||||
«Confirm Deploy» для enduro/fallback нет; пришлось бы подставить фиктивный или
|
||||
дублирующий UUID, что ломает fail-closed (enduro «получил бы» триггер деплоя) и
|
||||
смешивает семантику.
|
||||
- **C. Отдельный публичный entrypoint `stage_engine.initiate_confirm_deploy()`**,
|
||||
минующий `advance_stage` — отклонено: дублирует гарды
|
||||
(`deploy_require_manual_approve`, `self_deploy_applies`, idempotency `initiated`),
|
||||
и всё равно пришлось бы внутри `advance_stage` гасить `Approved`-на-`deploy` в
|
||||
no-op. Параметр-сигнал проще и держит единую точку правды.
|
||||
- **D. Сигнал через sentinel-маркер, записываемый webhook’ом** — отклонено: вызов
|
||||
синхронный в пределах одного `advance_stage`, persistence не нужна; параметр
|
||||
явнее и не плодит файловое состояние.
|
||||
|
||||
## Последствия
|
||||
**Плюсы**
|
||||
- Жест «запустить прод-деплой» отделён от «одобрить артефакт»; случайный approve
|
||||
на доске больше не роняет прод (BG-1, BG-2).
|
||||
- `Approved` на `deploy` детерминированно безопасен: no-op без отката/advance
|
||||
(закрывает R-2).
|
||||
- Fail-closed: нет статуса → нет деплоя, нет исключения (R-1, AC-7).
|
||||
- Минимальный диффузный риск: контракты `STAGE_TRANSITIONS`/`QG_CHECKS`/
|
||||
`check_deploy_status`/Фазы A/C/merge-gate/схема БД не тронуты (AC-9).
|
||||
- Реконсилятор F-1 на `deploy` (finished_agent=None) теперь попадает в no-op-ветку
|
||||
вместо прежнего неявного запуска Фазы B → прод-деплой невозможно инициировать
|
||||
автоматически, только явным человеческим `Confirm Deploy` (усиление safety).
|
||||
|
||||
**Минусы / цена**
|
||||
- Эксплуатационное предусловие: в Plane-проекте ORCH нужно создать статус доски
|
||||
«Confirm Deploy» (точное имя, регистр) и сбросить кэш состояний — см.
|
||||
`07-infra-requirements.md`. До создания статуса прод-деплой через approve не
|
||||
запустится (это и есть желаемое fail-closed-поведение).
|
||||
- Сигнатура `advance_stage` расширена одним kwarg (обратносовместимо).
|
||||
|
||||
**Хэндофф документации (golden source, в том же PR — стадия development).**
|
||||
ADR (этот файл) — артефакт архитектора. Переписать `Approve = Approved` →
|
||||
`Confirm Deploy` в `docs/architecture/README.md` (секция ORCH-036), `CLAUDE.md`
|
||||
(self-hosting/артефакты) и добавить запись в `CHANGELOG.md` обязан developer
|
||||
одновременно с кодом (AC-10), чтобы доки не описывали ещё не существующее
|
||||
поведение. В README на стадии architecture добавлена forward-looking пометка
|
||||
ORCH-059 (design), как принято для незамёрженных доработок.
|
||||
|
||||
## Связанные ADR
|
||||
- `adr-0007-executable-self-deploy.md` (ORCH-036) — задаёт Фазы A/B/C; ORCH-059
|
||||
меняет **только триггер** Фазы B (`Approved` → `Confirm Deploy`) и делает
|
||||
`Approved`-на-`deploy` no-op; Фазы внутренне не меняются.
|
||||
- `adr-0003-staging-gate.md` (ORCH-35) — паттерн условности self-hosting.
|
||||
- `adr-0007-reconciler.md` (ORCH-053) — реконсилятор F-1: поведение на `deploy`
|
||||
становится no-op (см. Последствия).
|
||||
Reference in New Issue
Block a user