feat(deploy): dedicated "Confirm Deploy" status triggers prod deploy
Split the overloaded `Approved` Plane status: it served BOTH as the human BRD
gate on `analysis` AND as the silent Phase B prod-deploy trigger on `deploy`
(ORCH-036), so a routine approve could launch a self-hosting prod restart.
ORCH-059 introduces a dedicated logical status `confirm_deploy` ("Confirm
Deploy") that triggers ONLY Phase B on `deploy`; `Approved` stays purely a
pipeline gate.
- plane_sync: map "Confirm Deploy" -> "confirm_deploy" in _PLANE_NAME_TO_KEY;
intentionally absent from _DEFAULT_STATES => fail-closed (no UUID -> .get
yields None, no KeyError, no blind deploy).
- webhooks/plane: handle_issue_updated routes "Confirm Deploy" (fail-closed
.get) to new handle_confirm_deploy (guarded to stage=="deploy") ->
_try_advance_stage(confirm_deploy=True).
- stage_engine: advance_stage gains keyword-only confirm_deploy=False; Phase B
block returns early for deploy+finished_agent is None but only initiates the
deploy when confirm_deploy=True; a plain Approved is a deterministic no-op
(returns before check_deploy_status -> no false БАГ-8 rollback).
- Phase A CTA now asks the operator for "Confirm Deploy", not "Approved".
Contracts unchanged: STAGE_TRANSITIONS, QG_CHECKS, check_deploy_status, hook
exit codes, Phases A/C, merge-gate, DB schema. Conditional like ORCH-35/36
(self-hosting only). Docs updated (CLAUDE.md, architecture/README.md, CHANGELOG).
Refs: ORCH-059
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -150,8 +150,15 @@ async def handle_issue_updated(data: dict, project_id: str = ""):
|
||||
# both enduro (b873d9eb) and orchestrator (e331bfb3) In Progress trigger the
|
||||
# pipeline. Using PLANE_STATES["in_progress"] here was the root-cause blocker.
|
||||
proj_states = get_project_states(project_id)
|
||||
# ORCH-059: the dedicated "Confirm Deploy" status is the prod-deploy trigger.
|
||||
# fail-closed via .get — environments without the status (enduro / API
|
||||
# fallback) resolve to None, so the branch simply never activates (no KeyError,
|
||||
# no blind deploy). Checked before `approved` so the two gestures never alias.
|
||||
confirm_state = proj_states.get("confirm_deploy")
|
||||
if new_state == proj_states["in_progress"]:
|
||||
await handle_status_start(data, project_id)
|
||||
elif confirm_state and new_state == confirm_state:
|
||||
await handle_confirm_deploy(data, project_id)
|
||||
elif new_state == proj_states["approved"]:
|
||||
await handle_verdict(data, project_id, approved=True)
|
||||
elif new_state == proj_states["rejected"]:
|
||||
@@ -160,6 +167,45 @@ async def handle_issue_updated(data: dict, project_id: str = ""):
|
||||
logger.info(f"issue {plane_id} updated to state {new_state[:8]}..., no pipeline action")
|
||||
|
||||
|
||||
async def handle_confirm_deploy(data: dict, project_id: str = ""):
|
||||
"""ORCH-059: a human flipped the issue to the dedicated "Confirm Deploy"
|
||||
status — the explicit trigger for the self-hosting prod deploy (Phase B).
|
||||
|
||||
Guarded to the `deploy` stage: "Confirm Deploy" is only meaningful on the
|
||||
approval-pending `deploy` stage (Phase A advanced the task there). On any
|
||||
other stage it is a no-op-with-log, so a stray Confirm Deploy can never
|
||||
perturb another gate.
|
||||
|
||||
Routes to the unified stage engine with ``confirm_deploy=True`` so ONLY this
|
||||
path initiates Phase B; a plain Approved on `deploy` stays a no-op (TRZ-3).
|
||||
"""
|
||||
plane_id = str(data.get("id") or "")
|
||||
task = get_task_by_plane_id(plane_id)
|
||||
if not task:
|
||||
logger.warning(f"Confirm Deploy for {plane_id} but no task found, ignoring")
|
||||
return
|
||||
|
||||
task_id = task["id"]
|
||||
current_stage = task["stage"]
|
||||
repo = task["repo"]
|
||||
work_item_id = task.get("work_item_id", "")
|
||||
branch = task.get("branch", "")
|
||||
|
||||
if current_stage != "deploy":
|
||||
logger.info(
|
||||
f"Confirm Deploy for {plane_id} but stage is '{current_stage}' "
|
||||
f"(not 'deploy'); no-op"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
f"Task {task_id}: Confirm Deploy status on `deploy` -> initiate Phase B prod deploy"
|
||||
)
|
||||
await _try_advance_stage(
|
||||
task_id, current_stage, repo, work_item_id, branch, confirm_deploy=True
|
||||
)
|
||||
|
||||
|
||||
async def handle_status_start(data: dict, project_id: str = ""):
|
||||
"""An issue moved into In Progress.
|
||||
|
||||
@@ -633,7 +679,8 @@ async def _rollback_stage(
|
||||
|
||||
|
||||
async def _try_advance_stage(
|
||||
task_id: int, current_stage: str, repo: str, work_item_id: str, branch: str
|
||||
task_id: int, current_stage: str, repo: str, work_item_id: str, branch: str,
|
||||
confirm_deploy: bool = False,
|
||||
):
|
||||
"""Thin async wrapper over the unified stage engine (ORCH-4 / M-3).
|
||||
|
||||
@@ -642,10 +689,15 @@ async def _try_advance_stage(
|
||||
is synchronous. We run it off the event loop via asyncio.to_thread so there
|
||||
is exactly one implementation shared with the launcher.
|
||||
|
||||
finished_agent is None on this webhook path (a human Approved status change,
|
||||
not a finished agent), so the agent-specific rollback branches inside the
|
||||
engine intentionally do not trigger — the webhook path only runs the QG and
|
||||
either advances or reports the failure.
|
||||
finished_agent is None on this webhook path (a human status change, not a
|
||||
finished agent), so the agent-specific rollback branches inside the engine
|
||||
intentionally do not trigger — the webhook path only runs the QG and either
|
||||
advances or reports the failure.
|
||||
|
||||
ORCH-059: ``confirm_deploy`` is threaded through (keyword-only on
|
||||
advance_stage). It is True ONLY on the "Confirm Deploy" path
|
||||
(handle_confirm_deploy) and gates Phase B of the self-hosting prod deploy; the
|
||||
plain Approved path (handle_verdict) leaves it at the default False.
|
||||
"""
|
||||
import asyncio
|
||||
from ..stage_engine import advance_stage
|
||||
@@ -658,6 +710,7 @@ async def _try_advance_stage(
|
||||
work_item_id,
|
||||
branch,
|
||||
None,
|
||||
confirm_deploy=confirm_deploy,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user