Branch Protection Blocks a Legitimate Merge

Your PR has approvals and green CI but GitHub, GitLab, or Bitbucket still won't let you merge. Find the exact rule that's failing and clear it without weakening protection.

Your pull request looks done: approvals in, all checks green, no conflicts. But the Merge button is still grayed out, or it shows something like Required statuses must pass before merging, Review required, or Merging is blocked. The protection itself is working as designed. What’s wrong is almost always a small mismatch between the rule and the current state of the PR: a renamed check, a check that ran on the wrong commit, an approval that got dismissed, or a CODEOWNERS slot nobody has filled.

Fastest fix: expand the blocked-merge box and read the first red item, then jump to the matching section below. On GitHub it’s the “Merging is blocked” / “Required checks” panel above the button; on GitLab it’s the merge widget; on Bitbucket it’s the Merge checks panel on the right. Each one names the exact rule that is failing — don’t guess.

Which bucket are you in?

Symptom in the merge boxMost likely causeJump to
A required check sits at Expected / Pending and never turns greenCheck name renamed, or workflow skipped by paths: filterCause 1
Check passed but still shows pendingPassed on an old commit SHA, or merge-commit vs head-commit conflictCause 2
”Review required” after you fixed a typoStale approval dismissed on new pushCause 3
”Review required from Code Owners”A CODEOWNERS path has no approval from the owning teamCause 4
”This branch is out-of-date with the base branch""Require branches to be up to date” + base movedCause 5
Required reviews count is short, or a “Changes requested” is stuckCount raised, or a reviewer’s block was never clearedCause 6
Unverified commit / “unresolved thread”Signed-commits rule, or GitLab unresolved threadsCause 7

One terminology note for GitHub, as of June 2026: classic branch protection rules still work, but GitHub now steers most repos toward rulesets (Settings > Rules > Rulesets). The rule names are nearly identical; the main difference is that admin bypass moved from the “Do not allow bypassing the above settings” checkbox into a per-ruleset Bypass list. Both screens are described below where it matters.

Common causes and fixes

Cause 1: required check name mismatch, or skipped workflow

This is the single most common blocker. The protection rule requires a check called, say, CI / test, but the GitHub Actions job now reports ci / run-tests (someone renamed the job, or added a matrix so it’s build (ubuntu-latest)). The new name never satisfies the old requirement, so the check sits at Expected forever.

A close cousin: the workflow has a paths: filter (only runs on src/**), but this PR only touches docs/**. GitHub’s own docs confirm that a workflow skipped by path filtering, branch filtering, or a commit-message condition leaves its checks in a Pending state — and a required check stuck at pending blocks the merge.

How to spot it: open the PR’s Checks list and read the exact reported name. Then open the rule (Settings > Branches, or Settings > Rules > Rulesets) and compare it to the required name. Matching is exact and case-sensitive. If the required check isn’t in the PR’s list at all, it was skipped.

Fix the name mismatch: make the two strings identical. Decide on the real reported name from the Checks tab (it’s <workflow-name> / <job-name>), then update the protection rule to that exact string. Re-trigger if needed:

gh workflow run ci.yml --ref my-branch

Fix a skipped (path-filtered) check without weakening the filter: the clean, GitHub-recommended approach is to make the job run always but no-op when paths don’t match (a “required but skippable” pattern), so it always reports a result. As a one-off unblock, you can also make a no-op change inside a monitored path:

touch src/.keep
git add src/.keep
git commit -m "ci: trigger required check for path-filtered workflow"
git push origin my-branch

Remove the .keep file in a follow-up commit after merge. The durable fix lives in the workflow: see GitHub’s guidance on “Handling skipped but required checks.”

Cause 2: check passed but shows pending (wrong SHA)

The check went green, but the merge box still lists it as required/pending. Per GitHub’s docs, required checks must pass against the latest commit SHA, and a check is only counted if it finished within the last 7 days. A check that ran on abc123 does not satisfy the requirement after you pushed def456.

There’s a second, subtler case: GitHub evaluates the test merge commit (your branch merged onto the base) separately from your head commit. If the merge commit has a status, that one must pass; the PR will say Showing checks for the merge commit. A flaky or outdated merge-commit run can block you even when the head commit is green.

Fix: re-run CI against the current HEAD, or push an empty commit to force a fresh run on the latest SHA:

git commit --allow-empty -m "ci: re-run checks on latest commit"
git push origin my-branch

If you use a merge queue, add the merge_group trigger to the workflow — without it, required checks never run while the PR sits in the queue, and it stalls indefinitely:

on:
  pull_request:
  merge_group:

Cause 3: approval dismissed after a new commit

A reviewer approved at abc123; you pushed a one-line typo fix as def456. With “Dismiss stale pull request approvals when new commits are pushed” turned on, GitHub silently revoked the approval, and the badge now reads Review required.

How to spot it: the approval count dropped right after your last push, with no reviewer changing their vote.

Fix: use GitHub’s Re-request review control — the circular-arrow icon next to the reviewer’s name in the Reviewers box. Leave a one-line comment noting the change was trivial. (Pushing an empty commit re-triggers CI but does not re-request a human review, so do both if both are blocking.)

Cause 4: CODEOWNERS review missing

A CODEOWNERS entry such as src/billing/** @org/billing-team means any PR touching billing code needs at least one approval from billing-team, no matter how many other approvals you have. The merge box shows Review required from Code Owners.

How to spot it: the PR touches an owned path and the Code Owners line is yellow with no owning-team approval. Check .github/CODEOWNERS (or the repo-root / docs/ copy).

Fix: find the owner and request review directly:

cat .github/CODEOWNERS   # or: cat CODEOWNERS

Then @-mention the owning team or an individual member and request their review. If the only listed owner is unavailable, see the FAQ on temporary co-owners.

Cause 5: branch is out-of-date with base

The rule “Require branches to be up to date before merging” means your branch must contain every commit currently on the base. If main moved after you branched, the merge box shows This branch is out-of-date with the base branch.

Fix: click Update branch on the PR, or do it locally:

git checkout feature/my-pr
git fetch origin
git merge origin/main          # or: git rebase origin/main
git push --force-with-lease origin feature/my-pr

Use --force-with-lease (never a bare --force) so you don’t clobber a teammate’s push you haven’t fetched. Note that updating the branch creates a new SHA, which may re-trigger CI and, if “dismiss stale approvals” is on, re-open Cause 3.

Cause 6: required approvals short, or “Changes requested” stuck

Two variants here. Either an admin raised the required-approval count (1 to 2) after your PR already had its one approval, so it’s now one short. Or a reviewer clicked Request changes, you fixed everything, but that reviewer never re-submitted, so their orange “changes requested” block persists and silently overrides other approvals.

How to spot it: in the Reviewers box, an orange marker means changes requested; a green check means approved. One orange marker blocks the merge regardless of green ones.

Fix: ping the blocking reviewer to re-review and approve. If they’re unavailable and you have admin rights, dismiss that stale review (the ”…” menu on the review, then “Dismiss review”) with a written reason. To compare the required count against current approvals, open the rule and read the “Require a pull request before merging” approval number.

Cause 7: signed commits, or unresolved threads

Signed commits (GitHub/Bitbucket): a rule requires every commit to be GPG/SSH-signed, but one commit isn’t (often a commit made through the web editor, or a local clone without signing configured). The commits list shows that commit without a green Verified badge.

git config --global user.signingkey <your-key-id>
git config --global commit.gpgsign true
# Re-sign the last N commits:
git rebase --exec 'git commit --amend --no-edit -S' HEAD~N
git push --force-with-lease

Unresolved threads (GitLab): with Settings > Merge requests > Merge checks > “All threads must be resolved” enabled, even one open thread blocks the merge, and the open-threads counter turns orange. Open the Overview / discussions, click Resolve thread on each addressed item, then recheck the merge widget. A thread that should not block but matters can be moved to an issue instead of force-resolving it.

How to confirm it’s fixed

  1. Refresh the PR. The blocked-merge box should now show every requirement with a green check, and the Merge button should be solid (not grayed).
  2. If a check just re-ran, confirm in the Checks tab that the green run is on the same SHA as the PR’s latest commit (the short SHA shown next to the head ref).
  3. On GitLab, the merge widget should list zero blocking checks and the open-threads counter should read 0.
  4. Only then merge. If the button is live but you’re still unsure who can merge, that’s a “restrict who can merge” rule (Cause 6 territory), not a check failure.

Admin override (last resort)

If the merge is genuinely time-critical and the blocker can’t be cleared in time (the one required reviewer is on leave, say), a repo admin may have a bypass. On classic branch protection it’s the “Merge without waiting for requirements to be met (bypass branch protections)” option, available only if “Do not allow bypassing the above settings” is not checked. On rulesets, the admin must be on the ruleset’s Bypass list. Either way the override is logged in the audit log. Document the reason in the PR before you use it, and prefer fixing the actual rule afterward.

Prevention

  • Keep required check names identical to workflow name: / job names, and rename both in the same PR. The required name is <workflow name> / <job name>, exactly as it appears in the Checks tab.
  • For path-filtered jobs, use the “required but skippable” pattern so the check always reports a result instead of stalling at pending.
  • Before raising the required-approval count or adding a CODEOWNERS path, audit open PRs so none are silently stranded.
  • Give every owned path at least two possible reviewers in CODEOWNERS to avoid single-person bottlenecks; remove anyone who has left.
  • Turn on “dismiss stale approvals” only for protected branches (main, release) where it earns its friction.
  • If you use a merge queue, add merge_group to every required workflow up front.
  • Write the rationale in CONTRIBUTING.md so teammates can self-diagnose a block.

FAQ

Q: I’m an admin and still can’t merge — how? A: Either “Do not allow bypassing the above settings” is checked on the classic rule, or (on rulesets) you’re not on the ruleset’s Bypass list. In both cases admins are bound by the same rules. Satisfy the rule, get added to the Bypass list, or temporarily relax the setting.

Q: The check is green but GitHub still shows it pending. Why? A: It almost certainly passed on an older commit SHA, or GitHub is showing checks for the test merge commit rather than your head commit. Required checks only count against the latest SHA and only within 7 days. Re-run CI on the current HEAD (an empty commit forces it).

Q: A required check never appears at all — it’s not even in the list. A: The workflow was skipped (path filter, branch filter, or commit-message condition), so its check stays pending. Either make the job a “required but skippable” no-op so it always reports, or touch a monitored path to trigger it.

Q: Our only required reviewer is out for two weeks. A: Add a temporary co-owner to CODEOWNERS in a small PR (merge it with an admin bypass if needed); that co-owner can then approve your blocked PR. Or, with admin rights, dismiss the stale review request and assign an available reviewer.

Q: Does GitLab have the same protections? A: Yes — Protected Branches plus Merge checks (Settings > Repository > Protected branches, and Settings > Merge requests). Rules cover allowed-to-merge roles, required approvals, code owners, and “all threads resolved.” The diagnosis flow is the same: read the merge widget, fix the first red item.

Q: How do I find who changed the rule that broke our workflow? A: GitHub repos have an audit log (Settings > General > scroll to the log, or org-level Settings > Audit log) filterable by protected_branch / ruleset events. GitLab and Bitbucket have equivalent audit sections.

Tags: #git #version-control #Troubleshooting