一次发布,只有当公共边缘与你选择的文件一致时才完成——而不是当最后一条上传命令退出码为零。本教程把这个句子变成可重复的过程,使用 release-and-recovery 系列笔记描述的检查。到最后阶段之前,一切都安全地在本地进行;唯一的远端步骤是结尾的只读探针。

工作流有四个阶段,每个阶段产生下一阶段消费的证据:构建、校验、发布、验证。

开始前先让三个事实变得明确,让失败变得廉价:如何运行构建(make build)、活动站点服务的是哪个单一目录、如何把那个目录重命名回上一个树。先把这三个答案写下来,就能把整个过程从一次豪赌变成带可见回滚的脚本。其余教程假设你能回答它们。

1. 构建单一可复现的产物

从干净的控制树开始,而不是服务器上目前的东西。small-systems 笔记主张你能命名的边界;静态构建只需要少数几个。运行项目目标:

$ make build
…
 Pages            │  130 │ 128
 Static files     │   42 │   42
 Total in 171 ms

构建会把指纹写入 data/buildinfo.toml——源提交与构建时间——首页会渲染它。把指纹留在产物内,让后来的读者能证明哪棵源树产生了这些字节,审计时不必猜测。

2. 针对离线产物契约做验证

在触碰线上主机之前,先运行不需要网络的检查。这些检查在产物内部确认一致性:

$ make check
validated 257 HTML documents; internal targets and script-free contract are intact

验证器验证每一张生成的 SVG、遍历每一个内部链接与片段、强制无 JS 契约,并校验下载项校验和。它在坏产物上失败,而不是在慢网络上失败——这正是 reliable-defaults 所说的把主路径变得无聊。

3. 为每个字节命名清单

发布声明必须能说出具体哪些字节变了,并能证明线上服务的就是它们。把产物哈希成排序清单:

$ cd public && find . -type f | sort | sed 's#^./##' | xargs sha256sum > /tmp/release.sha256
$ grep robots.txt /tmp/release.sha256
robots.txt  f6ad425cbc898cef00d4b62bef6b17dc627de9ee0c910b31dcc1ee4d054f3d24

release-manifest 笔记为每个已发布字节命名,使"边缘一致"的声明可以逐字节核对。在整个发布窗口内保留此清单;替换后要和线上那棵树一比。

4. 通过交换完整目录来发布

不要逐文件覆盖线上树。先在现有树旁边暂存完整树,然后交换 Caddy 服务的那个单一目录名——atomic-switch 笔记称之为完整目录发布:

$ rsync -a public/ /srv/www/example-site.new/
$ mv /srv/www/example-site.new /srv/www/example-site

因为服务根是一个目录名,交换是瞬时的,绝不会出现写到一半。旧树还在,因此回滚就是朝反方向的一个 rename(reliable-defaults)。

5. 确认公共边缘与你暂存的一致

在信任替换之前,先证明线上状态与头部与产物匹配。read-only-release-smoke-tests 与 read-public-edge 两篇笔记都把这一步作为最后的关卡——运行这个 helper 并读它的输出:

$ ./downloads/check-http-headers.sh https://www.xbcatm.com/
HTTP/2 200
content-type: text/html; charset=utf-8
content-security-policy: default-src 'self'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'none'; style-src 'self'; img-src 'self' data:

带正确 CSP 与内容类型的 200 是第一个一致。第二个一致是字节:把线上树哈希,与第 3 步的清单比较。匹配时,边缘就是你暂存的树。如果线上内容不同,停下——你看的是错的树。

6. 结束前确认证书窗口

发布还没有完成直到证书也健康。check-tls-expiry 在不破坏 TLS 会话的情况下读取有效期窗口:

$ ./downloads/check-tls-expiry.sh www.xbcatm.com 30
subject=CN=www.xbcatm.com
issuer=C=US, O=Let's Encrypt, CN=YE1
notBefore=Aug 21 08:37:13 2026 GMT
notAfter=Nov 19 08:37:12 2026 GMT

certificate remains valid for at least 30 day(s)

临近过期的证书会把"成功"发布变成迫在眉睫的事件。certificate-expiry 笔记与发布清单都把 TLS 窗口当作发布条件的一部分,而不是后来的意外。把 notAfter 读成具体日期,而不是仪表盘颜色。

7. 记住 DNS 缓存窗口

如果你的发布改了 DNS 记录(主机变更、CAA 更新或新的边缘),记住传播遵循一个不由你掌握的进度表。dns-change-window 说明为什么回滚必须包含缓存感知的窗口,而不只是那条记录。一个在本地看起来完整的发布,在远端解析器上可能仍处于传播中途,长达旧 TTL 那么久。

8. 备份一条可恢复的路径,而不只是字节

发布是恢复的另一面。发布前,确认存在只做一次恢复演练的记录——backup-rehearsal 强调备份只有在从它恢复过一次之后才有用。本教程假设"恢复"那个周期步骤已经能工作,因为恢复是让发布可逆而非有风险的那一半。

9. 记录后来者可以审计的结果

当替换完成、窗口检查完毕,留下后续操作者能信赖的事实:commit、清单哈希、两个探针输出。postmortem 条目在指名事实——哪些字节、哪个状态、哪些日期——时,比断言"它工作过"更有用。把同样的结果写进 changelog,让站点自身的演进保持可审计。

路径对应到系列

阶段证据笔记
构建buildinfo 指纹small-systems, atomic-switch
校验offline verify 通过reliable-defaults, release-checklist
清单每个文件 SHA-256release-manifest
发布完整目录交换atomic-switch, caddy-static-files
验证HEAD 一致 + 清单哈希read-only-smoke-tests, public-edge
证书有效期日期certificate-expiry
记录commit + 探针输出postmortem, changelog

每个阶段消费前一阶段的证据。任一段失败你都要停下,旧树仍是线上服务的那棵——这正是让整个过程可以安全练习的性质。

来源