文件上传成功,不等于发布成功
直接把新文件同步进线上目录,会出现一个短暂但真实的混合版本窗口:新 HTML 引用了还没上传的 CSS,旧页面加载了已经替换的脚本。静态站不需要数据库,却仍然需要发布边界。一个简单方案是把每次构建当成不可变制品,上传到独立 release 目录,验证后一次切换 current 符号链接。
让切换成为一个文件系统操作
假设所有目录位于同一个文件系统,Nginx 的 root 指向 /srv/example/current。先上传到 releases/20261001-001,再创建临时链接并用 rename 覆盖 current。GNU mv 的 -T 把目标当作普通路径,防止意外移动进链接指向的目录。这个操作只保证目录入口切换的原子性,不保证浏览器已经加载的旧页面同步更新。
release=/srv/example/releases/20261001-001
test -f "$release/index.html" || exit 1
ln -s "$release" /srv/example/current.next
mv -Tf /srv/example/current.next /srv/example/current资源缓存也属于发布协议
给 CSS、JS 使用内容哈希文件名,例如 app.a31f9c.css,并对它们设置 immutable。HTML 则使用短缓存或 no-cache。注意 no-cache 不是不存储,而是使用前重新验证。为兼容发布前打开的页面,应保留旧哈希资源或使用跨版本共享的资源目录;仅保留旧 release 目录但不再提供其 URL,并不能避免旧页面资源 404。
验收必须走真实请求路径
发布前检查所有内部链接,发布后至少请求首页、一篇深层文章、样式、RSS 和 sitemap。除了状态码,还要校验内容:有些 SPA 会把不存在路径也返回首页 200。记录制品摘要,可以发现 CDN 仍在返回旧内容。
curl -fsS https://example.com/ -o /tmp/home.html
curl -fsS https://example.com/assets/app.a31f9c.css -o /tmp/app.css
sha256sum /tmp/app.css
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/does-not-exist回滚不是临时想出来的命令
发布前记录 current 的旧指向。回滚用同样的临时链接加 rename 操作,之后重新做 HTTP 检查。不要在发布脚本末尾自动删除所有旧版本:保留数量、保留时间和磁盘告警应该是独立策略。第一次发布没有旧版本,回滚含义是撤销新增站点和 DNS,而不是把 current 指向一个不存在的目录。