from __future__ import annotations
import argparse
import json
import subprocess
import sys
from dataclasses import dataclass, field
from typing import Optional
CROSS_PLATFORM_RELEVANT_PREFIXES = (
"Cargo.toml",
"Cargo.lock",
"src/",
"crates/",
"handlers/",
"models/",
"scripts/",
"smoke-test/",
"Dockerfile",
".github/workflows/",
"release-plan.toml",
)
@dataclass(frozen=True)
class AdmissionDecision:
admitted: bool
reasons: list[str] = field(default_factory=list)
def classify_cross_platform_relevance(changed_files: list[str]) -> bool:
for path in changed_files:
for prefix in CROSS_PLATFORM_RELEVANT_PREFIXES:
if path == prefix or path.startswith(prefix):
return True
return False
def _aggregate_run_status(runs: list[dict]) -> str:
if not runs:
return "missing"
if any(r["conclusion"] == "success" for r in runs):
return "success"
if any(r["status"] in ("queued", "in_progress") for r in runs):
return "in_progress"
return "failed"
def evaluate_admission(
*,
tag_sha: str,
ci_status: str,
cross_status: str,
cross_relevant: bool,
security_status: str,
has_successful_release: bool,
has_active_release: bool,
) -> AdmissionDecision:
reasons: list[str] = []
if has_successful_release:
reasons.append(
"a successful Release already exists for this tag; successful release "
"assets are immutable and must not be mutated — use a new version"
)
if has_active_release:
reasons.append(
"a Release run is already active for this tag; refusing to run a "
"concurrent release"
)
if ci_status != "success":
reasons.append(
f"CI / Release push gate on {tag_sha} is {ci_status} (must be completed+success)"
)
if security_status != "success":
reasons.append(
f"Security audit gate on {tag_sha} is {security_status} (must be completed+success)"
)
if cross_relevant:
if cross_status != "success":
reasons.append(
f"Cross-Platform Verification gate on {tag_sha} is {cross_status} "
"(must be completed+success; the commit touched cross-platform-relevant paths)"
)
else:
if cross_status == "missing":
pass elif cross_status != "success":
reasons.append(
f"Cross-Platform Verification run on {tag_sha} is {cross_status}"
)
return AdmissionDecision(admitted=not reasons, reasons=reasons)
def _gh_run_list(workflow: str, fields: list[str], limit: int = 100) -> list[dict]:
cmd = [
"gh", "run", "list",
"--workflow", workflow,
"--json", ",".join(fields),
"--limit", str(limit),
]
out = subprocess.run(cmd, check=True, capture_output=True, text=True)
return json.loads(out.stdout)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--tag", required=True, help="release tag, e.g. v1.2.0-rc.2")
parser.add_argument("--tag-sha", required=True, help="commit SHA the tag points at")
parser.add_argument(
"--current-run-id",
default=None,
help="the pipeline run invoking this gate (excluded from active-release checks)",
)
parser.add_argument("--github-output", default=None)
parser.add_argument(
"--json",
action="store_true",
help="emit the decision as JSON on stdout (for tests and CI logs)",
)
args = parser.parse_args()
repo = subprocess.run(
["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"],
check=True, capture_output=True, text=True,
).stdout.strip()
files = subprocess.run(
["gh", "api", f"repos/{repo}/commits/{args.tag_sha}",
"--jq", ".files[].filename"],
check=True, capture_output=True, text=True,
).stdout.splitlines()
cross_relevant = classify_cross_platform_relevance(files)
ci_runs = [
r for r in _gh_run_list("CI / Release", ["headSha", "event", "status", "conclusion"])
if r.get("event") == "push" and r["headSha"] == args.tag_sha
]
cross_runs = [
r for r in _gh_run_list("Cross-Platform Verification", ["headSha", "status", "conclusion"])
if r["headSha"] == args.tag_sha
]
security_runs = [
r for r in _gh_run_list("Security audit", ["headSha", "status", "conclusion"])
if r["headSha"] == args.tag_sha
]
release_runs = [
r for r in _gh_run_list(
"CI / Release", ["displayTitle", "status", "conclusion", "databaseId"]
)
if r["displayTitle"] == f"Release {args.tag}"
]
current_run_id = args.current_run_id
successful_release = any(
r["conclusion"] == "success"
and (current_run_id is None or str(r.get("databaseId")) != current_run_id)
for r in release_runs
)
active_release = any(
r["status"] in ("queued", "in_progress")
and (current_run_id is None or str(r.get("databaseId")) != current_run_id)
for r in release_runs
)
decision = evaluate_admission(
tag_sha=args.tag_sha,
ci_status=_aggregate_run_status(ci_runs),
cross_status=_aggregate_run_status(cross_runs),
cross_relevant=cross_relevant,
security_status=_aggregate_run_status(security_runs),
has_successful_release=successful_release,
has_active_release=active_release,
)
if args.json:
print(json.dumps({
"admitted": decision.admitted,
"reasons": decision.reasons,
"cross_relevant": cross_relevant,
}))
else:
for reason in decision.reasons:
print(f"::error::{reason}", file=sys.stderr)
if decision.admitted:
print(f"Release {args.tag} admitted at {args.tag_sha} (CI/cross-platform/security same-SHA gates passed)")
if args.github_output is not None:
admitted = "true" if decision.admitted else "false"
with open(args.github_output, "a", encoding="utf-8") as handle:
handle.write(f"admitted={admitted}\n")
return 0 if decision.admitted else 1
if __name__ == "__main__":
raise SystemExit(main())