ci: fold six CI jobs into four with parallel and background steps; cancel superseded and closed-PR runs
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 (
ignoreFindingsunsupported, 79 production-export findings). - Configuration: fallow has no knob for tsconfig discovery.
Layout
| Before | After |
|---|---|
| Lint & Format, Typecheck & Package | Lint, Typecheck & Package: di-seams, lint, format and typecheck in one parallel: group |
| Repo Guards, Compatibility & Provenance | Repo 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 Tests | unchanged |
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: truestep fails the job at itswait:step, and the steps after the wait are skipped. This holds for a plainrun:step and for a composite action such asrun-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)
| Job | main, 3 recent runs | a096d79 |
|---|---|---|
| Lint & Format + Typecheck & Package | 28–41s + 59–84s (2 jobs) | 86s (1 job) |
| Repo Guards + Compatibility & Provenance | 330–395s + 533–794s (2 jobs) | 955s (1 job) |
| Coverage | 563–642s on main (PR adds diff CRAP) | 785s |
| Integration Tests | 126–169s | 167s |
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: onpull_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.ymlanddocs.ymlgain theci.ymlconcurrency group, so a new push replaces the PR's older run (main pushes still never cancel each other).test-app-build-cache.ymlkeeps its job-levelcancel-in-progress: false.