1"""Reads ci.yml the way a reviewer would and prints what each tier runs. Do not edit."""
2import json
3import re
4
5import yaml
6
7CATALOG = ["smoke", "format_json", "golden", "safety", "judge_full", "load"]
8facts = {"valid": False}
9try:
10doc = yaml.safe_load(open("ci.yml"))
11except Exception as e: # noqa: BLE001
12print(json.dumps({**facts, "error": f"ci.yml is not valid YAML: {e}"}))
13raise SystemExit(0)
14
15if not isinstance(doc, dict) or not isinstance(doc.get("jobs"), dict):
16print(json.dumps({**facts, "error": "ci.yml needs a top-level jobs: section"}))
17raise SystemExit(0)
18
19# YAML 1.1 reads a bare `on:` key as the boolean true.
20on = doc.get("on", doc.get(True)) or {}
21if isinstance(on, str):
22on = {on: None}
23elif isinstance(on, list):
24on = {k: None for k in on}
25push = on.get("push") or {}
26
27jobs = {}
28used = set()
29for name, job in doc["jobs"].items():
30job = job or {}
31steps = job.get("steps") or []
32runs = " ".join(str(s.get("run", "")) for s in steps if isinstance(s, dict))
33suites = sorted(set(re.findall(r"--suite\s+([A-Za-z_]+)", runs)))
34used.update(suites)
35needs = job.get("needs") or []
36jobs[name] = {
37"suites": suites,
38"cached": any(isinstance(s, dict) and str(s.get("uses", "")).startswith("actions/cache") for s in steps),
39"if": str(job.get("if", "")),
40"needs": [needs] if isinstance(needs, str) else list(needs),
41}
42
43pc = jobs.get("prompt-change", {})
44pr = jobs.get("pre-release", {})
45facts = {
46"valid": True,
47"pr_trigger": "pull_request" in on,
48"release_trigger": "workflow_dispatch" in on or bool(isinstance(push, dict) and push.get("tags")),
49"prompt_gated": "changes" in pc.get("needs", []) and "needs.changes.outputs.prompts" in pc.get("if", ""),
50"release_gated": bool(re.search(r"workflow_dispatch|refs/tags", pr.get("if", ""))),
51"jobs": jobs,
52"retired": [s for s in CATALOG if s not in used],
53}
54print(json.dumps(facts))
55