devAlice
← Alice 之道

11. Release gates — 让「完成」成为事实的那条线

Release gate 是什么、没有它「完成」如何腐烂、Auto 与 Manual 关卡分工、累积关卡实战,以及 lint+build+test 全通却缺了 42 项的那一天的教训。

第 10 篇——PRD以一句话收尾。PRD 描述目的地。

这一篇讲的是操作者如何知道自己到了。

Release gates。把「完成」从感觉变成事实的、具体的、原子单元的、机器可检验的条件。没有它,PRD 是一个愿望。有了它,PRD 就是一条带里程标的路,每一个标都由操作者的自信以外的东西来检验。

我认为 Release gates 的价值不在于检查清单本身,而在于它把承诺变成代码。以前验收标准活在散文里;如今每一行都对应一个可执行的检查,由于那个代码化的检查,「完成」才从感觉变成可追溯的事实。

0. 出发前提 — 「完成」是最昂贵的词

第 7 篇——验证循环代码层面提过这个主张。操作者感觉「完成了」的那一刻,在报告能发出去之前 lint、构建、测试自动触发。这一动作把一个脆弱的词——「完成」——从操作者的记忆转移到系统的强制执行。

Release gate 是同一个动作,往上抬一级。

在代码层面,「完成」意味着这个切片编译过、测试过。在 release 层面,「完成」意味着系统有资格上线。这两个词不是同一个词,也不能由同一个机制来强制。

测试通过并不告诉你 privacy policy 存在。构建通过并不告诉你 OAuth redirect URI 在 allow-list 里。lint clean 并不告诉你操作者的真实姓名是否从每一层元数据中清除了。那些问题属于另一道关卡。

1. 一个 Release gate 实际上是什么

Release gate 是一张

表的每一行是一个原子的、外部可验证的条件。每一行带一个状态:

  • ✅ 通过 — 已验证,这一行当前成立
  • ⏳ 进行中 — 在做,尚未成立
  • 🔒 等待操作者 — 卡在只有人能做的步骤上 (Dashboard 点击、注册账号、DNS 修改)
  • ❌ 未开始 — 已知的需求,没动
  • 不适用 — 这一行是推测性加入的,情形变化后已经无意义

这张表是 PRD 的一部分。它不住在另一份文档里。理由和第 10 篇 §4 一样——路的形状内建于目的地文档。没有人需要问「什么算完成?」——文档自己回答。

关卡是累积的。关卡 1 假设关卡 0 已通过。关卡 9 假设 0 到 8 都通过。这个顺序不是偏好,是依赖——你不能在通过合规(关卡 1)之前通过基础设施(关卡 2),就像你不能在拥有域名之前上线服务一样。

Release gate 不是检查清单。它是意图与实在之间的契约。

2. Auto-Gate 与 Manual-Gate

不是所有关卡行都是同一种东西。两种,差别很重要。

Auto-Gate 行是机器能验证的条件。例子:

  • pnpm build 以 0 退出
  • pnpm lint 以 0 退出
  • pnpm test 以 0 退出
  • grep -r "operator-real-name" . 什么都不返回
  • curl -I https://devalice.jaceclub.com 返回 200
  • verify:assets 脚本报告 15/15 SHA-256 匹配

如果一行能表达为以 0 或非零退出的 shell 命令,它就属于 Auto-Gate 集合。系统可以在每次 commit、每次 push、每晚 cron 时触发它——操作者不必醒着。

Manual-Gate 行是只有人能决定的条件。例子:

  • 「Privacy policy 在两种语言里都读得通」
  • 「负责人批准 M2 里程碑定义」
  • 「种子内容覆盖了五个不同的用户痛点」
  • 「OAuth scope 看起来符合信任等级」
  • 「Lighthouse『Best Practices』提出的问题是可接受的取舍」

这些行等待一个人。它们是关卡里无法自动化的部分,假装它们能自动化是大多数 release 事故的根源。

纪律:

纪律理由
每个 Auto-Gate 行都有脚本——而且 CI 跑它否则这一行只在纸上存在,不在实际中存在
每个 Manual-Gate 行点名批准——不只是「批准」否则没人批准也算通过
Auto 和 Manual 在表里视觉上区分操作者不应该需要记住哪个是哪个
Manual 批准留下痕迹 (commit、comment、PR review)记忆会腐烂;commit 不会

3. 累积模式的实战

一个真实项目——devalice——的真实形状关卡表,节录形式:

关卡 0: M0 发布 (临时域名)
  - pnpm build 通过           [Auto]   ✅
  - Vercel preview 部署       [Auto]   ✅
  - 分类路由 × 4              [Auto]   ✅
  - 种子指南 × 4              [Manual] ✅ (负责人审核 2026-05-10)
  - ESLint clean              [Auto]   ✅
  - Vercel env vars 已设      [Manual] 🔒 (操作者)

关卡 1: 合规
  - Privacy Policy (ko + en)  [Manual] ✅
  - 内容许可 (CC BY)          [Auto]   ✅ (文件存在,header 文本匹配)
  - 代码许可 (MIT)            [Auto]   ✅
  - About 页面存在            [Auto]   ✅
  - Cookie 提示 (PIPA)        [Manual] ✅

关卡 2: 基础设施
  - 自有域名绑定              [Manual] 🔒
  - SSL 启用                  [Auto]   — (依赖关卡 2 第 1 行)
  - 生产 env 分离             [Manual] 🔒

关卡 3: 安全
  - 无硬编码 secret           [Auto]   ✅ (grep)
  - .env.example 完整         [Auto]   ✅
  - RLS 启用                  [Manual] 🔒 (等待 DB push)
  - OAuth redirect whitelist  [Manual] 🔒
  - 输入校验                  [Auto]   ✅
  - 资源 SHA-256 匹配         [Auto]   ✅ (verify:assets 15/15)

...

散文做不到、这张表做得到的有三件事。

状态一眼可见。 扫一眼这一列。数 ✅ 与 🔒 的个数。画面立刻出现。没有人需要为了知道「我们在哪里」去读三段散文。

Blocker 自我暴露。 关卡 2 里的一个 🔒 行就是下一件必须发生的事。表告诉操作者、负责人和下一次会话先看什么。

关卡自我排序。 「上线吗?」缩减为「关卡 N 最后一个 🔒 解开了吗?」关于时机的争论在开始之前就结束。

4. 「完成」是谎言的那一天 — 没在代码中强制的关卡的代价

每当有人问「为什么要做到这一步」时,我都会回到这个故事。

在另一个项目里——姑且叫它 Track D——四个子阶段在几周内依次上线。D-0、D-1、D-2、D-3。每个阶段在最后都报告了「success」。lint 通过。build 通过。cargo test 通过。Lighthouse 分数还行。团队负责人读完报告就继续往下走了。

几周后的一次例行审计中,我们发现实际交付物在四个子阶段里少了 42 项。不是 bug——结构性承诺缺失。spec 里说会存在的四个 UI primitive 还没接进来。spec 里说会被分解的八个队列状态没被分解。spec 里说会被删除的七个文件还在树里。spec 里说会被移除的九个遗留后端 handler 还在被调用。

这是怎么发生的?每份报告都是诚实的。交付物确实能编译。测试确实通过。报告者——这里是一个被委托的智能体——没有任何能告诉它出问题的信号。

理由很简单。spec 里的承诺是散文。它们没有在代码中被强制。 没有任何检查会触发「spec 命名了四个 UI primitive——树里存在四个 UI primitive 吗?」。没有任何检查会触发「spec 说文件 X 被删除——文件 X 不见了吗?」。验证套件检查的是编译,不是承诺。

修复是一个谁也不想做的子阶段:D-Catchup。回去。把每一条承诺对照树检查一遍。把缺失的接上。把本该消失的删除。然后——这才是真正持久的改变——给未来每一次委托都加上 verify:d-N 脚本,并放到 merge gate 上,这样接下来的四个子阶段就算想骗我们也骗不了。

教训就是这个:

承诺不是 release gate。在代码中强制的承诺才是。

D-Catchup 之后,那个项目里的每一次委托都在同一份 PR 里附带三样东西:

  1. 委托说明书——做什么、为什么、怎么做
  2. verify:phase-N.mjs 脚本——遍历每一条承诺,缺失时以非零退出
  3. CI 规则——pnpm run verify:phase-N 不以 0 退出就阻断 merge

这是委托尺度上的 release gate。和 PRD 里的 M0–M10 关卡是同样的形状,只是应用到了更小的单位。

5. verify 脚本的形状

verify 脚本是故意写得又小又笨。它不是测试框架。它是一个字面意义上的 walker。

对 spec 的每一行,对应一个块:

// 行 D-1.3 — 队列必须分解为 8 个状态
const queueStates = readQueueStates();
if (queueStates.length !== 8) {
  fail("Queue has " + queueStates.length + " states, expected 8");
}

三条属性重要。

与 spec 1:1。 spec 第 3 行就是脚本的第 3 个检查。spec 加一行,脚本就要加一个检查才能通过。两份文档同步移动。

输出就是收据。 报告者不说「已验证」。他们粘贴原始 stdout。verify:d-1 passed 18 / 18。这一行不在报告里,报告就不被接受。

失败要响亮、要具体。 「Queue has 5 states, expected 8」胜过「verify failed」。操作者应该不打开脚本就能知道缺了什么。

脚本是手写的。不是生成的。不是框架驱动的。它是操作者把 spec 读了一遍,用代码表达出来,坐在 merge gate 上。

6. Manual-Gate 行 — 如何保持诚实

Auto-Gate 行容易保持诚实。CI 跑检查,要么通过要么失败。Manual-Gate 行才是纪律显现的地方。

我用的几种模式:

每个 Manual 行点名批准人。 不是「团队批准」——写下角色和日期。Manual ✅ 负责人,2026-05-10。如果批准人变了,那一行回到 ⏳,直到新批准人重新批准。

Manual 行留下 commit 痕迹。 把一行从 ⏳ 翻到 ✅ 的 commit,消息里要包含批准人名字和一行说明他们审了什么。[devAlice] chore(gate-1): privacy policy ko/en — 负责人批准。六个月后有人问「等等,谁决定这没问题的?」git log 自己回答。

Manual 行有明确的复审节奏。 在 M0 批准的 privacy policy 不是永久批准的。PRD 命名一个复审触发——M2 发布时复审每两个季度复审一次。触发一旦发生,那一行翻回 ⏳ 直到被重新确认。

Manual 行不把「我会去做」当状态。 要么有负责人和截止日期,要么就是 ❌。

7. 陷阱

Release gates 以可预测的方式失败。

7.1 只增不闭的关卡

关卡 3 表两个季度只在加行,从来没有过所有行都 ✅ 的瞬间。那不是关卡,那是愿望清单。拆开。非阻塞的行变成「关卡 3 part B」或者降级为「上线后追踪」。关卡的目的是关闭

7.2 没检查就通过的关卡

一行写着 Auto ✅,但任何地方都没有跑它的脚本。两周后那一行根本没在通过,没人知道。每个 Auto 行都需要一个可执行的检查,并在行里命名。 检查不存在,那一行就是 ❌ 而不是 ✅。

7.3 从未成为关卡的承诺

这就是 D-Catchup 模式。spec 说 X 会成真。没有东西检查 X 是否为真。报告者对他们验证过的——构建、测试、lint——是诚实的。没人验证的东西,就是会坏掉的东西。值得在 spec 里承诺的就值得做成关卡。

7.4 「口头发生」的 Manual 批准

一行写着 Manual ✅,因为三周前某次会话里负责人说了句「看起来不错」。git 里没记录。下次有人问为什么通过,痕迹没有了。每一次 Manual 批准都留下一个 commit 或一次 PR review。如果它不在 repo 里,它就没发生过。

7.5 「我们上线后再测」的关卡

一行写着 pending — 上线后验证。那不是 release gate,那是排队中的事后剖析。要么它在上线前可验证(移到 ⏳ 并验证),要么它不是上线标准(移到另一个「上线后追踪」章节)。二选一。

8. 一条原则

Release gates 的整个形状收敛为一句话。

「凡是操作者的心智会当成『完成』的,让系统先去检查。凡是系统检查不到的,写下做这件事的人是谁。」

Release gates 是 PRD 不再是愿望、成为契约的那一层。Auto-Gate 行是由机器强制的契约。Manual-Gate 行是由具名人留下痕迹强制的契约。这两者的组合,就是操作者说一个切片上线时所指的东西。

而一旦关卡是实在的——一旦每一行要么是以 0 退出的脚本,要么是 git 里记录的人工批准——大部分日常工作就不再需要操作者守在键盘前。这就是第 12 篇——Ralph loop的主题。PRD 说要造什么,关卡说什么时候造好了,循环则在操作者睡觉时 24/7 自己走完两者之间的路。