PR catch-up: run automatically when main goes green
Nobody should have to run scripts/merge-main.sh or add catch-up by hand in the normal flow. Each time a push to main passes CI fast guards, pr-catch-up.yml now picks open pull requests against main and runs the existing catch-up merge on them.
Selection (scripts/ci/auto_catch_up_select.py): the head is an unprotected branch of this repo (no branch protection rule or ruleset, since merge refuses those), not a draft, no no-auto-catch-up label, has a check suite, has been quiet for 30 minutes and was pushed within 14 days, and either the PR is CONFLICTING, or the Actions bot's CI fast guards comment is about the current head (marker on the first line, failed-on sha matching headRefOid) with a step headed "(red on main too" and the head does not contain the green commit yet (GraphQL Ref.compare against the workflow run's head_sha). A stale guard comment on an up-to-date branch is skipped. At most vars.CMUX_AUTO_CATCH_UP_MAX (default 15) per run, most recently pushed first, since each catch-up push re-runs the PR's CI. Reads are batched GraphQL: open PRs newest-updated first with their first and last 100 comment ids, stopping at the age limit; bodies only for the bot's comments on PRs that passed the other rules, paging the middle comments only for a PR with more than 200 whose guard comment was not found; up to 3 aliased mergeability re-reads 20 s apart; one aliased comparison for red-on-main heads. A live read-only dry run read 645 PRs in about 95 s and found 226 conflicting in-repo PRs, so expect the first day to work through that backlog 15 at a time.
Workflow: a read-only select job, then merge and finish as a matrix (fail-fast off; merge runs 15 at once since finish waits for the whole matrix, finish 3). Automatic entries use their own concurrency groups (pr-catch-up-auto-merge-N, pr-catch-up-auto-push-N), never cancelled in progress, so an automatic run cannot replace a writer's pending request; two runs cannot both land a push because each push is a compare-and-swap on the head it merged. Each entry pins the head the selector judged, so a head that moved is refused. merge hands its result to finish in a per-PR artifact, which catch_up_pr.py read-result validates (40-hex ids, check-ref-format names, known status, the pinned head, a one-line refusal without workflow commands) before any step uses it; finish then re-verifies the merge against the PR as before. The auto path selects nothing without the route App key and pushes only with the route App token.
Noise: the auto path comments only for a push, a conflict a person must resolve, a refused push, or a merge needing workflow permission, with no mentions and no issue reference, marked <!-- cmux-auto-catch-up head=<sha> --> so that head is never tried again; a stuck conflict is reported once per head. Up to date, error or lost runner, branch moved and unverified stay silent. Silent outcomes are bounded by a ledger artifact (pr-catch-up-auto-ledger) the select job keeps: a head selected twice is left alone. The label and /catch-up paths behave as before.
Tests: tests/test_ci_auto_catch_up_select.py (each rule and skip reason, stale guard comments, the green-commit comparison, protection and rulesets, PRs with more than 100 and 200 comments, the ledger, mergeability re-reads, cap, ordering, paging, a replayed GraphQL fixture through the CLI) wired into ci-guards.yml as linux-guard; tests/test_ci_catch_up_pr.py covers the matrix, trigger, the auto comment staying silent on errors, and read-result against crafted artifacts (line breaks, bad shas, ../ branch names, unknown status, wrong head). The registry, wiring, workflow-run-source and determinism checks and actionlint pass.
Needs the no-auto-catch-up label to exist before anyone can opt out. Docs: workflow header, docs/ci/merge-main.md, and a paragraph in CLAUDE.md telling agents to git pull --no-rebase on a rejected push instead of force-pushing.
Callsign: unregistered
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.
Summary by cubic
PR catch-up now runs automatically: when a push to main passes CI fast guards, the workflow picks open pull requests against main that conflict with it or are red only because of main, and runs the existing catch-up merge on each — nobody has to add the catch-up label or run merge-main.sh by hand anymore.
- Selection (
scripts/ci/auto_catch_up_select.py) uses batched GraphQL and picks in-repo, non-draft heads without theno-auto-catch-uplabel that have been quiet for 30 minutes and pushed within 14 days; it takes at mostvars.CMUX_AUTO_CATCH_UP_MAX(default 15) per run, most recently pushed first. - Red-on-main candidates must have the guard comment about the current head and a head that lacks the green commit, so a stale comment on an up-to-date branch no longer takes a slot; protected, ruleset, deleted, and check-suite-less heads are skipped.
- Merge and finish jobs run as a matrix capped in parallel, handing over per-PR artifacts that finish validates (via
catch_up_pr.py read-result) before using them; automatic entries get their own concurrency groups, so they never replace a writer's pending request. - The automatic path pins the head it judged, pushes only with the route App token, and comments only when it pushed or a person must act — blocked, push-denied, and needs-workflows are reported once per head, errors stay silent.
- The attempt ledger is read only from this repository and overwritten on re-run, so a fork cannot supply one and re-runs no longer fail on an existing name.
- Manual label and
/catch-uprequests behave as before.
Migration
- The
no-auto-catch-uplabel must exist so authors can opt out. - Expect the first day to work through the existing conflict backlog 15 at a time.
- Docs and CLAUDE.md now tell agents to
git pull --no-rebaseon a rejected push instead of force-pushing.
Summary by CodeRabbit
-
New Features
- Pull requests that conflict with
mainor are failing only because ofmaincan now be automatically caught up after its fast CI checks pass. - Eligible pull requests are processed in parallel, up to a configurable limit. Automatic catch-up skips opted-out pull requests and branches without recent activity; branches without the required authorization are not pushed.
- Automatic updates report successful pushes and cases requiring action, while other outcomes remain silent.
- Pull requests that conflict with
-
Documentation
- Added guidance on automatic catch-up, opting out, and recovering from a rejected push.