Skip to content

Scheduler pre-review update-branch discards in-flight checks under queue saturation: 76/77 merges since 09-04 had 0/12 required contexts at merge time #1935

Description

@seonghobae

Summary

Under queue saturation, the scheduler's pre-review update_branch path discards in-flight checks. A scheduler run waits 2h+ for a runner, executes after main has moved, picks one behind PR from its scan — its own PR (#1926) or another one (a codex/originweave-mv3-owner run updated #1857) — and merges main into that head before any review is dispatched. Every completing run therefore has one near-random victim, and the scheduler's completion rate is the PR cancellation rate. The push cancels every queued check on the old head and puts the new head at the back of the queue. main moves roughly every 28 minutes (77 merges into this repository in ~36h), so no PR head survives long enough for its 12 required contexts to complete.

Measured on #1926 (2026-09-05):

item value
scheduler run for the PR 33963177129, pull_request_target, created 11:22:53Z, completed 13:44:41Z (success)
bot push c730ae0b (Merge branch 'main' into …) 13:44:37Z
scheduler log line PR #1926: update_branch: current head has no OpenCode approval; branch is outdated before review dispatch; branch update requested with OpenCode app token inside GitHub Actions as OpenCode GitHub App
check-runs on the replaced head 7920a70b 28 total: 22 cancelled within seconds of the push (queued since 11:22:53Z), 3 success, 3 skipped

Same mechanism observed on #1812 (c9244ceb, 13:31:29Z) and, after the #1926 merge, on #1484 (777ee3ed, 13:58:42Z: 21 of 30 check-runs on the old head cancelled at the push, 8 successes orphaned, new head starts from zero). The pushes are not a burst caused by the merge: they are a steady drip, one per PR scheduler run that finishes its queue wait, and with main moving every ~28 minutes almost every run finds its PR behind. The 5–12 bot-pushed branches seen in the 20 minutes after 6f8c51d7 are the scheduler runs that happened to complete in that window.

Consequence on merge governance

Reconstructing the check state at merged_at for every PR merged into .github since 2026-09-04 (77 PRs, REST commits/{sha}/check-runs, only runs completed before merged_at counted, skipped counted as satisfied):

required contexts satisfied at merge PRs
0 / 12 76
7 / 12 1
12 / 12 0

Depth ≥ 2 jobs (opencode-review, coverage-evidence, trivy-fs, scorecard, osv-scan, dependency-review, both CodeQL compatibility analysis (...)) had no check-run at all before merge on 75–77 heads: their upstream gate job never got a runner. Every merge since 09-04 was therefore an admin bypass with the required review evidence absent. merged_by is populated on these, so it is not a bypass signal; the check state at merged_at is.

Where

  • .github/workflows/pr-review-merge-scheduler.yml:139UPDATE_BRANCHES is true for pull_request_target runs, so every PR's own event run performs the update.
  • scripts/ci/pr_review_merge_scheduler_core.py:4774-4785if behind_by and trigger_reviews:update_branch; there is no check for required check-runs still queued/in progress on the current head.
  • CLAUDE.md states the scheduler "updates a PR branch only when the latest review is approved, no current-head check has failed, and GitHub reports the PR as behind". The pre-review path above is a second, undocumented update path.

Proposed fix (smallest root-cause change)

  1. In pr_review_merge_scheduler_core.py, before the pre-review update_branch, decide wait when the current head still has required check-runs in queued/in_progress — do not discard evidence in flight. One helper, one guard, one contract test.
  2. Optionally, set UPDATE_BRANCHES to false for pull_request_target runs and leave the refresh to the schedule/push sweeps, which are rate-limited by BRANCH_UPDATE_LIMIT.
  3. Correct the CLAUDE.md sentence to describe both update paths.

Related: #1931 (queue delay invalidates the dispatch side via base_sha drift — same family, other half), #1929/#1927 (dispatch actor), #1925 (CodeQL dispatch never succeeds).

Instrument notes for whoever re-measures: the pulls list endpoint omits merged_by; a check-run's started_at is null while queued; the closed event after merge creates fresh check-runs on the same head, so "latest check-run per name" reads post-merge state unless filtered by merged_at.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions