← Return to field notes
systems / release engineering field guide

A deployment is a proof chain, not a file copy

A static site becomes trustworthy when its artifact is identified, staged, tested, promoted, and easy to reverse.

reference maintained created 2026-08-22 updated 2026-08-22 8 min 7 sections 6 figures
subscribe via RSS report a correction sec intro ~8 min left read 0%
opening contract reference · maintained
problem
A green local build does not prove that the public site is coherent. The wrong directory can be copied, a clean URL can fall through to the wrong document, an asset can arrive unreadable, or a reload can succeed while the browser still receives a broken release. Treating deployment as file transfer hides these failure modes until a reader finds them.
scope
A practical release contract for statically generated sites: build one candidate, give the bytes an identity, transfer them to a disposable stage, validate the router and representative pages, preserve the current release, promote the staged bytes through an explicit serving-layer boundary, and verify the public surface after reload.
environment
Static SvelteKit or similar output served by Nginx, a container, or another deterministic HTTP server. The pattern also applies to documentation sites, personal sites, and small internal frontends where a full deployment platform would be heavier than the application.

Assumptions

  • The repository can produce a complete static artifact from a pinned or reviewable source revision.
  • The operator has a staging destination and an authenticated path to the serving host.
  • The serving layer can be syntax-checked and reloaded without replacing the host or mutating application data.

Limitations

  • A digest proves which bytes moved; it does not prove that the source was correct, the content is safe, or the browser experience is accessible.
  • The examples use generic paths and domains. They deliberately omit private hostnames, addresses, credentials, topology, and service-specific recovery commands.
  • This is a static-release pattern, not a database migration strategy, a supply-chain attestation, or a substitute for security and accessibility review.
table of contents 7 sections
  1. 1 Build one candidate, then stop changing it
  2. 2 Give the bytes an identity before they move
  3. 3 Stage the tree before touching the live root
  4. 4 Treat the router as part of the application
  5. 5 A reload is an operation, not evidence
  6. 6 Make rollback boring enough to use
  7. 7 The release contract

Build one candidate, then stop changing it

A release starts before the network. Run the tests, type checks, and static build against one source revision, then treat the resulting build directory as a candidate rather than a scratch folder. If a second build is created after the archive is made, there are now two releases and the operator has to guess which one was tested.

The exact commands vary by stack. The invariant is that the artifact comes from the same gate that produced the evidence. For a JavaScript static site, a minimal local contract looks like this:

candidate build auto · bash
set -eunpm testnpm run checknpm run buildtar -czf release.tar.gz -C build .sha256sum release.tar.gz > release.tar.gz.sha256

Give the bytes an identity before they move

A commit identifies intent. A digest identifies the exact artifact. A deployment needs both. Record the source revision, artifact digest, target environment, and previous-release reference in a small manifest or receipt. A timestamp helps humans find the event, but it is not an identity: two different artifacts can share a minute, and the same artifact can be promoted more than once.

The digest is deliberately boring. It lets the receiving side answer a narrow question without trusting the transport narrative: do these bytes match the candidate that was tested? That question does not require a heavyweight platform, only a stable archive and a verification command.

Four identities a small release should carry
FieldQuestion it answersEvidence
Source revisionWhat change was intended?Commit or immutable source reference
Artifact digestWhat exact bytes moved?SHA-256 over the release archive
TargetWhere was it promoted?Named environment and serving root
RollbackWhat was live before it?Timestamped previous-release archive

Stage the tree before touching the live root

Copying directly into the live document root collapses transfer, extraction, validation, and promotion into one irreversible-looking action. A temporary stage restores the boundaries. Transfer the archive, verify its digest, extract it to a new directory, and inspect the files there before the active root changes.

The stage check should be specific. Confirm the entry document exists, a representative deep route resolves to the expected generated file, the feed or sitemap is present when advertised, and assets are readable by the serving process. A directory listing is not a browser test, but it catches a surprising number of bad copies.

Promotion as a chain of accountable states Every transition has a different failure mode and a different piece of evidence.
  1. build Tests, type checks, and static generation pass.
  2. identify Revision and digest are recorded.
  3. stage The candidate is extracted away from live traffic.
  4. validate Routes, assets, and serving config are checked.
  5. promote The previous root is archived before replacement.
  6. verify Public HTTPS and a hydrated browser are exercised.

Treat the router as part of the application

A static generator can produce the right files while the server still serves the wrong URL behavior. Clean routes, trailing slashes, fallback documents, feeds, redirects, and content types are part of the application contract. Test them against the staged tree and the serving configuration, not only against a local development server.

For Nginx, try_files makes the lookup order visible: check the requested file or directory, then redirect internally to the chosen fallback. That is powerful precisely because it is concrete. If a note route works only because a development server invents a fallback, the release is not ready.

The header-only commands below are reachability smoke checks. Pair them with representative body markers and a browser pass before calling the release verified.

serving-layer gate auto · bash
# syntax/referenced-file gatenginx -t# header-only reachability smoke checkscurl --fail --silent --show-error --head https://example.com/curl --fail --silent --show-error --head https://example.com/notes/examplecurl --fail --silent --show-error --head https://example.com/feed.rss

A reload is an operation, not evidence

Reloading a server only says that the process accepted an instruction. It does not say that the new root is complete, that the edge points at the intended origin, or that client-side hydration can load its assets. Verification must cross layers: inspect the serving process, request the public HTML, follow one deep route, fetch machine-readable surfaces, and load a representative page in a real browser.

The browser pass is not ceremony. It catches stale asset manifests, failed module requests, console exceptions, duplicate landmarks, horizontal overflow, and interaction regressions that curl cannot see. Keep the browser fixture small and repeatable: home, one heavy note, one mobile viewport, and one reduced-motion pass are more useful than an unbounded manual tour.

  • HTTP: status, content type, and representative body markers from a body fetch.
  • Server: configuration test, container/process health, and reload result.
  • Browser: no failed resources or console errors, no document overflow, and usable article controls.
  • Record: release digest, verification time, and the exact rollback archive.

If the only proof is “the reload returned zero,” the release has not been verified.

release rule

Make rollback boring enough to use

Rollback is not a dramatic recovery plan reserved for outages. It is the normal answer to a failed smoke check. Archive the previous root before promotion, keep the archive tied to the release receipt, and make the reversal use the same serving-layer validation as the forward path. A rollback that has never been rehearsed is a hope with a filename.

The safest small-site rollback is intentionally unglamorous: stop the promotion, restore the last known tree, validate the configuration, reload, and re-run the public probes. Do not “fix forward” while the release identity is still unclear; that produces a second unknown artifact and makes the incident harder to explain.

generic rollback shape auto · bash
set -eursync -a --delete previous-release/ live-root/nginx -tnginx -s reload# reachability smoke check; repeat body/browser checkscurl --fail --silent --show-error --head https://example.com/
What the digest does not prove

A matching digest proves byte identity between two copies. It does not prove that the content is correct, that a dependency is safe, that a route is accessible, or that a browser can hydrate the page. Keep semantic, security, accessibility, and browser checks in the release contract instead of asking one hash to carry them.

The release contract

The pattern is small enough to run without a deployment platform and strict enough to prevent the most expensive category error: confusing a successful copy with a successful release. Build once. Identify the bytes. Stage them. Validate the router and the reader-facing surface. Preserve the previous state. Promote only after the evidence is present. Verify from outside the process. Keep the reversal close.

That chain scales upward. Larger systems add signed attestations, immutable registries, progressive traffic shifts, and automated policy gates; they do not remove the underlying questions. What changed? What exactly moved? What is serving it? How do we know? What was live before it? How do we return there without improvising?

evidence ledger 8 claims
  1. implemented
    Artifact-first promotion path

    A working static-site release path builds locally, archives and hashes one candidate, validates a remote stage, creates a rollback archive, promotes the same tree, checks the serving configuration, and probes the public routes.

  2. field-observed
    Rollback is cheapest before promotion

    Preserving the previous document root before rsync turns a failed release into a bounded reversal instead of an improvised reconstruction.

  3. sourced
    SvelteKit defines a static output boundary

    The adapter-static documentation describes the pages and assets written by the adapter and warns that trailing-slash and SSR choices affect the generated result.

    inspect source ↗
  4. sourced
    Nginx route resolution is explicit

    Nginx documents try_files as a sequence of file checks followed by an internal redirect, making clean-URL behavior a testable serving contract rather than an assumption.

    inspect source ↗
  5. sourced
    Rsync has path-shape semantics

    The rsync manual distinguishes copying a directory itself from copying the contents selected by a trailing slash, so the source path belongs in the release procedure.

    inspect source ↗
  6. sourced
    SHA-256 is a checkable artifact identity

    GNU Coreutils documents sha256sum as a command that computes and checks SHA-256 digests; the digest proves byte identity, not semantic correctness.

    inspect source ↗
  7. sourced
    Nginx separates testing from reload

    Nginx documents -t as syntax and referenced-file testing, while -s reload starts new workers with a new configuration and gracefully retires old workers; the release contract keeps those operations distinct.

    inspect source ↗
  8. sourced
    curl --head is header-only

    The curl man page defines --head as fetching headers only; header probes are reachability smoke checks, not body-identity or browser verification.

    inspect source ↗
linked artifacts 6 attached
  • reference
    SvelteKit adapter-static documentation

    Static pages/assets output, fallback trade-offs, and trailing-slash requirements.

    open ↗
  • reference
    Nginx try_files documentation

    Official route and file-resolution semantics for a static server.

    open ↗
  • reference
    Rsync manual

    Copy semantics, remote-shell transport, and the distinction between source paths with and without a trailing slash.

    open ↗
  • reference
    GNU SHA-2 utilities

    Reference for computing and checking SHA-256 digests over release artifacts.

    open ↗
  • reference
    Nginx command-line parameters

    Official semantics for configuration testing and graceful reload.

    open ↗
  • reference
    curl man page

    Official semantics and limits of header-only HTTP probes.

    open ↗