Skip to content

ci: narrow the two most expensive non-Linux lanes to their real input closure #3945

Description

@Astro-Han

Problem

Two lanes account for 93% of the non-Linux runner time a pull request costs. Measured over the 300 most recent first-parent commits on main (2026-08-20 .. 2026-08-26), replaying each lane's paths filter against each commit's diff:

lane hit rate median expected cost per PR
release-windows-check 21% 18.7 min 179s
windows_recovery 54% 3.3 min 113s
runtime-host-owner-platform (win+mac) 10% 1.0 min 13s
gitoxide-helper-admission (win+mac) 4% 1.6 min 9s
windows-sandbox-w0 3% 2.8 min 5s
runtime-host-peer-admission 0.3% 3.6 min ~0

The four narrow lanes are already close to free. The two expensive ones are wide because their paths lists name whole source trees rather than what the lane executes.

windows_recovery runs ten recovery test files. Their static import closure is 435 source files across packages/{core,storage,runtime,runtime-host}. Its filter currently admits all 1,347 files under those four src/** trees. Replayed against the same 300 commits, the current list matches 49% and the closure matches 33%.

release-windows-check has the same shape. It hangs on apps/desktop/src/main/*.ts and apps/desktop/package.json, both high-churn, while what it actually exercises is the installer, the update path, and the rollback path.

Desired outcome

Each lane's paths list is derived from the closure of what that lane runs rather than hand-maintained. scripts/ci-test-plan.mjs already owns the workspace graph and reverseDependencyClosure; these lists should be projections of it, locked by a contract test the way the postinstall launcher chain is in #3914.

Expected saving is roughly 100s of Windows and macOS runner time per pull request, about half an hour of scarce capacity per day at the current merge rate. That is a real but moderate win. The larger reason is drift: eight workflows each keep their own hand-copied path list, and a moved file silently drops a lane from the pull request with nothing to catch it.

A separate commit in the same pull request should set timeout-minutes: 45 across the pull-request lanes. Current limits are 120 for ci.yml against a 19.8-minute worst observed successful run and 90 for release-windows-check against 19.7 minutes, so a hung job holds a scarce runner slot far longer than any real run needs. 45 is roughly 2.5x the p95 of the slowest lane, which leaves the flake-retry and cold-cache headroom that a 30-minute limit would not.

Alternatives or workarounds

Leaving the lists hand-maintained keeps the drift risk, which is worse than the runner cost.

Static import analysis can miss a dynamic import or a runtime-only dependency, so a derived list is still a pre-filter, not proof of the lane's real input. That is already the standing position for windows_recovery: the unfiltered push: [main] run and the nightly run are what make a missed match recoverable, and both must stay unfiltered.

Per-lane timeout tiers were considered and rejected. The six cheap lanes currently sit at 30 minutes, but that value was inherited rather than chosen, and their combined hit rate is low enough that raising their worst case to 45 costs far less than the two expensive lanes save.

windows-sandbox-w0 was examined and needs no change. Its 3% hit rate reflects genuinely stable code, not a filter that is too narrow.

Claude Opus measured the hit rates, timing distribution, and import closure. A human contributor must verify the derived lists and own the change.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions