<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Release · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/tags/release/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/tags/release/index.xml" rel="self" type="application/rss+xml"/><item><title>发布一个静态站点并证明边缘一致</title><link>https://hk.xbcatm.com/zh/tutorials/ship-a-static-release/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/tutorials/ship-a-static-release/</guid><description>构建并暂存静态产物，切换发布指针，验证公共响应样本，并明确回滚与证据边界。</description><content:encoded>&lt;p&gt;一次发布，只有在所需公共边缘检查与选定产物一致时才算完成，而不是上传退出码为零就完成。本教程串起&lt;strong&gt;构建&lt;/strong&gt;、&lt;strong&gt;校验&lt;/strong&gt;、&lt;strong&gt;发布&lt;/strong&gt;与&lt;strong&gt;验证&lt;/strong&gt;，同时区分本地检查和部署变更。发布会改变服务器；结尾的探针才是只读网络观察。&lt;/p&gt;
&lt;p&gt;命令假定使用 Bash、GNU find/sort/sha256sum、rsync、Python 3 与项目固定版本的 Hugo。构建步骤从仓库根运行。接触线上主机前，先在一次性目录演练&lt;a href="https://hk.xbcatm.com/zh/decisions/adr-complete-directory-release/"&gt;目录切换示例&lt;/a&gt;。明确实际服务根、部署负责人、上一版目标与回滚过程。下面的 &lt;code&gt;/srv&lt;/code&gt; 布局要求部署已配置完成，不是仓库会自动安装的东西。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/tutorial-release.detail.zh.svg" alt="构建并验证一份准确产物，最后一次构建后生成清单，核验全新的空暂存目录。选择前失败停止，旧版不变；选择时替换发布指针。之后对公共响应与产物文件进行采样核对，失败时新版仍被选中，直到显式回滚到所记录上一版并重新探测。原子命名空间替换不能保证跨请求一致或断电持久性。磁盘检查、交付字节、TLS 身份、到期、DNS 缓存变化与独立备份各有证据要求。" width="720" height="4720"&gt;&lt;figcaption&gt;指针切换改变失败边界：之前停止可保留旧版，之后需要显式回滚并重验。磁盘检查与边缘样本证明不同事实。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="1-从已记录输入构建一份产物"&gt;1. 从已记录输入构建一份产物&lt;/h2&gt;
&lt;p&gt;从干净源码版本开始，运行项目构建并记录已安装生成器版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git status --short
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git rev-parse HEAD
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;短状态为空可支持干净工作树的判断，但命令本身不会强制执行。在称其为某个提交的发布前，应检查所有变更。&lt;code&gt;make build&lt;/code&gt; 将短提交号和当前 UTC 构建时间写入 &lt;code&gt;data/buildinfo.toml&lt;/code&gt;，再生成 &lt;code&gt;public/&lt;/code&gt;。页面中的标记说明记录了哪些输入，不能证明有未提交修改的工作树只含那个提交。&lt;/p&gt;
&lt;p&gt;固定 Hugo 与 vendored 主题可减少漂移，但构建时间字段使不同时间的普通构建不一定逐字节相同。应构建选定发布一次、验证后传输这份准确产物，而不是在服务器另行重建。&lt;/p&gt;
&lt;h2 id="2-检查完整离线契约"&gt;2. 检查完整离线契约&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;make check&lt;/code&gt; 检查当前产物的生成视觉资源、离线包新鲜度、内部链接、无脚本输出、来源链接及下载校验和。完整仓库关卡则使用：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make check-upstream
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整关卡还运行回归测试、双语文章检查和审阅清单新鲜度校验，再重新构建并检查输出。因此要在最后一次构建后才生成发布清单。构建成功及审阅记录未过期，不能证明每篇文章技术正确，也不证明公共服务器使用了预期头部。页数与耗时会变化，应保留实际输出，不与文章里复制来的计数作比较。&lt;/p&gt;
&lt;h2 id="3-用清单标识产物字节"&gt;3. 用清单标识产物字节&lt;/h2&gt;
&lt;p&gt;计算哈希前，确认 &lt;code&gt;public/&lt;/code&gt; 只包含目录与常规文件，并停止所有写入，直到暂存完成。下面的命令不会创建快照，也不会检查每种特殊文件类型。清单放在 &lt;code&gt;public/&lt;/code&gt; 外，避免把自身纳入哈希。NUL 分隔文件名可保留空格及其他 shell 敏感字符：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 sha256sum
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; public &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;GNU 校验行的摘要在文件名前。&lt;code&gt;sha256sum -c&lt;/code&gt; 成功说明已列出的文件匹配，不证明没有额外文件。全新的空发布目录可避免目标端残留旧文件，但不能发现计算哈希后在源端新增的额外文件。要证明常规文件清单完整一致，应按相同路径、排序与哈希规则重算暂存清单，在选择指针前比较完整列表。另行检查符号链接：&lt;code&gt;find -type f&lt;/code&gt; 不包含它们，而意外链接可能暴露预期根外的文件。将清单及其自身摘要留在发布记录中；校验和证明相对于清单的一致性，不能证明不可信清单的真实性。&lt;/p&gt;
&lt;h2 id="4-暂存并选择完整发布"&gt;4. 暂存并选择完整发布&lt;/h2&gt;
&lt;p&gt;假定 Caddy 已服务 &lt;code&gt;/srv/www/example-site/current&lt;/code&gt;，它是指向 &lt;code&gt;releases/&lt;/code&gt; 内部的符号链接，所有权与读取权限正确。&lt;code&gt;releases&lt;/code&gt; 父目录须事先存在。发布者必须串行执行，并选择唯一发布 ID。把此块作为独立 Bash 脚本，在部署机器上包含已验证 &lt;code&gt;public/&lt;/code&gt; 与 &lt;code&gt;release.sha256&lt;/code&gt; 的目录中运行。运行前按部署策略检查产物的所有权与读取权限。此块自行设置清单路径和遇错停止策略；前置条件、复制或校验和失败时，必须在选择指针前停止。它拒绝暂存产物内的符号链接；这是会修改部署的步骤。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/example-site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;release_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20261015T120000Z
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;release&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/releases/&lt;/span&gt;&lt;span class="nv"&gt;$release_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/current&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;previous&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;readlink &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;/current&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="c1"&gt;# Fail if this release already exists.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rsync -a public/ &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;links&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -type l -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; -n &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$links&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Refusing symlinks in the staged artifact&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Ownership and readable permissions must match the deployment policy.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python3 - &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$release_id&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;&amp;lt;&amp;#39;PYTHON&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;from pathlib import Path
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;import os, sys, uuid
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;base = Path(sys.argv[1])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;release = base / &amp;#34;releases&amp;#34; / sys.argv[2]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;current = base / &amp;#34;current&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;if not current.is_symlink() or not release.is_dir():
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; raise SystemExit(&amp;#34;Expected current symlink and a complete release directory&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pending = base / (&amp;#34;.current-&amp;#34; + uuid.uuid4().hex)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;pending.symlink_to(os.path.relpath(release, base), target_is_directory=True)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;os.replace(pending, current)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;print(&amp;#34;selected:&amp;#34;, os.readlink(current))
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;PYTHON&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Previous target: %s\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$previous&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;临时链接与 &lt;code&gt;current&lt;/code&gt; 共用父目录，因此替换通过同文件系统的一次 rename 完成。普通 &lt;code&gt;mv new existing-directory&lt;/code&gt; 不会交换两个非空目录。不要用此片段转换已有的真实目录服务根，应单独规划迁移。在预发布环境验证真实服务器的根解析及文件系统行为。&lt;/p&gt;
&lt;p&gt;旧目标仍在磁盘上，不等于切换后旧资源 URL 仍可访问。应在新服务命名空间或稳定资源库中保留旧版指纹资源字节。浏览器多个请求可能跨越两个发布，缓存也可能持有旧 HTML。原子命名空间替换不是跨请求快照隔离，也不是断电持久性保证。&lt;/p&gt;
&lt;h2 id="5-验证实际交付字节与策略"&gt;5. 验证实际交付字节与策略&lt;/h2&gt;
&lt;p&gt;切换后，用固定表示选择比较公共响应样本与相应产物文件：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.xbcatm.com/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -sS --max-time &lt;span class="m"&gt;20&lt;/span&gt; -H &lt;span class="s1"&gt;&amp;#39;Accept-Encoding: identity&amp;#39;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -D served.headers -o served.html -w &lt;span class="s1"&gt;&amp;#39;%{http_code}&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/index.html served.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh public/downloads/check-http-headers.sh &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从暂存时使用的同一产物目录运行此块。头部辅助脚本使用该可信产物内的副本；此步骤无需源码工作树。&lt;code&gt;cmp&lt;/code&gt; 成功证明这个首页样本逐字节匹配。遇到意外重定向、返回 200 的代理错误页、CDN 转换或编码响应时，需要调查。检查 &lt;code&gt;served.headers&lt;/code&gt; 中的 &lt;code&gt;Content-Encoding&lt;/code&gt;、缓存年龄、CSP 与内容类型。头部脚本打印响应及最终状态，不会强制所有预期条件。&lt;/p&gt;
&lt;p&gt;对中文内容、嵌套文章、CSS、SVG、下载项、重定向及错误路由重复适用检查。记录 URL 到文件的映射，包括目录索引。示例的逐项校验只证明所列文件的一致性，不证明暂存清单完整，也不证明所有公共缓存节点的交付结果。完整边缘论断需要更广泛抓取和表示感知检查；少数探针始终只是样本。&lt;/p&gt;
&lt;p&gt;切换后的检查失败时，新版仍被选中，直到你采取行动。用新临时链接指向已记录的 &lt;code&gt;previous&lt;/code&gt; 值，再次原子替换 &lt;code&gt;current&lt;/code&gt;，随后重做探针。不要只移走新树，诊断期间也不要删除证据或上一版。&lt;/p&gt;
&lt;h2 id="6-分别检查-tls-身份和到期窗口"&gt;6. 分别检查 TLS 身份和到期窗口&lt;/h2&gt;
&lt;p&gt;从仓库根运行到期辅助脚本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh static/downloads/check-tls-expiry.sh www.xbcatm.com &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它提取证书、打印日期、检查剩余到期间隔，却不是完整 TLS 身份测试：当前实现没有让主机名或信任验证失败成为致命错误。应按照&lt;a href="https://hk.xbcatm.com/zh/tutorials/audit-a-public-edge/"&gt;公共边缘信任教程&lt;/a&gt;执行显式验证握手与 SAN 检查。不能从“至少还有 30 天有效期”推断信任链或吊销检查成功。&lt;/p&gt;
&lt;p&gt;临近过期是另一项发布风险。保留实际 &lt;code&gt;notAfter&lt;/code&gt;、信任结果与客户端环境，不要把历史签发者或日期复制到发布日志。&lt;/p&gt;
&lt;h2 id="7-dns-变化时纳入缓存窗口"&gt;7. DNS 变化时纳入缓存窗口&lt;/h2&gt;
&lt;p&gt;纯内容发布不一定改变 DNS。若主机或边缘变化，应在缓存过期期间继续维护旧目标服务。变更时才降低 TTL，不会缩短已经按旧 TTL 缓存的条目。解析器 serve-stale、负缓存和客户端缓存会让窗口更复杂；TTL 不是保证所有客户端都完成迁移的统一截止点。CAA 变化影响签发策略，不负责把流量路由到新内容。&lt;/p&gt;
&lt;h2 id="8-保留演练过的恢复路径"&gt;8. 保留演练过的恢复路径&lt;/h2&gt;
&lt;p&gt;上一版目录有利于快速回滚，但不是独立备份。磁盘故障、误清理或部署账户被攻破，都可能影响所有本地发布。应保留源码、构建输入、配置及适当的独立产物备份，并在隔离环境演练恢复。一次成功演练证明的是那个时刻的那条恢复路径，不会永久保证之后的变更。&lt;/p&gt;
&lt;h2 id="9-精确记录结果"&gt;9. 精确记录结果&lt;/h2&gt;
&lt;p&gt;保留源码版本与干净工作树状态、构建元数据和工具版本、清单摘要、选中与上一版目标、探针时间、HTTP/TLS 观察及任何回滚。说明覆盖的是样本、全部映射 URL，还是多个观测点。关卡失败时，应记录失败与恢复过程，而不是“它工作过”。&lt;/p&gt;
&lt;p&gt;切换前失败可以让旧版保持不变；切换后恢复则是需要单独验证的显式操作。区分这两点，才能让发布路径可审计，而不假装每次失败都会自动恢复旧树。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://download.samba.org/pub/rsync/rsync.1"&gt;rsync 手册&lt;/a&gt;，归档复制与源路径尾斜线语义，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc8767.html"&gt;RFC 8767：Serving Stale Data&lt;/a&gt;，TTL 过期后的 DNS serve-stale 行为，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://manpages.debian.org/trixie/coreutils/sha256sum.1.en.html"&gt;GNU sha256sum 手册，Debian 软件包&lt;/a&gt;，校验和输出及验证选项，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.python.org/3/library/os.html#os.replace"&gt;Python &lt;code&gt;os.replace&lt;/code&gt;&lt;/a&gt;，原子替换与文件系统限制，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy &lt;code&gt;file_server&lt;/code&gt;&lt;/a&gt;，根与符号链接处理，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl 手册&lt;/a&gt;，状态捕获、响应头及内容解码，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.openssl.org/3.0/man1/openssl-s_client/"&gt;OpenSSL &lt;code&gt;s_client&lt;/code&gt;&lt;/a&gt;，显式证书验证，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://reproducible-builds.org/docs/definition/"&gt;Reproducible Builds：定义&lt;/a&gt;，产物身份与可复现性的区别，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>静态网站的发布检查清单</title><link>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-checklist-for-a-static-site/</guid><description>每个发布都值得一组小、快、针对性强、且会为真实原因失败的检查。</description><content:encoded>&lt;p&gt;静态网站发布很简单，但简单并不免除发布检查的需要。有用的检查足够小，能在每次发布运行，也足够具体，能为真实的理由而失败。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-checklist.detail.zh.svg" alt="四个编号阶段分别构建固定生成器版本的干净制品，要求 index.html、404.html、robots.txt、sitemap.xml 非空并复核链接，在待发布和公共路由测试确切制品，最后记录提交、生成器版本和下载校验和。Hugo 源码预览位于独立分支，会重新构建；HEAD 检查元数据，GET 支持正文比较。" width="720" height="2570"&gt;&lt;figcaption&gt;检查对象应是生成制品。干净输出不等于可复现；占位符搜索只是复核线索，真正的链接门禁应基于解析器。发布前服务确切制品，发布后比较两种语言的公共 GET 正文，并保存提交、生成器版本和下载校验和。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="构建干净的制品"&gt;构建干净的制品&lt;/h2&gt;
&lt;p&gt;使用固定版本的生成器，构建到一次性的输出目录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo version
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo --cleanDestinationDir --gc --minify --destination public
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;--gc&lt;/code&gt; 回收未使用的构建缓存资源，不会清理陈旧的已发布页面。&lt;code&gt;--cleanDestinationDir&lt;/code&gt; 会移除目标目录中的陈旧文件，因此只能用于一次性制品目录，不能指向源码或其他数据所在的目录。把打印出的版本与项目固定版本比较；打印版本本身不会强制版本一致。记录构建输入并验证生成的制品。干净的目标目录不能证明逐字节可复现：时间戳与其他非确定性输入需要单独控制和比较。不要在构建之后手改生成的 HTML。&lt;/p&gt;
&lt;h2 id="检查制品而不只是源码"&gt;检查制品，而不只是源码&lt;/h2&gt;
&lt;p&gt;检视生成的树，确认期望的入口点存在。把下面的断言块作为独立 shell 脚本运行；任何必需文件缺失或为空时，都应在打印清单之前停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/index.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/404.html
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/robots.txt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s public/sitemap.xml
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后搜索意外的占位符、本地路径与仅开发用的主机：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rg -l &lt;span class="s1"&gt;&amp;#39;localhost|127\.0\.0\.1|TODO|example\.com|/Users/&amp;#39;&lt;/span&gt; public --glob &lt;span class="s1"&gt;&amp;#39;*.html&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这只是复核线索，不是自动发布门禁：本清单这类示例本来就会命中，翻译页中也一样。这项仅检查 HTML 的扫描不会覆盖 XML 订阅或 JSON 搜索输出。&lt;code&gt;rg&lt;/code&gt; 无匹配时退出 1，发生错误时退出 2。逐个复核列出的文件，区分围栏示例与有效链接或属性，并用基于解析器的链接检查作为真正门禁。公开链接中的私有路径属于发布错误。&lt;/p&gt;
&lt;h2 id="验证公共路由"&gt;验证公共路由&lt;/h2&gt;
&lt;p&gt;让隔离的类生产服务器直接指向这份制品，才能进行更可靠的发布前路由检查。Hugo 开发服务器会重新构建内容；下面是独立的源码预览，不能证明它服务的就是已经检查过的 &lt;code&gt;public/&lt;/code&gt; 字节。第一条命令在一个终端运行，其余请求在另一个终端运行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hugo server --bind 127.0.0.1 --port &lt;span class="m"&gt;1313&lt;/span&gt; --baseURL http://127.0.0.1:1313/ &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --disableFastRender --disableLiveReload --renderToMemory
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/notes/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/resources/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -I http://127.0.0.1:1313/missing-page/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;HEAD 检查不比较正文。对待发布服务器再运行 GET，发布后将公共正文与制品比较，并检查两种语言。确认缺失路由在生产服务器上返回 404 状态。一个外观精美但状态 200 的错误页，仍然会破坏监控与缓存。&lt;/p&gt;
&lt;h2 id="记录结果"&gt;记录结果&lt;/h2&gt;
&lt;p&gt;在发布记录中保留源码提交、生成器版本与每个可下载资源的校验和。这样无需依赖记忆，就能把线上文件与产生它的源码进行比较。&lt;/p&gt;
&lt;p&gt;这份检查清单的可下载版本在&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;资源&lt;/a&gt;一节中。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo command reference&lt;/a&gt;，干净生产构建的生成器标志，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，HTTP 验证与响应头检查选项，已核验 2026-07-30。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，可下载的配套资源。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/commands/hugo_server/"&gt;Hugo server command reference&lt;/a&gt;，开发服务器重新构建、内存渲染与实时刷新控制，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布清单应点名每一个已发布的字节</title><link>https://hk.xbcatm.com/zh/notes/a-release-manifest-should-name-every-published-byte/</link><pubDate>Thu, 16 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-manifest-should-name-every-published-byte/</guid><description>生成确定性的文件清单，并在传输前后比较。</description><content:encoded>&lt;p&gt;静态制品是一个目录，而不是单个可执行文件。只记录源码提交或首页校验和会留下缺口：缺失的图片、陈旧别名或变化的下载可能悄悄留下来。传输前，应冻结制品并枚举其中每个常规文件。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-manifest.detail.zh.svg" alt="冻结的常规文件制品被传输到目标端，两端分别生成位于制品树外的有序清单。目标清单中多出示意的 old.js 记录；两份清单汇入完整记录比较。另一条分支对源端清单做哈希以生成发布标识。逐项校验不能发现额外文件，字节一致也不能证明作者身份。" width="720" height="1990"&gt;&lt;figcaption&gt;测量前停止写入、拒绝符号链接和特殊文件，并将清单保存在制品树外。逐项校验发现缺失或变化的字节；重新生成并比较完整清单还可发现多余文件。对可信源端清单做哈希可得到紧凑标识，真实性与元数据仍需独立控制。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="生成稳定的列表"&gt;生成稳定的列表&lt;/h2&gt;
&lt;p&gt;从包含 &lt;code&gt;public/&lt;/code&gt; 的目录，在 Bash 中使用 GNU findutils 与 coreutils 运行以下命令。清单应保存在被测目录之外。发布只能包含目录和常规文件：先检查并拒绝符号链接或特殊文件，因为它们不在该清单的契约内。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/release.sha256&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; public
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;nonregular&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find . ! -type d ! -type f -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$nonregular&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 -r sha256sum --
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;先排序文件名，再计算哈希。工具之间用 NUL 分隔路径，可保留空格、制表符与内嵌换行；GNU &lt;code&gt;sha256sum&lt;/code&gt; 会在普通可校验输出中转义特殊文件名。不要用 &lt;code&gt;awk '{print $2}'&lt;/code&gt; 解析该输出：文件名中可以合法包含空白。单独赋值会保留 &lt;code&gt;find&lt;/code&gt; 失败时的退出状态；只检查其输出，可能把扫描失败误认成空结果。扫描或管道一旦失败，就应把清单视为不完整，不得发布。测量期间停止写入；这些命令不会创建文件系统快照。&lt;/p&gt;
&lt;h2 id="传输后验证"&gt;传输后验证&lt;/h2&gt;
&lt;p&gt;在目标端的同一个可丢弃 Bash 会话中，按顺序运行后续片段，使路径变量与 &lt;code&gt;set&lt;/code&gt; 选项保持生效。将清单与制品路径设为实际位置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/path/to/release.sha256
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site.new
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$artifact&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sha256sum -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;检查列出的文件能发现缺失或改变的字节，却不能发现多余文件。因此按相同流程重新生成目标端清单，并比较完整记录：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;observed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;manifest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.observed.XXXXXX&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -f &amp;#34;$observed&amp;#34;&amp;#39;&lt;/span&gt; EXIT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$artifact&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;nonregular&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;find . ! -type d ! -type f -print -quit&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -z &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$nonregular&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; find . -type f -print0 &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C sort -z &lt;span class="p"&gt;|&lt;/span&gt; xargs -0 -r sha256sum --
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$observed&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;diff -u &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$observed&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;没有差异且退出状态为零，表示常规文件的路径与哈希一致。多出一个陈旧文件也会使检查失败。临时输出位于制品之外，因此检查不会包含自身。空目录、权限、所有者与时间戳不在测量范围内；部署依赖这些属性时需另行验证。&lt;/p&gt;
&lt;h2 id="为发布记录增加聚合摘要"&gt;为发布记录增加聚合摘要&lt;/h2&gt;
&lt;p&gt;对清单本身做哈希，得到紧凑的发布标识：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sha256sum &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$manifest&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其含义依赖固定的规则：相对路径、排序区域、算法、工具格式，以及包含哪些元数据。将可信清单与源码提交、构建器版本一起保存。&lt;/p&gt;
&lt;h2 id="不要把完整性等同于真实性"&gt;不要把完整性等同于真实性&lt;/h2&gt;
&lt;p&gt;SHA-256 摘要一致，是字节一致的强实用证据，并不能证明作者身份。攻击者若能同时替换制品与清单，也能让检查通过。签名与可信分发是独立控制；只应校验通过预期可信渠道取得的清单。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/sha2-utilities.html"&gt;GNU Coreutils: sha2 utilities&lt;/a&gt;，校验和生成与验证；通过下列上游手册源码核对，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/coreutils/coreutils/master/doc/coreutils.texi"&gt;GNU Coreutils 手册源码&lt;/a&gt;，非 tagged 校验和的转义、&lt;code&gt;--check&lt;/code&gt; 与 NUL 分隔排序，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://man7.org/linux/man-pages/man1/sha256sum.1.html"&gt;sha256sum(1)&lt;/a&gt;，命令行为与输出格式，已核验 2026-07-16。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/bash/manual/html_node/Simple-Command-Expansion.html"&gt;GNU Bash: Simple Command Expansion&lt;/a&gt;，纯赋值命令保留命令替换的退出状态，已核验 2026-10-18。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>发布在公共边缘一致之前不算完成</title><link>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</link><pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/a-release-is-not-complete-until-the-public-edge-agrees/</guid><description>把生成制品与访问者实际能收到的响应进行比较。</description><content:encoded>&lt;p&gt;静态发布消除了应用运行时的复杂度，但不会消除生成目录与公共响应之间的边界。发布检查应同时覆盖两边。&lt;/p&gt;
&lt;p&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/diagrams/release-path.svg" alt="一条小的静态发布路径" /&gt;
&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/release-edge.detail.zh.svg" alt="先在使用预期配置的待发布服务器上检查确切制品，切换目录后在公共边缘重复检查。将 index.html、下载资源和 404.html 与解码后的 GET 正文配对。字节比较与预期状态、类型、缓存、安全头分别检查。重复中英文样本，并在新目录通过检查前保留上一个已知良好目录。" width="720" height="2200"&gt;&lt;figcaption&gt;切换前后使用同一份制品。下载行对应文中的 public/downloads/check-http-headers.sh。cmp 比较前会解码受支持的内容编码；服务器转换需有明确比较规则。状态和响应头另行检查，并保留前一目录供主动回滚。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="从干净的制品开始"&gt;从干净的制品开始&lt;/h2&gt;
&lt;p&gt;使用固定生成器构建到干净的目录。记录源码提交、生成器版本、输出文件列表，以及用户会下载的文件的校验和。一次干净的构建能捕获复制式部署可能会意外保留的陈旧文件。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make verify
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find public -type f -print &lt;span class="p"&gt;|&lt;/span&gt; sort
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;文件列表是证据，不是装饰。如果某条路由消失，diff 应能说明是该来源被移除，还是生成器未能产出它。&lt;/p&gt;
&lt;h2 id="演练三类响应"&gt;演练三类响应&lt;/h2&gt;
&lt;p&gt;发布前，用预期服务器配置测试待发布制品。切换目录后，在公共边缘重复检查；探测仍在线的旧站不能验证新发布。请求一个 HTML 页面、一个静态资源与一个缺失路径。检查状态码、内容类型、缓存策略与安全头：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -eu
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D page.headers -o page.body https://www.example.test/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -fsS -D asset.headers -o asset.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/downloads/check-http-headers.sh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl --compressed -sS -D missing.headers -o missing.body &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; https://www.example.test/does-not-exist
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/index.html page.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/downloads/check-http-headers.sh asset.body
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cmp public/404.html missing.body
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把每个片段作为独立 shell 脚本运行，或放在可丢弃的 shell 会话中执行。&lt;code&gt;set -eu&lt;/code&gt; 让这个顺序脚本在构建、传输或比较失败后停止，避免后面的成功命令掩盖先前失败。缺失路径的请求有意不用 &lt;code&gt;-f&lt;/code&gt;：404 是预期结果，仍应被捕获。检查各响应头文件的最终状态：依次为 &lt;code&gt;200&lt;/code&gt;、&lt;code&gt;200&lt;/code&gt;、&lt;code&gt;404&lt;/code&gt;；字节比较不会自动完成状态码检查。这里不跟随重定向。&lt;code&gt;--compressed&lt;/code&gt; 在比较前解码受支持的内容编码；若服务器有意转换 HTML，需要另行说明比较规则。&lt;code&gt;cmp&lt;/code&gt; 相等时不输出，有差异时返回非零。在 &lt;code&gt;/zh/&lt;/code&gt; 及其错误页上重复检查，不能直接假设两种语言一致。&lt;/p&gt;
&lt;h2 id="让回滚保持简单"&gt;让回滚保持简单&lt;/h2&gt;
&lt;p&gt;在新目录通过检查之前，不要删除上一个目录。回滚应选择一个已知良好目录，而不是在事故中凭记忆重新构造一个。&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;发布检查清单&lt;/a&gt;与&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;HTTP 响应头检查&lt;/a&gt;都刻意做得足够小，能在干净的 shell 里运行。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc9110.html"&gt;RFC 9110 — HTTP Semantics&lt;/a&gt;，状态与表示语义，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://curl.se/docs/manpage.html"&gt;curl manual&lt;/a&gt;，响应检查选项，已核验 2026-07-09。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hk.xbcatm.com/zh/resources/"&gt;静态网站发布检查清单&lt;/a&gt;，本地配套资源。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>通过切换完整目录发布静态网站</title><link>https://hk.xbcatm.com/zh/notes/publish-a-static-site-by-switching-complete-directories/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/notes/publish-a-static-site-by-switching-complete-directories/</guid><description>在活动树旁边预置一份完整制品，并只在验证后替换目录名。</description><content:encoded>&lt;p&gt;直接把新文件复制到活动的静态目录上，会造成一段访问者可能从一个版本拿到 HTML、从另一个版本拿到资源的时期。更安全的模式是：在活动树旁预置一棵完整的树，验证它，再改变目录名。&lt;/p&gt;
&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/atomic-switch.detail.zh.svg" alt="由单一发布者完成同文件系统与预置树核验后，live 存放旧版，stage 存放新版。第一次把 live 移到 rollback，live 缺失而 stage 与 rollback 保留。第二次把 stage 移到 live。成功时 live 为新版、rollback 为旧版；失败时 live 仍缺失，需要显式尝试恢复。恢复失败必须检查路径；发布后还须验证公共响应。" width="720" height="1890"&gt;&lt;figcaption&gt;两次移动之间 live 缺失，崩溃可能延长空窗。恢复需要尝试并验证。浏览器各次请求彼此独立，旧资源 URL 必须继续可访问。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="把所有发布目录放在同一文件系统上"&gt;把所有发布目录放在同一文件系统上&lt;/h2&gt;
&lt;p&gt;把活动树、预置树与回滚树放在同一文件系统上。以下片段假设使用同一个 Bash 会话、GNU Coreutils，且只有一个发布进程独占这些目录名。任何命令或复制管道任一侧失败，都应停止：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -P /srv/www
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在该父目录下创建唯一命名的中转目录。不要把未解析的变量或宽泛通配符当成删除目标来复用。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp -d /srv/www/site.new.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="填充并验证预置目录"&gt;填充并验证预置目录&lt;/h2&gt;
&lt;p&gt;把生成制品复制进空目录，然后检查代表性文件。最后一条命令只显示数量；应与已核验制品的期望数量或清单比较，不能把任意数字都当作通过：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tar -C public -cf - . &lt;span class="p"&gt;|&lt;/span&gt; tar -C &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -xf -
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;/index.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;/404.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;find &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; -type f &lt;span class="p"&gt;|&lt;/span&gt; wc -l
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当工具支持目录参数时，对预置路径运行链接与校验和检查。还要确认服务用户能穿过目录并读取文件：&lt;code&gt;mktemp -d&lt;/code&gt; 初始权限较严，解包后的制品必须具备预期权限。未通过验证的预置目录绝不能获得活动目录的名字。&lt;/p&gt;
&lt;h2 id="用显式回滚路径切换名字"&gt;用显式回滚路径切换名字&lt;/h2&gt;
&lt;p&gt;在第一次移动前解析每个路径：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;live&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date -u +%Y%m%dT%H%M%SZ&lt;span class="k"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;rollback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/www/site.old-&lt;span class="nv"&gt;$stamp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; -d &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; ! -e &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt; ! -L &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;df -P &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;live_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat -c %d -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;stage_device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;stat -c %d -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage_device&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;test&lt;/span&gt; -s &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;/index.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mv -T -- &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Restore failed; inspect %s and %s before retrying.\n&amp;#39;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$rollback&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$live&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是两步切换，不是一次原子替换：两次移动之间活动路径不存在，崩溃后可能一直缺失，直到恢复。GNU &lt;code&gt;mv -T&lt;/code&gt; 可防止已有目录被悄悄当作容器、把发布树移进其中，但不能协调并发发布进程。文件系统检查结果不一致时不要继续。平台特定的原子交换或符号链接替换能消除名字缺失窗口，但必须结合实际服务器测试。&lt;/p&gt;
&lt;h2 id="验证公共边缘"&gt;验证公共边缘&lt;/h2&gt;
&lt;p&gt;切换之后，请求首页、一个深层路由、一个带指纹的资源与一个缺失路径。把公共字节与预置制品比较。在检查完成与观察窗口结束之前，保留回滚目录。&lt;/p&gt;
&lt;p&gt;即使目录名切换是原子的，浏览器请求 HTML 与资源仍不是同一笔事务。让旧指纹资源继续可访问，才能服务缓存中或传输中的 HTML；仅保留一个不对外服务的回滚目录并不能做到这一点。文件系统重命名也不自动保证崩溃后的持久性，这需要平台相应的持久化步骤。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pubs.opengroup.org/onlinepubs/9799919799/functions/rename.html"&gt;POSIX &lt;code&gt;rename()&lt;/code&gt;&lt;/a&gt;，名字替换语义，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/rename.2.html"&gt;Linux &lt;code&gt;rename(2)&lt;/code&gt;&lt;/a&gt;，文件系统与替换行为，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/tar/manual/tar.html"&gt;GNU tar manual&lt;/a&gt;，归档创建与解包，已核验 2026-06-01。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/Target-directory.html"&gt;GNU Coreutils: 目标目录&lt;/a&gt;，&lt;code&gt;-T&lt;/code&gt; 与目录嵌套风险，已核验 2026-10-15。&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gnu.org/software/coreutils/manual/html_node/stat-invocation.html"&gt;GNU Coreutils: stat&lt;/a&gt;，文件系统设备号检查，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>