Release hygiene: one version, one Node line, every env var written down - #340
Conversation
|
Two things found after the description above was written. The Node bump broke a gate, and the gate was right. The relay unit job went Checked the other places that read test output instead of an exit code: the The musl binding was the one real risk of the base change, and it loads. All 11 checks green, |
d4aee38 to
a5fceee
Compare
Four items from the toekomstbestendigheid report of 2026-09-02 that need no
product decision. Nothing here changes behaviour on purpose; what it changes is
that the repo now says one thing instead of four.
One version (point 9). The root package.json is the version. relay.js reads
relay/package.json at runtime instead of restating the number, both image
labels take it as an ARG IMAGE_VERSION build-arg with a matching default,
docker-publish.yml passes the tag on a tagged build, and
scripts/post-deploy-verify.sh reads it rather than asserting a literal. Before
this: root 3.1.0, relay 3.0.0, admin 0.9.0-beta, relay label 3.0.0, admin label
1.0.0, and the deploy check asserting /health returns 3.0.0 while the relay
already answered 3.1.0. tests/version-consistency.test.mjs (8 checks) fails the
build the next time any copy drifts. Verified end to end: the built prod image
reports {"ok":true,"version":"3.1.0",...} on /health.
One Node line (point 2). Node 24, the newest LTS. Images move from
node:25-alpine3.21 to node:24-alpine3.24 by digest, CI and the devcontainer
from 20 to 24, and .nvmrc plus an engines field of ">=22 <25" pin the project
to the two lines that still get security fixes. Both the old versions were end
of life: the image that production would get on its next deploy ran an
unsupported runtime, and so did every CI job. Alpine 3.24 also matches the
rust:1.98-alpine builder stage, so the musl the crypto binding is compiled
against is now the musl it is loaded on. Proof: relay, admin and root suites
all green under node:24-alpine (176 + 37 + 123), and the full production
Dockerfile builds.
Every env var written down (point 8). deploy/.env.example goes from 3 variables
to 77: all 72 the relay and the admin panel read, plus 5 that compose or the
installer consume. Each with what it does, required or optional, its default
and the file that reads it. The code read 57 names and 40 of them were
documented nowhere. tests/env-documented.test.mjs fails on the next
undocumented name, and on documentation for a variable nothing reads.
A release process (point 1). docs/RELEASE.md: how a release is cut, tagged,
published and deployed, including what is not automated. CHANGELOG.md gets a
3.1.0 section covering the 268 commits since v3.0.0, grouped and with PR
numbers, and states honestly that the entries which sat under [Unreleased] are
folded in because the v3.0.0 tag was cut a month after the 3.0.0 section was
written. docs/PROJECT-STATUS.md, obsolete by its own second line since
2026-05-27, becomes a pointer to the CHANGELOG and RELEASE.md; the five
findings other documents cite by number are kept, with their status today.
No tag and no deploy: those belong together, and docs/RELEASE.md says so.
The Node 24 bump turned the relay unit job red in seven seconds, and the cause is worth writing down rather than papering over. The silent-suites gate in test.yml greps the log for lines that summary() in test/_requires.js prints, and TAP prefixes those with "# ". Node's default reporter for a non-TTY changed from tap to spec between Node 20 and Node 24, so the prefix disappeared, the grep found nothing, and the gate read that as "no suite asserted nothing" and failed on the mismatch with its expected list. The gate was right to fail: its input had silently changed shape. Pinning --test-reporter=tap on that one step makes the contract explicit instead of inherited from whatever Node decides a default should be. Verified under node:24-alpine: 176 pass, 0 fail, and the gate reports exactly parasign-signs-quota, as it did on Node 20. Checked the other places that read test output rather than an exit code: the heartbeat's issue body already filters on both TAP and spec forms (not ok, and the spec cross and "fail" line), so it needs no change.
Review found two wrong pointers in deploy/.env.example. Both were wrong in the same way: written from what the variable looked like it did rather than from a grep. So the fix is the two corrections plus the check that would have caught them. HTTP_PORT and HTTPS_PORT were credited to docker-compose.yml as the nginx container's port mapping. Neither is true. Compose does not read them, and this repo's compose file has no nginx service at all: production terminates TLS with the system nginx, as the comment at the top of docker-compose.yml says. They are read by deploy/preflight.sh, which picks a free port before install, and deploy/post-install.sh, which compares them against the ports actually published and rewrites .env to match. Both entries now say that. PARAMANT_VERSION said "install.sh only". frontend/install.sh and frontend/install-pi.sh read it too, each with its own fallback pin, and check 9 of tests/static-sanity.sh is what holds all three to that shape. Named all three here and in docs/RELEASE.md step 9, which had the same omission in prose. New E6 in tests/env-documented.test.mjs: every file named in a "read in:" line must exist and must mention the variable, whole-word so PORT is not satisfied by HTTP_PORT. Verified by putting the old wrong pointer back and watching it name both lines. A wrong pointer is worse than a missing one, because it sends the reader to the wrong file with confidence. Three smaller corrections from the same review: - RESEND_API_KEY and TOTP_SECRET were written in the required form (a bare assignment) while their own text says optional and the code reads both with a fallback. Commented out, so form and words agree. - .github/dependabot.yml still described the relay base as node:22-alpine. - SECURITY.md names node:22-alpine under a dated 2026-04-13 audit heading. That block is a record of what was measured then, so it stays as it was, with a note above it saying what the base is now. - docs/RELEASE.md claimed post-deploy-verify.sh catches a mismatch between the tag and /health. It does not: it compares /health against the package.json of the checkout it runs from, and from a tagged checkout those agree by construction. Rewritten to say what it actually catches, which is production still serving an older build after a deploy that looked like it worked. Tests: relay 176, admin 37, root 124 (122 pass, the same 2 pre-existing skips), all green on Node 22 and again under node:24-alpine, with the silent-suites gate reporting exactly parasign-signs-quota. bash -n and eslint clean.
main moved while this branch was open. Rebased onto 99909a1 and folded what arrived into the CHANGELOG section, because a release section that is a snapshot of the moment it was written is the exact failure this PR is fixing. New in the section: the ParaSign product page at /parasign (#325), the messaging guide (#331), the homepage rewrite for a buyer (#328), the site claims pinned to the code that makes them true (#327), the heartbeat that cannot be green without evidence (#338), and the route suites that boot a real relay.js (#341). Count in the intro goes from 268 commits and 52 PRs to 277 and 58, and the intro now says it was rebased rather than pretending it was written in one pass. #341 is worth naming for what it is: point 3 of the toekomstbestendigheid report was that relay.js, 6488 lines and 68 routes, was loaded by no unit test at all. It is now loaded by the route suites. That point is not closed, but it is no longer zero. Two conflicts, both resolved toward main's newer structure: - product-heartbeat.yml: #338 moved the live job out into heartbeat.yml. Took main's file whole and applied the Node 24 bump to both files instead, so the bump follows the job rather than the filename. All seven node-version entries across the five workflows now read '24'. - test.yml: #341 added a second silent-suites gate for the route job and set both expected sets to empty. Kept main's exclusion lists and applied --test-reporter=tap to both steps. That second one matters more after the rebase than before. With a non-empty expected list, the spec reporter made the gate fail, which is how it was found. With an empty expected list, the spec reporter makes it PASS: the grep finds nothing, silent is empty, and a gate that can no longer see anything reports green. A dead gate that reports green is precisely what these two gates exist to prevent, so both now pin the reporter rather than inherit a Node default. Tests after the rebase, on Node 22 and again under node:24-alpine: relay 175, admin 40, root 145 (143 pass, 2 pre-existing skips), both silent-suite gates empty as expected. static-sanity PASS including the style guard, bash -n and eslint clean. The root job now runs npm ci first, per #338; tests/README.md and the local commands in docs/RELEASE.md match what CI does.
a5fceee to
9a5ecd5
Compare
|
Rebased onto Worth saying why the checks went quiet for half an hour before this: GitHub does Two conflicts, both resolved toward main's newer structure. product-heartbeat.yml. #338 moved the live job out into test.yml. #341 added a second silent-suites gate for the new route job and That second one is more important after the rebase than before, and it inverts CHANGELOG. The six PRs that landed while this was open are in the 3.1.0 docs/RELEASE.md step 3 was rewritten to match what Tests after the rebase, on Node 22 and again under |
#340 pinned one Node line across four places: `engines` (">=22 <25") in the root, relay and admin manifests, `.nvmrc`, the `node-version` steps in every CI job, and the base image in both Dockerfiles. Its own words: the range "makes adopting Node 26 a deliberate bump rather than an accident". Dependabot opened #343 and #346 (node 24-alpine3.24 -> 26-alpine3.24) within the hour. Node 26 is the next even line but does not reach Active LTS until late October 2026, and no `.npmrc` sets engine-strict, so `npm ci` inside the image never checks `engines`: both PRs went green while putting the image on a runtime all three manifests forbid. The docker updater for /relay and /admin now ignores semver-major on "node" only. The rust builder keeps its majors (#313 was a real break that had to be seen), and node minor/patch, Alpine rebuilds and digest updates still arrive weekly. Adopting a new LTS line stays a hand bump of the whole grid.
docker-compose.yml heeft geen env_file. .env vult alleen ${VAR} in het
compose-bestand zelf in, dus een variabele zonder eigen regel in x-relay-env
bereikt nooit een container. De vier receipt-variabelen uit #342 hadden die
regel niet, dus fase 1d schreef een vlag die nergens aankwam en fase 6h zou
pas aan het eind sterven, met 3.1.0 al live.
PARAMANT_INLINE_RECEIPT_HEADER en de drie PARAMANT_RECEIPT_-variabelen staan
nu in x-relay-env, allemaal met een lege default: de vlag telt alleen bij
exact 1 en de caps vallen leeg terug op hun ingebouwde getallen. Bewezen met
docker compose config: met de vlag in .env rendert hij op alle vijf de
relays, zonder de vlag op geen enkele.
Fase 4 controleert dat nu op de server voordat er iets herstart wordt, zodat
een ontbrekende doorgifte blokkeert op een leesbare assertie in plaats van op
een smoketest achteraf. Runbook: de regel dat compose byte-identiek is aan
41501bb klopte al niet meer sinds #340 en is vervangen door wat er nu speelt.
Verder uit ronde 2:
- de bufferguard telt binnen elk /v2/outbound-blok in plaats van per bestand.
Een conf met proxy_buffer_size op /v2/inbound liet het outbound-blok leeg;
nagespeeld en gedicht met een tweetraps-awk die ook idempotent is
- fase 5b resolvet het doelpad met readlink -f en weigert alles dat buiten
de docroot uitkomt, met een teller die geassert wordt
- 6h claimt de 16 KB alleen als het headerblok er echt boven zit; anders
WARN met NOT PROVEN en geen ok-regel, zodat de samenvatting niet suggereert
dat het bewezen is
- de taintscan erft over een toewijzing, dus s3cr="$tok" telt ook. Een hop,
bewust, en die grens staat in de test opgeschreven
Points 1, 2, 8 and 9 from the toekomstbestendigheid report of 2026-09-02. Only
the parts that need no product decision. No tag, no deploy: those belong
together, and
docs/RELEASE.mdsays so.9. Versions: four places, three answers
Measured before: root
package.json3.1.0,relay/package.json3.0.0,admin/package.json0.9.0-beta, relay image label 3.0.0, admin image label1.0.0, a
VERSIONliteral inrelay.jsreading 3.1.0, andscripts/post-deploy-verify.shasserting that/healthreturns 3.0.0 whilethe relay already answered 3.1.0. The one check that could have caught the
drift was part of it.
Now the root
package.jsonis the version and nothing else states it:relay.jsdoesrequire('./package.json').version, and the runtime stage ofrelay/Dockerfilecopiespackage.jsonin for exactly that read.ARG IMAGE_VERSION, defaulted to the samestring.
docker-publish.ymlpasses the tag on a tagged build and thepackage.jsonversion onmain, so the label is never a short sha.post-deploy-verify.shreadsrelay/package.jsonintoEXPECT_VER.0.9.0-betato the project version.tests/version-consistency.test.mjs(8 checks) fails the build the next timeany copy drifts, so the list does not have to be remembered.
Verified end to end, not just on paper: the production Dockerfile builds, the
image carries
org.opencontainers.image.version=3.1.0, and a relay booted fromit answers
{"ok":true,"version":"3.1.0","sector":"relay",...}on/health.2. Node: two of the three versions were end of life
node:25-alpine3.21(EOL)node:24-alpine3.2420(EOL)242024engines>=22 <25.nvmrc24Node 24 is the newest LTS line.
>=22 <25is exactly the two lines stillgetting security fixes: it keeps a Node 22 dev machine (the NUC) working while
blocking 20, 23 and 25, all end of life, and it makes adopting Node 26 a
deliberate bump rather than an accident.
Alpine 3.24 is a second, unplanned win: the crypto binding is compiled in the
rust:1.98-alpinestage, which is Alpine 3.24. It was being loaded on a 3.21runtime. Those now match.
Proof, run locally rather than asserted: all three suites under
node:24-alpine(Node 24.20.0) and a full build ofrelay/Dockerfileincluding the liboqs/musl chain.
8. Environment variables: 40 of 57 documented nowhere
deploy/.env.exampledocumented three names.relay.js,relay/liband theadmin panel read 57, and 40 of them appeared in neither
.env.example.It now documents 77: all 72 that the relay or admin reads, plus 5 that
docker-compose.ymlorinstall.shconsumes. Each entry says what thevariable does, whether it is required or optional, its default, and the file
that reads it. Grouped by area, generated from the code and then written by
hand.
tests/env-documented.test.mjsholds the line in both directions:process.envname with no entry fails the build (verified by addingone and watching it go red),
NOT_READ_BY_NODEwith a stated reason,read in:line,The extractor also catches
env.Xin the modules that take an injectable envobject (
developer-gate,webauthn,entitlements), which a plainprocess.envgrep misses.DEVELOPER_ALLOWLISTandNODE_ENVare documentedbecause of it.
Two honest findings that fell out of the sweep, written into the file rather
than fixed here:
LOG_LEVELandADMIN_TOTP_SECRETare passed to the relaysby
docker-compose.ymland read by nothing. Removing them from the composefile is a change to the running stack, not a documentation fix.
1. Release process
docs/RELEASE.md: what a version number means, cutting a release in ninesteps, rolling back, and a section on what is still not automated. It names the
gap between
docker-publish.yml(which pushes images) anddocs/RUNBOOK-DEPLOY-3.0.0.md(which says there is no registry) instead ofpretending it is closed, and defers that to a deploy-time decision.
CHANGELOG.mdgets a## [3.1.0] - unreleasedsection covering the 268commits since
v3.0.0, grouped into Security, Added, Changed, Fixed, Removed,and Build/CI/dependencies, with PR numbers where a commit carried one. One
trap, documented in
RELEASE.mdso the next person does not fall in it: atrailing
(#nn)is not always a PR.#19and#21in this range are securityfindings, and PR #21 is an unrelated dependabot bump. Checked with
gh pr viewbefore writing them down.
The existing
[Unreleased]body is folded into the 3.1.0 section unchanged,with the reason stated: the CHANGELOG's 3.0.0 section is dated 2026-05-27 and
the
v3.0.0tag was cut on 2026-06-24, so that backlog never had a releasedsection to go to. Rewriting it would be guessing at what it meant.
docs/PROJECT-STATUS.mddeclared itself obsolete in its own second line on2026-05-27 and kept being cited by three other documents. It is now a pointer
table to the CHANGELOG, RELEASE.md, DEPLOY-3.1.md and
gh pr list. The fivefindings that
R010,cross-repo-coordination.mdandaudit-readiness-checklist.mdcite by number are kept, each with its status asmeasured today: finding 1 is fixed by this PR, 2 is still open and belongs to
the tagging step, 4 and 5 are superseded.
ROADMAP.mdno longer claims thatPROJECT-STATUS is regenerated each run.
Tests
Everything below ran locally on this branch.
test.ymlruns itmain, so +13 new checks and no new skips)node:24-alpinetests/static-sanity.shbash -non every.shnpx eslint@9 .parasign-signs-quotadocker build -f relay/Dockerfile/healthreports 3.1.0Note on check 10 of static-sanity: it was red on
mainbecause commitc45d1c6carries an attribution trailer. This branch's commit has none, perAGENTS.md, so the gate is green.Not in this PR
Deliberately, because each needs a decision rather than a change:
docs/RELEASE.mdsteps 5 to 8).install.shstill pinsv3.0.0. It clones that git tag, so bumping itbefore
v3.1.0exists points the self-host installer at something nobody canclone. It is step 9 of the release.
docs/RUNBOOK-DEPLOY-3.0.0.mdkeeps its name and its content. Renaming it ischeap; reconciling registry-pull versus build-on-server is the actual
question, and it is a production decision.