Files
orchestrator/docs/work-items/ORCH-059/06-adr/ADR-001-confirm-deploy-status.md

12 KiB
Raw Blame History

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.updatedhandle_issue_updatedhandle_verdict(approved=True)_try_advance_stageadvance_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-ветку:

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, но деплоил только по сигналу:

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 = ApprovedConfirm 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 (ApprovedConfirm 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 (см. Последствия).