Run only the CI a pull request's change can affect, and everything nightly
Stacked on #881 ("Build with Go 1.26.9 for eleven standard-library advisories"), which is stacked on #880 ("Bump golang.org/x/net to v0.60.0 for GO-2026-6617"). It's stacked only so its own Security check runs green; the change here is just .github/workflows/test.yml. When #880 merges, GitHub retargets this PR to main.
Problem
Every pull request runs the whole Go pipeline, whatever it changes. #878 ("Tell agents that rich-text fields support Mermaid diagrams") changed 29 lines of SKILL.md and one eval case, and still ran all 14 Test jobs. Measured on its first commit:
| Wall clock | Runner-minutes | |
|---|---|---|
| Test workflow | 6.0 min (16:26:23 → 16:32:20 UTC) | 31.9 |
| Slowest jobs | Race shards 3.7–5.7 min, Tests 3.7, Integration 2.5, CLI Surface 2.3, Lint 1.6 |
The last 40 Test runs took 4.6–7.0 min, most of them 5.7–6.2. The race shards set the wall clock.
Change
A new Classify changes job sorts a pull request's files with dorny/paths-filter (already pinned in this workflow, predicate-quantifier: every):
| The PR changes | Runs | Skips |
|---|---|---|
| anything outside the two lists below | everything, as today | nothing |
only skills/**/*.md, skill-evals/** | Tests, Integration Tests (embedded skill, drift fixtures, eval patterns) | Lint, Security, CLI Surface Check, race shards |
only root *.md, .github/*.md | Tests' naming check (check-naming scans Markdown) | every Go step and job |
Pushes to main, workflow_dispatch and a new nightly schedule (05:23 UTC) are never narrowed. The schedule also gives govulncheck a daily run against the moving vulnerability database. govulncheck lives only in this workflow, which is how GO-2026-6617 sat unseen on main until someone pushed.
Concurrency: a new push to a pull request cancels that PR's run in flight. Pushes to main, the schedule and dispatches never cancel.
Caching is already there. setup-go v7 restores the module and build cache on every job (Cache hit for: setup-go-Linux-x64-ubuntu24-go-1.26.7-… in #878's Tests log). Nothing to add.
Required checks still report
main requires Tests, Lint, Security, Race Detection, Integration Tests, Nix flake builds and Secret Scanning.
- No workflow-level
paths. A filtered-out workflow never runs, and its required checks stay pending forever. - Job-level
if. GitHub reports a job skipped by its condition as passing: "A job that is skipped will report its status as 'Success'. It will not prevent a pull request from merging, even if it is a required check." (docs) - Race Detection keeps
always(). A failed shard still fails it. It skips only whengoisfalse, which is when the shards skipped by design. - Fail-safe both ways. A path narrows the run only if it is listed, so an unlisted file runs everything, and so does any change to
test.yml, including this PR. The gates read!cancelled() && needs.changes.outputs.X != 'false', so a failed or empty classifier runs every job. The classifier's own step rejects any answer other thantrueorfalse. - Tests never skips. On prose-only PRs it still runs the naming check, so the required check has something to answer for.
Proof
- This PR changes
test.yml, so its own run is the code path: everything runs. - A docs-only probe PR stacked on this branch is the narrowed path. Results are in the comment below.
Timings (estimated now, measured on the probe below)
| PR kind | Before | After |
|---|---|---|
| Go/code change | ~6 min, ~32 runner-min | same, plus about 10 s for the classifier; superseded pushes get cancelled |
| Skill/eval only (like #878) | ~6 min, ~32 runner-min | ~4 min (Tests bounds it), ~8 runner-min |
| Prose only | ~6 min, ~32 runner-min | ~1 min, ~2 runner-min |
Not done here (proposals)
- Race sharding on PRs. Still the critical path for code PRs. More shards would cut wall time, but each one adds about 1 min of setup and compile.
- CodeQL isn't required, so it could take a workflow-level
paths-ignorefor prose. It's left alone because code scanning flags PRs that have no analysis.
Summary by cubic
Makes the test workflow change-aware: a pull request now runs only the jobs its changed files can affect, and a nightly schedule (05:23 UTC) runs the full suite. Previously every PR ran all 14 Test jobs (~6 min, ~32 runner-minutes) even when it changed only prose or a skill's text.
A new Classify changes job sorts each PR's changed files and is fail-safe both ways: any unlisted file, any change to this workflow, or a classifier that fails or answers nothing runs everything. The gates are job-level ifs under !cancelled(), so GitHub reports skipped required checks as passing instead of leaving them pending, and Tests never skips entirely (it still runs the naming check on prose-only PRs). Skill/eval-only PRs skip Lint, Security, CLI Surface Check and the race shards; prose-only PRs run only the naming check.
Pushes to main, dispatches and the nightly schedule are never narrowed. The schedule also gives govulncheck a daily run against the moving vulnerability database; it lives only in this workflow, which is how GO-2026-6617 sat unseen on main until someone pushed.
Concurrency: A new push to a pull request cancels that PR's run in flight. Every non-PR run gets its own concurrency group so a newer pending run can't silently replace a queued nightly run.