1698 字
8 分钟
批量合并 11 个 PR 踩的两个坑:draft 状态和 dependabot 分组冲突

博客仓库(StevenLi-phoenix/blog)攒了 11 个 open PR:9 个是每日新闻 roundup 的内容 PR(claude/elegant-hypatia-* 分支,由 CI 里跑的 roundup bot 自动生成),2 个是 dependabot 的依赖更新。目标很直接:CI 都过的话,按时间顺序 fast-forward 合并进 main。

先确认 CI 状态,别盲目合并#

批量操作前先把每个 PR 的 checks 拉一遍,绿了才动手:

Terminal window
for n in 85 86 87 89 91 93 94 95 97 98 99; do
echo "PR #$n:"
gh pr checks $n --repo StevenLi-phoenix/blog | tail -5
done

11 个全绿——Astro Check and BuildScan posts for secrets(或 dependabot PR 上的 quality)都是 pass。GitHub 上没有字面意义的 “fast-forward merge” 按钮,gh pr merge 支持的三种策略里,--rebase 是最接近的:它把 PR 的提交逐个搬到 base 分支顶端、不产生 merge commit,效果上是线性历史,等价于 fast-forward——但要注意它不是真正的 git merge --ff-only:GitHub 的 rebase-and-merge 会重写 committer 信息、生成新的提交 SHA,跟本地 git rebase 之后如果恰好是祖先关系那样保留原 SHA 不是一回事(GitHub Docs)。对这次的场景(纯内容/依赖 PR,不关心提交身份)够用。

于是写了个循环:

Terminal window
for n in 85 86 87 89 91 93 94 95 97 98 99; do
gh pr merge $n --repo StevenLi-phoenix/blog --rebase --delete-branch
done

结果只有 dependabot 的 #89 成功,其余全部报错:

GraphQL: Pull Request is still a draft (mergePullRequest)

#98 报的是另一种错误(后面细说),先看 draft 这个。

坑一:roundup PR 是设计上的 draft,得先 ready#

gh pr checks 只看 CI 状态,不看 PR 是不是 draft——两者是正交的字段,checks 全绿完全可能挂在一个 draft PR 上。查了一下这几个 claude/elegant-hypatia-* PR:

Terminal window
gh pr view 91 --repo StevenLi-phoenix/blog \
--json isDraft,mergeable,mergeStateStatus \
-q '"draft=" + (.isDraft|tostring) + " mergeable=" + .mergeable + " state=" + .mergeStateStatus'
# draft=true mergeable=MERGEABLE state=CLEAN

确实是 draft。这不是意外——博客的 roundup 流水线本来就故意把生成的 PR 开成 draft,靠一个专门的 Auto-promote green draft PRs workflow(挂在 Build and Check 跑完之后)来判断 CI 是否全绿,绿了才自动 gh pr ready + 合并。这次这 9 个 PR 之所以还挂着没被自动合并,大概率是 auto-promote 的调度窗口没赶上(它现在是”PR 构建触发 + 每日兜底”的节奏,不是每次 CI 完成都秒判)。

GitHub 的 GraphQL mergePullRequest mutation 在 PR 处于 draft 状态时会拒绝——这一点被 gh CLI 的一个已知 issue 印证:cli/cli#11129 里描述的是同一个错误信息 Pull Request is still a draft,触发点是走完 gh pr merge 的交互式编辑提交信息流程后在最后一步 submit 失败,官方给出的 workaround 就是先把 PR 标成 ready for review,再合并——commit message 的编辑在那个 issue 里甚至会因为这个失败流程而丢失,不可恢复,因为 gh 没有把失败前编辑过的内容落盘。这次是脚本化调用 --rebase(没有交互式编辑),所以不存在丢内容的问题,但报错本身完全对得上。

修复:批量 gh pr ready,再重新跑合并循环。

Terminal window
for n in 85 86 87 91 93 94 95 97 99; do
gh pr ready $n --repo StevenLi-phoenix/blog
done
✓ Pull request StevenLi-phoenix/blog#85 is marked as "ready for review"
...

之后同一个合并循环全部通过,9 个 PR 逐一 rebase 合并、分支自动删除。

坑二:dependabot 分组更新,合并顺序会互相打架#

剩下 #98(“bump the patch-updates group across 1 directory with 8 updates”)。它一开始合并时报的不是 draft 错误,是:

GraphQL: Pull Request has merge conflicts (mergePullRequest)

这个是真冲突,不是状态问题。查了一下时间线:#89(“bump the minor-updates group … with 9 updates”)在这轮批量合并里比 #98先跑,而且真的合并成功了——它和 #98 都会改 package.jsonpnpm-lock.yaml,只是分组不同(minor vs patch)。#89 先落地之后,#98 的 diff 基线(它 fork 出来时的 package.json)就过时了,Git 三方合并在这种改动集中在同一批依赖声明、行号又相邻的文件上时,很容易失去足够的上下文去自动消解,直接判冲突。

这其实是 dependabot 分组更新的已知行为:多个 group PR 同时存在时,只要它们touch 同一个 lockfile,先合并的那个几乎必然让其余的产生冲突(dependabot-core 相关讨论提到过类似问题——多个自动合并的更新落在依赖文件相邻行时,diff context 被破坏,自动合并失败)。分组本身是为了减少 PR 数量,但只要还是”多个组、同一个锁文件”,组间冲突就没法从根上避免,除非把所有更新塞进一个组。

修复不用手动 resolve conflict,评论触发 dependabot 自己重新计算并 rebase:

Terminal window
gh pr comment 98 --repo StevenLi-phoenix/blog --body "@dependabot rebase"

@dependabot rebase 命令的语义是:只要你没有手动改过这个 PR 的分支,dependabot 会基于当前 base 分支重新算一遍依赖树、强推一个新的提交上去,等效于重新生成一次这个 PR(dependabot 文档摘录)。等了几分钟后重新看状态:

Terminal window
gh pr view 98 --repo StevenLi-phoenix/blog \
--json isDraft,mergeable,mergeStateStatus
# draft=false mergeable=MERGEABLE state=CLEAN

CI 重新跑(Astro Check and Build + quality)全绿,gh pr merge 98 --repo StevenLi-phoenix/blog --rebase --delete-branch 一次成功。至此 11 个 PR 全部清空。

收尾要做的两件事#

合并全部走的是 GitHub 侧的 rebase-and-merge,本地分支没动过。这种情况下本地仓库会落后于 origin/main,但因为本地没有自己的提交,落后的部分对本地来说必然是可以 git pull --ff-only 干净应用的(真正的字面意义 fast-forward,不像 GitHub 的 rebase-and-merge 那样重写 SHA):

Terminal window
git status --short # 本地干净,落后 13 commits
git pull --ff-only

以及部署侧的确认——CI 最后一次 Build and Check + Deploy 跑绿、线上返回 200,才算这轮合并真正完成,而不是”GitHub 页面显示 merged”就算数:

Terminal window
curl -s -o /dev/null -w "%{http_code}\n" https://blog.lishuyu.app/
# 200

经验总结#

  • gh pr checks 绿不等于能合并。 draft 状态和 mergeable 状态是两个独立字段,批量合并前两个都要查,只查 checks 会在 draft PR 上撞见 GraphQL: Pull Request is still a draft
  • draft PR 想合并,先 gh pr ready 这不是 bug,是 GitHub GraphQL mergePullRequest 的既定行为(cli/cli#11129 有同款报错的先例)。
  • GitHub 没有字面意义的 fast-forward 合并按钮rebase and merge 是效果最接近的选项——线性历史,但会重写提交 SHA,跟本地 git merge --ff-only 保留原 SHA 不是一回事。
  • dependabot 分组更新,多个组同时开着 PR 时合并顺序会互相冲突,只要它们碰同一个锁文件。不用手动 resolve,@dependabot rebase 评论就能让它基于最新 base 重新算一遍。
  • 合并完成不等于任务完成。 本地 git pull --ff-only 同步、CI 最后一次 build/deploy 绿、线上探活 200,三者都过了才算收尾。
批量合并 11 个 PR 踩的两个坑:draft 状态和 dependabot 分组冲突
https://blog.lishuyu.app/posts/2026-07-07-gh-pr-merge-batch-draft-dependabot/
作者
猫猫魔女
发布于
2026-07-07
许可协议
CC BY-NC-SA 4.0