Files
solvecraft/scripts/ci/kernel_rig_plan.py
jyviko c97092e385 Kernel rig: a solidcore pin change runs every area and the ladder
The workflow triggers on the root Cargo.toml and Cargo.lock, and the plan gets their diff
(--pin-diff): a changed line that names solidcore's repository (Cargo.toml's git rev, Cargo.lock's
source) selects every area and the ladder. A path filter cannot tell a pin from another
dependency's change, so the plan reads the lines; another dependency's change runs no area.
The fillet rig is not triggered: the kernel rig's fillets shard runs the same cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ
2026-11-02 20:37:38 -05:00

164 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Select the kernel rig areas a pull request affects (.github/workflows/kernel-rig.yml).
Usage: kernel_rig_plan.py (--all | --paths FILE [--pin-diff DIFF]) [--root DIR] [--github-output FILE]
FILE holds the changed paths relative to the repository root, separated by NUL (git diff -z);
"-" reads standard input. DIFF is the pull request's unified diff of the root Cargo.toml and
Cargo.lock (PIN_FILES). --all selects every area (pushes, merge groups, the release gate, a run
by hand). --root is the repository (default: two folders up from this script).
The areas are the folders of tests/kernel-cases/ (the ladder aside), so a new area joins by
having a folder with its cases, its KNOWN_FAILING and its PATHS. A changed path selects:
- every area, when it matches tests/kernel-cases/PATHS-ALL (the shared core: SMLib, geom, the
kernel's core, the rig's grader and runner, the workflow);
- every area, when the solidcore pin moves: a changed line of DIFF names solidcore's repository
(the root Cargo.toml's git rev, Cargo.lock's source). Another dependency's change in those
files runs none, so a pin is told apart by its lines, which a path filter cannot see;
- an area, when it is under tests/kernel-cases/<area>/ or matches that area's PATHS: one git
pathspec glob a line (`*` within a folder, `**` across folders), `#` starts a comment.
The ladder spans every operation: it runs whenever any area does, and on a change under
tests/kernel-cases/ladder/.
The coverage check (`cargo xtask kernel-cases --coverage`) runs in the build job. The build runs
when a shard does, or when the command registry or the cases may have changed (COVERAGE below),
so a new command without a case fails on its own pull request.
Prints and optionally appends to $GITHUB_OUTPUT:
shards=["booleans", ..., "ladder"] the shard matrix, JSON (empty list: no shard runs)
build=true|false the build job (and the coverage check) runs
"""
import argparse
import json
import os
import re
import sys
CASES = "tests/kernel-cases"
LADDER = "ladder"
# Changes here may add or remove a registry command, or the case that covers one: the coverage
# check must see them even when no area runs.
COVERAGE = (
"crates/engine/src/cmd/**",
"tests/kernel-cases/EXEMPT",
"tests/kernel-cases/**/recipe.json",
"xtask/src/kernel_cases.rs",
)
# The files that pin solidcore, and the repository a pin's lines name.
PIN_FILES = ("Cargo.toml", "Cargo.lock")
SOLIDCORE = "github.com/tangency-labs/solidcore"
def pin_moved(diff):
"""Whether a unified diff of PIN_FILES changes a line that names solidcore's repository (an
added or removed line, not its file headers or context)."""
return any(
line.startswith(("+", "-")) and not line.startswith(("+++ ", "--- ")) and SOLIDCORE in line for line in diff.splitlines()
)
def glob_regex(glob):
"""A git pathspec glob (`:(glob)` magic) as a regular expression over a whole path: `**/`
matches any folders (none too), a trailing `/**` everything below, `*` and `?` stay within
one folder."""
out, i = [], 0
while i < len(glob):
if glob.startswith("**/", i):
out.append("(?:.*/)?")
i += 3
elif glob.startswith("**", i):
out.append(".*")
i += 2
elif glob[i] == "*":
out.append("[^/]*")
i += 1
elif glob[i] == "?":
out.append("[^/]")
i += 1
else:
out.append(re.escape(glob[i]))
i += 1
return re.compile("".join(out) + r"\Z")
def read_globs(path):
"""The globs of a PATHS file: one a line, blank lines and `#` comments skipped."""
with open(path, encoding="utf-8") as f:
lines = [line.strip() for line in f]
globs = [line for line in lines if line and not line.startswith("#")]
for g in globs:
if g.startswith("/") or g.startswith(":") or "\\" in g:
raise SystemExit(f"{path}: `{g}`: a glob is relative to the repository root, with / and no pathspec magic")
return globs
def areas(root):
"""Every area (a folder of tests/kernel-cases/ but the ladder) with its PATHS globs."""
base = os.path.join(root, CASES)
out = {}
for name in sorted(os.listdir(base)):
if name == LADDER or not os.path.isdir(os.path.join(base, name)):
continue
paths = os.path.join(base, name, "PATHS")
if not os.path.isfile(paths):
raise SystemExit(f"{CASES}/{name}/PATHS is missing: every area lists the source files whose change reruns it")
out[name] = read_globs(paths)
return out
def matches(globs, path):
return any(glob_regex(g).match(path) for g in globs)
def select(root, paths, everything=False, pin_diff=""):
"""Return (shards, build): the areas to run, in order and the ladder last, and whether the
build job runs. `pin_diff` is the diff of PIN_FILES."""
by_area = areas(root)
if everything or pin_moved(pin_diff):
return [*by_area, LADDER], True
shared = read_globs(os.path.join(root, CASES, "PATHS-ALL"))
hit, ladder = set(), False
for path in paths:
if matches(shared, path):
return [*by_area, LADDER], True
if path.startswith(f"{CASES}/{LADDER}/"):
ladder = True
for area, globs in by_area.items():
if path.startswith(f"{CASES}/{area}/") or matches(globs, path):
hit.add(area)
shards = [a for a in by_area if a in hit]
if shards or ladder:
shards.append(LADDER)
return shards, bool(shards) or any(matches(COVERAGE, p) for p in paths)
def read_paths(src):
data = sys.stdin.buffer.read() if src == "-" else open(src, "rb").read()
return [p.decode() for p in data.split(b"\0") if p]
def main():
ap = argparse.ArgumentParser()
group = ap.add_mutually_exclusive_group(required=True)
group.add_argument("--paths")
group.add_argument("--all", action="store_true")
ap.add_argument("--pin-diff")
ap.add_argument("--root", default=os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
ap.add_argument("--github-output")
a = ap.parse_args()
pin_diff = ""
if a.pin_diff:
with open(a.pin_diff, encoding="utf-8", errors="replace") as f:
pin_diff = f.read()
shards, build = select(a.root, [] if a.all else read_paths(a.paths), everything=a.all, pin_diff=pin_diff)
lines = [f"shards={json.dumps(shards)}", f"build={'true' if build else 'false'}"]
print("\n".join(lines))
if a.github_output:
with open(a.github_output, "a", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
if __name__ == "__main__":
main()