release: nix flake with tag-pinned hashes, reproducible tarballs (milestone 40)

flake.nix fetches the release tarballs and carries their SRI hashes in a generated block. The cut tool builds the release locally with the toolchain gates.yml pins, in a normalized nine-variable environment, writes the hashes into flake.nix, and commits it with build.zig.zon as the single bump commit. The package job verifies the pins on the bump commit and the publish job verifies them again on the tag, before anything is uploaded.

The tarballs are written by dist_stage (std.tar.Writer, flate gzip) instead of the runner's tar and gzip, and -ffile-prefix-map keeps checkout paths out of the C objects; two checkouts at different absolute paths produce byte-identical archives. nxdns version, /api/version and the admin footer report the version only: the bump commit cannot know its own sha.
This commit is contained in:
2026-09-08 21:45:22 +02:00
parent 3e57f43e08
commit 22abcd9b7b
41 changed files with 1739 additions and 135 deletions
+29 -5
View File
@@ -62,6 +62,7 @@ env:
ZIG_VERSION: "0.16.0"
# Exact patch, not a floating "24" (ruling 12).
NODE_VERSION: "24.19.0"
NPM_VERSION: "11.17.0"
# The author's commit- and tag-signing key. `git verify-tag` alone proves
# only that *some* key in the keyring signed the tag, so the signature's
@@ -271,18 +272,29 @@ jobs:
cache: npm
cache-dependency-path: admin/package-lock.json
# The same pinned toolchain and normalized environment as the frontend
# job in gates.yml: the cut pinned the bundle's bytes into flake.nix
# before this run existed, and the pin check below compares against them.
- name: Assert the pinned Node and npm
run: |
test "$(node --version)" = "v${NODE_VERSION:?}"
test "$(npm --version)" = "${NPM_VERSION:?}"
test ! -e /nonexistent/npmrc-user
test ! -e /nonexistent/npmrc-global
# The same normalized environment the cut builds the bundle in: exactly
# nine variables, a file mode from the umask, a C locale, UTC and a zero
# build timestamp. Each of those can move the bytes the release hashes
# cover, and this job's bundle has to reproduce the one the cut pinned.
- name: Build the web UI
working-directory: admin
run: |
npm ci
npm run build
run: env -i PATH="$PATH" HOME="$HOME" LC_ALL=C LANG=C TZ=UTC SOURCE_DATE_EPOCH=0 CI=true npm_config_userconfig=/nonexistent/npmrc-user npm_config_globalconfig=/nonexistent/npmrc-global sh -c 'umask 022 && npm ci && npm run build'
# Step 8.
- name: Build the release artifacts
run: >
zig build dist
-Dversion-string="$VERSION"
-Dgit-commit="$TAG_COMMIT"
-Dadmin-dist=admin/dist
-Doptimize=ReleaseSafe
@@ -290,7 +302,19 @@ jobs:
run: >
zig build verify-dist
-Dversion-string="$VERSION"
-Dgit-commit="$TAG_COMMIT"
-Dadmin-dist=admin/dist
-Doptimize=ReleaseSafe
# Unconditional, and before the image push, the draft and every upload:
# the tag's tree IS the bump commit's tree, so the flake block must pin
# these exact bytes and name this exact version. A consumer who resolves
# the tag through the flake gets hashes that were written before this run
# existed; this is where the claim is proved, while nothing has yet left
# the runner.
- name: Verify the flake pins
run: >
zig build verify-pins
-Dversion-string="$VERSION"
-Dadmin-dist=admin/dist
-Doptimize=ReleaseSafe