milestone 14: build, package, sign and publish releases

This commit is contained in:
2026-08-08 12:38:29 +02:00
parent 6c507992e4
commit cdacc560b7
48 changed files with 7272 additions and 543 deletions
+60 -9
View File
@@ -16,7 +16,8 @@ PLAN §18 sets five:
- blocklist lookup p95 < 1 ms;
- cached response p95 < 5 ms;
- memory with ~1M blocked domains < 100 MiB;
- stripped static binary < 10 MiB per arch, < 15 MiB with the embedded frontend.
- stripped static binary 10,485,760 bytes per arch, 15,728,640 bytes with
the embedded frontend.
They are household-scale numbers, and they are deliberately unambitious. 100
qps is far more than a house generates; the point of the target is not speed
@@ -59,17 +60,38 @@ baseline for the machine development happens on, not a claim about the target
platform; a Cortex-A76 is far slower and those numbers do not transfer.
CI does gate on the one performance property that *is* deterministic: binary
size. The `cross` job strips the release binaries and asserts them under the
§18 budgets. Size is a function of the input, not of the runner's mood, so it
is exactly the kind of thing a shared runner can measure honestly.
size. The `package` job builds the release artifacts and `zig build verify-dist`
asserts both §18 budgets against them. Size is a function of the input, not of
the runner's mood, so it is exactly the kind of thing a shared runner can
measure honestly.
The budgets are asserted as exact byte counts, and the asset-free budget gets
its own build against a generated empty assets directory rather than against
`web/dist-placeholder`. The placeholder is not buildable by `dist` at all —
that is the guard against a release shipping a stub admin page — and letting it
back in through a size check would have defeated the guard for the sake of one
number.
## What the test suite is
The blocking CI (Gitea Actions, `.gitea/workflows/ci.yml`) runs five jobs, all
required: the Zig suite with `-Dintegration`; the same suite cross-built for
aarch64 and executed under qemu-user; the frontend (format, lint, typecheck,
121 vitest cases, build); the cross-build with the two stripped-size asserts;
and a Docker smoke run that boots the image and polls `/api/health`.
Every blocking check lives in `.gitea/workflows/gates.yml`, which is a
`workflow_call` workflow with nothing in it but jobs. `ci.yml` calls it on push
and pull request for `master`, and `release.yml` calls it before it builds
anything publishable. That shape exists for one reason: a check that lived in
`ci.yml` alone would be a check a release could skip.
Five jobs, all required:
- `test` — the Zig suite with `-Dintegration`.
- `test-aarch64` — the same suite cross-built for aarch64 and executed under
qemu-user, plain tier only.
- `frontend` — format, lint, typecheck, the vitest cases, build.
- `package``zig build dist` and `zig build verify-dist`, which is where the
size budgets, the ELF static-linkage assert and the archive layout checks
are.
- `container` — builds the image, asserts the binary inside it is byte-identical
to the one in the matching tarball, and smoke-tests it by booting the
container and polling `/api/health`.
The Zig suite has three tiers, gated by build flags:
@@ -159,3 +181,32 @@ Two honest gaps remain, stated so nobody has to rediscover them:
- There is no freshness check on `web/dist`. CI cannot embed a stale bundle,
because the jobs that pass `-Dweb-dist` rebuild the frontend immediately
beforehand. A local build can, and will do it without a warning.
## What a signed release does not prove either
The same distinction applies one level out, to the artifacts. A release is
signed, and the signature is worth having: it says the artifact came from this
project's pipeline and reached you unaltered. It does not say the binary was
built from the source in this repository, because the machine that ran the
build also held the signing key. An attacker with that machine produces
something that verifies cleanly and contains whatever they put in it.
The control that closes that gap is a reproducibility gate — an independent
build, in a different directory on a different machine, landing on the same
bytes. It does not exist. It is a recorded deferral (`specs/milestone-14.md`
ruling 12), not something nobody thought of, and until it exists no document
here describes the build as reproducible: nobody has measured whether it is.
The cheap inputs to reproducibility are already in place — `gzip -n`,
`--mtime=@0`, `LC_ALL=C`, `TZ=UTC`, exact Zig and Node pins — which makes the
gate cheap to add later and proves nothing on its own.
What the release pipeline is required to hold to is narrower: two runs of
`zig build dist` on the same commit **in the same directory** produce
byte-identical tarballs. Same-directory determinism is a much weaker property
than reproducibility, and conflating the two is exactly the kind of claim this
page exists to refuse.
[Verify a release](../how-to/verify-a-release.md) states the same limits where
an operator will actually meet them, and gives the rebuild-and-compare recipe
with the caveat that a differing hash is not evidence of tampering while this
gap is open.