Run only the CI a pull request's change can affect, and everything nightly

Merged
RO
1mo ago
+95-2

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 clockRunner-minutes
Test workflow6.0 min (16:26:23 → 16:32:20 UTC)31.9
Slowest jobsRace 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 changesRunsSkips
anything outside the two lists beloweverything, as todaynothing
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/*.mdTests' 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 when go is false, 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 than true or false.
  • 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 kindBeforeAfter
Go/code change~6 min, ~32 runner-minsame, 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-ignore for 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.