diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 0b1d743..88f503a 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -92,6 +92,31 @@ sentinel-файлы (`/.deploy-state-//`), без мигр Подробнее: [adr-0007](adr/adr-0007-executable-self-deploy.md), детально — `docs/work-items/ORCH-036/06-adr/ADR-001-executable-self-deploy.md`. +#### Выделенный статус-триггер прод-деплоя «Confirm Deploy» (ORCH-059 — design) +Перегрузка: один Plane-статус `Approved` служил И человеческим гейтом BRD на +`analysis` (`check_analysis_approved`), И триггером Фазы B прод-деплоя на `deploy` +— привычный жест approve молча запускал прод-рестарт (групповой self-hosting +риск). ORCH-059 разделяет жесты: вводится отдельный логический статус +`confirm_deploy` («Confirm Deploy»), который триггерит **ТОЛЬКО** Фазу B на +`deploy`; `Approved` остаётся исключительно гейтом конвейера. +- `_PLANE_NAME_TO_KEY` += `"Confirm Deploy" → "confirm_deploy"`; в + `_DEFAULT_STATES` ключ НЕ добавляется (нет UUID для enduro/fallback) → + **fail-closed**: нет статуса → нет деплоя, без `KeyError` (доступ через `.get`). +- `handle_issue_updated` маршрутизирует `Confirm Deploy` → `handle_confirm_deploy` + (гард `stage=="deploy"`) → `_try_advance_stage(..., confirm_deploy=True)`. +- `advance_stage` получает kwarg `confirm_deploy: bool=False`; блок Фазы B + (`deploy`+`finished_agent is None`+self-hosting) деплоит ТОЛЬКО при + `confirm_deploy=True`, иначе (обычный `Approved`) — **no-op** (`check_deploy_status` + не запускается → нет ложного отката БАГ-8). +- CTA Фазы A (`_handle_self_deploy_phase_a`) просит «Confirm Deploy», не «Approved». +- Условность как ORCH-35/36 (только `orchestrator`); Фазы A/C, `STAGE_TRANSITIONS`, + `QG_CHECKS`, `check_deploy_status`, merge-gate, схема БД — без изменений. +- Эксплуатация: в Plane-проекте ORCH создать статус «Confirm Deploy» + сброс кэша + состояний (`docs/work-items/ORCH-059/07-infra-requirements.md`). + +Детально — `docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md` +(уточняет/триггер Фазы B относительно adr-0007). + ### Post-deploy наблюдение прода + реакция на деградацию (ORCH-021 — реализовано) Конвейер заканчивался на `deploy → done` и **забывал про прод**: «успех» = health-check в момент рестарта (~60с). Класс «зелёный деплой, красный прод» (прецедент ET-8 — diff --git a/docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md b/docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md new file mode 100644 index 0000000..3077474 --- /dev/null +++ b/docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md @@ -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 (см. Последствия). diff --git a/docs/work-items/ORCH-059/07-infra-requirements.md b/docs/work-items/ORCH-059/07-infra-requirements.md new file mode 100644 index 0000000..7435c50 --- /dev/null +++ b/docs/work-items/ORCH-059/07-infra-requirements.md @@ -0,0 +1,44 @@ +# 07 — Требования к инфраструктуре: ORCH-059 + +Work Item: **ORCH-059** · Repo: `orchestrator` +Связано: `06-adr/ADR-001-confirm-deploy-status.md`, `02-trz.md` §6. + +> Топология контейнеров/портов/деплоя НЕ меняется (см. `docs/operations/INFRA.md`). +> Единственное инфра-требование ORCH-059 — конфигурация Plane-доски проекта ORCH. + +## IR-1. Статус доски «Confirm Deploy» в проекте ORCH (предусловие эксплуатации) +- В Plane-проекте **ORCH** создать кастомный статус доски с **точным** именем + `Confirm Deploy` (case-sensitive, ровно один пробел) — должно посимвольно + совпасть с ключом `_PLANE_NAME_TO_KEY["Confirm Deploy"]`. Несовпадение → + fail-closed (деплой не запустится), не краш (R-9). +- UUID статуса генерирует Plane; код резолвит его через `get_project_states` + (`GET /workspaces//projects//states/`). Хардкодить UUID не нужно. +- **Размещение** на доске — рядом с approval-pending/`deploy` (рекомендация + эксплуатации, на поведение кода не влияет). +- **Только проект ORCH** (self-hosting). Для enduro и прочих проектов статус НЕ + создаётся и НЕ требуется — `self_deploy_applies` истинно лишь для `orchestrator`. + +## IR-2. Сброс кэша состояний после создания статуса +`get_project_states` кэширует резолв per-project на время жизни процесса +(`_STATES_CACHE`). После создания статуса в Plane закэшированный словарь не +содержит `confirm_deploy` (R-5). Применить ОДНО из: +- вызвать `reload_project_states()` (или полный сброс), либо +- штатно перезапустить прод по конвейеру `deploy-staging → deploy` (рестарт + процесса очищает кэш). + +> Внеплановый ручной рестарт прод-контейнера для применения этой задачи **не +> требуется** и противопоказан (self-hosting групповой риск). Выкат — только через +> штатный staging→deploy. + +## IR-3. Контрольная проверка готовности среды +После IR-1+IR-2: +1. `get_project_states()` содержит `confirm_deploy` с непустым UUID, + отличным от `approved` (AC-1, TC-02). +2. Перевод тестовой задачи стадии `deploy` (sandbox) в `Confirm Deploy` запускает + Фазу B; перевод в `Approved` — нет (AC-2/AC-3). + +## Что НЕ меняется +- Порты (8500 prod / 8501 staging), контейнеры, compose-профили, env-карта, + деплой-хук, схема БД, sentinel-каталоги ORCH-036 — без изменений. +- HTTP-эндпоинты (`POST /webhook/plane` тот же канал, событие + `work_item.updated`). diff --git a/docs/work-items/ORCH-059/10-tech-risks.md b/docs/work-items/ORCH-059/10-tech-risks.md new file mode 100644 index 0000000..eb2b72f --- /dev/null +++ b/docs/work-items/ORCH-059/10-tech-risks.md @@ -0,0 +1,25 @@ +# 10 — Технические риски: ORCH-059 + +Work Item: **ORCH-059** · Repo: `orchestrator` · ведёт: архитектор +Связано: `06-adr/ADR-001-confirm-deploy-status.md`. + +| ID | Риск | Вероятн. | Влияние | Митигация | Проверка | +|----|------|----------|---------|-----------|----------| +| R-1 | Ключ `confirm_deploy` отсутствует в `_DEFAULT_STATES` / у проектов без статуса → `KeyError` в webhook-пути | Сред | Выс (краш обработчика) | Доступ ТОЛЬКО через `.get("confirm_deploy")`; `_DEFAULT_STATES` не содержит ключ намеренно; отсутствие → ветка не активируется (fail-closed) | TC-03, AC-7 | +| R-2 | `Approved` на `deploy` после правки вызывает `check_deploy_status` (вердикта нет) → ложный откат БАГ-8 / ложный advance | Выс | Выс (петля dev↔deploy, ложный rollback прода) | Блок Фазы B возвращается рано для `deploy + finished_agent is None` self-hosting в ОБОИХ случаях; `Approved` → `note=approved-on-deploy-noop`, QG не запускается | TC-05, TC-07, TC-11, AC-3 | +| R-3 | Самоправка прода требует внепланового рестарта прод-контейнера | Низ | Выс (встаёт конвейер всех проектов) | Изменение — чистая маршрутизация в коде; выкат через штатный `deploy-staging` (8501) → `deploy`; sentinel-состояние ORCH-036 не трогаем | AC-9, RG-01 | +| R-4 | `Confirm Deploy` прислан на не-`deploy` стадии (оператор ошибся) → срабатывает как обычный approve и продвигает чужой гейт | Низ | Сред | `handle_confirm_deploy` гардит `stage == "deploy"`; иначе no-op с логом | TC-04 (+ ручная верификация) | +| R-5 | Кэш `get_project_states` закэширован до создания статуса «Confirm Deploy» → ключ не виден после конфигурации Plane | Сред | Сред (деплой не запускается) | После создания статуса в Plane — `reload_project_states(orch)` или штатный рестарт по стадии `deploy`; зафиксировано в `07-infra-requirements.md` | ручная верификация | +| R-6 | Новый kwarg `confirm_deploy` ломает существующие вызовы `advance_stage` (launcher/reconciler/finalizer) | Низ | Выс | keyword-only с дефолтом `False`; все вызовы передают `finished_agent`; не-`deploy`/finished_agent≠None пути не затронуты | RG-01, AC-9 | +| R-7 | Регрессия идемпотентности Фазы B (двойной `Confirm Deploy`) | Низ | Сред | Внутренности `_handle_self_deploy_phase_b` (маркер `initiated`) не меняются; меняется только триггер | TC-08, AC-5 | +| R-8 | Реконсилятор F-1 на `deploy` (finished_agent=None) меняет поведение | Низ | Низ (улучшение) | Намеренно: раньше неявно мог войти в Фазу B, теперь → no-op. Прод-деплой инициируется только явным `Confirm Deploy`. Документировано в ADR/README | RG-01 | +| R-9 | Несовпадение имени статуса в Plane и `_PLANE_NAME_TO_KEY` (регистр/пробел) → ключ не резолвится | Сред | Сред (деплой не запускается, fail-closed) | Точное имя «Confirm Deploy» (case-sensitive) — требование среды в `07-infra-requirements.md`; маппинг ровно этой строкой | TC-01, TC-02 | + +## Сводный вывод +Все риски — низкого/среднего остаточного уровня после митигаций. Доминирующий +класс — **fail-closed**: любая неполнота конфигурации (нет статуса, протухший кэш, +недоступный API) приводит к «деплой не запускается», а не к «деплой запускается +вслепую» или к крашу. Контракты конвейера (`STAGE_TRANSITIONS`, `QG_CHECKS`, +`check_deploy_status`, Фазы A/C, merge-gate, схема БД) не затрагиваются, поэтому +поверхность регрессии ограничена тремя модулями (`plane_sync.py`, +`webhooks/plane.py`, `stage_engine.py`).