ci: keep open PR branches up to date with main automatically - #675
Merged
Conversation
`main`'s protection sets `required_status_checks.strict = true`, so a PR must
be up to date with the base to merge. That is the right setting — it is what
stops two independently-green PRs from combining into a broken main — but it
means every merge to main invalidates every other open PR.
Paid by hand, that costs twice: the CI re-run, and the latency between "CI went
green" and "somebody noticed and pushed the back-merge". With several PRs open
the second cost dominates, and a PR discovers it is stale at merge time, which
is the worst moment to find out. Four PRs went stale together this way.
This workflow brings every open, non-draft PR forward whenever main moves. With
auto-merge (now enabled repo-wide), a green and approved PR lands unattended.
Two things it gets right that the obvious version does not:
* It reads `behind_by` from the compare API rather than `mergeStateStatus`.
Mergeability is computed LAZILY — the first query kicks off the computation
and returns UNKNOWN, and only a later query sees the truth. A workflow keyed
on it would skip exactly the PRs it exists to fix, on precisely the run where
main just moved. Observed live on this repo: two PRs reported
`mergeable: UNKNOWN` alongside a fully-populated check list.
* It requires `behind_by` to actually be an integer before using it as one.
`gh api` writes its error body to STDOUT, so on a 404 (deleted or fork-side
branch) the variable holds `{"message":"Not Found",...}` rather than being
empty; an emptiness check alone passes that to `[ "$x" -eq 0 ]`, which errors
and — being false — falls through to attempting an update on a branch that
does not exist. Both paths verified against the live API.
It never force-pushes (`update-branch` merges), skips drafts so a push to main
does not burn a CI run per draft, and reports a per-PR outcome table rather than
failing the run when one branch conflicts — a conflict is the author's to
resolve and must not hide the result for the others.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cuttlefisch
enabled auto-merge
August 6, 2026 11:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four PRs went stale against main simultaneously today, each then needing a
manual back-merge plus a full CI re-run before it could land. This removes the
manual step.
Why they go stale
main's protection setsrequired_status_checks.strict = true— a PR must be upto date with the base to merge. That is the right setting; it is what stops two
independently-green PRs from combining into a broken main. But it means every
merge to main invalidates every other open PR.
Done by hand the cost is paid twice: once in CI minutes, and once in the latency
between "CI went green" and "somebody noticed and pushed the back-merge". With
several PRs open the second cost dominates, and a PR discovers it is stale at
merge time — the worst moment to find out.
What this does
On every push to main, bring every open non-draft PR forward. Combined with
auto-merge (now enabled repo-wide), a PR that is green and approved lands without
anyone watching it.
Two bugs the obvious version would have shipped
Both were found by testing against the live API rather than reasoning about it.
1.
mergeStateStatusis computed lazily. The first query for a PR kicks offthe computation and returns
UNKNOWN; only a later query sees the truth. Aworkflow keyed on
state == "BEHIND"would therefore skip exactly the PRs itexists to fix, on precisely the run where main just moved and nothing has been
recomputed — silently, looking like a successful no-op run. Observed live on this
repo minutes before writing it: two PRs reported
mergeable: UNKNOWNalongside afully-populated check list. Fixed by reading
behind_byfrom the compare API,which is derived from the commit graph and correct on first read.
2.
gh apiwrites its error body to stdout. On a 404 — deleted branch,fork-side head —
$behindholds{"message":"Not Found",...}, not an emptystring. An emptiness check alone passes that straight to
[ "$behind" -eq 0 ],which errors and, being false, falls through to attempting an update on a branch
that does not exist. Fixed with a
caseguard requiring an actual integer; allfour input shapes (
5,0, empty, error-JSON) unit-tested.Deliberate non-behaviours
gh pr update-branchmerges, so nobody's local clonehas history rewritten under it.
run per push to main for nothing.
resolve, and must not hide the outcome for every other PR. Each result goes in
a per-PR summary table with a totals line.
would otherwise start overlapping runs racing on the same branches, doubling
the CI they trigger.
Also enabled
allow_auto_mergeon the repository, which was off — that is whatlets a green PR land without a manual merge command.
🤖 Generated with Claude Code