Skip to content

ci: hold every pull request lane to one 45-minute limit - #3948

Open
Astro-Han wants to merge 1 commit into
apache:mainfrom
Astro-Han:ci/windows-lanes-input-closure
Open

ci: hold every pull request lane to one 45-minute limit#3948
Astro-Han wants to merge 1 commit into
apache:mainfrom
Astro-Han:ci/windows-lanes-input-closure

Conversation

@Astro-Han

@Astro-Han Astro-Han commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Every pull-request-triggered lane now carries timeout-minutes: 45.

The old 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 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.mjs now 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 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.

Refs #3945

What this PR no longer does, and why

An earlier revision of this branch also narrowed the paths filters on windows-recovery and release-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-recovery runs six gates. Splitting the closure by package:

gate group files reached share of PRs matched
storage (6 test files) 102 15.4%
runtime (2 test files) 206 18.4%
runtime-host (3 test files) 660 38.1%
all 675 39.8%

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 of runtime, runtime-host, storage and core. 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. Meanwhile release-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.
  • Mutation-checked the new contract test; each of these turns it red: 4560; deleting the timeout-minutes line; adding a quoted-id job with no limit; putting the limit at the wrong indentation.
  • npm run format:check — clean.
  • Every pull-request-triggered workflow re-parsed with yaml to confirm all nine jobs report 45.
  • The hit-rate and closure figures above were measured by replaying each lane's paths list against the last 300 first-parent commits on main with GitHub's glob semantics; the closure figures were cross-checked against an independent oxc-parser walk.
  • Not run: the repository-wide suite. No workflow logic changed beyond the limits, so no lane needed a live run to verify.

AI use

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

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-by trailer. A human contributor reviews the final diff and owns the change.

Checklist

  • Tests cover the change and fail without it
  • Lint, format and the affected suite pass locally (no TypeScript changed, so typecheck has nothing to cover; CI runs it regardless)

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@Astro-Han
Astro-Han force-pushed the ci/windows-lanes-input-closure branch from 5c03994 to 5ec2195 Compare August 26, 2026 17:22
@Astro-Han Astro-Han changed the title ci: narrow the two most expensive non-Linux lanes to their real input closure ci: derive the Windows recovery filter from what its steps reach Aug 26, 2026
@Astro-Han
Astro-Han force-pushed the ci/windows-lanes-input-closure branch from 5ec2195 to 72e576b Compare August 26, 2026 18:51
@Astro-Han Astro-Han changed the title ci: derive the Windows recovery filter from what its steps reach ci: hold every pull request lane to one 45-minute limit Aug 26, 2026
@Astro-Han
Astro-Han marked this pull request as ready for review August 26, 2026 19:20
@Astro-Han
Astro-Han requested review from M4n5ter and jackwener August 26, 2026 19:21
@github-actions github-actions Bot added the effort/S Under 100 readable lines label Aug 27, 2026
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
Astro-Han force-pushed the ci/windows-lanes-input-closure branch from 72e576b to bbaf798 Compare August 27, 2026 04:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/S Under 100 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant