ci: fold six CI jobs into four with parallel and background steps; cancel superseded and closed-PR runs

Merged
TH
4d ago
+172-106

Restructures ci.yml to use the new step-level parallel: and background:/wait: keywords (changelog). The CI workflow goes from six jobs to four, which takes two runner slots per PR off the queue.

Why fallow got slow

Since #3370 (fallow 2.104 → 3.32), "Run Fallow audit" takes 440–800s on CI instead of 6–9s. Bisected locally on the resolve imports row of fallow --performance: 3.19.0 takes 0.6s and 3.20.0+ takes 8.2–8.7s (206s on a 4-vCPU runner). Since 3.20, fallow walks all 28 root-tsconfig references for every import, stats each one and rebuilds a GlobSet each time: ~1.5M glob builds and 20M statx per run. Removing references gives identical findings, so the fix belongs upstream in fallow.

Dead ends, measured:

  • Caching .fallow: a PR head restored from a cache built at its base still took 524s for the audit and 209s for health. The cache only helps an identical tree, so I removed the caching steps.
  • Pinning fallow 3.19: it breaks the config (ignoreFindings unsupported, 79 production-export findings).
  • Configuration: fallow has no knob for tsconfig discovery.

Layout

BeforeAfter
Lint & Format, Typecheck & PackageLint, Typecheck & Package: di-seams, lint, format and typecheck in one parallel: group
Repo Guards, Compatibility & ProvenanceRepo Guards & Compatibility: the fallow audit runs as a background: step beside the guards; wait: fallow-audit comes before production-exports, which shares the .fallow cache dir
Coverage, Integration Testsunchanged

scripts/gate/workflows.ts now opens parallel: groups, so the gate manifest still credits each gate inside a group to its lane (new test in workflows.test.ts).

Failure semantics

Checked on a throwaway branch (run 38764903724, deleted since):

  • A failing background: true step fails the job at its wait: step, and the steps after the wait are skipped. This holds for a plain run: step and for a composite action such as run-gate.
  • A background step that is never waited on still fails the job, at the implicit "Wait for all background steps".

Timings (ran time; queue time excluded)

Jobmain, 3 recent runsa096d79
Lint & Format + Typecheck & Package28–41s + 59–84s (2 jobs)86s (1 job)
Repo Guards + Compatibility & Provenance330–395s + 533–794s (2 jobs)955s (1 job)
Coverage563–642s on main (PR adds diff CRAP)785s
Integration Tests126–169s167s

Trade-off: each PR now uses 4 runner slots instead of 6, and about 12% fewer runner-seconds (~2250s → ~1990s). With an ~80-minute queue, throughput is what counts. But on an idle queue, wall time is about 2.5 minutes worse, because the merged guards job (~16 min) is now the critical path. Parallel steps buy almost nothing on a 4-vCPU runner. A cold fallow audit already uses every core, so the work beside it just takes turns: freerange took 562s beside the audit, against ~250s alone. The real speedup needs the fallow fix upstream.

Also in this PR: run cancellation

  • pr-cancel-on-close.yml: on pull_request: closed, it cancels the PR's unfinished runs (same-repo PRs only, because a fork PR's token is read-only). A refused cancel fails the job, unless the run had already finished.
  • agent-guidance.yml and docs.yml gain the ci.yml concurrency group, so a new push replaces the PR's older run (main pushes still never cancel each other). test-app-build-cache.yml keeps its job-level cancel-in-progress: false.