系统实践
一次生产部署为什么不能只看 HTTP 200
Git提交、CI、Cloudflare Pages、域名、HTTPS、页面响应和人工验收是不同的证据层。结合高源外一次真实发布,讲清“上线成功”到底需要证明什么。

前段时间,我在“高源外”发布了一篇文章:《当科学课不再只是“知道答案”》。
第一次发布以后,GitHub Actions 运行成功,Cloudflare Pages 完成部署,正式网址可以打开,服务器返回 HTTP 200。从通常的技术指标看,这次上线已经成功了。
但事情没有结束。
我重新读了一遍文章,又对正文做了调整,第二次发布。后来我看到文章末尾的“后记”,觉得它和正文的收束重复,于是又让 Codex 删除这一段,重新发布一次。
第三次发布后,我关心的不只是页面还能不能打开。我还要确认:后记真的消失了吗?那句不再需要的文字还有没有留在生成页面里?前面已经确认保留的 AI 段落、新结尾和三条图片说明是否仍然存在?手机和电脑上还能不能正常阅读?
这次很普通的内容修改,让我重新意识到:
HTTP 200 只能证明一个请求得到了成功响应,不能证明一次生产交付已经完整。
“部署成功”其实包含很多句话
我们常把“代码已经提交”“CI绿了”“平台显示部署成功”“网站能打开”合并成一句话:上线了。
但这些状态回答的是不同问题。
- Git 提交回答:这次到底保存了哪一版改动?
- 推送回答:远程仓库是否已经收到这次提交?
- CI 回答:这版代码完成了哪些自动检查?
- Cloudflare Pages 回答:哪个构建产物被部署到了哪个项目?
- 域名回答:用户访问的地址是否指向目标服务?
- HTTPS 回答:浏览器是否通过安全连接访问了目标站点?
- HTTP 200 回答:这个请求是否收到了成功状态码?
- 内容检查回答:返回的是不是我们想发布的页面?
- 人工验收回答:页面是否符合这次修改的真实意图?
任何一层失败,都可能让最终结果与预期不同。反过来,某一层成功,也不能替其他层作证。
第一层:Git 提交证明“是哪一版”
那篇科学教育文章在仓库里留下了三次清楚的提交:第一次发布、第二次修改、第三次删除后记。
提交的价值不只是“代码保存了”。它把一次改动固定成了可以识别的版本:哪些文件改变了,改变发生在什么时候,部署的提交是否就是刚刚验收的那一版。
如果本地改完了文件,却没有进入提交,那么 CI 根本看不到这些变化。如果提交存在,但推送的是另一条分支,生产环境也不会自动得到它。
因此,“我已经改了”与“生产流水线得到这次修改”是两件事。
一个提交能够证明仓库里有一组确定的变化,但它不能证明:
- 测试已经通过;
- 构建能够完成;
- 远程仓库已经收到;
- 生产平台已经使用;
- 页面内容符合人的最终意图。
第二层:CI 证明“完成了哪些检查”
高源外的生产工作流在 main 分支收到推送后,会安装锁定版本的依赖,运行单元测试,执行 Astro 检查和正式构建,然后再把生成的静态产物交给 Cloudflare Pages。
CI 成功很重要。它至少说明,同一份提交在一套明确的运行环境里走完了工作流中列出的步骤。
但“CI通过”的边界也必须说清楚。
如果工作流只运行单元测试,它就不能证明浏览器里的页面布局正常;如果测试没有检查某段文章内容,它也不会知道一段不该出现的文字仍然存在;如果构建时使用了错误的环境配置,构建成功仍可能得到错误目标的产物。
CI 不是一个抽象的质量印章。它只能证明工作流实际执行并通过的那些检查。
所以看见绿色勾号以后,我还需要知道:
- 它对应哪个提交?
- 实际运行了哪些命令?
- 哪些检查根本没有包含在工作流里?
- 生成的产物是否就是后面部署的那一份?
第三层:Cloudflare Pages 证明“产物被部署了”
在高源外的工作流里,正式构建完成后,Cloudflare 的部署步骤会把 dist 目录发布到指定 Pages 项目,并携带当前 Git 提交的标识。
当这个步骤成功时,可以证明平台接受了这次构建产物,并完成了相应部署。
但平台日志里的“Success”仍然不是最终用户页面的全部事实。
它不能单独证明自定义域名已经正确指向这个项目,不能证明用户所在地的 DNS 和 CDN 已经得到最新状态,也不能证明访问某个具体路径时返回的是正确内容。
如果一个账号里有多个项目或环境,甚至还需要确认:部署成功的是不是我们真正想发布的那个项目和分支。
第四层:域名和 HTTPS 是两道不同的门
高源外使用 free-e.net 和 www.free-e.net。生产工作流里有单独的步骤检查并配置这两个自定义域名。
域名配置成功,主要说明平台侧已经接受了这个域名与项目的关系。真正访问时,还要看 DNS 是否把请求带到了正确位置。
HTTPS 又是另一层。浏览器出现安全连接,说明证书、域名和连接过程在这次访问中能够正常工作。它不证明后面的文章内容正确,也不证明应用的业务功能可用。
有时候我们看到浏览器地址栏的小锁,就下意识觉得“网站没问题”。实际上,小锁只回答了连接安全问题,并没有检查页面是不是旧版本、是不是错误页,或者文章是否缺了一段。
第五层:HTTP 200 到底证明了什么
HTTP 200 的含义很具体:服务器成功处理了这次 HTTP 请求,并用 200 OK 状态返回了响应。
这当然是重要证据,但它的范围没有我们想象得那么大。
一个页面返回 200,仍然可能是:
- 自定义的错误页面也错误地返回了 200;
- 首页正常,目标文章路径却不是预期内容;
- 页面骨架存在,但图片或脚本加载失败;
- CDN 仍然返回旧版本;
- 标题正确,正文却漏了关键段落;
- 内容都存在,但手机端出现横向溢出或遮挡。
因此,HTTP 200 只能进入证据链,不能成为证据链的终点。
第六层:内容检查证明“返回了什么”
删除后记后的那次发布,我在正式页面上检查了几个明确条件:
- 页面响应为 HTTP 200;
- “后记”模块已经不存在;
- 被要求删除的那句话已经不存在;
- 原来保留的 AI 段落仍然存在;
- 新结尾仍然存在;
- 三条图片说明仍然存在。
这比只看状态码多走了一步。它开始验证“这次修改想改变什么”和“生产页面实际上返回了什么”之间是否一致。
但文本存在也不是最后一层。某句话在 HTML 中存在,不代表它在页面上没有被样式隐藏;图片标签存在,不代表图片比例合适;目录链接存在,不代表点击和滚动体验正常。
内容断言适合确认可机器判断的要求,视觉和阅读体验仍需要浏览器检查。
最后一层:人工验收确认“这是不是我真正想要的”
第一次发布后,所有自动流程都可以成功,但我重新阅读文章后仍然可能改变判断。
这并不意味着部署系统出了故障。恰恰相反,技术流水线完成了它的职责:准确地把当时的版本交付到了线上。后来删除后记,是一次内容判断的变化,只有真正阅读文章的人才能作出。
自动测试可以检查标题有没有、图片数量对不对、页面是否溢出,却很难替我判断一段后记是不是重复,一次收束是不是太用力,或者一篇文章读起来是否仍然像我自己。
因此,人工验收不是自动化不够先进时的临时补丁。只要交付结果最终服务于人,它就是发布链路中独立的一层证据。
每一层证据,都有自己的边界
| 证据 | 能证明什么 | 不能单独证明什么 |
|---|---|---|
| Git 提交 | 改动形成了可识别版本 | 测试、部署和页面正确 |
| 远程推送 | 远程仓库收到目标提交 | CI已经完成 |
| CI 通过 | 配置中的自动检查通过 | 未被测试覆盖的质量 |
| 构建成功 | 生成了符合构建规则的产物 | 产物已经上线 |
| Pages 部署成功 | 平台接受并部署了产物 | 自定义域名和目标页面正确 |
| 域名可访问 | 请求能经域名到达服务 | HTTPS、内容和功能正确 |
| HTTPS 正常 | 本次安全连接成立 | 页面内容正确 |
| HTTP 200 | 本次请求得到成功响应 | 返回的是目标版本和目标内容 |
| 内容断言通过 | 指定内容条件成立 | 整体视觉和阅读体验合格 |
| 人工验收 | 结果符合本次真实意图 | 后续运行永远不会出问题 |
这张表真正想表达的,不是发布一定要变得复杂,而是结论要和证据相匹配。
我现在使用的最小发布闭环
对高源外这样的静态内容站,一次普通文章发布并不需要庞大的运维体系,但至少需要下面这条链路:
- 检查本次改动范围,确认没有混入无关文件。
- 运行测试、类型检查和正式构建。
- 用浏览器检查桌面端和手机端。
- 提交并记录明确的版本。
- 推送后确认 CI 对应正确提交。
- 确认 Cloudflare Pages 部署成功。
- 通过正式域名访问目标路径。
- 检查 HTTP 状态、标题和关键内容。
- 由人完成最后一次阅读和确认。
如果中间任何一步失败,就把结论停在那一步,不用后面的成功替它补证据。
这套方法与我在《Node.js 生产部署:从手工 git pull 到可回滚发布》里强调的边界是一致的,也和数据库备份必须经过恢复验证是同一个道理:执行完成,只能证明执行完成;真正要交付的结果,需要针对结果本身验证。
写在最后
做生产系统以后,我越来越少说一句笼统的“已经好了”。
我更愿意把它拆开:代码已经提交,测试已经通过,构建已经完成,产物已经部署,正式域名可以访问,目标内容已经核对,最后由人验收。
这样说看起来慢一点,却能让每一个结论找到自己的依据。
HTTP 200 很重要。
但它只是告诉我们,门打开了。
至于门后是不是我们要交付的东西,还要继续往里看。

