将堆叠拉取请求合并到基础分支
当多个拉取请求串成一条链(PR N 基于 PR N-1 的分支),且需要最终合并到真实的基础分支时使用。关键原则:中间分支的 CI 不是关卡,基础分支的保护才是。 因此,即时地将每一层重定向到基础分支,让原生的 CI 对其进行验证。切勿在 CI 触发器中硬编码中间分支名称。
仓库:向 gh 传递 -R owner/repo,或从克隆的仓库中运行(gh repo view --json nameWithOwner -q .nameWithOwner 可解析出)。下文中的 R 即该 owner/repo。
原则
- 合并顺序由真实依赖关系决定(内容依赖 + 被多个层编辑的文件)。首先确立该顺序,切勿为了方便而重排。
- 每一层仅合并到基础分支,一次只合并一层。切勿将子分支合并到其旧的先决条件分支。
- 在某层 CI 生效之前,先将当前基础分支合并到该层,这样它的差异就只包含自身的变更,并已包含所有已合并的内容。
- 最顶层的分支累积了所有层的变更;将顶层分支重定向到基础分支后运行一次 CI,即可验证整个堆叠。可作为可选的预检步骤。
第 0 步:确立顺序并检测冲突
确定仓库的真实基础分支(从 git 远程获取的默认分支,例如 develop 或 main;用户的代理指令文件(AGENTS.md / CLAUDE.md)中可能列出已知的基础分支)。针对每个 PR,检测其与基础分支的冲突,而非与其过时的堆叠基础分支(后者会显示为“干净”从而误导你):
git fetch origin <base> <head> --quiet
git merge-tree --write-tree origin/<base> origin/<head> | grep -iE 'CONFLICT|changed in both' || echo "clean vs base"
顺序经验法则:先合并先决条件内容,再合入门控(例如,先合并 lint 清理,再合并启用 lint);当两个层编辑了同一个文件时,较早的层优先合并。
第 1..N 步:按顺序处理每个层
PR=<number>
gh pr edit $PR -R $R --base <base> # 从堆叠分支重定向
# 为头分支创建独立的工作树,保持主 checkout 干净:
WT=../<repo>-<head> # 主 checkout 外部的任意路径
git worktree add "$WT" <head> 2>/dev/null || true
git -C "$WT" fetch origin --quiet
git -C "$WT" merge origin/<base> # 同步基础分支;解决冲突(见下文)
git -C "$WT" push
gh pr ready $PR -R $R # 当该层为当前活动层时
gh pr checks $PR -R $R --watch # 原生 CI,base=<base>
# 仅审核通过后:
gh pr merge $PR -R $R --squash --delete-branch
# 然后移至下一层(其重定向和基础同步将吸收本层)
GitHub 原生堆叠(merge-async)
使用 GitHub 原生堆叠功能创建的 PR 拒绝上述经典步骤:
gh pr edit --base:GraphQL: Cannot change the base branch because the pull request is part of a stack. (updatePullRequest)gh pr merge --auto:GraphQL: Auto-merge is not supported for stacked pull requests.gh pr merge:This pull request is part of a stack and must be merged using the asynchronous merge REST API.
使用异步 API,从最底层的 PR 开始合并(GitHub 会自动将下一层 PR 重定向到基础分支,需等待约 30 秒):
sha=$(gh pr view $PR -R $R --json headRefOid -q .headRefOid)
u=$(gh api -X PUT repos/$R/pulls/$PR/merge-async -f merge_method=squash -f sha=$sha \
-f merge_action=direct_merge --jq '.details.uuid // .status')
gh api repos/$R/pulls/$PR/merge-async/$u --jq '.status+" "+(.details.message//"")' # 轮询直到状态不为 "pending"
- 通过此方式合并一个堆叠 PR 也会同时合并其下方所有打开的 PR,因此只合并你真正要落地的层。
- 每次 squash 合并后,下一层通常会因双方都触及的文件(如 ADR、版本列表)而变“脏”:将基础分支合并到该层,解决冲突,推送,等待 CI,然后对其实施 merge-async。
- 在不撤销陈旧审核的仓库中(检查分支保护中的
required_pull_request_reviews.dismiss_stale_reviews),审核状态会在基础同步后保留。真正的门控通常是每个触及路径的 CODEOWNERS 审批,而非审批数量(pr-approval-gate会列出缺口)。 - 为在落地过程中保持每个打开的 PR 与其基础分支同步,可执行一次基础同步过程:将基础分支合并到每个打开的 PR 分支并推送(普通合并,不强制推送)。
冲突解决
- 被多个层编辑的工作流文件(可重用的构建工作流、CI 协调器工作流):保留双方的修改,不要选择某一侧。
- 格式化冲突(例如 Spotless/Prettier 应用清理层与基础分支上的内容变更):对于冲突文件,采用基础分支的内容,然后对其重新运行格式化工具(
mvn -pl <module> -am spotless:apply或pnpm fmt)。采用清理侧会丢弃真实更改;采用基础分支而不重新格式化则会失败。格式化工具的运行需要构建解析(某些仓库从需要云端登录的私有注册表拉取依赖;如果构建无法获取依赖,请刷新凭据)。
安全性
- 不要强制推送、重写历史或推送到受保护分支。
- 遵循仓库的提交作者约定(某些仓库禁止 AI 合著者签名)。
- 每次合并到基础分支前,等待人工审核批准。
- 如果存在中间分支 CI 白名单 PR(本技能试图替代的反模式),建议关闭它;如果已合并,请恢复添加的分支名称和事件类型。
相关
opening-a-stacked-pr-sequence:创建堆叠(基础分支、合并顺序)以及每种合并类型的重排修复(stack.sh restack)。pr-approval-gate:每一层仍需要的代码所有者。resolve-pr-threads-and-conflicts-and-merge:在线程和审批就绪后合并一层。