496 lines
17 KiB
Python
496 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""Strictly preflighted, discoverable, and self-cleaning armed mode."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import configparser
|
|
import re
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from hermes_handoff_exec import Outcome, Runner, Vantage
|
|
from hermes_handoff_model import EPHEMERAL, FAIL, NOT_RUN, PASS, CheckResult, CheckSpec
|
|
from hermes_handoff_policy import (
|
|
EXPECTED_BASE,
|
|
EXPECTED_REMOTE,
|
|
EXPECTED_REPO,
|
|
GITEA_CLIENT,
|
|
arm_ephemeral_policy,
|
|
register_ephemeral_pull,
|
|
)
|
|
from hermes_handoff_rules import strict_json
|
|
|
|
CONFIRMATION = "ARM EPHEMERAL HERMES HANDOFF PUSH"
|
|
REF_PREFIX = "ephemeral/hermes-handoff-acceptance"
|
|
REF_RE = re.compile(rf"{REF_PREFIX}/[a-z0-9][a-z0-9-]{{7,63}}\Z")
|
|
TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9-]{7,63}\Z")
|
|
SHA_RE = re.compile(r"[0-9a-f]{40}\Z")
|
|
DRAFT_TITLE_PREFIX = "WIP: "
|
|
EXPECTED_HEAD_REF = "feature/hermes-full-handoff-acceptance"
|
|
EXPECTED_REMOTE_URL = "https://scm.bstein.dev/atlas/titan-iac.git"
|
|
PROTECTED_NAMES = frozenset(
|
|
{
|
|
"default",
|
|
"develop",
|
|
"head",
|
|
"main",
|
|
"master",
|
|
"prod",
|
|
"production",
|
|
"release",
|
|
"stable",
|
|
"trunk",
|
|
}
|
|
)
|
|
|
|
|
|
class ArmingError(ValueError):
|
|
"""Raised when an ephemeral run is not safe to start."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ArmRequest:
|
|
"""Every fixed and caller-confirmed input to one armed run."""
|
|
|
|
repo: str
|
|
remote: str
|
|
token: str
|
|
confirmation: str
|
|
base: str = EXPECTED_BASE
|
|
expected_head: str = ""
|
|
expected_base_sha: str = ""
|
|
|
|
@property
|
|
def ref(self) -> str:
|
|
return f"{REF_PREFIX}/{self.token}"
|
|
|
|
|
|
def normalise_branch(name: str) -> str:
|
|
"""Return a branch name stripped of ref decoration, for comparison."""
|
|
return name.strip().removeprefix("refs/heads/").strip("/").lower()
|
|
|
|
|
|
def assert_push_target_allowed(ref: str) -> None:
|
|
"""Raise unless ``ref`` is the one ephemeral branch grammar."""
|
|
if normalise_branch(ref) in PROTECTED_NAMES:
|
|
raise ArmingError(f"refusing to push to protected branch {ref!r}")
|
|
if not REF_RE.fullmatch(ref):
|
|
raise ArmingError(f"push target {ref!r} is not an ephemeral acceptance ref")
|
|
|
|
|
|
def _bounded_read(path: Path, limit: int = 1024 * 1024) -> str:
|
|
data = path.read_bytes()
|
|
if not data or len(data) > limit:
|
|
raise ArmingError(f"local Git metadata is empty or exceeds {limit} bytes")
|
|
return data.decode("utf-8", "strict")
|
|
|
|
|
|
def _attest_worktree(root: Path, expected_head: str, expected_base_sha: str) -> None:
|
|
"""Bind cwd, common Git metadata, branch, remote, and exact local HEAD."""
|
|
resolved = root.resolve(strict=True)
|
|
marker = resolved / ".git"
|
|
if not marker.is_file():
|
|
raise ArmingError(
|
|
"armed mode must run at the root of the existing linked worktree"
|
|
)
|
|
line = _bounded_read(marker, 4096).strip()
|
|
if not line.startswith("gitdir: "):
|
|
raise ArmingError("linked-worktree Git metadata is malformed")
|
|
gitdir = Path(line.removeprefix("gitdir: ")).resolve(strict=True)
|
|
common = (gitdir / _bounded_read(gitdir / "commondir", 4096).strip()).resolve(
|
|
strict=True
|
|
)
|
|
head = _bounded_read(gitdir / "HEAD", 4096).strip()
|
|
expected_ref = f"refs/heads/{EXPECTED_HEAD_REF}"
|
|
if head != f"ref: {expected_ref}":
|
|
raise ArmingError("armed mode must run from the existing PR #19 feature branch")
|
|
ref_path = common / expected_ref
|
|
actual_head = _bounded_read(ref_path, 4096).strip() if ref_path.is_file() else ""
|
|
if actual_head != expected_head:
|
|
raise ArmingError(
|
|
"local worktree HEAD does not match the exact reviewed PR head"
|
|
)
|
|
remote_ref = common / "refs" / "remotes" / EXPECTED_REMOTE / EXPECTED_BASE
|
|
remote_main = (
|
|
_bounded_read(remote_ref, 4096).strip() if remote_ref.is_file() else ""
|
|
)
|
|
if remote_main != expected_base_sha:
|
|
raise ArmingError("local origin/main does not match the exact release base SHA")
|
|
parser = configparser.RawConfigParser(interpolation=None)
|
|
try:
|
|
parser.read_string(_bounded_read(common / "config"))
|
|
remote_url = parser.get(f'remote "{EXPECTED_REMOTE}"', "url")
|
|
except (configparser.Error, KeyError) as exc:
|
|
raise ArmingError("local Git remote metadata is malformed") from exc
|
|
if remote_url != EXPECTED_REMOTE_URL:
|
|
raise ArmingError("origin is not the fixed Atlas titan-iac HTTPS repository")
|
|
|
|
|
|
def preflight(
|
|
request: ArmRequest, expected_repo: str, worktree: Path | None = None
|
|
) -> None:
|
|
"""Validate arming and local provenance before any subprocess/network call."""
|
|
if request.confirmation != CONFIRMATION:
|
|
raise ArmingError("confirmation phrase does not match")
|
|
if expected_repo != EXPECTED_REPO or request.repo != EXPECTED_REPO:
|
|
raise ArmingError(
|
|
"armed repository is not the fixed Atlas titan-iac repository"
|
|
)
|
|
if (
|
|
request.remote != EXPECTED_REMOTE
|
|
or normalise_branch(request.base) != EXPECTED_BASE
|
|
):
|
|
raise ArmingError("armed remote and pull-request base are fixed to origin/main")
|
|
if not TOKEN_RE.fullmatch(request.token):
|
|
raise ArmingError(
|
|
"ephemeral token must be 8-64 lowercase alphanumeric or dash characters"
|
|
)
|
|
if not SHA_RE.fullmatch(request.expected_head) or not SHA_RE.fullmatch(
|
|
request.expected_base_sha
|
|
):
|
|
raise ArmingError("exact reviewed head and release base SHAs are required")
|
|
assert_push_target_allowed(request.ref)
|
|
_attest_worktree(
|
|
worktree or Path.cwd(), request.expected_head, request.expected_base_sha
|
|
)
|
|
arm_ephemeral_policy(request.ref)
|
|
|
|
|
|
def _spec(identifier: str, title: str) -> CheckSpec:
|
|
return CheckSpec(
|
|
id=f"ephemeral.{identifier}",
|
|
title=title,
|
|
group="ephemeral",
|
|
rule="armed",
|
|
steps=(),
|
|
mandatory=True,
|
|
scope=EPHEMERAL,
|
|
)
|
|
|
|
|
|
def _result(
|
|
identifier: str,
|
|
title: str,
|
|
status: str,
|
|
reason: str,
|
|
outcomes: Sequence[Outcome] = (),
|
|
evidence: dict[str, Any] | None = None,
|
|
) -> CheckResult:
|
|
return CheckResult(
|
|
spec=_spec(identifier, title),
|
|
status=status,
|
|
reason=reason,
|
|
evidence=evidence or {},
|
|
outcomes=list(outcomes),
|
|
)
|
|
|
|
|
|
def _protected_refusal_result() -> CheckResult:
|
|
accepted: list[str] = []
|
|
for candidate in ("main", "master", "refs/heads/main", "HEAD", f"{REF_PREFIX}/x"):
|
|
try:
|
|
assert_push_target_allowed(candidate)
|
|
except ArmingError:
|
|
continue
|
|
accepted.append(candidate)
|
|
return _result(
|
|
"protected-branch-refusal",
|
|
"Arming against a protected branch is refused before any network call",
|
|
FAIL if accepted else PASS,
|
|
f"guard accepted {accepted}"
|
|
if accepted
|
|
else "every protected and malformed target was refused",
|
|
evidence={"accepted": accepted},
|
|
)
|
|
|
|
|
|
def _detail(outcome: Outcome) -> str:
|
|
return (
|
|
outcome.error or outcome.combined[:200] or f"exit status {outcome.returncode}"
|
|
)
|
|
|
|
|
|
def _pr_problems(payload: Any, request: ArmRequest) -> list[str]:
|
|
if not isinstance(payload, dict):
|
|
return ["pull request is not an object"]
|
|
wanted = {
|
|
"state": "open",
|
|
"draft": True,
|
|
"merged": False,
|
|
"base": {"ref": EXPECTED_BASE, "sha": request.expected_base_sha},
|
|
"head": {"ref": request.ref, "sha": request.expected_head},
|
|
}
|
|
problems: list[str] = []
|
|
if (
|
|
not isinstance(payload.get("number"), int)
|
|
or isinstance(payload.get("number"), bool)
|
|
or payload["number"] <= 0
|
|
):
|
|
problems.append("number is malformed")
|
|
for field in ("state", "draft", "merged"):
|
|
if (
|
|
type(payload.get(field)) is not type(wanted[field])
|
|
or payload.get(field) != wanted[field]
|
|
):
|
|
problems.append(f"{field} does not match")
|
|
for group, values in (("base", wanted["base"]), ("head", wanted["head"])):
|
|
observed = payload.get(group)
|
|
if not isinstance(observed, dict):
|
|
problems.append(f"{group} is malformed")
|
|
continue
|
|
for field, value in values.items():
|
|
if observed.get(field) != value:
|
|
problems.append(f"{group}.{field} does not match")
|
|
return problems
|
|
|
|
|
|
def _discover(outcome: Outcome, request: ArmRequest) -> tuple[list[int], str]:
|
|
if not outcome.ok:
|
|
return ([], f"pull-request discovery failed: {_detail(outcome)}")
|
|
try:
|
|
payload = strict_json(outcome.stdout)
|
|
except ValueError as exc:
|
|
return ([], f"pull-request discovery returned malformed JSON: {exc}")
|
|
if not isinstance(payload, list):
|
|
return ([], "pull-request discovery did not return an array")
|
|
matches: list[int] = []
|
|
for index, item in enumerate(payload):
|
|
if (
|
|
not isinstance(item, dict)
|
|
or not isinstance(item.get("head"), dict)
|
|
or not isinstance(item.get("base"), dict)
|
|
):
|
|
return ([], f"pull-request discovery row {index + 1} is malformed")
|
|
if item["head"].get("ref") != request.ref:
|
|
continue
|
|
problems = _pr_problems(item, request)
|
|
if problems:
|
|
return ([], f"discovered pull request is invalid: {', '.join(problems)}")
|
|
matches.append(item["number"])
|
|
if len(matches) != 1:
|
|
return (
|
|
matches,
|
|
f"expected one exact draft pull request, discovered {len(matches)}",
|
|
)
|
|
return (matches, "")
|
|
|
|
|
|
def _closed_pr_problems(payload: Any, request: ArmRequest) -> list[str]:
|
|
"""Validate the same exact PR identity with state changed to closed."""
|
|
problems = _pr_problems(payload, request)
|
|
if isinstance(payload, dict) and payload.get("state") == "closed":
|
|
problems = [
|
|
problem for problem in problems if problem != "state does not match"
|
|
]
|
|
elif "state does not match" not in problems:
|
|
problems.append("state is not closed")
|
|
return problems
|
|
|
|
|
|
def _cleanup(
|
|
runner: Runner,
|
|
vantage: Vantage,
|
|
request: ArmRequest,
|
|
api: str,
|
|
numbers: Sequence[int],
|
|
discovery_error: str = "",
|
|
) -> CheckResult:
|
|
outcomes: list[Outcome] = []
|
|
problems = [discovery_error] if discovery_error else []
|
|
for number in sorted(set(numbers)):
|
|
closed = runner.run(
|
|
(GITEA_CLIENT, "PATCH", f"{api}/pulls/{number}", "--field", "state=closed"),
|
|
vantage,
|
|
)
|
|
outcomes.append(closed)
|
|
if not closed.ok:
|
|
problems.append(f"pull request #{number} close failed")
|
|
continue
|
|
try:
|
|
payload = strict_json(closed.stdout)
|
|
except ValueError:
|
|
problems.append(f"pull request #{number} close response is malformed")
|
|
continue
|
|
expected = _closed_pr_problems(payload, request)
|
|
if expected:
|
|
problems.append(f"pull request #{number} close response does not match")
|
|
deleted = runner.run(
|
|
(GITEA_CLIENT, "DELETE", f"{api}/branches/{request.ref}"), vantage
|
|
)
|
|
outcomes.append(deleted)
|
|
if not deleted.ok:
|
|
problems.append("ephemeral branch deletion failed")
|
|
remaining = runner.run(
|
|
("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"), vantage
|
|
)
|
|
outcomes.append(remaining)
|
|
if not remaining.ok or remaining.stdout.strip():
|
|
problems.append("ephemeral branch absence could not be verified")
|
|
for number in sorted(set(numbers)):
|
|
observed = runner.run((GITEA_CLIENT, "GET", f"{api}/pulls/{number}"), vantage)
|
|
outcomes.append(observed)
|
|
if not observed.ok:
|
|
problems.append(f"pull request #{number} closure could not be verified")
|
|
continue
|
|
try:
|
|
payload = strict_json(observed.stdout)
|
|
except ValueError:
|
|
problems.append(f"pull request #{number} verification is malformed")
|
|
continue
|
|
expected = _closed_pr_problems(payload, request)
|
|
if expected:
|
|
problems.append(f"pull request #{number} is not exactly closed")
|
|
return _result(
|
|
"cleanup-verified",
|
|
"Every ephemeral ref and pull request is removed and the removal is verified",
|
|
FAIL if problems else PASS,
|
|
"; ".join(problems)
|
|
if problems
|
|
else "the exact branch and draft pull request are gone",
|
|
outcomes,
|
|
{"ref": request.ref, "pull_requests": sorted(set(numbers))},
|
|
)
|
|
|
|
|
|
def run_armed(
|
|
runner: Runner, vantage: Vantage, request: ArmRequest
|
|
) -> list[CheckResult]:
|
|
"""Create, discover, validate, and clean one exact ephemeral branch/PR."""
|
|
guard = _protected_refusal_result()
|
|
if guard.status != PASS:
|
|
return [
|
|
guard,
|
|
_result(
|
|
"feature-branch-push",
|
|
"A unique ephemeral feature branch can be pushed and removed",
|
|
NOT_RUN,
|
|
"protected-ref guard failed",
|
|
),
|
|
_result(
|
|
"draft-pull-request",
|
|
"A draft pull request can be opened and closed",
|
|
NOT_RUN,
|
|
"protected-ref guard failed",
|
|
),
|
|
_result(
|
|
"cleanup-verified",
|
|
"Every ephemeral artifact is removed",
|
|
NOT_RUN,
|
|
"nothing was created",
|
|
),
|
|
]
|
|
api = f"/api/v1/repos/{request.repo}"
|
|
reserve = min(6000.0, max(300.0, runner.command_timeout * 10))
|
|
existing = runner.run(
|
|
("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
if not existing.ok or existing.stdout.strip():
|
|
reason = (
|
|
_detail(existing) if not existing.ok else "the ephemeral ref already exists"
|
|
)
|
|
return [
|
|
guard,
|
|
_result(
|
|
"feature-branch-push",
|
|
"A unique ephemeral feature branch can be pushed and removed",
|
|
FAIL,
|
|
reason,
|
|
[existing],
|
|
),
|
|
_result(
|
|
"draft-pull-request",
|
|
"A draft pull request can be opened and closed",
|
|
NOT_RUN,
|
|
"branch preflight failed",
|
|
),
|
|
_result(
|
|
"cleanup-verified",
|
|
"Every ephemeral artifact is removed",
|
|
NOT_RUN,
|
|
"nothing was created",
|
|
),
|
|
]
|
|
local_head = runner.run(
|
|
("git", "rev-parse", "HEAD"), vantage, reserve_seconds=reserve
|
|
)
|
|
pushed = (
|
|
runner.run(
|
|
("git", "push", request.remote, f"HEAD:refs/heads/{request.ref}"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
if local_head.ok and local_head.stdout.strip() == request.expected_head
|
|
else Outcome(
|
|
argv=("git", "push"),
|
|
vantage=vantage.name,
|
|
error="local HEAD changed after static preflight",
|
|
)
|
|
)
|
|
verified = runner.run(
|
|
("git", "ls-remote", request.remote, f"refs/heads/{request.ref}"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
branch_ok = verified.ok and verified.stdout.split() == [
|
|
request.expected_head,
|
|
f"refs/heads/{request.ref}",
|
|
]
|
|
branch_result = _result(
|
|
"feature-branch-push",
|
|
"A unique ephemeral feature branch can be pushed and removed",
|
|
PASS if branch_ok else FAIL,
|
|
"the exact reviewed HEAD was pushed"
|
|
if branch_ok
|
|
else "push could not be verified exactly",
|
|
[local_head, pushed, verified],
|
|
{"ref": request.ref, "head": request.expected_head},
|
|
)
|
|
numbers: list[int] = []
|
|
discovery_error = ""
|
|
create_outcomes: list[Outcome] = []
|
|
if branch_ok:
|
|
created = runner.run(
|
|
(
|
|
GITEA_CLIENT,
|
|
"POST",
|
|
f"{api}/pulls",
|
|
"--field",
|
|
f"title={DRAFT_TITLE_PREFIX}Hermes handoff acceptance ephemeral probe",
|
|
"--field",
|
|
f"head={request.ref}",
|
|
"--field",
|
|
f"base={EXPECTED_BASE}",
|
|
"--field",
|
|
"body=Ephemeral acceptance probe. Closed and deleted by the harness.",
|
|
),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
discovered = runner.run(
|
|
(GITEA_CLIENT, "GET", f"{api}/pulls?state=all&limit=50"),
|
|
vantage,
|
|
reserve_seconds=reserve,
|
|
)
|
|
create_outcomes = [created, discovered]
|
|
numbers, discovery_error = _discover(discovered, request)
|
|
for number in numbers:
|
|
register_ephemeral_pull(number)
|
|
else:
|
|
discovery_error = "branch creation was not verified"
|
|
draft_result = _result(
|
|
"draft-pull-request",
|
|
"A draft pull request can be opened and closed against the ephemeral branch",
|
|
PASS if len(numbers) == 1 and not discovery_error else FAIL,
|
|
f"discovered exact draft pull request #{numbers[0]}"
|
|
if len(numbers) == 1 and not discovery_error
|
|
else discovery_error,
|
|
create_outcomes,
|
|
{"pull_requests": numbers},
|
|
)
|
|
cleanup = _cleanup(runner, vantage, request, api, numbers, discovery_error)
|
|
return [guard, branch_result, draft_result, cleanup]
|