Setting up a GitHub merge queue Jump to heading
Enabling a merge queue looks like a single toggle and is not. The toggle depends on a ruleset that forbids direct merges, a workflow that listens for a trigger most pipelines do not have, and a check name that must match between three different places. Miss any one and the symptom is identical: pull requests enter the queue and sit there until they time out. This recipe walks the whole chain in order, as part of merge queues and required checks.
When to use this approach Jump to heading
- Your default branch breaks a few times a month from changes that were individually green.
- More than three or four pull requests merge per day, so the ordering actually matters.
- Your pipeline finishes in minutes rather than an hour — a queue amplifies pipeline slowness.
- You are willing to forbid manual merges on the default branch, which the queue requires to mean anything.
- If your team merges twice a week, a queue adds ceremony without removing a problem you have; strengthen pre-push validation rules instead.
Step 1 — Add the merge_group trigger to the pipeline Jump to heading
The queue builds a candidate branch and waits for check runs on it. A workflow that only triggers on pull_request never runs there, so the queue waits for a result that cannot arrive.
# .github/workflows/ci.yml
name: ci
on:
pull_request:
merge_group: # the candidate branch the queue builds
push:
branches: [main]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test What changed: the same job now reports on ordinary pull requests and on the queue’s candidate branch, so one definition serves both.
# Verification: commit the workflow, then confirm the trigger is parsed
gh workflow view ci.yml --yaml | grep -A3 '^on:' Step 2 — Create the ruleset that forbids direct merges Jump to heading
A queue that people can step around is a suggestion. The ruleset is what makes it a rule, and it is also where the required check names live.
# Inspect what protects the default branch today
gh api repos/:owner/:repo/rulesets --jq '.[] | {id, name, target}'
# Read the exact check-run name from the most recent run on main
gh api repos/:owner/:repo/commits/main/check-runs --jq '.check_runs[].name' Create a ruleset targeting the default branch with pull requests required, direct pushes blocked, and the merge queue enabled. Paste the check-run name from the command above into the required-checks list rather than typing what you think the job is called.
# Verification: a direct push must now be refused by the server
git push origin HEAD:main 2>&1 | grep -i 'protected\|rejected' What changed: merging is now possible only through the queue, which is the precondition for anything the queue reports to be meaningful.
Step 3 — Choose the merge method and batch size Jump to heading
The queue can land entries as merge commits, squashes, or fast-forwards. Fast-forward keeps history linear and, more importantly, means the commit on the default branch is bit-for-bit the commit that was tested.
Start with a batch size of one. It is the slowest and the least surprising, and you can raise it once you know your entry failure rate — the arithmetic for that is in batching pull requests to cut CI cost.
Step 4 — Queue the first pull request and watch the refs Jump to heading
The first run is where name mismatches surface. Watch the queue’s own refs rather than the web interface, because the refs show whether a candidate branch was built at all.
# A candidate branch appears while an entry is in flight
git ls-remote origin 'refs/heads/gh-readonly-queue/*'
# Follow the run that the candidate branch triggered
gh run list --event merge_group --limit 5 If ls-remote shows a candidate branch but gh run list shows nothing, the trigger from Step 1 is missing or misspelled. If neither shows anything, the pull request never entered the queue — usually because the ruleset requires a check that has not reported, so the entry is still waiting for approval conditions.
# Verification: after the run is green, main should point at the tested commit
git fetch origin && git log --oneline -1 origin/main Step 5 — Define the break-glass path before you need it Jump to heading
There will be an incident where the queue’s latency is unacceptable. Decide now who may bypass it and what they must do afterwards, because the alternative is someone discovering the bypass under pressure and it becoming routine.
# Record every bypass so the exception stays an exception
gh api repos/:owner/:repo/rulesets/RULESET_ID/history --jq '.[] | {actor: .actor.login, at: .created_at}' SAFETY WARNING — granting bypass rights to a whole team removes the guarantee the queue exists to provide, and it does so silently: the branch stays protected, the queue stays configured, and nothing reports that the last twelve merges skipped it. Scope bypass to a named break-glass role, alert on its use, and review those alerts.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why does my entry time out with no pipeline run? Jump to heading
Almost always the missing merge_group trigger. The queue creates its candidate branch, no workflow listens for that event, no check run is ever reported, and the entry waits until the queue’s timeout expires. Confirm with gh run list --event merge_group: an empty list with a live candidate branch is conclusive.
Can required checks differ between pull requests and the queue? Jump to heading
They can, and doing so undermines both. If the queue requires less than the pull request did, changes reach the default branch having passed a weaker gate; if it requires more, authors see green on their branch and an unexplained rejection afterwards. Run the same job for both events and require the same name.
What happens to an entry when someone pushes to its branch? Jump to heading
The entry is removed from the queue, because the thing that was about to be tested no longer exists. The pull request returns to its normal state and has to be approved and enqueued again. This is worth explaining to the team, since an innocent “just fixing a typo” push during a long queue is a common source of confusion.
Related Jump to heading
- Merge Queues & Required Checks — the parent topic: what a queue guarantees and what it costs.
- Choosing Required Status Checks That Actually Gate — deciding which results deserve to block a merge.
- Debugging a Stuck Merge Queue — the diagnostic order when entries stop landing.