Skip to content

CI/CD: cut the fixed per-PR toll — 63 checks, 117 runner-min, 20 min wall clock #15357

Description

@mrveiss

Problem

A one-line change pays a fixed 20-minute wall-clock / 63-check / ~117 runner-minute toll, and
that toll is re-paid several times per PR for reasons unrelated to the code under review.

Measured on PR #15355 (gh pr checks) and the last 200 workflow runs:

Metric Value
Checks per PR 63 timed
Checks finishing in <20s 40
Checks over 5 min 11
Runner-minutes per full pass 117
Wall-clock critical path 20 min
Median queue delay 0s (capacity is not the bottleneck)
Workflow files 61

The bottleneck is fan-out and re-triggering, not runner capacity. Nothing in the pipeline scales
to diff size: 12 test shards, code-quality (18m), security (14m) and pre-commit --all-files
(21m) run identically for a one-line change and a 50-file refactor.

Tasks

Wave 1 — independent, highest leverage:

Wave 2 — depends on Wave 1 landing:

Acceptance Criteria

  • Wall-clock critical path for a one-line Python PR drops below 12 min
  • Runner-minutes per full PR pass drop below 70
  • A doc-only PR runs no Python test shard and no api-wiring job
  • No required status check is lost or weakened; branch protection contexts unchanged
  • Ruleset dismiss_stale_reviews_on_push disabled (2026-08-31) — stops automated pushes discarding approvals
  • Each task's before/after measured with gh pr checks on a real PR and posted on its child issue

Activity

  1. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    Amplifier found while starting Task 1 — automated pushes also destroy the approval

    Ruleset "Require review for external contributors" applies to refs/heads/Dev_new_gui:

    required_approving_review_count: 1
    dismiss_stale_reviews_on_push:   true
    

    This is not in classic branch protection (required_pull_request_reviews: null), which is why it
    did not show up in the original measurement.

    Consequence: every automated push to a PR branch — the base-merge from auto-update-pr-branches
    (#15359) and the auto-fix bot commits (#15362) — does two destructive things, not one:

    1. restarts all 63 checks (~20 min wall, ~117 runner-min), and
    2. dismisses the existing approval, sending the PR back to REVIEW_REQUIRED.

    Current state of the queue, all 5 open PRs:

    PR mergeable mergeStateStatus reviewDecision
    #15356 MERGEABLE BLOCKED REVIEW_REQUIRED
    #15355 MERGEABLE BLOCKED REVIEW_REQUIRED
    #15354 MERGEABLE BLOCKED REVIEW_REQUIRED
    #15347 MERGEABLE BLOCKED REVIEW_REQUIRED
    #15343 MERGEABLE BLOCKED REVIEW_REQUIRED

    Nothing is blocked on a failing test. Every one is blocked on review, and the automation actively
    re-blocks anything that gets reviewed. This raises the priority of #15359 and #15362 from
    "wasted runner-minutes" to "the reason the merge queue does not drain".

  2. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    Correction + action taken

    Correction to the comment above. Stale-review dismissal is not why the 5 open PRs read
    BLOCKED. Checked /pulls/{n}/reviews on all five: none has ever had a review, so there was
    no approval for a push to dismiss. Two further facts change the picture:

    So mergeStateStatus: BLOCKED / reviewDecision: REVIEW_REQUIRED on this repo is cosmetic.
    GitHub computes it without accounting for bypass actors, and it will keep reporting BLOCKED no
    matter what this ruleset says, short of removing the pull_request rule entirely.

    The real cost is misdiagnosis, not blocked merges. Every tool, dashboard and agent that reads
    mergeStateStatus to decide "can this land?" concludes the queue is jammed when nothing is
    actually stuck. That is worth its own child issue under #15357.

    Change applied to ruleset Require review for external contributors (id 17190144):

    dismiss_stale_reviews_on_push: true → false
    

    Unchanged, deliberately: required_approving_review_count: 1,
    require_extra_approval_for_unattributed_changes: true, and the admin bypass — those still guard
    the external-contributor case the rule is named for.

    This does not unblock anything today. What it does is stop #15359's base-merge commits and
    #15362's auto-fix commits from discarding an approval once reviews do happen, so the fixes in
    those issues no longer have to fight the ruleset as well.

  3. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    #15358's fix as originally filed would have broken the required verify-precommit-config check — check_gating_precommit_hooks.py treats a Skipped gating hook as a failure, and its one allowlisted hook is files: \.sh$, not always_run. Corrected design (split by hook class, not by file set) is on #15358. The measured saving stands; the mechanism changed.

  4. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    Two of my own children were wrong and are corrected:

    Net effect on the umbrella: Wave 1 is #15358 (corrected mechanism), #15360 (de-risked), #15361 (coverage comparison posted). Task 8 is withdrawn.

    The measured baseline is unchanged: 63 checks, 117 runner-minutes, 20 min wall clock on PR #15355.

  5. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    Session close — state of this umbrella

    No code was written. Nothing was committed, no branch or worktree was created, no PR opened. This umbrella is a backlog with a measured baseline, not work in progress.

    Verified against the code

    Child State
    #15358 Premise sound, mechanism corrected — original fix would have broken a required check
    #15360 De-risked against the #12986 canonical-filter guard — no conflict
    #15361 Coverage comparison posted; hardened is a strict superset of docker-smoke-test

    Not verified — do not implement as written

    #15362, #15363, #15364 each carry a caveat comment. Two of four Wave-1 siblings had wrong premises on inspection, so these are hypotheses with measurements attached, not agreed fixes.

    Withdrawn

    Applied outside the tracker

    Ruleset Require review for external contributors: dismiss_stale_reviews_on_push: true → false. Reversible with one API call. required_approving_review_count: 1, require_extra_approval_for_unattributed_changes: true and the admin bypass are unchanged — whether to also disable the second one is an open decision.

    Blocked

    All code work is blocked on the local worktree ceiling (12 live / ceiling 10), whose override needs an edit to a file gated by #15369. Neither is fixable from inside this repo by an agent.

    Baseline to measure against

    63 checks · 117 runner-minutes · 20 min wall clock · median queue 0s — PR #15355.

  6. mrveiss commented on Aug 31, 2026

    @mrveiss
    OwnerAuthor

    Measured from the consuming side: one session's worth of the toll

    A day of continuous PR work against this repository, offered as evidence for the sub-issues rather than as a new finding.

    The base-merge cost is not theoretical (#15359)

    Every merge puts every other open PR behind, and the standing practice here is to re-level them all. Today that meant roughly twenty update cycles across 4-8 PRs, each buying a full re-run. At #15359's own measurement of 117 runner-minutes per re-run, that is on the order of 39 runner-hours spent re-proving unchanged code.

    The decisive fact is that branch protection does not ask for it:

    gh api repos/:owner/:repo/branches/Dev_new_gui/protection --jq '.required_status_checks.strict'
    false
    

    Verified directly today. So the re-run cost buys nothing the repository requires. A narrower rule preserves the actual safety — update only when the base commit touches a file the PR also touches — which catches genuine semantic conflicts and skips the large majority that cannot interact.

    The two-machine dependency is the other half (#15395)

    Independently confirmed while triaging a bump: code-quality runs runs-on: [self-hosted, Linux, X64] and consumes ./.github/actions/setup-python-ci. Across a full session both runners reported busy=true continuously, and code-quality was the single most common reason a PR sat at 10/10 required contexts without becoming mergeable.

    Two observations that may help #15395's case:

    • actions/cache@v6 is already proven on that hardware. The bump is gated on runner.environment == 'self-hosted', and code-quality completed success on the bumped head (9e28baa631). So a move off self-hosted is not blocked by an unverified action version.
    • busy=true with zero in-progress runs is not evidence of a wedge. I asserted that earlier in the session and was wrong — 44 runs completed within a minute of the claim. Recording it here because a false wedge diagnosis would justify restarting healthy runners, and it makes the next real detection easier to dismiss.

    What compounds them

    These two multiply rather than add: a base-merge triggers a full re-run, and the re-run's slowest required context is pinned to a pool of two. Fixing either alone leaves most of the wall-clock in place.

    Not offered as a decision

    Both #15359 and #15395 change what the repository trusts, and #15395 explicitly supersedes a premise (#15310/#15311) that was acted on earlier today. That is an owner call. This comment is only the consumption-side measurement, since the sub-issues were filed from the CI side and this is what the toll looks like from a queue trying to drain.

  7. mrveiss commented on Sep 1, 2026

    @mrveiss
    OwnerAuthor

    Task 1 delivered, Task 2 should not be built as written

    Task 1 — enforce-precommit scoped to the diff (#15358). Merged as 76980d2e66 (PR #15392).
    Measured on its own run: the job went 1142s → 150s, the sweep step 1008s → 105s (cold
    cache). The umbrella predicted −21 min; the realised saving is ~16.5 min off the critical path,
    and the 105s figure should fall further once the new ~/.cache/pre-commit cache is warm — nothing
    cached hook environments before, so every run rebuilt all of them before checking a single file.

    #15358 stays open on AC3 alone (a seeded violation proving the #14181/#14878 guards still fail).

    Task 2 — stop the unconditional base-merge (#15359) — recommend NOT building it. Its preferred
    fix reads mergeStateStatus, which auto-update-pr-branches.yml:166 explicitly forbids
    ("#12818: do NOT filter on mergeStateStatus. It is computed LAZILY"), and gating the refresh on
    conflicting would delete the semantic-conflict net rather than cheapen it — semantic conflicts
    are by definition the ones that do not conflict textually.

    The cost that task targets is not a defect in that workflow. It is the price of eager,
    speculative per-PR CI
    : branch protection has strict: false, so a PR's checks can be stale at
    merge and are simply accepted, and the base-merge exists to cover that gap. A merge queue removes
    the reason it exists. #14054 is the real fix for Task 2, and its triggers landed today
    (b9effcf723, PR #15394) — all ten required contexts now fire on merge_group. Retiring
    auto-update-pr-branches is #14054's AC4.

    Detail recorded on #15359 itself.

    Two findings from doing Task 1

    Measurement correction for this umbrella

    This issue records "median queue delay 0s — capacity is not the bottleneck". That held when
    measured and still holds as the general case, but it is not invariant: at 17:00 on 31 Aug the four
    deepest queues were the four self-hosted-pinned workflows, 29 of 89 queued runs against two
    machines, while hosted capacity ran 7 jobs against a ~20 ceiling. PR #15394 removed that
    contributor entirely (#15395). Worth re-measuring the 0s figure now that it has.

  8. modified the milestones: v0.9.0, v0.10.0 on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions