feat(cancel): STOP-status task cancellation + relaunch-hole close (ORCH-090)
Introduce the dedicated Plane STOP status as a single declarative task-cancel
mechanism: stop the active agent (graceful SIGTERM cascade), cancel all jobs
(terminal `cancelled`, never requeued), remove the worktree + delete the remote
feature branch (never main, never force-push), drive the task to the new
system-terminal state `cancelled` and tombstone the natural keys so a later
"To Analyse" re-creates it from scratch (docs artefacts preserved). STOP during a
critical merge/deploy window is deferred until the irreversible step finishes
honestly. Also closes the relaunch hole: handle_status_start relaunch is gated to
the `analysis` stage; the only pipeline-start entry point remains "To Analyse".
Cross-cutting (adr-0026): the "task terminal" predicate is widened {done} ->
{done, cancelled} in serial_gate / task_deps / stages sink + reaper/worker
requeue guards. STAGE_TRANSITIONS exit-gates / QG_CHECKS / check_* are unchanged
(`cancelled` is a sink, not a new edge). Additive, never-raise, restart-safe,
under kill-switch ORCH_STOP_STATUS_ENABLED (off -> zero regression).
New: src/cancel.py (leaf), src/gitea.py (delete_remote_branch), tasks columns
cancelled_at/cancel_requested_at, jobs status `cancelled`, GET /queue `stop` block.
Tests: tests/test_stop_status.py (TC-01..TC-14 + D7); full suite green (1345).
Docs updated in-PR (architecture README, CLAUDE.md, README.md, .env.example,
CHANGELOG). ADR-001 D4 refinement: plane_issue_id is tombstoned too (the lookup
ORs on it) — original UUID recoverable from the parseable suffix.
Refs: ORCH-090
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
65
src/gitea.py
Normal file
65
src/gitea.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""ORCH-090 (ADR-001 D8 / adr-0026): minimal Gitea branch helpers.
|
||||
|
||||
Leaf module — a single never-raise helper used by the STOP-cancellation path to
|
||||
delete a cancelled task's REMOTE feature branch. Deliberately tiny and dependency
|
||||
-light (only ``config`` + ``httpx``) so it can be imported from the stage engine
|
||||
without cycles.
|
||||
|
||||
Self-hosting safety (NFR-3): this helper deletes ONLY the named feature branch
|
||||
via the Gitea API. It NEVER touches ``main`` (a guard rejects it outright) and
|
||||
NEVER force-pushes — there is no push path here at all.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
from .config import settings
|
||||
|
||||
logger = logging.getLogger("orchestrator.gitea")
|
||||
|
||||
# Branches that must never be deleted by an automated cancel (self-hosting safety).
|
||||
_PROTECTED_BRANCHES = {"main", "master"}
|
||||
|
||||
|
||||
def delete_remote_branch(repo: str, branch: str) -> bool:
|
||||
"""Delete a remote feature branch in Gitea (never-raise).
|
||||
|
||||
``DELETE /api/v1/repos/{owner}/{repo}/branches/{branch}``. Used by
|
||||
``stage_engine.cancel_task`` to reset a cancelled task's progress (D8). A 404
|
||||
(branch already gone) is treated as success — the goal state (branch absent) is
|
||||
reached. Returns True iff the branch is confirmed absent after the call.
|
||||
|
||||
Guards:
|
||||
* empty repo/branch -> no-op (False);
|
||||
* a protected branch (``main``/``master``) -> refused with an error log
|
||||
(NFR-3: STOP must never delete ``main``).
|
||||
Any network/API error is logged and swallowed (the worktree is cleaned locally
|
||||
regardless); returns False so the caller can note a best-effort miss.
|
||||
"""
|
||||
if not repo or not branch:
|
||||
return False
|
||||
if branch.strip().lower() in _PROTECTED_BRANCHES:
|
||||
logger.error(
|
||||
"delete_remote_branch REFUSED for protected branch %r in %s (self-hosting safety)",
|
||||
branch, repo,
|
||||
)
|
||||
return False
|
||||
owner = settings.gitea_owner
|
||||
url = f"{settings.gitea_url}/api/v1/repos/{owner}/{repo}/branches/{branch}"
|
||||
headers = {"Authorization": f"token {settings.gitea_token}"}
|
||||
try:
|
||||
resp = httpx.delete(url, headers=headers, timeout=10)
|
||||
if resp.status_code in (204, 200):
|
||||
logger.info("Deleted remote branch %s in %s/%s", branch, owner, repo)
|
||||
return True
|
||||
if resp.status_code == 404:
|
||||
logger.info("Remote branch %s already absent in %s/%s", branch, owner, repo)
|
||||
return True
|
||||
logger.warning(
|
||||
"delete_remote_branch %s in %s/%s returned %s: %s",
|
||||
branch, owner, repo, resp.status_code, resp.text[:200],
|
||||
)
|
||||
return False
|
||||
except Exception as e: # noqa: BLE001 - never-raise
|
||||
logger.warning("delete_remote_branch error for %s/%s/%s: %s", owner, repo, branch, e)
|
||||
return False
|
||||
Reference in New Issue
Block a user