-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck_required_checks.py
More file actions
140 lines (116 loc) · 5.92 KB
/
Copy pathcheck_required_checks.py
File metadata and controls
140 lines (116 loc) · 5.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env python3
"""check_required_checks.py - confront the gate policy against the real repo.
read_ci_verdict consumes .github/required-checks.json, the canonical gate policy
(family G7). The doc-drift `gate` kind already proves the *prose* matches that
JSON; nothing proved the JSON matches the *repo*. This meta-check closes that
half (the second part of the required-checks work), on three decidable rules:
RC1 - a `decisional` name has no scripts/gates/<name>.sh runner. The runners
are the single source of what each gate runs (CONTRIBUTING.md), and each
decisional gate's workflow calls exactly `bash scripts/gates/<name>.sh`,
so a present runner is the stdlib-exact witness that the gate is real and
runnable -- no YAML parser needed (the repo ships none; parsing job names
would be the fragile heuristic METHOD section 6 rejects). This is *why*
"Analyze (python)" cannot be decisional: it has no runner (it is gated by
a code-scanning ruleset, not a status check), so
listing it under `decisional` fires RC1.
RC2 - a `decisional` name is not in `required`. The verdict must not be decided
on a check the branch protection does not even require; this ties the two
name sets so they cannot drift into disjoint sets.
RC3 - the branch ruleset's required_status_checks contexts (.github/rulesets/
main.json) do not match `required` as a set. `required` is the canonical
list; the ruleset payload is the native enforcement posted verbatim by
setup-layer-b.sh (no generation step). Hard-coding the list there mirrors
`required`, so RC3 makes that mirror decidable: any name required-but-not-
enforced, or enforced-but-not-required, reddens instead of drifting in
silence (the unguarded-mirror channel METHOD section 8, precedent C8).
The one channel RC1 leaves open -- a job renamed in YAML without renaming its
runner -- is closed by the live arm: read_ci_verdict runs on a real PR every
close, where a divergent name surfaces that decisional check as MISSING.
The core (`confront`) is pure and offline-testable; the thin shell collects the
runner names and the ruleset contexts from the filesystem and the policy via
load_policy.
"""
from __future__ import annotations
import json
import sys
from dataclasses import dataclass
from pathlib import Path
from read_ci_verdict import PolicyError, load_policy
# Floor-coverage claim (METHOD section 2 / v0.41): three rule codes, one floor.
CODES = frozenset({"RC1", "RC2", "RC3"})
FLOOR = "test_check_required_checks.py"
_REPO_ROOT = Path(__file__).resolve().parent.parent
_REQUIRED_CHECKS_PATH = _REPO_ROOT / ".github" / "required-checks.json"
_RULESET_PATH = _REPO_ROOT / ".github" / "rulesets" / "main.json"
_GATES_DIR = _REPO_ROOT / "scripts" / "gates"
@dataclass(frozen=True)
class Finding:
code: str
name: str
detail: str
def confront(
decisional: tuple[str, ...],
required: tuple[str, ...],
runner_names: frozenset[str],
ruleset_contexts: frozenset[str],
) -> list[Finding]:
"""Policy defects, in `decisional` order then RC3 (sorted). Pure: all inputs
are passed in, so the floor exercises the rules offline without touching the
filesystem."""
findings: list[Finding] = []
for name in decisional:
if name not in runner_names:
findings.append(Finding("RC1", name, f"no scripts/gates/{name}.sh runner"))
if name not in required:
findings.append(Finding("RC2", name, "decisional but not in required"))
required_set = frozenset(required)
for name in sorted(required_set - ruleset_contexts):
findings.append(Finding("RC3", name, "required but not in ruleset required_status_checks"))
for name in sorted(ruleset_contexts - required_set):
findings.append(Finding("RC3", name, "in ruleset required_status_checks but not required"))
return findings
def collect_runner_names(gates_dir: Path) -> frozenset[str]:
"""The gate names that have a scripts/gates/<name>.sh runner (file stems)."""
return frozenset(path.stem for path in gates_dir.glob("*.sh"))
def collect_ruleset_contexts(ruleset_path: Path) -> frozenset[str]:
"""The status-check contexts the branch ruleset requires (main.json). Single
source of the native-enforcement list, confronted against the policy's
`required` by RC3."""
data = json.loads(ruleset_path.read_text(encoding="utf-8"))
contexts: set[str] = set()
for rule in data.get("rules", []):
if rule.get("type") == "required_status_checks":
checks = rule.get("parameters", {}).get("required_status_checks", [])
for check in checks:
context = check.get("context")
if context is not None:
contexts.add(context)
return frozenset(contexts)
def main(argv: list[str] | None = None) -> int:
try:
policy = load_policy(_REQUIRED_CHECKS_PATH)
except PolicyError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
try:
contexts = collect_ruleset_contexts(_RULESET_PATH)
except (OSError, json.JSONDecodeError) as exc:
print(f"error: cannot read ruleset {_RULESET_PATH}: {exc}", file=sys.stderr)
return 2
names = collect_runner_names(_GATES_DIR)
findings = confront(policy.decisional, policy.required, names, contexts)
print(f"Gate policy : decisional {sorted(policy.decisional)} vs runners {sorted(names)}")
print(f" required {sorted(policy.required)}")
print(f" ruleset {sorted(contexts)}")
if not findings:
print(
"\nRESULT : PASS (exit 0) - decisional gates runnable + required; "
"ruleset matches required"
)
return 0
for f in findings:
print(f" [{f.code}] {f.name!r}: {f.detail}")
print(f"\nRESULT : FAIL (exit 1) - {len(findings)} policy defect(s)")
return 1
if __name__ == "__main__":
sys.exit(main())