一次发布,只有当公共边缘与你选择的文件一致时才完成——而不是当最后一条上传命令退出码为零。本教程把这个句子变成可重复的过程,使用 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-256 | release-manifest |
| 发布 | 完整目录交换 | atomic-switch, caddy-static-files |
| 验证 | HEAD 一致 + 清单哈希 | read-only-smoke-tests, public-edge |
| 证书 | 有效期日期 | certificate-expiry |
| 记录 | commit + 探针输出 | postmortem, changelog |
每个阶段消费前一阶段的证据。任一段失败你都要停下,旧树仍是线上服务的那棵——这正是让整个过程可以安全练习的性质。
来源
- GNU Coreutils: sha2 utilities,文件级校验与清单,已核验 2026-08-11。
- POSIX
rename(),完整目录发布,已核验 2026-08-11。 - Caddy
file_server,根选择与交换,已核验 2026-08-11。 - OpenSSL
s_client,证书日期,已核验 2026-08-11。