fix(deploy): terminal-window-aware guard so done tasks hold Done in Plane (ORCH-094)
A DB stage=done task with 0 active jobs flapped in Plane between `Awaiting Deploy` and `Monitoring after Deploy` instead of holding `Done` (verified live on ORCH-061, task 47): the three deploy-phase setters were terminal-blind, so any stale/duplicate/unknown caller under the bot token re-stamped an intermediate status over the terminal Done, forever. - New leaf src/deploy_status_guard.py (pure, never-raise, config-gated): decide() -> ALLOW | CONVERGE_DONE | SUPPRESS on the entry of set_issue_awaiting_deploy / set_issue_deploying / set_issue_monitoring. A deploy-phase status is legitimate iff the task is non-terminal OR (done AND post-deploy window active); otherwise done converges to Done idempotently, cancelled is suppressed (FR-2, D1/D2). - D3: move post_deploy.arm_monitor ABOVE the terminal-sync block in advance_stage so window_active is True when the legitimate first Monitoring is set (the task is already DB-done by then); a re-drive after the window closes converges to Done. - D4: run_post_deploy_monitor no-ops without a status PATCH / re-queue when the task became cancelled mid-window (zombie-tick guard, FR-3). - D5: additive `reason` kwarg on the three setters + one structured log line per verdict (work_item/caller/target/db_stage/window_active/verdict); new read-only db.get_task_by_work_item_id; post_deploy.window_active helper. - Flags deploy_status_guard_enabled (kill-switch -> 1:1) / deploy_status_guard_repos (CSV; empty = self-hosting only). STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys / DB schema untouched (reads existing tasks.stage). Tests: TC-01..TC-12 across 5 new test modules + config flags; updated the reason-kwarg assertions in test_deploy_terminal_sync / test_deploy_approve. Full regress green (1413). Docs: CHANGELOG, CLAUDE.md, docs/architecture/README.md (status -> реализовано), .env.example. Refs: ORCH-094 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -951,32 +951,67 @@ def set_issue_code_review(work_item_id: str, project_id: str = None):
|
||||
_set_issue_state_direct(work_item_id, state_id, project_id)
|
||||
|
||||
|
||||
def set_issue_awaiting_deploy(work_item_id: str, project_id: str = None):
|
||||
def _deploy_status_guarded(work_item_id: str, target: str, reason: str | None) -> bool:
|
||||
"""ORCH-094: apply the terminal-window-aware guard for a deploy-phase setter.
|
||||
|
||||
Returns True iff the caller should PROCEED with the normal PATCH (verdict
|
||||
ALLOW). On CONVERGE_DONE it drives the task to terminal ``Done`` here (the
|
||||
idempotent convergence target) and returns False; on SUPPRESS it does nothing
|
||||
and returns False. never-raise: any error degrades to ALLOW (proceed), keeping
|
||||
behaviour 1:1 with pre-ORCH-094 (the guard leaf itself fails safe to ALLOW).
|
||||
"""
|
||||
try:
|
||||
from . import deploy_status_guard
|
||||
verdict = deploy_status_guard.decide(work_item_id, target, reason=reason)
|
||||
if verdict == deploy_status_guard.CONVERGE_DONE:
|
||||
set_issue_done(work_item_id)
|
||||
return False
|
||||
if verdict == deploy_status_guard.SUPPRESS:
|
||||
return False
|
||||
return True
|
||||
except Exception as e: # noqa: BLE001 - never-raise; proceed (1:1) on doubt
|
||||
logger.warning(f"deploy_status_guard wrapper error for {work_item_id}: {e}")
|
||||
return True
|
||||
|
||||
|
||||
def set_issue_awaiting_deploy(work_item_id: str, project_id: str = None, reason: str = None):
|
||||
"""ORCH-066: set issue to 'Awaiting Deploy' — self-deploy Phase A approval-pending.
|
||||
|
||||
Degrades to the project's In Review UUID when 'Awaiting Deploy' is not created.
|
||||
ORCH-094: terminal-window-aware — a task whose DB stage is terminal converges to
|
||||
Done instead of stamping a spurious deploy status (``reason`` = caller, FR-4).
|
||||
"""
|
||||
if not _deploy_status_guarded(work_item_id, "awaiting", reason):
|
||||
return
|
||||
project_id = _resolve_project_id(work_item_id, project_id)
|
||||
state_id = get_project_states(project_id)["awaiting_deploy"]
|
||||
_set_issue_state_direct(work_item_id, state_id, project_id)
|
||||
|
||||
|
||||
def set_issue_deploying(work_item_id: str, project_id: str = None):
|
||||
def set_issue_deploying(work_item_id: str, project_id: str = None, reason: str = None):
|
||||
"""ORCH-066: set issue to 'Deploying' — self-deploy Phase B prod deploy in flight.
|
||||
|
||||
Degrades to the project's In Progress UUID when 'Deploying' is not created.
|
||||
ORCH-094: terminal-window-aware (see :func:`set_issue_awaiting_deploy`).
|
||||
"""
|
||||
if not _deploy_status_guarded(work_item_id, "deploying", reason):
|
||||
return
|
||||
project_id = _resolve_project_id(work_item_id, project_id)
|
||||
state_id = get_project_states(project_id)["deploying"]
|
||||
_set_issue_state_direct(work_item_id, state_id, project_id)
|
||||
|
||||
|
||||
def set_issue_monitoring(work_item_id: str, project_id: str = None):
|
||||
def set_issue_monitoring(work_item_id: str, project_id: str = None, reason: str = None):
|
||||
"""ORCH-066: set issue to 'Monitoring after Deploy' — post-deploy window open.
|
||||
|
||||
Degrades to the project's Done UUID when 'Monitoring after Deploy' is not
|
||||
created (so the board shows Done, exactly as before ORCH-066).
|
||||
ORCH-094: terminal-window-aware — the LEGITIMATE first Monitoring (DB already
|
||||
``done`` by the time line 404 runs, but the post-deploy window is active) is
|
||||
allowed; a stale Monitoring after the window has closed converges to Done.
|
||||
"""
|
||||
if not _deploy_status_guarded(work_item_id, "monitoring", reason):
|
||||
return
|
||||
project_id = _resolve_project_id(work_item_id, project_id)
|
||||
state_id = get_project_states(project_id)["monitoring"]
|
||||
_set_issue_state_direct(work_item_id, state_id, project_id)
|
||||
|
||||
Reference in New Issue
Block a user