Cut a Platform release¶
A release of Nerthus.Platform is a tag, the image and wheels the tag
builds, and one merge request that pins them. This guide is the procedure and which job refuses
what. The rules it executes: an image is pinned only when a release tag built it, the release
merge request is the one write path to versions.lock, a build of main is never pinned, and
no job writes a release file.
Versions follow semver by contract impact. A rollback ships as a higher version: a pin only
moves forward, so undoing 0.3.0 means 0.3.1 carrying the revert.
Two lanes¶
| Job | Runs on | Builds | Pushes |
|---|---|---|---|
image:smoke |
every merge request, and main |
both variants, and runs the container | nothing |
image:release |
protected v* tags only |
the same, identically | v<semver>, and writes image-digest.txt |
image:smoke names what it builds sha-<12 hex>, a grammar ci/image.py check_release_tag
refuses by name, so a main build cannot be pinned even by a hand that means to.
The procedure¶
-
Check the numbers. On a clean checkout of
origin/main:It prints the three declarations that must agree (
VERSION,pyproject.toml,crates/nerthus-core/Cargo.toml), the last release tags on the remote, and whatversions.lockpins today. It runs the codelock:checkruns, so it cannot disagree with it. It cuts nothing and edits nothing. -
Bump, in one commit.
VERSION,[project] versioninpyproject.tomland[package] versionincrates/nerthus-core/Cargo.toml, plusCargo.lock.lock:checkthen refuses the staleuv.lock; presslock:updateon that pipeline to push the re-locked one. Do not touchversions.lock: it describes the previous release's artifacts, correctly, until the new ones exist.lock:checklets it lag and refuses it to lead. -
Merge that, and wait for
mainto be green. A tag on a red tree is a release nobody measured. -
Tag it, with notes.
The notes say what a converging host does differently: a new environment variable, a migration that expands, a role that now refuses to start without something. The reader is somebody deciding whether to pin this on a host, not somebody reading commits.
-
Wait for
image:release. It builds, runs the container against a bundled and an external Postgres, asserts the registry credential is in neither the image's history nor its filesystem, pushesregistry.gitlab.com/margonem/nerthus/it/nerthus.platform:v0.3.0and publishesimage-digest.txt.publish:wheelsruns beside it and uploads both wheels and the source distribution. -
Open the release merge request. One commit, the only kind that may change
versions.lock:version: the new number;[wheels]: each published wheel's sha256, read off the registry (glab api "projects/86196907/packages/<id>/package_files"), never recomputed locally, because what is pinned is what a consumer downloads;[image]:repository,tag(the release tag),digest(fromimage-digest.txt) andcommit(the tagged commit, in full).
Merge it on green.
-
Move a site's pin separately. A site runs what its pin in Nerthus.Infra names, and moving that pin is a deploy: its own merge request in that repository, after this one merged. No Platform site exists yet, so no release moves one.
Which job refuses what¶
| Refusal | Where | Why there |
|---|---|---|
image.tag is not a release tag, the tag does not name image.commit, or that commit is not in this history |
lock:check (ci/release.py pin) |
git ls-remote needs no clone; the ancestry half needs the history, so the job fetches it whole |
VERSION, pyproject.toml and the crate disagree, the tag does not name the tree, or versions.lock leads it |
lock:check (ci/release.py versions) |
cheap, and a contributor learns it in the lint stage |
image.commit does not exist |
reference:check (nerthus lock verify versions.lock) |
the check asks the GitLab API and makes a depth-1 fetch, and needs a built nerthus, which that job has; git cat-file on a shallow clone reports an old, present commit as missing |
a wheel's sha256 is not what the registry serves, or versions.lock pins no image |
verify:versions-lock |
it already downloads both wheels from the registry to hash them |
lock:check skips the pin check on a tag pipeline, and verify:versions-lock does not run there:
while a release tag builds, the artifacts those checks verify are the ones that pipeline is
producing, and versions.lock still describes the previous release. main runs both on every push,
which is where a wrong or unpinned file is refused, and why a release tag's pipeline can be green.
The credential in the image build¶
The image build installs from the group registry, and its credential arrives as a BuildKit secret:
two NAME=value lines mounted at /run/secrets/registry_credential and sourced inside one RUN,
written into no layer and no history, never a build argument. ci/image.py refuses a
docker/Dockerfile that names a credential as an ARG or an ENV, and image:release asserts,
with a positive control, that neither the token nor either variable name is in the image's history,
configuration or filesystem.