Debugging a stuck merge queue Jump to heading
A stuck queue presents as silence. The pull request is approved, the interface says it is queued, and nothing happens until a timeout ejects it an hour later. Because the queue sits between three systems — the ruleset, the pipeline and the forge’s own scheduler — the instinct to check the most visible one first usually wastes the most time. This is the diagnostic order that finds the break fastest, within merge queues and required checks.
When to use this approach Jump to heading
- Entries sit in the queue and time out without a pipeline run.
- A run happens, goes green, and the entry still does not merge.
- The queue worked yesterday and nothing obvious changed.
- Only some pull requests stick — typically documentation-only ones.
- If entries are being ejected after a red run, the queue is working correctly and the failure is real; start from the test output instead.
Step 1 — Ask whether a candidate branch exists Jump to heading
The first fork in the road: did the queue build anything at all? A candidate branch appears on the remote for the lifetime of an entry.
# A live entry has a candidate ref; no ref means the queue never started
git ls-remote origin 'refs/heads/gh-readonly-queue/*' If there is no candidate ref, the queue never accepted the entry — the problem is in the ruleset or the approval conditions, and Step 4 is where to go. If there is one, the queue is working and the pipeline is not answering, which is Step 2.
Step 2 — Confirm a merge_group run was triggered Jump to heading
# Runs triggered by the queue specifically
gh run list --event merge_group --limit 10 --json displayTitle,status,conclusion,createdAt An empty list with a live candidate branch means no workflow listens for the event. This is the single most common cause, and the fix is one line in the pipeline, described in setting up a GitHub merge queue.
# Which workflows declare the trigger?
grep -rl 'merge_group' .github/workflows/ || echo "no workflow listens for merge_group" Step 3 — Compare reported names against required names Jump to heading
A green run that does not release the entry means the names do not match. The gate waits for a literal string; a job renamed, a matrix dimension added, or a workflow renamed all change what gets reported.
# What the ruleset waits for
gh api repos/:owner/:repo/rulesets --jq '.[].rules[]?
| select(.type=="required_status_checks")
| .parameters.required_status_checks[].context' | sort > /tmp/required.txt
# What actually reported on the latest candidate run
gh api repos/:owner/:repo/commits/HEAD/check-runs --jq '.check_runs[].name' | sort > /tmp/reported.txt
diff /tmp/required.txt /tmp/reported.txt What the diff tells you: a line only in required.txt is a name the gate is waiting for and nobody produces — that is your stuck entry.
Step 4 — Check whether a required job was skipped Jump to heading
A path filter that skips a required job produces a permanent wait, because a skipped job reports nothing at all.
# On the stuck pull request, which required names are missing a result?
gh pr checks --json name,state --jq '.[] | select(.state=="PENDING") | .name'
# Confirm the job's condition excluded it
gh run view --json jobs --jq '.jobs[] | {name, conclusion}' | grep -i skip The fix is a fallback job reporting success under the same name, covered in choosing required status checks that actually gate.
Step 5 — Rule out permissions and a broken default branch Jump to heading
Two less common causes remain, and both are cheap to exclude.
# Can the queue's identity actually write to the branch?
gh api repos/:owner/:repo/branches/main/protection --jq '.restrictions // "no push restrictions"'
# Is the default branch itself red? Every candidate inherits its state.
gh run list --branch main --limit 5 --json conclusion,displayTitle If the default branch is broken, every candidate tree contains the breakage and every batch fails. The queue is reporting accurately; it is just reporting about someone else’s change.
SAFETY WARNING — resist the urge to fix a stuck queue by turning off the ruleset and merging by hand. Doing so lands untested trees at exactly the moment your confidence in the pipeline is lowest, and the merges that follow are the ones nobody reviews afterwards. If you must bypass, do it through the named break-glass path and record it.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why does the interface show the entry as running when nothing is running? Jump to heading
The queue reports its own state, which is “waiting for checks”, and has no way to know whether any check will ever start. That is why the diagnostic begins with the refs and the run list rather than the queue view: those two show what exists, while the queue view shows what is expected.
Only documentation pull requests get stuck. Why? Jump to heading
Because they match no path filter, so the expensive required jobs never run and never report. Everything else exercises at least one filter and reports normally. The fallback-job pattern fixes it permanently.
After fixing the cause, do stuck entries recover on their own? Jump to heading
Usually not — they are waiting on a check run that was never created for that specific candidate, and the fix applies to the next one. Remove and re-enqueue them once a fresh entry has been observed to land.
Related Jump to heading
- Merge Queues & Required Checks — the parent topic and the moving parts involved.
- Setting Up a GitHub Merge Queue — the configuration this diagnosis keeps returning to.
- Choosing Required Status Checks That Actually Gate — the fallback pattern for skipped jobs.