发布门禁如果只是在堆命令,很快就会变成一种形式负担
很多项目补 release gate 的直觉路径都很像:想到什么检查,就往脚本里再塞一条命令。短期看这很像在增强安全感,因为命令列表越来越长,流程也越来越“像样”。但长期通常会出现两个后果:一是执行时间越来越重,大家开始把门禁当成等待过程;二是判断越来越模糊,门禁变成“跑完就行”,而不是“用来决定到底能不能发”。
所以我这次补 MakePlans 的发布门禁时,没有先问“还缺哪些命令”,而是先问“当前这项目最不能承受哪些失败”。这个顺序差别很重要。因为命令只是形式,失败类型才决定门禁有没有意义。
我先锁的,不是所有错误,而是最容易制造 readiness 错觉的错误
对当前项目来说,最昂贵的失败并不只是页面报错或者接口 500,还有一种更隐蔽的问题:你以为它已经 ready 了。比如本地 npm run build 通过,但目标平台 npm run build:cf 失败;比如局部测试是绿的,但 auth / payment / workspace 这些高风险路径根本没被 smoke;比如 feature flag 的前后端语义已经发生偏移,你却因为默认关闭而误以为现在大概没问题。
这类失败的危险在于,它们会制造一种“表面通过”的错觉。你不是在发布一个明显坏掉的版本,而是在发布一个被误判为可发布的版本。对独立开发者来说,这比单点 bug 更贵,因为整个发布判断本身已经失真了。
所以这次 gate 最后锁住的,是几条明确的失败线
这次我最后保留下来的脚本不是很多,但每一条都有非常明确的职责。
第一条:lint
它锁静态质量问题,避免最基础的坏味道继续流到后面。lint 不是最重的检查,但它是最便宜的前置挡板。
第二条:test:smoke
它不是完整测试替代品,而是专门守住 auth、payment、workspace 这些高风险路径的第一道自动化门。因为这些地方一旦回归,代价通常比某个低风险页面样式出问题大得多。
第三条:完整测试
局部绿不代表系统层没退化,所以 full test 仍然要保留。它不是为了显得认真,而是为了防止“关键路径测了,其他地方悄悄坏了”。
第四条:真实 build
对这个项目来说,这一步尤其重要。因为我们已经被目标平台链路教育过一次:本地 build 通过,不等于 Cloudflare 目标链路也通过。也正因此,build:cf 被提升成真实发布 readiness 的硬检查,而不是可选补充项。
门禁真正的价值,不在于命令数量,而在于它是不是能把这些最昂贵的失败挡在下游之前。
自动化门禁和人工检查,必须明确分层
我现在很反感另一种常见误区:只要喜欢自动化,就试图把所有风险都塞进自动化。结果通常是两种,要么脚本越来越重,发布节奏被拖垮;要么大家慢慢学会无视它,把它当成一段必须等待的仪式。最后流程是更复杂了,判断却没有更准确。
这个项目后来明确保留了另一层:人工 smoke 和 release checklist。比如环境变量、第三方登录生产配置、支付和微信开关的真实状态、某些浏览器层观察,这些都不能假装已经由脚本解决。于是最后文档层也被纳入收口:docs/release_checklist.md 和 docs/process/release-smoke-checklist.md 不再只是附属文档,而是发布流程的一部分。
这层分工让我越来越确信:成熟的门禁不是把所有事情都自动化,而是清楚地区分哪些失败必须被机器自动挡住,哪些判断仍然必须由人亲自承担。
对独立开发者来说,门禁最大的作用是防止你被“差不多”说服
如果有团队,很多流程问题还有机会被别人提醒;但独立开发时,最容易忽略你的往往就是你自己。尤其当你已经连续几轮都在这个项目里工作,对系统越来越熟,你会特别容易对很多步骤产生“这次应该也没问题”的感觉。门禁真正存在的意义,就是在你最想快一点的时候,逼你重新面对那些最不该跳过的风险。
这也是为什么我最后保留下来的三个原则都很朴素:
先锁失败类型,再补命令
不先定义最危险的失败,脚本再长也没有意义。
优先挡住会制造 readiness 错觉的问题
最昂贵的不是报错本身,而是错误地相信自己已经准备好了。
自动化和人工检查必须分层
不要因为喜欢脚本,就假装所有发布风险都能由脚本承担。
对我来说,这次补 gate 最值得保留的,并不是脚本变完整了,而是发布过程终于第一次更像系统了。它不再只是散落在脑子里的经验和记忆,而是被明确写进脚本、测试和 checklist 的边界。这样做当然不能让发布绝对安全,但它至少能持续挡住那些最常见、最昂贵、也最容易被“差不多”心态放过去的失败。