分支保护规则让 PR 合不了

PR 有 approve、CI 全绿、没有冲突,但 GitHub、GitLab、Bitbucket 的合并按钮还是灰的。找出到底是哪条规则没满足,在不削弱保护的前提下解开。

PR 看起来已经准备好了:approve 都有了、check 全绿、没有冲突。但 Merge 按钮还是灰的,或者提示 Required statuses must pass before mergingReview requiredMerging is blocked。保护规则本身没问题,它就是这么设计的。真正卡住的,几乎都是规则和 PR 当前状态之间的一个小错位:check 改了名、check 跑在了错误的 commit 上、approve 被自动撤销,或者某个 CODEOWNERS 位置还没人补上。

最快的办法:展开「合并被阻止」那一栏,只看第一条红色项,然后直接跳到下面对应的小节。GitHub 上它是按钮上方的「Merging is blocked / Required checks」面板;GitLab 上是 merge 控件;Bitbucket 上是右侧的 Merge checks 面板。每个平台都会明确告诉你是哪条规则没满足,别靠猜。

先判断你属于哪一类

合并框里的现象最可能的原因跳转
某个必需 check 一直是 Expected / Pending,永远不变绿check 改名了,或工作流被 paths: 过滤跳过原因 1
check 明明过了却还显示 pending过在了旧的 commit SHA 上,或 merge commit 与 head commit 结果冲突原因 2
改了个错别字之后变成「Review required」新提交导致旧的 approve 被自动撤销原因 3
「Review required from Code Owners」某个 CODEOWNERS 路径缺对应团队的 approve原因 4
「This branch is out-of-date with the base branch」开了「require up to date」且目标分支有了新提交原因 5
approve 数量不够,或有「Changes requested」卡着提高了数量要求,或某 reviewer 的 block 一直没清掉原因 6
commit 未验证 / 「unresolved thread」强制签名规则,或 GitLab 的未解决讨论原因 7

关于 GitHub 的一点术语说明(截至 2026 年 6 月):经典的分支保护规则(branch protection rules)仍然能用,但 GitHub 现在更推荐大多数仓库改用 rulesets(Settings > Rules > Rulesets)。规则名字几乎一样;主要区别是管理员绕过的入口从「Do not allow bypassing the above settings」这个勾选框,搬到了每条 ruleset 各自的 Bypass list 里。下面用到的地方会分别说明两个界面。

常见原因与解决办法

原因 1:required check 名称不匹配,或工作流被跳过

这是最常见的卡点。保护规则要求一个叫 CI / test 的 check,但 GitHub Actions 现在上报的名字变成了 ci / run-tests(有人改了 job 名,或者加了 matrix 变成 build (ubuntu-latest))。新名字永远满足不了旧要求,于是这个 check 一直停在 Expected

还有个近亲场景:工作流带了 paths: 过滤(只在 src/** 变更时跑),而这个 PR 只动了 docs/**。GitHub 官方文档明确指出:被 path 过滤、branch 过滤或 commit message 条件跳过的工作流,它的 check 会停在 Pending 状态——而一个停在 pending 的必需 check 会一直挡着合并。

怎么判断:打开 PR 的 Checks 列表,看实际上报的名字。再打开规则(Settings > Branches,或 Settings > Rules > Rulesets),和必需名称逐字对比。匹配是完全精确、区分大小写的。如果必需的 check 根本不在 PR 的列表里,说明它被跳过了。

修名称不匹配:让两个字符串完全一致。从 Checks 标签页确认真正上报的名字(格式是 <工作流名> / <job 名>),然后把保护规则改成这个精确字符串。需要时重新触发:

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

修被跳过的(path 过滤)check 而不削弱过滤:最干净、也是 GitHub 推荐的做法,是让这个 job 总会运行、但在路径不匹配时空跑(即「required but skippable」模式),这样它永远会上报一个结果。作为一次性解锁,你也可以在被监控的路径里做一个空改动:

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

合并后再用一个后续 commit 删掉 .keep。长期的修法应放在工作流里,参考 GitHub 关于「Handling skipped but required checks」的指引。

原因 2:check 过了却显示 pending(SHA 不对)

check 变绿了,但合并框里还把它列为必需 / pending。按 GitHub 文档,必需 check 必须过在最新的 commit SHA 上,而且只有在最近 7 天内完成的 check 才算数。跑在 abc123 上的 check,在你推了 def456 之后就不再满足要求了。

还有个更隐蔽的情况:GitHub 会把 test merge commit(你的分支合到目标分支后的结果)和你的 head commit 分开评估。如果 merge commit 有状态,那就必须 merge commit 通过;这时 PR 会显示 Showing checks for the merge commit。一次过期或不稳定的 merge-commit 运行,可能在 head commit 全绿的情况下也把你挡住。

修法:在当前 HEAD 上重跑 CI,或推一个空 commit 强制在最新 SHA 上重新触发:

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

如果你用了 merge queue,记得给工作流加上 merge_group 触发器——不加的话,PR 在队列里时必需 check 根本不会跑,会无限卡住:

on:
  pull_request:
  merge_group:

原因 3:新提交后 approve 被自动撤销

reviewer 在 abc123 上 approve 了;你又推了个一行的错别字修复 def456。如果开了 “Dismiss stale pull request approvals when new commits are pushed”,GitHub 会悄悄把这个 approve 撤掉,徽章就变回了 Review required

怎么判断:approve 数量在你最后一次 push 之后立刻掉了,但没有任何 reviewer 改过票。

修法:用 GitHub 的 Re-request review 按钮——Reviewers 区域里 reviewer 名字旁边那个环形箭头图标。顺手留一句话说明这次改动很小。(推空 commit 会重跑 CI,但不会重新请求人工 review,所以如果两者都在挡你,就两件事都做。)

原因 4:code owner 的 review 没补齐

src/billing/** @org/billing-team 这样的 CODEOWNERS 条目,意味着任何动到 billing 代码的 PR 都至少需要 billing-team 里一个人的 approve,不管你已经有多少其他 approve。合并框会显示 Review required from Code Owners

怎么判断:PR 动了一个有 owner 的路径,且 Code Owners 那一行是黄色、没有 owning 团队的 approve。检查 .github/CODEOWNERS(或仓库根目录 / docs/ 下的副本)。

修法:找出 owner,直接请他 review:

cat .github/CODEOWNERS   # 或:cat CODEOWNERS

然后 @ 对应的团队或某个成员,请他 review。如果唯一列出的 owner 联系不上,见下面关于临时 co-owner 的 FAQ。

原因 5:分支落后于目标分支

“Require branches to be up to date before merging” 这条规则要求你的分支包含目标分支当前的每一个 commit。如果你拉出分支之后 main 又有了新提交,合并框会显示 This branch is out-of-date with the base branch

修法:点 PR 上的 Update branch,或在本地操作:

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

--force-with-lease(绝不要用裸的 --force),这样不会覆盖掉你还没 fetch 的队友的 push。注意:更新分支会产生新的 SHA,可能重跑 CI;如果开了「dismiss stale approvals」,还会重新触发原因 3。

原因 6:approve 数量不够,或「Changes requested」卡着

这里有两种情况。要么是管理员在你 PR 已经拿到那一个 approve 之后,把必需 approve 数从 1 提到了 2,于是现在差一个。要么是某个 reviewer 点了 Request changes,你全部改完了,但那个 reviewer 一直没重新提交,于是他那个橙色的「changes requested」一直在,悄悄盖过了其他 approve。

怎么判断:Reviewers 区域里,橙色标记表示 requested changes,绿色对勾表示 approved。哪怕只有一个橙色标记,也会盖过所有绿色、挡住合并。

修法:催那个挡着的 reviewer 重新看并 approve。如果他联系不上而你有管理员权限,就 dismiss 那条过期 review(review 上的「…」菜单里选「Dismiss review」),并写明原因。要对比必需数量和当前 approve 数,打开规则看「Require a pull request before merging」里的 approval 数字即可。

原因 7:强制签名,或未解决的讨论

强制签名(GitHub/Bitbucket):规则要求每个 commit 都用 GPG/SSH 签名,但其中一个没签(常见于网页编辑器里改出来的 commit,或本地 clone 没配置签名)。commit 列表里那个 commit 旁边没有绿色的 Verified 徽章。

git config --global user.signingkey <your-key-id>
git config --global commit.gpgsign true
# 给最近 N 个 commit 重新签名:
git rebase --exec 'git commit --amend --no-edit -S' HEAD~N
git push --force-with-lease

未解决的讨论(GitLab):开了 Settings > Merge requests > Merge checks > “All threads must be resolved” 之后,哪怕只有一个讨论没解决,也会挡住合并,未解决讨论的计数会变成橙色。打开 Overview / 讨论区,对每条已处理的讨论点 Resolve thread,然后重新看 merge 控件。某条不该挡合并、但又值得保留的讨论,可以转成 issue 而不是强行 resolve 掉。

怎么确认已经修好

  1. 刷新 PR。合并框里每条要求都应该带绿对勾,Merge 按钮应该是实心可点的(不再灰)。
  2. 如果某个 check 刚重跑过,去 Checks 标签页确认那次绿色运行是跑在和 PR 最新 commit 同一个 SHA 上的(head ref 旁边显示的短 SHA)。
  3. 在 GitLab 上,merge 控件应该列出零个阻断项,未解决讨论计数应为 0
  4. 确认无误再合。如果按钮亮了但你不确定谁能合,那是「restrict who can merge」规则(属于原因 6 范畴),不是 check 没过。

管理员绕过(最后手段)

如果合并确实非常紧急、又一时清不掉阻断(比如唯一的必需 reviewer 在休假),仓库管理员可能有绕过权限。在经典分支保护里它是 “Merge without waiting for requirements to be met (bypass branch protections)” 选项,只有在没勾「Do not allow bypassing the above settings」时才可用。在 rulesets 里,管理员必须在该 ruleset 的 Bypass list 中。无论哪种方式,绕过都会记进 audit log。用之前先在 PR 里写明原因,事后最好还是去把真正的规则修对。

预防建议

  • 让 required check 名称和工作流的 name: / job 名完全一致,改名时在同一个 PR 里两边一起改。必需名称的格式是 <工作流名> / <job 名>,和 Checks 标签页里显示的一字不差。
  • 对带 path 过滤的 job,用「required but skippable」模式,让 check 永远上报一个结果,而不是卡在 pending。
  • 在提高必需 approve 数、或新增 CODEOWNERS 路径之前,先盘一遍现有的 open PR,别让谁被悄悄困住。
  • 给每个有 owner 的路径在 CODEOWNERS 里至少留两个可选 reviewer,避免单点瓶颈;离职的人及时移除。
  • 只在受保护分支(mainrelease)上开「dismiss stale approvals」,那里它带来的摩擦才值得。
  • 如果用 merge queue,一开始就给每个必需工作流加上 merge_group
  • 把规则的来由写进 CONTRIBUTING.md,让队友能自己诊断卡点。

常见问答 (FAQ)

Q:我是管理员,却还是合不了,怎么回事? A:要么经典规则上勾了「Do not allow bypassing the above settings」,要么(在 rulesets 里)你不在该 ruleset 的 Bypass list 上。两种情况下管理员都受同样的规则约束。要么满足规则,要么把自己加进 Bypass list,要么临时放宽设置。

Q:check 是绿的,GitHub 却还显示 pending,为什么? A:基本可以肯定它过在了一个较旧的 commit SHA 上,或者 GitHub 显示的是 test merge commit 而不是你的 head commit 的 check。必需 check 只认最新 SHA,且只在 7 天内有效。在当前 HEAD 上重跑 CI(推一个空 commit 即可强制)。

Q:有个必需 check 根本没出现,列表里都没有。 A:那个工作流被跳过了(path 过滤、branch 过滤或 commit message 条件),所以它的 check 一直 pending。要么把这个 job 改成「required but skippable」的空跑让它总会上报,要么动一下被监控的路径来触发它。

Q:我们唯一的必需 reviewer 要离开两周。 A:用一个小 PR 往 CODEOWNERS 里加一个临时 co-owner(需要的话用管理员绕过合并它),之后这个 co-owner 就能 approve 你卡住的 PR。或者用管理员权限 dismiss 那条 pending 的 review 请求,改派一个在岗的 reviewer。

Q:GitLab 有同样的保护吗? A:有——Protected Branches 加 Merge checks(Settings > Repository > Protected branches,以及 Settings > Merge requests)。规则涵盖允许合并的角色、required approvals、code owners、以及「所有讨论已解决」。诊断思路一样:看 merge 控件,修第一条红色项。

Q:怎么查是谁改了规则、把我们的流程搞坏了? A:GitHub 仓库有 audit log(Settings > General 下拉到日志,或组织级 Settings > Audit log),可按 protected_branch / ruleset 事件筛选。GitLab 和 Bitbucket 也有对应的审计区。

相关阅读

标签: #git #version-control #排查