<?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>Decisions · XB Field Notes</title><link>https://hk.xbcatm.com/decisions/</link><description>Illustrated field notes on HTTP, DNS, TLS, Caddy, SSH, releases, and recovery.</description><generator>Hugo</generator><language>en-us</language><atom:link href="https://hk.xbcatm.com/decisions/index.xml" rel="self" type="application/rss+xml"/><item><title>The site is served as a static tree, not by a dynamic application server</title><link>https://hk.xbcatm.com/decisions/adr-static-not-dynamic-server/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/decisions/adr-static-not-dynamic-server/</guid><description>Decision record documenting the choice to serve a built static tree behind Caddy instead of a dynamic application, with the trade-offs accepted.</description><content:encoded>&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/adr-static.detail.en.svg" alt="In the build-time area, source pages and configuration feed the generator and dependencies. Generated pages and copied static files form public/. At request time a browser sends HTTPS requests to Caddy, which reads the selected release tree. CSP is enforced in the browser. The request path has no application server or database." width="720" height="1510"&gt;&lt;figcaption&gt;Intended architecture: build tools execute before serving; Caddy remains the web server. Browser CSP is separate. Publish only generated files: the root is not a symlink sandbox.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;The site is documentation and operations content: pages to be read, searched, and archived. A dynamic application server could render templates, manage sessions, or serve interactive components, but those are capabilities rather than current requirements. The content itself imposes no requirement for server-side state, authenticated per-user logic, or client-side rendering.&lt;/p&gt;
&lt;h2 id="decision"&gt;Decision&lt;/h2&gt;
&lt;p&gt;Serve the site as a pre-built static tree from Caddy, an immutable release directory selected through a controlled publication switch. The directory-release ADR explains the switch and its cross-request limits. No application server, scripting lane, or database lives behind the edge.&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;Browser ──HTTPS──► Caddy ──filesystem reads──► public/ release tree
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The intended no-script browser contract is delivered through headers (&lt;code&gt;script-src 'none'&lt;/code&gt;) and supported by build-time checks that reject executable scripts and broken artifacts. CSP is enforced by the browser; it does not prove the absence of an application server. The build output is &lt;code&gt;public/&lt;/code&gt;, which includes generated pages and files copied from the source &lt;code&gt;static/&lt;/code&gt; directory.&lt;/p&gt;
&lt;h2 id="consequences"&gt;Consequences&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Releases are simpler:&lt;/strong&gt; the content artifact has no application state or database schema to migrate. Publication, cache behavior, and rollback still need verification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The threat surface is narrower:&lt;/strong&gt; there is no application template evaluator or database query layer on the request path. Caddy and the browser still parse untrusted input, and the generator and dependencies still execute during builds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Caching is inspectable:&lt;/strong&gt; immutable release files have stable bytes. Validators, compression variants, and freshness directives still need correct server configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade-offs accepted:&lt;/strong&gt; no script-driven interactivity, authenticated personalization, or per-user HTML. Native document interactions remain available. Where that is genuinely needed, it would compose behind a separate service rather than be folded into this tree.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="boundaries-that-remain"&gt;Boundaries that remain&lt;/h2&gt;
&lt;p&gt;Publish only the generated tree, not the repository root, build credentials, or private content. Caddy’s root is not a filesystem sandbox: a symlink inside it can expose a target outside that root. Review artifact symlinks, permissions, and error routes before publication. A no-script CSP does not make HTML injection harmless or protect an exposed secret file.&lt;/p&gt;
&lt;p&gt;Keep the server patched, validate the actual public response, and retain a tested previous release. These are continuing operational responsibilities, even though the application tier was removed. The repository documents the intended architecture and sample configuration; it is not an inventory proving the present production host has no other services.&lt;/p&gt;
&lt;h2 id="why-this-beats-the-alternative"&gt;Why this beats the alternative&lt;/h2&gt;
&lt;p&gt;A dynamic tier adds a runtime, a state boundary, and a slower path to edit. For content whose value is in the document itself, that cost buys capabilities the content does not use. Static serving gives away nothing the site needs and removes the parts that would only complicate releases and the security boundary.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://caddyserver.com/docs/caddyfile/directives/file_server"&gt;Caddy documentation: &lt;code&gt;file_server&lt;/code&gt;&lt;/a&gt;, serving a static root without an application tier, checked 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: Content Security Policy&lt;/a&gt;, browser resource and execution policy, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Pin the vendored theme and the generator version</title><link>https://hk.xbcatm.com/decisions/adr-pin-vendored-theme-and-generator/</link><pubDate>Fri, 21 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/decisions/adr-pin-vendored-theme-and-generator/</guid><description>Decision record documenting the choice to vendor the theme and pin the Hugo release to make build inputs auditable and reduce unexplained output drift.</description><content:encoded>&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/adr-pinned.detail.en.svg" alt="An input ledger fixes a clean source revision, exact metadata, toolchain, configuration, environment and assets. The same inputs feed two separate empty destinations. Sorted path-to-SHA-256 manifests are compared and the inputs and result retained. Ordinary builds vary with the UTC timestamp. A historical earlier test differed at 165 of 4,695 paths, while four later patched Linux outputs matched. That result applies only to its recorded inputs, not future content or other platforms." width="720" height="3300"&gt;&lt;figcaption&gt;Pins narrow drift. Compare controlled builds and keep the inputs with the result; the recorded four-build match has a specific historical scope.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;The site is built with a Hugo generator and a PaperMod theme. Untracked upgrades of either can change output silently: the theme’s templates, the generator’s rendering rules, or both can alter the HTML bytes between two “identical” rebuilds. A release manifest identifies one artifact. Reproducing it is a stronger claim: source, environment, and instructions must be controlled well enough to generate identical bytes again.&lt;/p&gt;
&lt;h2 id="decision"&gt;Decision&lt;/h2&gt;
&lt;p&gt;Vendor the theme into the repository and record the exact generator version and theme version in a pin file, &lt;code&gt;data/upstream-pins.toml&lt;/code&gt;. The &lt;code&gt;make build&lt;/code&gt; target checks the installed Hugo version against &lt;code&gt;.hugo_version&lt;/code&gt;; &lt;code&gt;make check-upstream&lt;/code&gt;, also invoked separately by CI, cross-checks that version with the TOML pin and the PaperMod release recorded in &lt;code&gt;THIRD_PARTY_NOTICES.md&lt;/code&gt;. The latter checks recorded consistency, not a fresh comparison of every vendored file with upstream.&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 (pinned)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Themes that are vendored stay under &lt;code&gt;themes/&lt;/code&gt; in the tree so they are part of the commit and therefore part of the byte identity of a release.&lt;/p&gt;
&lt;h2 id="consequences"&gt;Consequences&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Controlled inputs:&lt;/strong&gt; pinning removes two sources of drift. It does not alone prove identical output.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Visible upgrades:&lt;/strong&gt; updating the generator pin or vendored theme bytes is an explicit reviewable change. Theme modifications remain visible in Git even if the release label is unchanged.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditable provenance:&lt;/strong&gt; a reader of any release can see which generator and theme produced the bytes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trade-off accepted:&lt;/strong&gt; the repository carries the vendored theme and the pin bookkeeping; the cost is small against silent output drift.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="reproducibility-still-needs-a-test"&gt;Reproducibility still needs a test&lt;/h2&gt;
&lt;p&gt;The current Makefile writes the current UTC time into &lt;code&gt;data/buildinfo.toml&lt;/code&gt; on each build. That timestamp is rendered in the site, so two normal builds at different times are intentionally not byte-identical. A commit label also does not describe uncommitted edits. Record a clean source revision and the exact build metadata alongside the artifact.&lt;/p&gt;
&lt;p&gt;To test deterministic rendering, work in an isolated checkout, fix the same build metadata, toolchain, configuration, environment, and asset inputs, then invoke Hugo twice into separate empty destinations. Generate sorted path-to-SHA-256 manifests for both outputs and compare them. Keep the chosen inputs with the result. Do not claim that &lt;code&gt;SOURCE_DATE_EPOCH&lt;/code&gt; is honored by this Makefile: it explicitly calls &lt;code&gt;date&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;An earlier complete-site two-build check with fixed build metadata produced 4,695 files in each output and 165 differing paths, including taxonomy display casing such as &lt;code&gt;HTTP&lt;/code&gt; versus &lt;code&gt;Http&lt;/code&gt;. That failed result remains part of the history; the version pins alone did not establish deterministic output.&lt;/p&gt;
&lt;p&gt;The original report describes a later isolated Linux check that fixed the same build metadata and Hugo clock, then compared four complete builds before and four after a content-only taxonomy change. The change adds explicit term metadata for the 28 language/term case collisions, corrects the Chinese nginx tag to 漏桶, and preserves its historical &lt;code&gt;/zh/tags/令牌桶/&lt;/code&gt; URL. All four patched outputs contained 4,695 files and had identical sorted path-to-SHA-256 manifests; every published path, tag member sequence, pagination target, and XML route was retained, with no build warnings. The reported conclusion concerns repeatability only for that source snapshot, Hugo 0.165.0 Extended, vendored theme and Linux environment. It does not establish cross-platform, cross-year, future-version or future-content reproducibility. Re-run the comparison after changing those inputs, and do not use two ordinary &lt;code&gt;make build&lt;/code&gt; invocations as a fixed-metadata test.&lt;/p&gt;
&lt;p&gt;The recovered article and diagram retain that historical report, but the recovered materials do not include the original run logs or comparison manifests for the later four-build result. It has not been independently reproduced in this review. The later recovery of taxonomy title metadata is a separate exercise and does not supply the missing evidence for the original experiment.&lt;/p&gt;
&lt;p&gt;Version pins are not permanent approval to run old software. Review upstream fixes, assess their relevance, update the recorded versions and vendored bytes together where appropriate, then run the full verification and inspect the generated diff.&lt;/p&gt;
&lt;h2 id="why-this-wins"&gt;Why this wins&lt;/h2&gt;
&lt;p&gt;A build whose inputs are not pinned can produce the right answer today and a different tree tomorrow, and neither the diff nor the manifest can tell you why. Pinning narrows the investigation when the build changes; timestamps, content, configuration, and environment remain possible causes. That is a useful boundary without overstating reproducibility.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://reproducible-builds.org/docs/definition/"&gt;Reproducible Builds: definitions&lt;/a&gt;, controlled inputs and bit-for-bit comparison, checked 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 command reference&lt;/a&gt;, explicit build destinations and configuration options, checked 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 documentation: version&lt;/a&gt;, reporting and matching the generator release, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://semver.org/"&gt;Semantic Versioning specification&lt;/a&gt;, version labeling conventions, not a reproducibility guarantee, checked 2026-10-15.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title>Publish complete release trees through an atomic pointer</title><link>https://hk.xbcatm.com/decisions/adr-complete-directory-release/</link><pubDate>Thu, 20 Aug 2026 00:00:00 +0000</pubDate><guid>https://hk.xbcatm.com/decisions/adr-complete-directory-release/</guid><description>Decision record for immutable release directories selected by an atomic symlink replacement, retaining a named rollback target.</description><content:encoded>&lt;figure class="technical-diagram"&gt;&lt;img loading="lazy" src="https://hk.xbcatm.com/visuals/adr-directory.detail.en.svg" alt="Before: current points to releases/v1 and pending names the checked current.next symlink to releases/v2. On one filesystem, os.replace(pending, current) replaces the current link. After: a new open reads v2; an already-open file descriptor still reads v1. Rollback needs a fresh link to the recorded v1 target and another replacement." width="720" height="1690"&gt;&lt;figcaption&gt;A same-filesystem symlink replacement selects complete trees. Old asset URLs must remain reachable. This local pattern does not prove production migration, browser-wide transactions, automatic rollback, or power-loss durability.&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id="context"&gt;Context&lt;/h2&gt;
&lt;p&gt;Copying changed files over a live tree exposes partial releases and makes the previous version difficult to name. Complete staging solves the partial-copy problem, but “rename the new directory over the old one” is not a sufficient publication procedure. &lt;code&gt;mv new existing-directory&lt;/code&gt; normally moves &lt;code&gt;new&lt;/code&gt; inside the destination. A plain POSIX rename cannot replace an existing non-empty directory. Renaming the old directory away and then the new one into place leaves a gap between two operations.&lt;/p&gt;
&lt;h2 id="decision"&gt;Decision&lt;/h2&gt;
&lt;p&gt;Keep immutable, versioned release directories and select one through a &lt;code&gt;current&lt;/code&gt; symlink. Configure the file server to serve that stable path. After checking a new tree, create a temporary symlink beside &lt;code&gt;current&lt;/code&gt; and atomically replace the symlink itself using a same-filesystem rename. The old release directory remains untouched under its versioned name.&lt;/p&gt;
&lt;p&gt;This is the implementation pattern for the complete-tree decision, not evidence that a production host has already been migrated to it. An existing real-directory serve root requires a separately planned migration. Never run the example against that path and assume a symlink will replace it safely.&lt;/p&gt;
&lt;h2 id="rehearse-the-switch-and-rollback-locally"&gt;Rehearse the switch and rollback locally&lt;/h2&gt;
&lt;p&gt;The following Python 3 exercise creates only disposable local files. It demonstrates the pointer operation on a POSIX filesystem, without starting Caddy or changing a deployment.&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;Expected output is &lt;code&gt;new open: v2&lt;/code&gt;, &lt;code&gt;already open: v1&lt;/code&gt;, and &lt;code&gt;rollback: v1&lt;/code&gt;. The successful rename makes the directory entry replacement atomic. It does not change a file descriptor already opened on the old file, and it does not test power-loss durability or HTTP routing.&lt;/p&gt;
&lt;h2 id="consequences"&gt;Consequences&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No half-copied live tree:&lt;/strong&gt; each release is complete before selection. Never mutate a selected or retained release directory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Named rollback:&lt;/strong&gt; replace &lt;code&gt;current&lt;/code&gt; with a fresh temporary symlink to the recorded previous target. Rollback is an action to perform and verify, not something a failed smoke test does automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Preflight evidence:&lt;/strong&gt; check the staged manifest, file ownership, readable permissions, and available space before switching. Keep the manifest outside the hashed tree.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serialized publishers:&lt;/strong&gt; allow one deployment at a time. A fixed temporary link name is safe in this isolated exercise; production needs coordination and a collision-safe pending name.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Storage trade-off:&lt;/strong&gt; retain previous releases long enough for rollback and in-flight clients; prune only after the retention window and checks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Atomic pointer replacement is not a transaction spanning a browser page and all its asset requests. A browser can receive old HTML before the switch and request assets afterward. Fingerprinted asset URLs help only if the old asset bytes remain reachable at those URLs from the new served tree or a retained asset store. Merely keeping the old release directory elsewhere on disk is insufficient. Caches can also retain old responses after either publication or rollback.&lt;/p&gt;
&lt;p&gt;The temporary link and &lt;code&gt;current&lt;/code&gt; must be on the same filesystem; their common parent is a convenient way to ensure this. Check the filesystem and server&amp;rsquo;s actual symlink behavior. Rename atomicity concerns concurrent visibility, not survival after a sudden power loss; stronger durability needs file/directory synchronization appropriate to the filesystem. The served root is not a sandbox, so reject unintended symlinks within release content.&lt;/p&gt;
&lt;h2 id="why-this-beats-in-place-sync"&gt;Why this beats in-place sync&lt;/h2&gt;
&lt;p&gt;The release can be named, verified before exposure, selected in one pointer operation, and restored from a known target. Those are useful guarantees without promising a globally instantaneous update, cross-request snapshot isolation, or automatic recovery after every failure.&lt;/p&gt;
&lt;h2 id="sources"&gt;Sources&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;, atomic replacement, non-empty directories, symlinks, and open descriptors, checked 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;, atomic successful replacement and non-empty-directory/cross-filesystem limits, checked 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;, root resolution and symlink security boundaries, checked 2026-10-15.&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item></channel></rss>