GitHub Copilot 堆叠会话怎么用:依赖改动怎么排

堆叠 PR 在 7 月 30 日进入公开预览。本文讲会话怎么串成栈、依赖关系落在哪、合并顺序与级联 rebase 的规则,以及 base 写错的返工代价。

GitHub Copilot 堆叠会话怎么用:依赖改动怎么排

前一个 PR 还没合,下一个改动又得建在它上面。github copilot 堆叠会话怎么用,说的就是让第二个会话从第一个会话的分支起步,PR 跟着排成一串。

作者CodePass 技术编辑

一个会话只开一个 PR,所以要堆

59 分钟、一个分支、一个 PR。官方文档把 cloud agent 的边界写成硬限制:一次只能在一个分支上工作,对每个被指派的任务恰好开出一个 PR,单次会话上限 59 分钟且不可延长,也无法跨仓库改动。一串互相依赖的改动因此塞不进同一个会话。

栈的定义在文档里写得很干脆:同一仓库内的一串 PR,每个 PR 以下面那个 PR 的分支为 base,形成有序链条,最终落到 main 这类分支上。堆叠会话把链条往上游挪一格,让第二个会话直接从第一个会话的分支起步,不必等第一个 PR 合完。评审侧的收益是每个 PR 只呈现自己那层的 diff,同事能并行审不同层。会话怎么起步、项目怎么绑定,可以看Copilot app 的会话起步流程

官方博客里那串会话是怎么长出来的

GitHub 官方博客 7 月 30 日那篇记录了作者一个十年老仓库的现代化过程:React 15(2016 年发布)、Less 预处理器、同期的 react-bootstrap。他先用 Plan mode 写了段完整需求想一把梭,交给 Claude Opus 4.8 出方案、GPT-5.5 做 Rubber Duck 复核,结果没成。

原因是他从 main 开的分支,而实际在跑的部署用的是改到一半的 dev 分支。Copilot 的处理是新建会话、关掉已开出的 PR、把做完的样式决策移植到基于 dev 的改动上。测试时控制台又冒出 findDOMNodecomponentWillReceiveProps 告警,来源是 react-bootstrap 而非他自己的代码。他没把替换 react-bootstrap 塞进同一个 PR,转而在提示里要求:先为现有工作开 PR,再起一个新会话从这份工作分叉,作为另一个 PR 合进 dev。App 随后做了三件事:基于 dev 开出第一个 PR,创建带着上文、排在前一个会话之后的堆叠会话并等他批准计划,最后开出跟在前一个 PR 后面的堆叠 PR。

依赖关系落在 trunk 和每层的 base 上

栈的依赖关系不靠额外的元数据表达,靠的就是每个 PR 的 base 分支。最底层那个 PR 的 base 叫 trunk,其余层依次往上摞。trunk 默认是仓库的默认分支,但也可以是 release 分支或者一条长期存在的特性分支。

命令行用 gh stack init --base release auth-layer 指定 trunk;网页端更朴素,把最底层 PR 建到你想要的分支上,剩下的自然往上长。GitHub CLI 不是必需品,底层就是标准 Git 操作,用 Jujutsu 或 Sapling 管本地分支的人照样能从 CLI 或网页开出一个栈。三条限制先记住:所有分支必须在同一仓库,跨 fork 不支持;GitHub Desktop 不支持栈;功能目前标注为公开预览,行为可能变。

检查和权限一律按栈底算

必需评审、必需状态检查、CODEOWNERS、代码扫描工作流,这四项全部按栈底(通常是 main)的规则评估,而不是按每个 PR 直接指向的那条分支。换句话说,中间层 PR 和最底层受同一套标准约束,没有「反正只合进上一层,先松一点」的空间。

CODEOWNERS 有个细节值得单独记:低层 PR 里对 CODEOWNERS 文件的改动计入它自身的评估,但不影响上面那些 PR。GitHub Actions 同样按栈底算,配置在指向 main 的 pull_request 事件上触发的工作流,会在栈里每个 PR 上跑一遍,不需要改配置,CI 消耗因此随层数线性增长。写工作流时可以用 github.event.pull_request.stack 判断当前 PR 是否属于某个栈,这个属性只在栈里存在。cloud agent 本身也吃 Actions 分钟和 AI credits,这部分账见Copilot 计费与直连 API 的对比

合并从下往上,rebase 是级联的

main ← PR1 ← PR2 ← PR3 这条链里,要合 PR3,PR1 和 PR2 就得都通过检查、拿到必需评审、满足分支保护规则。整个栈不必一次合完,但顺序只能自下而上。合掉顶层 PR 会把下面所有未合并的层一起带走;合掉中间某一层,下面的跟着合,上面的留着并自动重新指向栈基。

三种合并方式都支持,落地时是一次原子操作:merge commit 给整组生成一个合并提交并保留各 PR 的历史;squash 是每个 PR 一个压缩提交,合 n 个就产生 n 个;rebase 把提交依次重放成线性历史。线性历史是合并的硬性前提,往低层分支推新提交、或者 trunk 自己往前走,都会破坏它。修复靠级联 rebase:命令行跑 gh stack rebasegh stack push,网页端在合并框里点 Rebase stack。走 merge queue 时整个栈按顺序入队,其中一个被弹出,上面的全部跟着被移出;为了不拆散栈,merge group 允许超出配置上限最多 50%,再大就拆到连续几个 merge group 里。

base 写错要赔多少

官方博客那次返工就是现成的价目表。作者的第一版 PR 是基于 main 做的,等他发现线上跑的其实是 dev,那个 PR 只能关掉重来,前面花的时间和 token 全部沉没。

代价比普通改错 base 大,因为栈是有序链条。底层换了 trunk,上面每层的 base 都要跟着重排,线性历史会断,得跑一遍级联 rebase 才能恢复可合并状态。会话侧还有一份账:一个会话对应一个 PR,重来就是重新消耗一整轮 Actions 分钟和 AI credits,59 分钟上限也不会因为返工宽限。

开栈前先确认两件事:线上实际跑的是哪条分支,trunk 要不要显式指定。命令行用 gh stack init --base BRANCH,网页端把最底层 PR 直接建到那条分支上。真写错了,官方博客的处理路径值得抄:让 agent 新开会话、关掉旧 PR、把已做出的决策移植到正确分支,而不是手工重做一遍。

什么时候不该堆

改动之间彼此独立时,堆是净亏。合并要求写得很清楚:上层要合,它下面每一层都得达标。把三个无关的 PR 串成一串,等于让最底层那个的评审进度决定另外两个的命运,其中一个卡住,整条链一起停。这类情况开三个平行 PR 更省事。

另外几种情况被机制直接排除:分支不在同一个仓库、需要跨 fork 协作、团队用 GitHub Desktop 管提交,都不支持栈;cloud agent 也无法在一次运行里跨仓库改动。栈太长同样要掂量,CI 每层跑一遍,merge queue 只给 50% 缓冲,超了会被拆到多个 merge group。官方博客那次现代化只切了两层,链条短,中途因 trunk 前进而要级联 rebase 的次数也少,这和把一套工作台吃透的做法是同一个思路。

参考资料