pstack 合并安全机制:为什么 SHA 一过期就拒绝合并?
【免费下载链接】pstack-claudeClaude Code, Codex, Copilot, Pi, OpenCode, Gemini, and Prime Agent versions of Poteto's pstack. Rigorous agent workflows with Cursor primitives translated for other harnesses.项目地址: https://gitcode.com/GitHub_Trending/ps/pstack-claude
pstack 是一个把 Poteto 的严谨 Agent 工作流移植到 Claude Code、Codex、GitHub Copilot、Pi 等平台的技能栈。它的 Shipping(发货)工作流内置了一套合并安全机制:PR 的 SHA 一旦发生任何变化,合并请求就会被直接拒绝。这篇文章用通俗语言讲清楚——为什么一个 40 位十六进制字符串的变化,值得让整条合并流水线停下来。
一、为什么 SHA 过期就拒绝合并?
先理解一个核心事实:SHA 是 PR 内容的"指纹"。
每一次 commit 都会生成一个唯一的 SHA(也叫 OID)。只要 PR 里改了一个字节——哪怕只是注释——head 分支的 SHA 就会变。pstack 把这一点当作整个安全机制的地基:
审批、CI 通过、独立验证结论,全部只对某一个具体 SHA负责,而不是对"某个分支上的某个提交"负责。
这就回答了你看到的现象:如果验证完成之后,有人又 push 了一个新 commit、或者 base 分支前进了,旧 SHA 就"过期"了,此时再执行合并就可能是把一个从未被验证过的版本放进主干。
🛡️ 所以 pstack 的策略是:SHA 变了,一切结论作废,宁可拒绝合并,也不合并一个未经验证的修订版本。
官方文档 docs/reference.md 描述了各运行时的安装与配置,而合并安全的完整规则写在 merge-safety.md。
二、LandingRevision:唯一确定"合的是什么"的五元组
pstack 用一个叫LandingRevision的结构来锁定合并对象,定义见 landing.ts:
| 字段 | 含义 |
|---|---|
| 仓库 + PR 编号 | 哪个 PR |
| headRefOid | head 分支的 SHA(你的代码在哪一版) |
| baseRefName | 目标分支名(合到哪里) |
| baseRefOid | 目标分支的 SHA(目标在哪一版) |
注意:base 分支的 SHA 也在保护范围内。很多人只盯着 head,但 base 前进同样会让验证失效——同样的 patch 落到不同的主干上,冲突风险和行为都可能不同。
比较逻辑很简单:任何一项对不上,整份记录就判定为changed(见 sameLandingRevision):
changed→ 状态变了,拒绝并停止not-open→ PR 已关闭,停止still-pending→ 待合并机制仍在,不能绕过unavailable→ 读不到状态,绝不等于"没有请求",必须人工核实
这条规则值得划重点:"缺失的字段和不受支持的 API,永远不意味着请求不存在。"宁可停下来对账,也不能为了让检查通过而悄悄替换已保存的记录。
三、合并时如何用服务层 head 条件把关?
pstack 在 Shipping 工作流 第 7 步要求合并必须带上服务强制的 head 条件,而不是"合并前最后看一眼":
gh pr merge <pr> --repo <owner/repo> --squash --match-head-commit <verified-head>三种等价写法,任选其一,但必须携带完全验证过的那个 SHA:
gh的--match-head-commit- GraphQL
mergePullRequest的expectedHeadOid - REST 合并端点的
sha参数
为什么"最后看一眼再合并"不够?因为看和合之间有时间差。在这个窗口里 PR 完全可能被改写。带上 head 条件后,GitHub 会在执行合并的瞬间做原子校验:SHA 对不上,直接返回HTTP 409拒绝。最后那一下读取的竞态窗口,被服务端替你关掉了。
⚠️ 但要知道边界:head 条件不能原子地保护 base 分支。所以对 base 的观察要在合并前后各做一次并协调 retarget 权限,而不能宣称"目标完全受保护"。
四、cancel-pending:过期 SHA 的标准取消动作
改写分支或改 base 之前,必须先用ship-pr取消所有挂起的合并机制(auto-merge 和 merge queue 算两套独立机制)。它位于 watch-pr 脚本目录:
ship-pr inspect --repo "$owner/$repo" --pr "$pr" > "$record_file" ship-pr cancel-pending --record "$record_file"流程是:读出当前完整 LandingRevision → 保存记录 → 取消时重新读取现场 → 与保存记录逐项比对 → 只有比对一致才真正取消。这一步正是"SHA 过期就拒绝"的具体体现:取消操作本身也校验修订版本,防止你取消的其实是别人刚改过的 PR。
只有kind: cancelled加退出码 0,才允许后续的改写操作;其余结果一律停止、对账。
五、它不是嘴上说说:真实仓库的拒绝测试
pstack 有一支实弹验证脚本,会在一次性私有仓库里完整演练这套机制(绝不在生产 PR 上测试服务行为):
- 验证通过某个 head SHA,然后向分支再 push 一个新 commit(SHA 过期);
- 用旧 SHA 执行
gh pr merge --match-head-commit→ 被拒绝 ✅ - 直接调 REST 端点带旧
sha→ 收到HTTP 409✅ - 确认 PR 依然是
OPEN,没有被误合 ✅ - 换新的正确 SHA 再合并 → 成功,且 merge commit 确实进了目标分支 ✅
对应的行为测试见 integrity.test.ts,其中专门覆盖了两个刁钻场景:
- head 变了,即使之前的 CI 和检查全绿,也要拒绝;
- 只是 base 被 retarget 了(head 和检查都没变),同样拒绝。
配合 babysit 工作流 的"未知状态绝不允许合并"原则,形成闭环:状态不明时,停止比合并更安全。
六、快速上手:pstack 的安全合并四步法
📌 想在项目里获得这套保护,记住这四步即可:
- 安装插件:Claude Code 里执行
/plugin marketplace add加/plugin install pstack@pstack-claude(其他运行时安装方式见 README); - 用 babysit 把 PR 推到 merge-ready,拿到带 SHA 的验证结论;
- 合并带 head 条件:
--match-head-commit <verified-head>,SHA 一变立即失败; - 改写前 cancel-pending:任何 rebase、retarget 之前先取消挂起机制,只认
cancelled结果。
写在最后
"SHA 过期就拒绝合并"看起来严苛,本质是把验证结论的可信边界钉死在一个不可变的指纹上。
- 分支会漂移、CI 会重跑、审批会过期——但 SHA 不会;
- 服务端的 head 条件把最后时刻的竞态交给平台原子判定;
- 而 pstack 的 Shipping 检查清单 确保 Agent 不会偷懒跳过其中任何一环。
对新手来说,这套机制最大的价值是:即使你交给 Agent 的指令很简单,它合并进主干的代码也一定是经过验证的那一版。
【免费下载链接】pstack-claudeClaude Code, Codex, Copilot, Pi, OpenCode, Gemini, and Prime Agent versions of Poteto's pstack. Rigorous agent workflows with Cursor primitives translated for other harnesses.项目地址: https://gitcode.com/GitHub_Trending/ps/pstack-claude
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考