"""Gate: the repo's website passes the shared AgentReady checker. Usage: agent_ready_gate.py [--repo PATH] [--strict] [--install] Reads `[agent_ready]` from repo-contract.toml (see web.py). dist mode runs the `build` command in a throwaway copy of the tree (never the live dist), then `agent_ready/check.sh dist `; url mode runs `check.sh url `. `--install` runs `install` first (CI: fresh checkout). Exit: 0 pass / no site / skip, 1 FAIL (checker, build or missing config), 2 malformed config, 3 undeterminable (url mode, only with --strict). Without --strict an unreachable site degrades to a pass with a banner, like dependency-freshness: `--no-verify` is banned, so an offline commit must still go through, and CI (always online, always --strict) is the authority. """ from __future__ import annotations import argparse import os import subprocess import sys import tempfile from pathlib import Path if __package__ in (None, ""): # pragma: no cover - plain-script import path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from repo_contract.isolate import populate from repo_contract.web import CONFIG, TABLE, AgentReady, ConfigError, detect, load CHECKER = Path(__file__).resolve().parents[1] / "agent_ready" / "check.sh" EXIT_OK, EXIT_FAIL, EXIT_CONFIG, EXIT_UNDETERMINABLE = 0, 1, 2, 3 TAIL = 40 def _run_step(repo: Path, label: str, command: str) -> bool: """Run a build/install command quietly; show its tail only on failure.""" done = subprocess.run( ["bash", "-c", command], cwd=repo, capture_output=True, text=True, check=False, ) if done.returncode == 0: return True tail = (done.stdout + done.stderr).splitlines()[-TAIL:] print("\n".join(tail), file=sys.stderr) print( f"agent-ready: {label} failed (exit {done.returncode}): {command}", file=sys.stderr, ) return False def _run_checker(cfg: AgentReady, args: list[str], *, strict: bool) -> int: """Run the shared checker on `args` (mode + target); degrade exit 3 locally.""" argv = [str(CHECKER), *args, *(["--act"] if cfg.act else [])] code = subprocess.run(["bash", *argv], check=False).returncode if code == EXIT_UNDETERMINABLE and not strict: print( f"agent-ready: DEGRADED: {cfg.target} unreachable, passing locally; " "CI (--strict) is the authority", file=sys.stderr, ) return EXIT_OK return code def _check(repo: Path, cfg: AgentReady, *, strict: bool, install: bool) -> int: """Build if configured, then run the shared checker; returns the exit code. The build runs in a throwaway copy (isolate.py): the live `dist` may be bind-mounted into a serving container, and a gate must never empty it. """ if cfg.mode == "url": return _run_checker(cfg, ["url", cfg.target], strict=strict) if install and cfg.install and not _run_step(repo, "install", cfg.install): return EXIT_FAIL if not cfg.build: return _run_checker(cfg, ["dist", str(repo / cfg.target)], strict=strict) with tempfile.TemporaryDirectory(prefix="agent-ready-") as tmp: copy = Path(tmp) / repo.name copy.mkdir() populate(repo, copy) if not _run_step(copy, "build", cfg.build): return EXIT_FAIL return _run_checker(cfg, ["dist", str(copy / cfg.target)], strict=strict) def main(argv: list[str] | None = None) -> int: """CLI entry point; returns the process exit code.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo", type=Path, default=Path.cwd()) parser.add_argument("--strict", action="store_true", help="CI: offline fails") parser.add_argument("--install", action="store_true", help="run `install` first") args = parser.parse_args(argv) repo = args.repo.expanduser().resolve() try: cfg = load(repo) except ConfigError as exc: print(f"agent-ready: {CONFIG}: {exc}", file=sys.stderr) return EXIT_CONFIG if cfg is None: if detect(repo) is None: print("agent-ready: no website in this repo") return EXIT_OK print( f"agent-ready: web build detected but {CONFIG} has no [{TABLE}]; run " "~/src/utils/scripts/install_repo_contract_gate.sh", file=sys.stderr, ) return EXIT_FAIL if cfg.mode == "skip": print(f"agent-ready: skipped: {cfg.target}") return EXIT_OK if os.environ.get("GITHUB_ACTIONS") and not args.strict: # A repo's generic pre-commit workflow has no node toolchain or deps; # the dedicated agent-ready workflow builds and checks with --strict. print("agent-ready: CI pre-commit run; the agent-ready workflow enforces it") return EXIT_OK return _check(repo, cfg, strict=args.strict, install=args.install) if __name__ == "__main__": # pragma: no cover sys.exit(main())