ci: hold every pull request lane to one 45-minute limit - #3948
Open
Astro-Han wants to merge 1 commit into
Open
Conversation
Astro-Han
force-pushed
the
ci/windows-lanes-input-closure
branch
from
August 26, 2026 17:22
5c03994 to
5ec2195
Compare
Astro-Han
force-pushed
the
ci/windows-lanes-input-closure
branch
from
August 26, 2026 18:51
5ec2195 to
72e576b
Compare
Astro-Han
marked this pull request as ready for review
August 26, 2026 19:20
The limits were inherited rather than chosen: 120 on `ci.yml` against a 19-minute worst observed successful run, 90 on `release-windows-check` against 20, then 30, 25, 15 and 5 elsewhere, and none at all on `gitoxide-helper-admission`, which left its Windows and macOS matrix on GitHub's 360-minute default. A hung job therefore held a scarce runner for hours after any real run would have finished. 45 is about 2.3x the worst observed run of the slowest lane, which keeps the cold-cache and flake-retry headroom a 30-minute limit would not. Per-lane tiers buy nothing here: the cheap lanes rarely run at all, so raising their worst case costs far less than the two expensive ones save. `ci-test-plan.test.mjs` now reads the limit off every pull-request-triggered workflow, counting jobs by `runs-on` rather than by job id — a quoted id escapes an id pattern, and a two-space line inside a `run: |` block satisfies one — so a new lane cannot land without a limit and an existing lane cannot drift back to its own value. Generated-by: Claude Opus 5 (Claude Code)
Astro-Han
force-pushed
the
ci/windows-lanes-input-closure
branch
from
August 27, 2026 04:21
72e576b to
bbaf798
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Every pull-request-triggered lane now carries
timeout-minutes: 45.The old limits were inherited rather than chosen: 120 on
ci.ymlagainst a 19-minute worst observed successful run, 90 onrelease-windows-checkagainst 20, then 30, 25, 15 and 5 elsewhere — and none at all ongitoxide-helper-admission, which left its Windows and macOS matrix on GitHub's 360-minute default. A hung job held a scarce non-Linux runner for hours after any real run would have finished.45 is about 2.3x the worst observed run of the slowest lane, which keeps the cold-cache and flake-retry headroom a 30-minute limit would not. Per-lane tiers were considered and rejected: the cheap lanes rarely run at all, so raising their worst case costs far less than the two expensive ones save.
scripts/ci-test-plan.test.mjsnow reads the limit off every pull-request-triggered workflow, so a new lane cannot land without one and an existing lane cannot drift back to its own value. It counts jobs byruns-onrather than by job id — a quoted id escapes an id pattern, and a two-space line inside arun: |block satisfies one.Refs #3945
What this PR no longer does, and why
An earlier revision of this branch also narrowed the
pathsfilters onwindows-recoveryandrelease-windows-check, per the first half of #3945. I dropped that work after measuring it. Three findings, in order of how much they change the issue:1. The filter cannot be narrower than what the lane runs.
windows-recoveryruns six gates. Splitting the closure by package:Two of those eleven test files — the owner-death recovery gates — account for 655 of the 675 files on their own, because they fork
__tests__/fixtures/execution-host.ts, which boots a real Host and therefore reaches most ofruntime,runtime-host,storageandcore. That is what the gate tests, not an accident of how it is written. A correctly derived filter lands at 46.2% against today's 54.8% — the trigger rate is set by the lane's subject matter, not by the filter's precision.2. The projected runner saving does not survive a correct closure. #3945 quotes a 435-file import closure. Following imports alone misses this repository's other edge kinds —
fork(new URL('./x.js', import.meta.url))child entry points,run('npm', ['run', ...])chains, triple-slash references — and the corrected closure is 675 files, not 435. Meanwhilerelease-windows-check's hand list turns out to be too narrow rather than too wide, so deriving its filter widens it. Netted out against the medians in the issue, the expected non-Linux runner time per pull request moved by roughly +2s. There is no saving to collect.3. Materialising a 675-file closure into the workflow costs more than it returns. 30.0% of the last 300 first-parent commits touch an
import/export … from/<reference>line in those four source trees, and 16.3% add or delete a file there. Each of those would make the contract test red until the author regenerated a 699-line YAML block — and two concurrent PRs regenerating it conflict. That is a recurring tax on roughly one PR in three, paid for a saving that measures as zero.I also checked whether the Windows re-runs are redundant with the Linux suite: all three workspaces run
node --test "dist/**/*.test.js", so these files do run on Linux. But only two gates are Windows-only by construction (skip: process.platform !== 'win32'— NTFS alternate streams and the named-pipe endpoint), and every one of the remaining tests kills processes and renames or deletes files, with Windows-specific accommodations already in the source (CRASH_HARNESS_TIMEOUT_MS = win32 ? 180_000 : 60_000,maxRetries: win32 ? 20 : 0,terminateChildProcessTree). Those accommodations are evidence the tests did behave differently on Windows, so I found no safe cut there either.The remaining lever is to change what the lane runs on a pull request rather than which pull requests it runs on — for example leaving the owner-death gates to the unfiltered
push: [main]and nightly runs the lane already has. That trades pre-merge detection for trigger rate and is a maintainer call, not something to fold into a timeout change. I'll write it up on #3945 rather than guess at it here.Verification
node --test --test-concurrency=1 scripts/ci-test-plan.test.mjs— 44 tests, all pass.45→60; deleting thetimeout-minutesline; adding a quoted-id job with no limit; putting the limit at the wrong indentation.npm run format:check— clean.yamlto confirm all nine jobs report45.pathslist against the last 300 first-parent commits onmainwith GitHub's glob semantics; the closure figures were cross-checked against an independentoxc-parserwalk.AI use
Tool(s) and scope: Claude Opus 5 (Claude Code) made the change and did the measurement behind the section above, including the adversarial review that found the import-only closure defect in the dropped revision. The commit carries a
Generated-bytrailer. A human contributor reviews the final diff and owns the change.Checklist
Does this PR entail a change in behavior?