<?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>决策 · XB 现场笔记</title><link>https://hk.xbcatm.com/zh/decisions/</link><description>基于 HTTP、DNS、TLS、Caddy、SSH、发布与恢复的可复现运维现场笔记。</description><generator>Hugo</generator><language>zh-CN</language><atom:link href="https://hk.xbcatm.com/zh/decisions/index.xml" rel="self" type="application/rss+xml"/><item><title>站点以静态树提供服务，而非动态应用服务器</title><link>https://hk.xbcatm.com/zh/decisions/adr-static-not-dynamic-server/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/decisions/adr-static-not-dynamic-server/</guid><description>决策记录：选择在 Caddy 之后提供构建好的静态树，而非动态应用，并说明接受的取舍。</description><content:encoded>&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/adr-static.detail.zh.svg" alt="构建阶段，页面源码与配置进入生成器及依赖；生成页面与复制的 static/ 文件组成 public/。请求阶段，浏览器向 Caddy 发送 HTTPS 请求，Caddy 读取选中的发布树。CSP 由浏览器执行，请求路径没有应用服务器或数据库。" width="720" height="1510"&gt;&lt;figcaption&gt;预期架构：构建工具在服务前执行，Caddy 仍是 Web 服务器。浏览器 CSP 是另一项策略。只发布生成文件；服务根并不是符号链接沙箱。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;本站是文档与运维内容：用来阅读、检索、归档的页面。动态应用服务器可以渲染模板、管理会话或提供交互组件，但这些是可选能力，不是当前需求。内容本身对服务器端状态、按用户的认证逻辑或客户端渲染都没有要求。&lt;/p&gt;
&lt;h2 id="decision"&gt;Decision&lt;/h2&gt;
&lt;p&gt;用 Caddy 将一个预构建的静态树作为站点提供服务，通过受控的发布切换选择一个不可变发布目录。目录发布 ADR 解释了切换方式及其跨请求限制。边缘后面没有应用服务器、没有脚本通道、没有数据库。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;浏览器 ──HTTPS──► Caddy ──文件系统读取──► public/ 发布树
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期的浏览器无脚本契约由响应头（&lt;code&gt;script-src 'none'&lt;/code&gt;）下发，并由拒绝可执行脚本与残缺产物的构建检查支持。CSP 由浏览器执行，不能证明没有应用服务器。构建输出是 &lt;code&gt;public/&lt;/code&gt;，其中包括生成的页面和从源 &lt;code&gt;static/&lt;/code&gt; 目录复制的文件。&lt;/p&gt;
&lt;h2 id="后果"&gt;后果&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发布更简单：&lt;/strong&gt; 内容产物没有应用状态或数据库 schema 要迁移。发布切换、缓存行为与回滚仍须验证。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;威胁面更窄：&lt;/strong&gt; 请求路径没有应用模板求值器或数据库查询层。Caddy 和浏览器仍会解析不可信输入，生成器与依赖仍在构建时执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存可检查：&lt;/strong&gt; 不可变发布文件有稳定字节。校验器、压缩变体与新鲜度指令仍需正确服务器配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接受的取舍：&lt;/strong&gt; 没有脚本驱动的交互、认证个性化或每用户 HTML。原生文档交互仍然可用。若确实需要，应作为独立的服务来组合，而不是并进这棵树。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="仍然存在的边界"&gt;仍然存在的边界&lt;/h2&gt;
&lt;p&gt;只发布生成树，不发布仓库根、构建凭据或私有内容。Caddy 的 root 不是文件系统沙箱：根内的符号链接可能暴露根外目标。发布前应检查产物符号链接、权限与错误路由。无脚本 CSP 不会让 HTML 注入无害，也不能保护被公开的机密文件。&lt;/p&gt;
&lt;p&gt;持续更新服务器，验证实际公共响应，并保留经过测试的上一版。这些运维责任不会随应用层的移除而消失。仓库记录的是预期架构与示例配置，不是证明当前生产主机没有其他服务的资产清单。&lt;/p&gt;
&lt;h2 id="为什么它胜过替代方案"&gt;为什么它胜过替代方案&lt;/h2&gt;
&lt;p&gt;动态层带来一个运行时、一个状态边界和一条更慢的编辑路径。对价值在文档本身的内容，这笔成本买来了内容用不到的能力。静态服务没有牺牲站点需要的任何东西，同时移除了只会让发布与安全边界变复杂的部分。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&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;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy"&gt;MDN：内容安全策略&lt;/a&gt;，浏览器资源与执行策略，已核验 2026-10-15。&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>钉住 vendored 主题与生成器版本</title><link>https://hk.xbcatm.com/zh/decisions/adr-pin-vendored-theme-and-generator/</link><pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/decisions/adr-pin-vendored-theme-and-generator/</guid><description>决策记录：选择把主题纳入版本库并钉住 Hugo 版本，使构建输入可审计，并减少无法解释的输出漂移。</description><content:encoded>&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/adr-pinned.detail.zh.svg" alt="输入清单固定干净源码版本、准确元数据、工具链、配置、环境和资源。同一组输入进入两个独立空目录，再比较按路径排序的 SHA-256 清单，并保留输入与结果。普通构建会随 UTC 时间戳变化。历史上较早测试的 4,695 条路径中有 165 条不同；后续四次修补后 Linux 构建相同。该结果仅适用于所记录输入，不保证未来内容或其他平台。" width="720" height="3300"&gt;&lt;figcaption&gt;固定版本缩小漂移范围。比较受控构建并留存输入；历史四次构建一致的结论有明确边界。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;本站用 Hugo 生成器与 PaperMod 主题构建。两者任一未跟踪的升级都可能静默改变输出：主题的模板、生成器的渲染规则，或两者都可能改变两次&amp;quot;相同&amp;quot;重建之间的 HTML 字节。发布清单标识一份产物。复现它是更强的论断：源码、环境与步骤必须受到足够控制，才能再次产生相同字节。&lt;/p&gt;
&lt;h2 id="decision"&gt;Decision&lt;/h2&gt;
&lt;p&gt;把主题纳入仓库，并把确切的生成器版本与主题版本记录在钉住文件 &lt;code&gt;data/upstream-pins.toml&lt;/code&gt;。&lt;code&gt;make build&lt;/code&gt; 用 &lt;code&gt;.hugo_version&lt;/code&gt; 检查安装的 Hugo 版本；CI 另外调用的 &lt;code&gt;make check-upstream&lt;/code&gt; 将它与 TOML 中的版本及 &lt;code&gt;THIRD_PARTY_NOTICES.md&lt;/code&gt; 记录的 PaperMod 发布版交叉核对。后者检查记录一致性，不是重新逐文件比对 vendored 内容与上游。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hugo 0.165.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PaperMod v8.0
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data/upstream-pins.toml （钉住）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;被纳管的主题放在 &lt;code&gt;themes/&lt;/code&gt; 目录下，作为提交的一部分，因此也成为一次发布字节身份的一部分。&lt;/p&gt;
&lt;h2 id="后果"&gt;后果&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;输入受控：&lt;/strong&gt; 固定版本去除了两个漂移来源，但本身不能证明输出字节一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;升级可见：&lt;/strong&gt; 更新生成器版本或 vendored 主题字节，是显式可审查的变化。即使发布标签未变，主题修改仍然能在 Git 中看见。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;来源可审计：&lt;/strong&gt; 任何一次发布的读者都能看到是哪个生成器与主题产生了这些字节。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接受的取舍：&lt;/strong&gt; 仓库带上 vendored 主题与钉住簿记；相比静默的输出漂移，这个代价很小。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="可复现性仍需要测试"&gt;可复现性仍需要测试&lt;/h2&gt;
&lt;p&gt;当前 Makefile 每次构建都把当前 UTC 时间写入 &lt;code&gt;data/buildinfo.toml&lt;/code&gt;。时间会被渲染到站点，因此不同时间的两次普通构建有意不保持字节一致。提交标签也不能描述未提交修改。应把干净源码版本和准确构建元数据与产物一起保存。&lt;/p&gt;
&lt;p&gt;要测试渲染的确定性，可在隔离检出中固定相同的构建元数据、工具链、配置、环境和资源输入，再让 Hugo 分别输出到两个空目录。为两份输出生成按路径排序的 SHA-256 清单并比较，把选定输入与结果一起留存。不要声称这个 Makefile 会采用 &lt;code&gt;SOURCE_DATE_EPOCH&lt;/code&gt;，因为它明确调用了 &lt;code&gt;date&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;较早的完整站点两次构建检查在固定构建元数据后，两份产物各含 4,695 个文件，但有 165 条路径的字节不同，其中包括 &lt;code&gt;HTTP&lt;/code&gt; 与 &lt;code&gt;Http&lt;/code&gt; 等分类显示大小写。这个失败结果仍保留在历史记录中；仅固定版本并未建立输出确定性。&lt;/p&gt;
&lt;p&gt;原报告记载了一次后续隔离 Linux 检查：固定相同构建元数据与 Hugo 时钟，在一项仅涉及内容的分类修改前后各比较四次完整构建。修改为 28 个语言/分类大小写冲突组添加显式分类元数据，把中文 nginx 标签改为漏桶，并保留其历史 &lt;code&gt;/zh/tags/令牌桶/&lt;/code&gt; URL。修改后的四份产物各含 4,695 个文件，按路径排序的 SHA-256 清单完全相同；所有发布路径、标签成员顺序、分页目标和 XML 路由均保留，构建没有警告。报告中的结论仅涉及所记录源码快照、Hugo 0.165.0 Extended、vendored 主题和 Linux 环境下的重复一致性，不是跨平台、跨年份、未来版本或未来内容的可复现保证。改变这些输入后应重新比较，也不能用两次普通 &lt;code&gt;make build&lt;/code&gt; 代替固定元数据测试。&lt;/p&gt;
&lt;p&gt;恢复后的文章与配图保留了这项历史报告，但恢复材料不包含后续四次构建结果的原始运行日志或比较清单。本轮审查没有独立复现该结果。后来进行的分类标题元数据恢复是另一项工作，不能补足原实验缺失的证据。&lt;/p&gt;
&lt;p&gt;固定版本不代表永远批准运行旧软件。应审查上游修复、判断其影响，适当同步更新版本记录与 vendored 字节，然后执行完整验证并检查生成差异。&lt;/p&gt;
&lt;h2 id="为什么它胜出"&gt;为什么它胜出&lt;/h2&gt;
&lt;p&gt;输入未钉住的构建，今天可能给正确的答案，明天却给出不同的树，而 diff 与清单都无法告诉你为什么。固定版本缩小了构建变化时的调查范围；时间戳、内容、配置与环境仍可能造成差异。这个边界有用，但不应被夸大为可复现性的证明。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&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;li&gt;
&lt;p&gt;&lt;a href="https://gohugo.io/commands/hugo/"&gt;Hugo 命令参考&lt;/a&gt;，显式构建目的目录与配置选项，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://gohugo.io/getting-started/usage/"&gt;Hugo 文档：版本&lt;/a&gt;，报告并匹配生成器发布版，已核验 2026-10-15。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://semver.org/"&gt;语义化版本规范&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/decisions/adr-complete-directory-release/</link><pubDate>Thu, 20 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/zh/decisions/adr-complete-directory-release/</guid><description>决策记录：通过原子符号链接替换选择不可变发布目录，保留可命名的回滚目标。</description><content:encoded>&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/adr-directory.detail.zh.svg" alt="切换前，current 指向 releases/v1；pending 表示已核验的 current.next 符号链接，指向 releases/v2。在同一文件系统上用 os.replace(pending, current) 替换 current 链接。切换后新打开读取 v2，已打开的文件描述符仍读取 v1。回滚需创建指向已记录 v1 目标的新链接，再执行替换。" width="720" height="1690"&gt;&lt;figcaption&gt;同一文件系统上的符号链接替换选择完整发布树。旧资源 URL 必须继续可访问。本地示例不证明生产迁移、浏览器跨请求事务、自动回滚或断电持久性。&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;将变更文件直接覆盖在线上树中，会暴露部分发布，也让上一版难以命名。完整暂存解决了复制中途的问题，但“把新目录重命名覆盖旧目录”并不是足够的发布过程。&lt;code&gt;mv new existing-directory&lt;/code&gt; 通常把 &lt;code&gt;new&lt;/code&gt; 移入目的目录。普通 POSIX rename 不能替换已有的非空目录。先移走旧目录、再把新目录移入，则在两次操作之间留下空窗。&lt;/p&gt;
&lt;h2 id="decision"&gt;Decision&lt;/h2&gt;
&lt;p&gt;保留不可变、带版本名的发布目录，用 &lt;code&gt;current&lt;/code&gt; 符号链接选择其中一版。让文件服务器服务这个稳定路径。校验新树后，在 &lt;code&gt;current&lt;/code&gt; 旁创建临时符号链接，再通过同一文件系统上的 rename 原子替换符号链接本身。旧发布目录以原有版本名保留，不受修改。&lt;/p&gt;
&lt;p&gt;这是完整树决策的实现模式，不是生产主机已经迁移到此布局的证据。若当前服务根是真实目录，需要单独规划迁移，不能对那个路径直接执行示例并假设符号链接会安全替换它。&lt;/p&gt;
&lt;h2 id="在本地演练切换与回滚"&gt;在本地演练切换与回滚&lt;/h2&gt;
&lt;p&gt;下列 Python 3 练习只创建一次性本地文件，在 POSIX 文件系统上展示指针操作，不启动 Caddy，也不改变部署。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&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;for&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;v1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;v2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;releases&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&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="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symlink_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;releases/v1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_is_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readlink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&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;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;in_flight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;current.next&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symlink_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;releases/v2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_is_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;new open:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;already open:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_flight&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;symlink_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_is_directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&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;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;rollback:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;index.html&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期输出为 &lt;code&gt;new open: v2&lt;/code&gt;、&lt;code&gt;already open: v1&lt;/code&gt;、&lt;code&gt;rollback: v1&lt;/code&gt;。成功的 rename 使目录项替换具有原子性，却不会改变已经打开旧文件的描述符，也没有测试断电持久性或 HTTP 路由。&lt;/p&gt;
&lt;h2 id="后果"&gt;后果&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;线上没有复制到一半的树：&lt;/strong&gt; 选中前每个发布都已完整。不要修改已选中或留存的发布目录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可命名的回滚：&lt;/strong&gt; 创建指向已记录上一版目标的新临时链接，再替换 &lt;code&gt;current&lt;/code&gt;。回滚需要执行和验证，冒烟测试失败不会自动完成它。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切换前证据：&lt;/strong&gt; 检查暂存清单、所有权、可读权限与剩余空间。清单放在被哈希树之外。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发布串行化：&lt;/strong&gt; 同时只允许一个发布者。固定临时链接名适用于此隔离练习；生产环境需要协调与防碰撞的临时名称。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储取舍：&lt;/strong&gt; 为回滚与进行中的客户端保留旧版，经过留存窗口和检查后才清理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原子指针替换不是覆盖一个浏览器页面及其所有资源请求的事务。浏览器可能在切换前取得旧 HTML、切换后才请求资源。带内容指纹的资源 URL 只有在新服务树或独立留存资源库仍能通过旧 URL 提供旧字节时才有用。只是把旧发布目录留在磁盘别处并不够。发布和回滚后，缓存也可能继续持有旧响应。&lt;/p&gt;
&lt;p&gt;临时链接与 &lt;code&gt;current&lt;/code&gt; 必须位于同一文件系统，共用父目录是方便的保证方式。还须检查文件系统及服务器真实的符号链接行为。rename 的原子性描述并发可见性，不等于突然断电后的持久性；更强的持久性需要与文件系统匹配的文件和目录同步。服务根不是沙箱，应拒绝发布内容中意外出现的符号链接。&lt;/p&gt;
&lt;h2 id="为什么它胜过就地同步"&gt;为什么它胜过就地同步&lt;/h2&gt;
&lt;p&gt;发布可以命名、在暴露前验证、通过一次指针操作选中，并从已知目标恢复。这些保证有实际价值，无须承诺全球瞬时更新、跨请求快照隔离，或每次失败后的自动恢复。&lt;/p&gt;
&lt;h2 id="来源"&gt;来源&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/man2/rename.2.html"&gt;Linux man-pages：rename(2)&lt;/a&gt;，原子替换、非空目录、符号链接与已打开描述符，已核验 2026-10-15。&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>