Skip to content

ci: fix the #7205 relapse on the scheduled gate arm, and reap dead PR runs (#7966) - #7969

Merged
proggeramlug merged 3 commits into
mainfrom
ci/7966-gate-starvation
Aug 12, 2026
Merged

ci: fix the #7205 relapse on the scheduled gate arm, and reap dead PR runs (#7966)#7969
proggeramlug merged 3 commits into
mainfrom
ci/7966-gate-starvation

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Fixes the diagnosis in #7966. The issue's hypothesis was starvation. That is half of it; the other half is a bug this repo has now shipped three times.

1. #7205 relapsed on the arm #7856 created

Every scheduled gate carries:

group: <name>-${{ github.event_name }}-${{ github.event_name == 'push' && github.sha || github.ref }}

The github.sha arm is #7205's fix and it is guarded on the event being push — correct while the main-line arm was push: branches: [main]. #7856 moved that arm to schedule:. The guard stopped matching, the expression fell through to github.ref (constant refs/heads/main), and every scheduled run of a gate shared one group again.

GitHub keeps at most one PENDING run per group and cancels the previously pending one when a new run enters, regardless of cancel-in-progress — the finding #7205 was measured by. Observed 2026-08-12, identically on all ten gates:

13:37Z schedule pending      <- blocked on the group
07:46Z schedule cancelled    <- jobs: 0
02:37Z schedule cancelled    <- jobs: 0
19:15Z schedule queued       <- holds the group, 20h in the runner queue

jobs: 0 is the exact zero-execution signature of #7205. The oldest run holds the group; every newer one is cancelled on arrival. The gate cannot run again until that one run drains. gate-freshness itself — documented as built so it "cannot be starved by the condition it is alarming about" — was cancelled the same way at 13:38Z.

Groups are now keyed on github.run_id for every non-pull-request event. PR runs keep the shared per-ref group and keep coalescing.

2. The guard, so there is no fourth relapse

scripts/gc_gate_wiring_check.py gains check_schedule_group, swept over all 31 workflows rather than just the GC gates (the hazard reached gate-freshness and test.yml too). It requires github.run_id in the concurrency group of any workflow with a schedule: trigger.

It immediately found ten more workflows already carrying the same latent constant group — including test.yml's nightly full-workspace safety net and soak-autofix, whose group was the bare constant string soak-autofix. All fixed here.

Five new self-test cases (16 total). The first is the sabotage case: the existing CLEAN fixture has the bad shape, so a checker that could not fail on it would be worthless. lint is required, so a relapse is now a red build.

3. The capacity half — 51% of the queue is dead work

metric value
queued runs 1,529
concurrent 12–14
queued by event 794 pull_request, 181 push, 19 schedule
head branches among queued PR runs 63
still existing 2

GitHub does not reliably cancel a queued run when its PR merges and the branch auto-deletes. ~790 runs — 51% of the entire queue — were work for already-merged PRs, pinned in front of ten main gates. A six-hourly sweep cannot help when the queue in front of it is half garbage.

scripts/reap_stale_ci_runs.py + ci-queue-reaper.yml cancel QUEUED pull_request runs with no open PR. Deliberately timid: dry-run by default, --max cap, and it only ever touches event == pull_request + status == queued + no open PR — a push/schedule/tag/dispatch run is structurally unreachable, and an in-flight run is left alone. Keyed on open PRs, not branch existence, which is what keeps fork PRs safe. Self-tested with two sabotage cases.

Live dry run: 781 reapable of 1,000 sampled.

Also: zizmor's push: main arm was unfiltered while its PR arm was already scoped to .github/** — 90 queued runs whose subject could not have changed. Path-filtered, plus the concurrency block it never had.

Not starved — actually broken

Two of the eleven are not a scheduling problem:

  • gc-native-roots has never had a single successful run, any branch, any event. Three of four arms fail with three distinct causes (aarch64-linux SIGSEGV under PERRY_STACKMAP_WALKER=verify; Windows Rust panic; macos-14 gc_evacuation_liveness_assert.py reporting 0 copying minors / 0 objects copied).
  • llvm-inprocess failed its last three main runs, and worse, its PR "successes" show native-backend: skipped — the path filter skips the only real job and the workflow reports green. Hazard 4.

Filed separately; neither is fixed here.

What this does NOT close

  • Bootstrap. The reaper queues like everything else and cannot dig out an already-saturated queue. The first drain is a manual python3 scripts/reap_stale_ci_runs.py --apply (dry run is the default). I did not run it — it cancels ~780 runs on shared infrastructure and is a maintainer call.
  • Branch protection. parity and compile-smoke are required contexts whose jobs carry if: github.event_name == 'push' || … while test.yml's push: is tags only — so on an ordinary PR they never report, and a required context that never reports blocks the merge button forever. Every merge therefore needs an admin bypass, which bypasses the required contexts that do work. That is upstream of this whole incident: bypass is the normal path, so a pending gate stopped looking unusual. Server-side state, admin only.
  • perf(gc): main regressed the retain cluster 2.2-4.8x — retain now runs 2 full collections where it ran none (suspect #7901/#7902) #7965's class stays uncovered. gc-ratchet would not have caught it even had it run: its gating metrics have no full-mark-sweep count, the counter that found it (collection_kind: "full" 0 → 2) is not one of them and no gate in the repo ratchets one, the two dimensions that moved (wall time, RSS) are explicitly "gating": false in the shared_ci profile CI uses, and its probe corpus is not the gc-handoff workloads that showed it. The human counter census was not a lucky substitute for a starved gate — it was the only instrument covering that dimension.

No new required contexts

Nothing here is promoted to required. ci-queue-reaper is a janitor, not a gate, and cannot fail a merge. The only change to a required context's file is security-audit.yml's concurrency group and test.yml's — neither alters what those workflows assert or when their PR arm runs.

Validation

GC gate wiring OK (7 gates main-line-reachable and able to fail; 31 workflows checked)
gc_gate_wiring_check self-test: OK (16 cases)
reap_stale_ci_runs self-test: OK (11 cases)
check_file_size.sh: OK
all 31 workflow files parse as YAML

Full measurement log: gc-handoff/GATES-NOTES.md. Scheduling doc updated — including a correction to its own "Those blocks are correct. Do not 'fix' them again", which was true when written and false three days later, and is exactly the sentence that would send the next reader past this bug.

Summary by CodeRabbit

  • New Features

    • Added automatic cleanup for stale queued pull-request workflow runs, with safe dry-run and manual apply options.
    • Added safeguards to detect invalid scheduling and concurrency configurations.
  • Bug Fixes

    • Prevented scheduled, tagged, and manually triggered workflow runs from cancelling or blocking one another.
    • Preserved automatic cancellation of superseded pull-request runs.
    • Limited security workflow execution to relevant repository configuration changes.
  • Documentation

    • Documented the CI queue issue, scheduling behavior, cleanup process, and remaining limitations.

…runs

#7856 moved ten expensive gates' main-line arm from `push: branches: [main]` to a
staggered six-hourly `schedule:`. Their concurrency groups key on
`github.event_name == 'push' && github.sha || github.ref` -- #7205's fix, guarded on
the event being `push`. With the main-line arm now `schedule`, the guard stops
matching and the group falls through to `github.ref`, constant `refs/heads/main`.

GitHub keeps at most one PENDING run per concurrency group and cancels the previously
pending one when a new run enters, regardless of `cancel-in-progress`. Measured
2026-08-12, identically on all ten gates: oldest run `queued` holding the group, the
next two `cancelled` with `jobs: 0`, newest `pending`. `gate-freshness` itself --
the alarm for this -- was cancelled the same way.

Groups are now keyed on `github.run_id` for every non-pull-request event. PR runs
keep the shared per-ref group and keep coalescing.

`gc_gate_wiring_check.py` gains `check_schedule_group`, swept over all 31 workflows
rather than just the GC gates. It found ten further workflows with the same latent
constant group -- including `test.yml`'s nightly safety net and `soak-autofix`, whose
group was a bare constant string -- all fixed here. Five new self-test cases; the
first is the sabotage case, since the existing CLEAN fixture carries the bad shape.

Capacity half: 1,529 runs queued against 12-14 concurrent, of which 794 were
`pull_request` runs over 63 head branches -- 61 of which no longer existed. Roughly
790 runs, 51% of the queue, were dead work for already-merged PRs sitting in front of
the `main` gates. `scripts/reap_stale_ci_runs.py` + `ci-queue-reaper.yml` cancel
QUEUED pull-request runs with no open PR; dry-run by default, `--max` capped, and
structurally unable to touch a push/schedule/tag/dispatch run. `zizmor`'s `push: main`
arm is path-filtered to `.github/**` and gains the concurrency block it never had.

Refs #7966
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a2f98b5e-85eb-486f-bdad-3f9a2b9c02ac

📥 Commits

Reviewing files that changed from the base of the PR and between a769faf and 74f8906.

📒 Files selected for processing (29)
  • .github/workflows/auto-opt-app-patterns.yml
  • .github/workflows/benchmark.yml
  • .github/workflows/ci-queue-reaper.yml
  • .github/workflows/container-tests.yml
  • .github/workflows/coverage.yml
  • .github/workflows/eh-transport.yml
  • .github/workflows/feature-matrix.yml
  • .github/workflows/gate-freshness.yml
  • .github/workflows/gc-moving-witnesses.yml
  • .github/workflows/gc-native-roots.yml
  • .github/workflows/gc-parse-churn-gate.yml
  • .github/workflows/gc-ptr-shape-off-witness.yml
  • .github/workflows/gc-ratchet.yml
  • .github/workflows/gc-root-dominance.yml
  • .github/workflows/llvm-inprocess.yml
  • .github/workflows/node-compat-matrix.yml
  • .github/workflows/node-core-subset.yml
  • .github/workflows/node-suite-guard.yml
  • .github/workflows/npm-package-sweep.yml
  • .github/workflows/security-audit.yml
  • .github/workflows/soak-autofix.yml
  • .github/workflows/test.yml
  • .github/workflows/tls-budget.yml
  • .github/workflows/zizmor.yml
  • changelog.d/7969-gate-starvation.md
  • docs/src/testing/ci-gate-scheduling.md
  • gc-handoff/GATES-NOTES.md
  • scripts/gc_gate_wiring_check.py
  • scripts/reap_stale_ci_runs.py

📝 Walkthrough

Walkthrough

This change updates CI concurrency groups, adds scheduled concurrency validation, introduces stale queued pull-request run cleanup, and documents the gate-starvation incident and related workflow behavior.

Changes

CI gate reliability

Layer / File(s) Summary
Event-aware workflow concurrency
.github/workflows/*.yml
Pull-request runs continue to group by ref and cancel superseded runs. Non-pull-request runs now use unique github.run_id groups. The zizmor push trigger is limited to .github/** changes.
Scheduled-group enforcement
scripts/gc_gate_wiring_check.py
The wiring checker validates scheduled workflow concurrency groups, adds self-tests, scans all workflow files, and reports the workflow count.
Stale queued-run selection and cancellation
scripts/reap_stale_ci_runs.py
The reaper identifies queued pull-request runs whose branches have no open pull request. It supports self-tests, dry-run output, bounded cancellation, and --apply execution.
Reaper workflow and incident records
.github/workflows/ci-queue-reaper.yml, docs/src/testing/ci-gate-scheduling.md, gc-handoff/GATES-NOTES.md, changelog.d/7969-gate-starvation.md
The reaper runs on a schedule or manual dispatch with scoped permissions. Documentation records the concurrency regression, queue findings, enforcement checks, and remaining operational conditions.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant ci-queue-reaper
  participant reap_stale_ci_runs.py
  participant GitHubAPI
  GitHubActions->>ci-queue-reaper: schedule or manual dispatch
  ci-queue-reaper->>reap_stale_ci_runs.py: run self-test and cleanup
  reap_stale_ci_runs.py->>GitHubAPI: retrieve open PR branches and queued runs
  GitHubAPI-->>reap_stale_ci_runs.py: return branch and run data
  reap_stale_ci_runs.py->>GitHubAPI: cancel eligible runs when apply is enabled
Loading

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/7966-gate-starvation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug marked this pull request as ready for review August 12, 2026 15:27
@proggeramlug
proggeramlug merged commit e316ecb into main Aug 12, 2026
4 of 27 checks passed
@proggeramlug
proggeramlug deleted the ci/7966-gate-starvation branch August 12, 2026 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant