静态站点发布实践:产物校验、原子切换与失败回退
以 Astro 博客发布为例,把检查过的产物、提交版本、线上健康检查和失败恢复连接起来,同时保留明确的发布节奏。
个人博客采用静态生成之后,服务器的职责很简单:把 HTML、CSS、JavaScript 和图片交给浏览器。但“只是一堆静态文件”并不意味着发布可以随意覆盖目录。
上传到一半时,页面可能已经引用新资源,而新资源还没到位;检查通过的构建和部署时重新构建的内容可能不同;部署命令返回成功,也不代表域名实际提供了这一版页面。
在为这个 Astro 博客接通自动发布时,我希望让三个问题都有明确答案:这次发布的是什么,线上正在提供什么,失败后恢复到哪里。本文中的路径与账号均为演示名称,不包含实际服务器连接信息。
发布同一次检查生成的产物
一次流水线可以按下面的顺序组织:
提交 → 安装锁定依赖 → 类型检查与测试
→ 静态构建 → 产物校验 → 上传产物
→ 服务器校验 → 切换版本 → 域名健康检查
部署阶段应下载同一次运行中检查过的产物,避免在服务器上另装依赖、重新构建,产生第二份无法直接对应的结果。构建需要记录源提交标识,并在产物中写入一个简单的 release.json。
单纯写入提交标识也不是可重复构建的充分条件。依赖锁文件、运行时版本和构建变量都影响结果,因此需要一起固定或记录。博客采用 SSG,线上只需要静态服务,不需要为了提供页面常驻 Node 进程。
分离准备目录和服务目录
把构建文件直接同步到 Nginx 当前目录,会让准备阶段暴露给用户。我采用版本目录和符号链接表达发布状态:
/srv/site/
releases/
<commit-a>/
<commit-b>/
current → releases/<commit-b>
previous → releases/<commit-a>
assets/
新版本先完整落在自己的目录中,检查通过后才修改 current。在目标 Linux 环境中,通过同一文件系统内的临时链接重命名替换,可以让目录入口切换成为一次原子操作。不同平台的 mv 参数存在差异,不能把服务器命令原样当作所有系统的通用脚本。
“原子”只描述入口切换,不代表浏览器的一次访问只会接触一个版本。用户可能已经拿到旧 HTML,随后才请求旧哈希资源。因此还需要保留旧资源,不能切换后立即删除旧版本。
校验传输,也校验版本
我把校验分成两层:归档包校验确认传输后文件没有变化,文件清单校验确认解包内容符合预期。激活前还要检查主页、release marker 和目标目录,拒绝无效版本或非受控路径。
部署账号只需要操作站点发布目录。SSH 私钥通过 Secrets 注入,服务器身份使用预先核对并固定的 host key,而不是为了省事关闭主机校验。具体 GitHub 部署环境能使用哪些保护能力,需要按仓库和账号条件核对,不能假设所有私有仓库都具备同样选项。GitHub 环境文档说明了相关配置。
校验和能检查传输完整性,但如果包与清单同时被替换,它本身并不能证明来源可信。来源仍依赖受控的工作流、产物权限、账号与 SSH 信任关系。不同机制负责不同环节,不能只因为用了 SHA-256 就概括为整个供应链已经安全。
健康检查必须经过真实服务路径
切换后,部署脚本请求域名上的 release.json,核对其中提交是否与本次发布完全一致,再请求主页。这样可以发现 current 没有生效、Nginx 根目录不对,或请求落到旧站点等问题。
对 marker 请求增加版本参数并禁止缓存,有助于减少旧响应干扰;如果站点前方还有 CDN,也需要明确它的缓存规则。仅在服务器磁盘上读取 marker,无法证明域名服务链路正确。
主页返回 200 仍不是完整的页面验收。核心静态资源、链接和可访问性应由构建检查、浏览器检查等补充。健康检查负责快速判断这次版本是否被实际提供,不负责证明所有功能都正确。
失败恢复需要明确时机
部署锁避免两个激活过程交错操作。获得锁后先保存原来的 current,再完成切换和健康检查;失败时恢复旧链接,成功后才更新 previous。旧哈希资源保留在受控的资源目录中,避免已打开页面的资源请求失效。
还要处理首次部署没有旧版本、目录校验失败、资源同名但内容不同,以及流程被中断等情况。旧版本清理也应独立设计,不能删掉 current 或 previous 指向的目录。
这里的失败回退针对静态产物和目录入口。磁盘损坏、权限整体失效或服务器不可达,都可能让同机回退无法执行;包含数据库迁移的应用还需要另外设计数据兼容和恢复策略。
在这次项目历史中,真实自动发布已经成功运行,失败恢复路径做过本地测试。生产环境没有专门演练过真实故障回滚,因此不能把它描述成“线上故障恢复已验证”。一个存在的 previous 链接,也只能证明恢复入口存在。
自动化之后,保留发布决定
main 合并后自动更新网站,可以减少重复操作,但不代表每个小修改都应该立即上线。我现在把日常内容与界面调整留在本地功能分支集中检查,明确要求发布后,再走 PR、合并和自动流水线。
这让自动化承担机械步骤,让发布节奏仍然由协作决定。对于个人站点,目录、校验和一条清楚的恢复路径已经能解决很多问题;重点在于每一步能拿出什么证据,以及失败时能做什么。