Skip to content

CI round-trips, not CI compute: 71% of PR lifetime is the push–wait–fix loop #15932

Description

@mrveiss

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

  • Iteration phase (first→last commit) median drops below 95 min over 20 consecutive merged PRs
  • Bot-authored share of PR commits drops below 5%
  • Concurrency-cancelled run share drops below 10% of completed runs
  • No PR merges against a stale base — every merge is a merge-queue entry tested on final base
  • No PR-less pushed branch more than 50 commits behind base
  • All six child issues closed with code evidence

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:

PR Failed check-runs PR Failed check-runs
#15919 2 #15906 1
#15915 2 #15901 12
#15913 2 #15896 7
#15910 16 #15895 1
#15886 6

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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions