Skip to content

Commit 9b8fe12

Browse files
committed
Skip the vlt matrix on ci.yml-only changes
vlt-compatibility's PR and push filters list ci.yml because install-proof leaves out the cells ci.yml's vlt e2e rows already run. Most ci.yml edits don't touch those rows, yet each one reran the whole matrix (about 41 Linux + 38 Windows job-min per PR run, plus 22 macOS on push). In the last 24h that was 10 of the 24 merged PRs that triggered the workflow, and 5 of its 45 push runs. A new `changes` job runs scripts/vlt-compat-gate.py. It skips build, plan and everything after them only when ci.yml is the one changed file the event's filter matches and the vlt cells parsed from ci.yml are the same on base and head. matrix-coverage still runs, and schedule, dispatch or any doubt (missing base, parse error) run the full matrix. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YVLGaJFexMvqCdiWxbiHXB
1 parent e9be746 commit 9b8fe12

3 files changed

Lines changed: 240 additions & 4 deletions

File tree

‎.github/workflows/vlt-compatibility.yml‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ on:
4343
- 'scripts/vlt-historical-integrity.json'
4444
- 'scripts/gen-vlt-collation-golden.mjs'
4545
- 'scripts/ci-vlt-proof-suites.py'
46+
- 'scripts/vlt-compat-gate.py'
47+
- 'scripts/tests/test_vlt_compat_gate.py'
4648
- '.github/workflows/ci.yml'
4749
- 'scripts/tests/test_ci_vlt_rows.py'
4850
push:
@@ -84,6 +86,8 @@ on:
8486
- 'scripts/vlt-historical-integrity.json'
8587
- 'scripts/gen-vlt-collation-golden.mjs'
8688
- 'scripts/ci-vlt-proof-suites.py'
89+
- 'scripts/vlt-compat-gate.py'
90+
- 'scripts/tests/test_vlt_compat_gate.py'
8791
- '.github/workflows/ci.yml'
8892
- 'scripts/tests/test_ci_vlt_rows.py'
8993
schedule:
@@ -136,8 +140,41 @@ jobs:
136140
- name: Every era, suite and OS is covered
137141
run: python3 -B -m unittest scripts/tests/test_ci_vlt_rows.py -v
138142

139-
build:
143+
# Both filters list ci.yml for its vlt `e2e` rows, which install-proof
144+
# leaves out. A PR or push whose only matching change is ci.yml, with
145+
# those rows untouched, would rerun the matrix exactly as on the base:
146+
# matrix-coverage above still checks it, the rest is skipped.
147+
changes:
140148
if: github.event.pull_request.draft != true
149+
runs-on: ubuntu-latest
150+
timeout-minutes: 5
151+
outputs:
152+
matrix: ${{ steps.gate.outputs.matrix }}
153+
steps:
154+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
155+
with:
156+
persist-credentials: false
157+
# A PR's merge commit has the base it was merged onto as parent 1.
158+
fetch-depth: 2
159+
- id: gate
160+
env:
161+
EVENT_NAME: ${{ github.event_name }}
162+
PUSH_BEFORE: ${{ github.event.before }}
163+
run: |
164+
set -euo pipefail
165+
case "$EVENT_NAME" in
166+
pull_request) base=HEAD^1 ;;
167+
push) base="$PUSH_BEFORE" ;;
168+
*) base='' ;;
169+
esac
170+
if [ -n "$base" ] && ! git rev-parse --verify --quiet "$base^{commit}" >/dev/null; then
171+
git fetch --quiet --depth 1 origin "$base" || true
172+
fi
173+
python3 -B scripts/vlt-compat-gate.py --event "$EVENT_NAME" --base "$base" >> "$GITHUB_OUTPUT"
174+
175+
build:
176+
needs: changes
177+
if: needs.changes.outputs.matrix == 'true'
141178
strategy:
142179
fail-fast: false
143180
matrix:
@@ -373,7 +410,8 @@ jobs:
373410
steps: *install-proof-steps
374411

375412
plan:
376-
if: github.event.pull_request.draft != true
413+
needs: changes
414+
if: needs.changes.outputs.matrix == 'true'
377415
runs-on: ubuntu-latest
378416
timeout-minutes: 5
379417
outputs:
@@ -468,8 +506,8 @@ jobs:
468506
retention-days: 14
469507

470508
lock-diff:
471-
needs: native
472-
if: ${{ !cancelled() }}
509+
needs: [changes, native]
510+
if: ${{ !cancelled() && needs.changes.outputs.matrix == 'true' }}
473511
runs-on: ubuntu-latest
474512
timeout-minutes: 10
475513
steps:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""scripts/vlt-compat-gate.py: vlt-compatibility skips its matrix only when
2+
ci.yml is the one matching change and its vlt cells are untouched."""
3+
4+
import contextlib
5+
import importlib.util
6+
import io
7+
import unittest
8+
from pathlib import Path
9+
10+
ROOT = Path(__file__).parents[2]
11+
CI = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
12+
COMPAT = (ROOT / ".github" / "workflows" / "vlt-compatibility.yml").read_text(encoding="utf-8")
13+
14+
spec = importlib.util.spec_from_file_location("gate", ROOT / "scripts" / "vlt-compat-gate.py")
15+
gate = importlib.util.module_from_spec(spec)
16+
spec.loader.exec_module(gate)
17+
18+
PR = gate.event_paths(COMPAT, "pull_request")
19+
PUSH = gate.event_paths(COMPAT, "push")
20+
CI_PATH = ".github/workflows/ci.yml"
21+
22+
23+
def drop_a_vlt_row(text):
24+
lines = text.splitlines(keepends=True)
25+
for i, line in enumerate(lines):
26+
if "vlt:" in line and "--include-ignored vlt_pinned_matrix" in line:
27+
return "".join(lines[:i] + lines[i + 1:])
28+
raise AssertionError("no vlt row in ci.yml")
29+
30+
31+
class Filters(unittest.TestCase):
32+
def test_both_events_list_ci_yml_and_the_gate(self):
33+
for paths in (PR, PUSH):
34+
self.assertIn(CI_PATH, paths)
35+
self.assertIn("scripts/vlt-compat-gate.py", paths)
36+
self.assertIn("crates/socket-patch-core/src/vendor/**", PUSH)
37+
self.assertNotIn("crates/socket-patch-core/src/vendor/**", PR)
38+
39+
def test_globs(self):
40+
star = gate.glob_re("crates/*/src/**/*vlt*")
41+
self.assertTrue(star.match("crates/socket-patch-core/src/vendor/vlt.rs"))
42+
self.assertTrue(star.match("crates/socket-patch-cli/src/vlt_preflight.rs"))
43+
self.assertFalse(star.match("crates/a/b/src/vlt.rs"))
44+
self.assertTrue(gate.glob_re("crates/x/**").match("crates/x/a/b.rs"))
45+
self.assertFalse(gate.glob_re("crates/x/*.rs").match("crates/x/a/b.rs"))
46+
47+
48+
class Decision(unittest.TestCase):
49+
def test_ci_yml_alone_with_the_same_cells_skips(self):
50+
edited = CI + "\n# an unrelated edit\n"
51+
for event, paths in (("pull_request", PR), ("push", PUSH)):
52+
self.assertFalse(gate.needs_matrix(event, [CI_PATH, "README.md"], paths, CI, edited))
53+
54+
def test_a_changed_vlt_row_runs(self):
55+
self.assertTrue(gate.needs_matrix("pull_request", [CI_PATH], PR, CI, drop_a_vlt_row(CI)))
56+
57+
def test_another_matching_file_runs(self):
58+
changed = [CI_PATH, "scripts/check-vlt-legs.py"]
59+
self.assertTrue(gate.needs_matrix("pull_request", changed, PR, CI, CI))
60+
changed = [CI_PATH, "crates/socket-patch-core/src/vendor/npm.rs"]
61+
self.assertTrue(gate.needs_matrix("push", changed, PUSH, CI, CI))
62+
63+
def test_other_events_and_missing_inputs_run(self):
64+
for event in ("schedule", "workflow_dispatch"):
65+
self.assertTrue(gate.needs_matrix(event, [CI_PATH], PR, CI, CI))
66+
self.assertTrue(gate.needs_matrix("pull_request", [CI_PATH], [], CI, CI))
67+
self.assertTrue(gate.needs_matrix("pull_request", [CI_PATH], PR, None, CI))
68+
69+
def test_no_base_runs(self):
70+
out = io.StringIO()
71+
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(io.StringIO()):
72+
gate.main(["--event", "pull_request", "--base", "0" * 40])
73+
self.assertEqual(out.getvalue().split(), ["matrix=true"])
74+
75+
76+
class Workflow(unittest.TestCase):
77+
def test_heavy_jobs_wait_on_the_gate(self):
78+
for job in ("build", "plan"):
79+
block = COMPAT.split(f"\n {job}:\n", 1)[1].split("\n ", 1)[0]
80+
self.assertIn("needs: changes", block, job)
81+
self.assertIn("needs.changes.outputs.matrix == 'true'", COMPAT.split("\n lock-diff:\n", 1)[1][:200])
82+
self.assertIn("scripts/vlt-compat-gate.py", COMPAT.split("\n changes:\n", 1)[1])
83+
84+
85+
if __name__ == "__main__":
86+
unittest.main()

‎scripts/vlt-compat-gate.py‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python3
2+
"""Print whether a vlt-compatibility run needs its matrix (`matrix=true`).
3+
4+
The workflow's pull_request and push filters list ci.yml because
5+
install-proof leaves out the cells ci.yml's `e2e` rows already run
6+
(scripts/ci-vlt-proof-suites.py). Most ci.yml edits don't touch those rows,
7+
and then the matrix would run exactly as it did on the base. So the answer
8+
is `matrix=false` only when ci.yml is the one changed file the event's
9+
filter matches and its vlt cells are the same on both sides. Anything
10+
unexpected (a base that isn't there, a filter that doesn't parse) answers
11+
`matrix=true`.
12+
"""
13+
14+
import argparse
15+
import importlib.util
16+
import re
17+
import subprocess
18+
import sys
19+
from pathlib import Path
20+
21+
ROOT = Path(__file__).resolve().parents[1]
22+
WORKFLOW = ROOT / ".github" / "workflows" / "vlt-compatibility.yml"
23+
CI_PATH = ".github/workflows/ci.yml"
24+
25+
26+
def event_paths(text, event):
27+
"""The `paths:` list of `on.<event>` in the workflow text."""
28+
paths, in_on, in_event, in_paths = [], False, False, False
29+
for line in text.splitlines():
30+
if not line.strip() or line.lstrip().startswith("#"):
31+
continue
32+
depth = len(line) - len(line.lstrip(" "))
33+
if depth == 0:
34+
in_on = line.rstrip() == "on:"
35+
in_event = in_paths = False
36+
elif in_on and depth == 2:
37+
in_event = line.strip() == f"{event}:"
38+
in_paths = False
39+
elif in_event and depth == 4:
40+
in_paths = line.strip() == "paths:"
41+
elif in_paths and line.strip().startswith("- "):
42+
paths.append(line.strip()[2:].strip().strip("'\""))
43+
return paths
44+
45+
46+
def glob_re(pattern):
47+
"""GitHub's filter globs: `**` crosses `/`, `*` and `?` don't."""
48+
out, i = "", 0
49+
while i < len(pattern):
50+
if pattern.startswith("**/", i):
51+
out, i = out + "(?:.*/)?", i + 3
52+
elif pattern.startswith("**", i):
53+
out, i = out + ".*", i + 2
54+
elif pattern[i] == "*":
55+
out, i = out + "[^/]*", i + 1
56+
elif pattern[i] == "?":
57+
out, i = out + "[^/]", i + 1
58+
else:
59+
out, i = out + re.escape(pattern[i]), i + 1
60+
return re.compile(out + r"\Z")
61+
62+
63+
def ci_cells(text):
64+
path = ROOT / "scripts" / "ci-vlt-proof-suites.py"
65+
spec = importlib.util.spec_from_file_location("ci_vlt_proof_suites", path)
66+
module = importlib.util.module_from_spec(spec)
67+
spec.loader.exec_module(module)
68+
return module.ci_cells(text)
69+
70+
71+
def git(*args):
72+
return subprocess.run(["git", *args], cwd=ROOT, capture_output=True, text=True, check=True).stdout
73+
74+
75+
def needs_matrix(event, changed, filters, base_ci, head_ci):
76+
"""The decision on already-fetched inputs (see the module docstring)."""
77+
if event not in ("pull_request", "push") or not filters:
78+
return True
79+
rules = [glob_re(p) for p in filters]
80+
relevant = [f for f in changed if any(r.match(f) for r in rules)]
81+
if relevant != [CI_PATH]:
82+
return True
83+
return base_ci is None or ci_cells(base_ci) != ci_cells(head_ci)
84+
85+
86+
def main(argv=None):
87+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
88+
ap.add_argument("--event", required=True)
89+
ap.add_argument("--base", default="")
90+
ap.add_argument("--head", default="HEAD")
91+
args = ap.parse_args(argv)
92+
matrix = True
93+
if args.event in ("pull_request", "push") and args.base:
94+
try:
95+
changed = git("diff", "--name-only", args.base, args.head).split()
96+
filters = event_paths(WORKFLOW.read_text(encoding="utf-8"), args.event)
97+
base_ci = git("show", f"{args.base}:{CI_PATH}")
98+
head_ci = git("show", f"{args.head}:{CI_PATH}")
99+
matrix = needs_matrix(args.event, changed, filters, base_ci, head_ci)
100+
except Exception as e: # any doubt runs the matrix
101+
detail = getattr(e, "stderr", "") or e
102+
print(f"::warning::vlt-compat-gate: {str(detail).strip()}; running the matrix", file=sys.stderr)
103+
matrix = True
104+
if not matrix:
105+
print("::notice::only ci.yml changed and its vlt cells did not; skipping the vlt matrix",
106+
file=sys.stderr)
107+
print(f"matrix={'true' if matrix else 'false'}")
108+
return 0
109+
110+
111+
if __name__ == "__main__":
112+
sys.exit(main())

0 commit comments

Comments
 (0)