What every change passes before it ships
There is no separate QA step. A commit reaches main only by passing sixteen
required CI jobs — one of which, the cross-compile matrix, is itself seven
parallel builds — and ci-pass re-checks every one by name. A job that ran
without being listed there would be a job that cannot fail the build.
The Go code itself
| Gate | What it catches |
|---|---|
go vet, gofmt -l |
Suspicious constructs; unformatted source |
staticcheck |
Dead stores, impossible conditions, misused stdlib |
golangci-lint run (v2, zero issues) |
The aggregate linter set, errcheck included |
gosec -severity high -confidence high |
Injection, weak crypto, hardcoded credentials |
govulncheck |
Known CVEs on code paths actually reachable from this binary |
go build -race + go test -race |
Data races, in unit and integration runs |
go test -shuffle=on |
Tests that only pass in their usual order — shared state between tests |
go test ./internal/archcheck/ |
Layering violations: which package may import which |
go test ./internal/compat/ |
Golden-file breaks in the plugin/theme contract (VCB) |
| deadcode gate | Newly unreachable code, against a committed baseline |
| cross-compile matrix | Architecture-specific compile errors, on seven Linux ABIs |
go mod tidy drift, go mod verify |
Undeclared or unused dependencies; module contents that no longer match go.sum |
| reproducible build | Two cold builds of identical source that are not byte-identical |
Everything around the code
| Gate | What it catches |
|---|---|
| trufflehog (filesystem mode) | Committed secrets |
| shellcheck, markdownlint | Broken shell, malformed docs |
| heredoc audit | An unescaped backtick inside an unquoted heredoc — live command substitution in a file that is supposed to be config text |
| SPDX + license check | A source file with no licence header |
| ADR completeness, required docs | A design decision that shipped unrecorded |
| Governance / ethics / security-policy / community checks | Drift between the Constitution and the repository |
| source-sync | A missing entry point or required package, or a module that does not build |
| release-metadata consistency | .release-version and the top CHANGELOG.md section disagreeing |
In the browser
Playwright drives the real console and the real public site, on a fresh install
in CI's order (e2e.yml):
| Gate | What it catches |
|---|---|
| The design lint | Any console value off the token scale, on every console page |
| Screenshot baselines | A change to any of sixteen surfaces, console and public site, against approved pictures |
| Contrast | A text colour below WCAG AA against its surface, in both schemes |
| Halcyon's checks | Sideways scrolling, overlays, reflow at 320 px, keyboard focus and control names, on every page type |
| The specs | The behaviour itself: mail, chat, the editor, settings, members, the Mailboxes page, the page cache |
Lighthouse (lighthouse.yml) |
Home, a post and four console pages against their budgets |
A pull request's screenshots are compared with the baselines on main; a
baseline changes only when a commit names the surface it moved, and CI records
the new picture after the run is green.
Every gate is mutation-tested
A gate that has not been seen to fail on the thing it forbids has proved nothing. Each new check is run once against a deliberately broken copy of the code it guards and must fail at its own assertion. Three false greens this has caught:
- A test that judges output by calling the function under test compares the code with itself: loosening the validator loosened the assertion in the same edit. The unacceptable is stated independently.
- A seed that trips several rules kills a mutant of none of them: the hostile input carrying one forbidden character carried another too, so no single rule was ever deciding. One seed per rule.
- Two guards for one outcome are each unkillable by an outcome-only assertion: either alone failed the run, so "it failed" never said which fired. The specific reason is asserted.
Why some gates exist
The release-metadata check exists because of how releases work here: pushing .release-version
is the release. Nothing downstream re-derives the version. Without that check a
version can be bumped and announced while the notes describe a different release
— or a changelog can claim fixes shipped when no build was ever cut.
The cross-compile matrix earned its place the same way. Every other Go gate runs
on the runner's own linux/amd64, so nothing asked whether the source compiled
anywhere else. It did not: internal/sandbox named syscall.SYS_EPOLL_WAIT in
architecture-neutral code, and the arm64 and riscv64 ABIs have never defined it.
ARM VPSes are squarely this product's audience.
It then failed on its own first run, twice, on code written to fix that — a
32-bit int cannot hold SECCOMP_RET_KILL_PROCESS, and SYS_SOCKET does not
exist on 386, which multiplexes through socketcall(2). Both were in test files,
which is why they had survived a local per-architecture go build: go build
does not compile tests and go vet does. That is the argument for the gate in
one sentence — the person adding it had just spent a day on this exact class of
bug and still shipped two more.
What these gates do not prove
They run on linux/amd64; the cross-compile
matrix proves the other six architectures compile, not that they were tested,
and releases remain a single native build.
The reproducibility gate proves the build is deterministic: two cold builds,
each from an empty module cache and its own temporary directory, are
byte-identical. go.mod pins the Go toolchain and the release builds with
GOTOOLCHAIN=auto, so a tag decides its own compiler rather than inheriting
whatever the runner happened to have — which is what makes the property a
statement about the tag instead of about one afternoon.
The remaining variable is the C side: the release links SQLite statically, so
the host's gcc and libc still participate. Reproducing a published binary
exactly therefore means matching the release runner's C toolchain as well. The
Go version is recorded in the artifact itself (go version <file>); the C
toolchain is not, and pinning it is open work.
An earlier version of this section claimed builds were not reproducible. That was wrong, and it was published in two sets of release notes before being checked properly. The measurement behind it compared two binaries built while the working tree was being edited, which is not a reproducibility test at all. The controlled test — cold, independent caches, unmodified tree — passes, and is now a gate so the answer stops depending on who measured it.
The shell lint is worth a note of its own, because for a long time it could not
fail. It ran find scripts/ -name "*.sh" -exec shellcheck {} \;, and find
returns 0 whatever its -exec reports — so the step printed its success line
unconditionally. It runs through xargs -0 now. It had been concealing a real
error: a config-writing heredoc with an unquoted delimiter, where backticks are
live command substitution rather than text, so every generated nginx file
carried a comment with words silently deleted. The heredoc audit above gates
that whole class rather than the single instance.
Two style tools were evaluated and rejected rather than added: gofumpt (96
files, formatting preference rather than correctness) and go vet -vettool=shadow
(127 findings, overwhelmingly benign shadowing). A gate nobody can keep green
gets disabled, and a disabled gate protects nothing.