What "blocked" means and how to read it fast
A protected branch can refuse a merge for at least eight distinct reasons, and they need different fixes — so the first move is never to guess, it is to read the mergeable state. The authoritative source is the GraphQL MergeStateStatus enum, whose values map one-to-one to the lowercased strings the REST API returns (MergeStateStatus enum):
BLOCKED— "The merge is blocked." A required review, status check, or conversation is unmet.BEHIND— "The head ref is out of date." Strict checks require the branch to be up to date first.DIRTY— "The merge commit cannot be cleanly created." A merge conflict.UNSTABLE— "Mergeable with non-passing commit status." A non-required check is red or pending; merge is still allowed.CLEAN— "Mergeable and passing commit status." Green; merge is allowed.DRAFT— blocked because the PR is a draft.UNKNOWN— "The state cannot currently be determined" — mergeability has not been computed yet; query again.
Note the REST mergeable_state field that returns these values is described by GitHub staff as undocumented and "in flux" (community discussion #24299) — so trust the GraphQL enum for the definitions, and treat the REST strings as the lowercased equivalents.
Diagnose it
Pull the mergeable state directly. The REST call is quick:
# mergeable is a boolean; mergeable_state is the lowercased MergeStateStatus.
gh api repos/{owner}/{repo}/pulls/{number} \
--jq '{mergeable, mergeable_state, draft}'
If mergeable_state is unknown, GitHub has not finished computing mergeability — wait a second and re-query. Then route on the value:
dirty→ merge conflict. Go to resolve a conflict.behind→ out of date under strict required checks. Update the branch.blocked→ a required gate is unmet. Enumerate which one (below).unstable→ mergeable; a non-required check is failing. You can merge, but a check is red — decide whether that matters.clean→ not blocked by protection; if the button is still grey, it is a permission issue, not a rule.
For blocked, list what the branch actually requires so you know which gate to clear:
# Branch protection (classic). Shows required reviews, checks, conversation
# resolution, signatures, linear history, enforce_admins.
gh api repos/{owner}/{repo}/branches/{branch}/protection \
--jq '{
reviews: .required_pull_request_reviews,
checks: .required_status_checks.contexts,
strict: .required_status_checks.strict,
conversation: .required_conversation_resolution.enabled,
signatures: .required_signatures.enabled,
linear: .required_linear_history.enabled,
enforce_admins: .enforce_admins.enabled
}'
Rulesets can also apply — and multiple rulesets layer on top of branch protection, with "the most restrictive version of the rule" winning (About rulesets). So a branch with no classic protection can still be blocked by a ruleset. Check both.
Root causes
Each maps to a documented branch-protection setting (About protected branches):
- Required reviews unmet — the PR has fewer approving reviews than required.
- Stale approvals dismissed on push — "dismiss stale pull request approvals when commits are pushed that affect the diff." Your approvals were removed by your own push.
- Required conversation resolution — "Requires all comments on the pull request to be resolved before it can be merged."
- Required Code Owner review — a path with a CODEOWNERS entry was touched and the owner has not approved.
- Required signed commits — an unsigned commit is on the branch; "contributors and bots can only push commits that have been signed and verified."
- Out of date under strict checks — "Require branches to be up to date before merging" means the branch "must be up to date with the base branch before merging" — this is the
behindstate. - Required linear history — "prevents collaborators from pushing merge commits," so a standard merge-commit merge is refused; use squash or rebase.
- Required status checks — a required check is not
success,skipped, orneutral. (If a required check is stuck on Expected, see required status check stuck.)
Resolution options
Conflict (dirty). Update your branch against the base and resolve locally:
git fetch origin
git switch <your-branch>
git merge origin/<base-branch> # or: git rebase origin/<base-branch>
# resolve conflicts, then:
git add -A && git commit # (no commit needed after a clean rebase)
git push
Behind (behind). Same update — bring the base into your branch (or use the PR's "Update branch" button). Under strict mode the branch "must be up to date with the base branch before merging" (About protected branches), so the update produces a new tip that must satisfy the required checks before merge is allowed.
Blocked on reviews. Get the required approvals. If approvals keep vanishing, the branch has stale-dismissal on — that is working as configured, not a bug; coordinate so reviews land after the last push.
Blocked on conversations. Resolve every open conversation thread in the PR.
Blocked on linear history. Switch the merge method to squash or rebase; a merge commit is disallowed.
Blocked on signatures. Re-create the offending commits signed, or sign them, then force-push.
Recommended approach (with tradeoffs)
Lead with the mergeable state every time — it collapses an eight-way guess into one lookup and tells you whether you are fixing a conflict, updating a branch, or chasing an approval. The tradeoff to understand is strict mode ("up to date before merging"): it guarantees every merge was tested against the current base, which is genuinely safer, but on a busy base branch it forces a serialize-and-retest cycle that can starve a PR (update → checks re-run → base moves again → update again). On high-traffic branches, a merge queue is the better tool than strict mode — it batches and tests updates for you instead of making each author race the base. The blast radius of leaving strict mode on a hot branch is wasted CI and frustrated authors, not data loss.
Avoid the reflex of toggling enforce_admins off to push a merge through. It works, but it bypasses the exact protection the branch exists for, and it is invisible to everyone who later assumes the rule held.
Validation steps
After clearing the gate, confirm the state flipped before you rely on the button:
gh api repos/{owner}/{repo}/pulls/{number} --jq '.mergeable_state'
# want: "clean" (or "unstable" if you have accepted a non-required red check)
clean means every required gate is satisfied. unstable means you can merge but a non-required check is not green — a deliberate decision, not a block.
Prevention strategies
- Pick rulesets over scattered branch-protection toggles for new repos — they layer predictably, are visible to anyone with read access, and support granular bypass instead of all-or-nothing admin override (About rulesets).
- Use a merge queue instead of strict mode on branches with many concurrent PRs.
- Decide on stale-approval dismissal deliberately. It is the right call for security-sensitive repos and a constant friction source elsewhere; either way, document it so authors are not surprised when approvals disappear.
Operational lessons
The recurring trap is reading the PR page instead of the PR state. The page shows a grey button and a wall of green-and-grey rows; the API shows one word that names the cause. behind and dirty look identical in the UI ("This branch is out of date" / "can't merge") but mean different things — one needs an update, the other needs conflict resolution.
When a merge is blocked, the question an on-call engineer actually has is "what change is sitting behind this gate, and what does it need to ship?" The mergeable state, the required-check results on the head commit, the reviewers, and the commit that triggered it are all parts of one answer. Correlating them — instead of clicking through four GitHub tabs — is what turns a blocked PR from a mystery into a one-line "needs one more approval and a rebase."