博客仓库(StevenLi-phoenix/blog)攒了 11 个 open PR:9 个是每日新闻 roundup 的内容 PR(claude/elegant-hypatia-* 分支,由 CI 里跑的 roundup bot 自动生成),2 个是 dependabot 的依赖更新。目标很直接:CI 都过的话,按时间顺序 fast-forward 合并进 main。
先确认 CI 状态,别盲目合并
批量操作前先把每个 PR 的 checks 拉一遍,绿了才动手:
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 -5done11 个全绿——Astro Check and Build 和 Scan 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,不关心提交身份)够用。
于是写了个循环:
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-branchdone结果只有 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:
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,再重新跑合并循环。
for n in 85 86 87 91 93 94 95 97 99; do gh pr ready $n --repo StevenLi-phoenix/blogdone✓ 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.json 和 pnpm-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:
gh pr comment 98 --repo StevenLi-phoenix/blog --body "@dependabot rebase"@dependabot rebase 命令的语义是:只要你没有手动改过这个 PR 的分支,dependabot 会基于当前 base 分支重新算一遍依赖树、强推一个新的提交上去,等效于重新生成一次这个 PR(dependabot 文档摘录)。等了几分钟后重新看状态:
gh pr view 98 --repo StevenLi-phoenix/blog \ --json isDraft,mergeable,mergeStateStatus# draft=false mergeable=MERGEABLE state=CLEANCI 重新跑(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):
git status --short # 本地干净,落后 13 commitsgit pull --ff-only以及部署侧的确认——CI 最后一次 Build and Check + Deploy 跑绿、线上返回 200,才算这轮合并真正完成,而不是”GitHub 页面显示 merged”就算数:
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 GraphQLmergePullRequest的既定行为(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,三者都过了才算收尾。