Merge queue
How GitHub's merge queue interacts with `gh pr merge` on this repo. The `UNSTABLE` vs `CLEAN` vs `BLOCKED` states, what cancels auto-merge, and the speculative-branch double-run.
The dev branch has a merge queue. main does not. This affects every
PR that targets dev — that's basically every PR.
gh pr merge flags
| Command | Behavior |
|---|---|
gh pr merge <n> | Queue the PR right now. Rejects if required checks aren't green. |
gh pr merge <n> --auto | Enable auto-merge. PR enters the queue once required checks pass. |
gh pr merge <n> --rebase | Rejected: "merge strategy set by merge queue" |
gh pr merge <n> --squash | Rejected: same |
gh pr merge <n> --disable-auto | Cancel auto-merge. |
The strategy is configured on the queue itself (squash with merge-commit metadata, currently). Trying to override per-PR fails.
State semantics
gh pr view <n> --json mergeStateStatus returns one of:
| State | Meaning |
|---|---|
CLEAN | Required checks green, ready to merge. |
UNSTABLE | Required green, optional checks (Vercel deploy, Cursor Bugbot) failing or pending. The queue still accepts. |
BLOCKED | Required check failing or not yet run. |
UNSTABLE is fine — most of this repo's PRs sit in UNSTABLE during normal
operation because Vercel deploy and Bugbot are not required. BLOCKED means
fix something.
The speculative-branch double-run
When a PR enters the queue, GitHub creates a synthetic branch:
gh-readonly-queue/dev/pr-<n>-<sha>This branch is "what dev would look like with your PR merged on top." The
full CI pipeline runs a second time against it. Only when that
speculative run passes does GitHub fast-forward dev to include your PR.
This roughly doubles wall-clock time to merge. Don't be surprised when a PR sits in "queued position 1" for 25-30 minutes after CI on the PR itself went green — that's the speculative run.
Pushing to a queued PR
Pushing a new commit cancels auto-merge. The autoMergeRequest field on
the PR clears. Re-enable it:
git push # fixup commit lands
gh pr merge <n> --auto # re-queueThis catches teams off-guard regularly. If you're amending in response to a
reviewer comment, expect to re-run gh pr merge --auto after the amend.
Required vs optional checks
Required checks (block merge):
Lint,Format,Typecheck,Unit Tests,BuildVisual Regression(when affected scope includesapps/web)CI Success(aggregator that depends on all the above)
Optional checks (informational, don't block):
Deploy Web to Vercel,Deploy Admin to Vercel,Deploy Docs to Vercel,Deploy Dev Docs to VercelLocal smoke (web),Smoke (admin),Smoke (web)Cursor BugbotCodeQL,Analyze (*)— security scans, eventual blocker but not today
Vercel deploys fail in pre-deploy scenarios where the Vercel project isn't
provisioned yet (e.g. when VERCEL_PROJECT_ID_DEV_DOCS isn't set). The PR
is still mergeable; the deploy turns green on a follow-up commit after the
project is wired.
Bypassing the queue (don't, but)
There is no override for the queue under normal circumstances. The only
way to bypass it is git push origin pr-x/topic:dev --force from an admin
account, which is destructive (rewrites the queue's expectations of dev)
and audited. Never do this.
If a hot-fix to dev is genuinely urgent (a bad PR landed and broke prod),
the procedure is:
- Open a revert PR targeting
dev - Mark it
[REVERT]in the title gh pr merge --autolike any other — the queue will accept it on a clean CI run
The revert ships through the queue in 5-30 minutes, which is the worst-case SLA. If that's too slow, the right escalation is to ask GitHub Support to pause the queue.
Common queue states
| Symptom | Diagnosis |
|---|---|
| PR sits at "queued position 1" forever | Speculative branch CI running. Normal. Check gh-readonly-queue/dev/pr-* runs. |
gh pr merge returns "merge strategy set by merge queue" | You passed --rebase or --squash. Drop the flag. |
| Auto-merge silently disappeared | Someone (you?) pushed a commit. Re-run gh pr merge <n> --auto. |
PR shows BLOCKED when required checks look green | Stale check entry. Re-running often clears it. If not, check for missing required check (e.g. a renamed job). |
| Queue rejects a PR with "required status check is expected" | A new required check was added but hasn't run on this PR yet. Push an empty commit (git commit --allow-empty -m chore: trigger ci) to retrigger. |