Problem
CI compute is healthy. CI round-trips are what cost us. Measured across the last 40 merged
PRs and the last 400 workflow runs:
| Signal |
Measurement |
| Merged-PR cycle time |
median 2.8 h · avg 2.9 h · p90 5.5 h · max 8.9 h (n=40) |
| Iteration phase (first commit → last commit) |
127 min avg — 71% of PR lifetime (n=20) |
| Tail (last commit → merge) |
50 min avg |
| CI critical path |
8.9 min across 72 checks (sample PR) |
| Commits per merged PR |
~6 median; tail of 15, 21, 26 |
| Bot-authored commits on PRs |
15 of 87 across 12 PRs (17%) |
| Runs cancelled by concurrency |
103 of 400 (26%) |
Runs parked action_required |
28 of 400 (7%) |
| Runner queue wait |
≈ 0.0 min — runners are not the constraint |
| Checks per PR vs required |
72 run, 10 required by the Main ruleset |
| Base velocity |
14–40 commits/day (7-day window) |
| Base-freshness enforcement |
required_status_checks.strict = false; merge queue not enabled, though all 10 required contexts already report in merge_group |
| Worktrees behind base |
6 of ~18 are 44–1076 commits behind; 4 of those 6 are local-only, invisible to every workflow |
Read together: an 8.9-minute suite is being paid for roughly six times per PR, plus once more for
every bot commit, plus up to 15 minutes of parked-run dead air each time — and the verdict it
produces may describe a base the change will never land on.
Two conclusions this explicitly rules out, so nobody re-proposes them:
- Review capacity is not the bottleneck. 3 open PRs, 2.8 h median merge.
- Runner speed is not the bottleneck. Queue wait ≈ 0; faster hosted runners would buy nothing.
Tasks
Wave 1 — no blockers
Wave 2
Expected effect
Each avoided push saves ~9 min of suite plus its share of park and cancellation churn. Cutting the
iteration phase from ~6 pushes to ~2 targets the 127-minute block that dominates the cycle. The
merge queue and the worktree guard remove the class of re-run that is caused by discovering base
staleness after the suite has already run.
Acceptance criteria
Expected speedup — measured, not assumed
CI failures during iteration are real and were mismeasured twice before this section was correct.
Both bad numbers came from a query whose shape decided the answer:
- "4 failures in 400 runs" —
gh run list --limit 400 spans 75 minutes on this repo
(06:23:54Z to 07:38:57Z, ~5.3 runs/min), not a base rate. A quiet window, and all four failures
were Code Quality / Security Scanning.
- "0 failed checks across the last 20 merged PRs" — selection bias.
gh pr checks reports the
latest check per name on a merged PR's head, which is green because it merged. That query
measures the merge gate and structurally cannot return non-zero.
Querying failed check-runs across each merged PR's own commits — reds during iteration, which is
what this umbrella is about — gives a very different picture:
49 failed check-runs across 9 merged PRs — every one was red at least once on the way. Failures
cost iteration time; they were invisible because the query looked at the wrong end.
Inter-commit gaps (n=75 across 12 PRs, median 10.8 min, p90 43 min) show where the time sits:
| Gap band |
Count |
Reads as |
| < 3 min |
10 |
rapid successive pushes — the source of the cancellation churn |
| 3–15 min |
31 |
brackets the 8.9-min critical path — push, wait for a verdict, push again |
| > 15 min |
34 |
genuine authoring |
| Change |
Median PR saving |
Confidence |
| #15933 — preflight returns the verdict locally, for both reds and confirmation |
25–35 min |
high — ~5.4 failed check-runs per PR are real reds it can catch |
| #15934 + #15935 — bot pushes and their parks removed |
15–20 min |
high — purely mechanical, no habit change |
| #15936 — policy consolidation |
~0 min |
the 8.9-min critical path is set by the smoke tests, not the policy jobs; the win is runner spend and triage noise |
| #15937 — merge queue |
~0 min, possibly +9 min in the tail |
correctness, not speed: a green verdict starts describing the tree that will actually land |
| #15938 — fresh worktrees |
~0 min on a typical PR |
prevents the catastrophic case — see the issue-13719 revival that would have regressed newer code |
Realistic landing: 2.9 h → ~2.1 h, roughly 25–30%.
Two things this umbrella explicitly does not buy, so they are not claimed later:
- It does not shorten the 8.9-min critical path. That is the floor for any merge.
- It does not touch the >15-min authoring gaps, which are the single largest block of the 127 min
and are real work, not waste.
The original < 60 min iteration-phase target stands corrected to < 95 min — an acceptance
criterion nobody can hit is not a criterion. That correction survives; the premise downgrade that
briefly accompanied it does not.
Method note for anyone extending this analysis: two of the three wrong conclusions in this
umbrella's history came from query shape, not from bad data. State the query alongside the number.
Correction log — #15937 had a blocking gap that does not exist
The original filing claimed two *-required-context shims lacked a merge_group trigger and would
hang the queue. Audited before starting work: all 10 required contexts already report in a
merge_group run. The shims are pull_request-only by design — they are the complement of the
real workflow's path filter, and adding merge_group to them would emit a duplicate context. Full
table on #15937.
#15937 therefore has no code prerequisite; it is an enablement change and the follow-on
reassessment of auto-update-pr-branches.yml. Enabling the queue changes how every merge in the
repository behaves, so it is a decision for the repository owner rather than something to land in a
PR — flagged, not actioned.
That is one of five findings today where a query over one field produced a conclusion about a
structure. Logged together because the pattern is the finding, not any one instance:
| # |
The claim |
What the query actually measured |
| 1 |
"4 failures in 400 runs" |
gh run list --limit 400 spans 75 minutes here at ~5.3 runs/min — a quiet window, not a base rate |
| 2 |
"0 failed checks across 20 merged PRs" |
gh pr checks reads the merge gate on a merged head; a working gate cannot return non-zero. Reds during iteration: 49 across 9 PRs |
| 3 |
"duplicate flake8/mypy pre-commit hook ids" |
hooks: is a YAML sequence — repeated ids are the supported multi-configuration idiom and all four entries run. Now a constraint on #15934 |
| 4 |
"two shims lack merge_group, the queue will hang" |
the shims are the complement of a path filter that only applies on pull_request; the real workflows run unconditionally in merge_group. All 10 contexts report |
| 5 |
a fleet worktree census reporting 17 trees |
the inventory parsed git worktree list --porcelain for branch lines and silently dropped a detached-HEAD tree, which has none. Actual count 18 |
Instances 2 and 5 were caught by a peer session; 3 and 4 were mine to retract. The response to #5 is
the one worth copying: the fix is not to special-case the missing field but to stop keying on it —
plain git worktree list cannot lose a row the way a --porcelain field parse can.
Standing rule: state the query alongside the number, and check the complement before calling
something missing. The shape to watch for is a negative result indistinguishable from the intended
one — which is also exactly what #15956 turns out to be, in a mechanism whose whole job was to ask a
human.
Problem
CI compute is healthy. CI round-trips are what cost us. Measured across the last 40 merged
PRs and the last 400 workflow runs:
action_requiredMainrulesetrequired_status_checks.strict= false; merge queue not enabled, though all 10 required contexts already report inmerge_groupRead together: an 8.9-minute suite is being paid for roughly six times per PR, plus once more for
every bot commit, plus up to 15 minutes of parked-run dead air each time — and the verdict it
produces may describe a base the change will never land on.
Two conclusions this explicitly rules out, so nobody re-proposes them:
Tasks
Wave 1 — no blockers
workflow_run, not a 15-minute cron → Approve parked runs on workflow_run, not a 15-minute cron #15935Wave 2
Expected effect
Each avoided push saves ~9 min of suite plus its share of park and cancellation churn. Cutting the
iteration phase from ~6 pushes to ~2 targets the 127-minute block that dominates the cycle. The
merge queue and the worktree guard remove the class of re-run that is caused by discovering base
staleness after the suite has already run.
Acceptance criteria
Expected speedup — measured, not assumed
CI failures during iteration are real and were mismeasured twice before this section was correct.
Both bad numbers came from a query whose shape decided the answer:
gh run list --limit 400spans 75 minutes on this repo(06:23:54Z to 07:38:57Z, ~5.3 runs/min), not a base rate. A quiet window, and all four failures
were
Code Quality/Security Scanning.gh pr checksreports thelatest check per name on a merged PR's head, which is green because it merged. That query
measures the merge gate and structurally cannot return non-zero.
Querying failed check-runs across each merged PR's own commits — reds during iteration, which is
what this umbrella is about — gives a very different picture:
49 failed check-runs across 9 merged PRs — every one was red at least once on the way. Failures
cost iteration time; they were invisible because the query looked at the wrong end.
Inter-commit gaps (n=75 across 12 PRs, median 10.8 min, p90 43 min) show where the time sits:
issue-13719revival that would have regressed newer codeRealistic landing: 2.9 h → ~2.1 h, roughly 25–30%.
Two things this umbrella explicitly does not buy, so they are not claimed later:
and are real work, not waste.
The original
< 60 miniteration-phase target stands corrected to< 95 min— an acceptancecriterion nobody can hit is not a criterion. That correction survives; the premise downgrade that
briefly accompanied it does not.
Method note for anyone extending this analysis: two of the three wrong conclusions in this
umbrella's history came from query shape, not from bad data. State the query alongside the number.
Correction log — #15937 had a blocking gap that does not exist
The original filing claimed two
*-required-contextshims lacked amerge_grouptrigger and wouldhang the queue. Audited before starting work: all 10 required contexts already report in a
merge_grouprun. The shims arepull_request-only by design — they are the complement of thereal workflow's path filter, and adding
merge_groupto them would emit a duplicate context. Fulltable on #15937.
#15937 therefore has no code prerequisite; it is an enablement change and the follow-on
reassessment of
auto-update-pr-branches.yml. Enabling the queue changes how every merge in therepository behaves, so it is a decision for the repository owner rather than something to land in a
PR — flagged, not actioned.
That is one of five findings today where a query over one field produced a conclusion about a
structure. Logged together because the pattern is the finding, not any one instance:
gh run list --limit 400spans 75 minutes here at ~5.3 runs/min — a quiet window, not a base rategh pr checksreads the merge gate on a merged head; a working gate cannot return non-zero. Reds during iteration: 49 across 9 PRsflake8/mypypre-commit hook ids"hooks:is a YAML sequence — repeated ids are the supported multi-configuration idiom and all four entries run. Now a constraint on #15934merge_group, the queue will hang"pull_request; the real workflows run unconditionally inmerge_group. All 10 contexts reportgit worktree list --porcelainforbranchlines and silently dropped a detached-HEAD tree, which has none. Actual count 18Instances 2 and 5 were caught by a peer session; 3 and 4 were mine to retract. The response to #5 is
the one worth copying: the fix is not to special-case the missing field but to stop keying on it —
plain
git worktree listcannot lose a row the way a--porcelainfield parse can.Standing rule: state the query alongside the number, and check the complement before calling
something missing. The shape to watch for is a negative result indistinguishable from the intended
one — which is also exactly what #15956 turns out to be, in a mechanism whose whole job was to ask a
human.