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>
66 lines
2.7 KiB
Python
66 lines
2.7 KiB
Python
"""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
|