Making a release#
A release is a git tag. Pushing v1.2.3 starts both workflows: one builds and packages
the agent and the UI, the other builds the OS image, and both publish into the same
directory on the download host. Neither job knows what the other produced, so neither
writes the checksum file — a helper rescans the whole directory every time, which is what
lets whichever job finishes last leave a complete, correctly signed set. One version
number covers the whole product, and the release notes are that version's section of the
changelog rather than a sentence that never changes.
What a version number covers#
One number for the agent, the web UI, the installer, the packaging and the OS image. They
are built and released together from one tag —
CHANGELOG.md. The format is
Keep a Changelog and the versioning is
Semantic Versioning.
Versions are written bare in prose — 0.5.1, not v0.5.1 — and with the v only when naming
a git tag or a release directory.
Before you tag#
- The changelog section exists. Move the work out of
## [Unreleased]into## [<version>] - <date>. Both jobs read that section as the release body, and a missing one publishes the literal text "No CHANGELOG.md section for <version>". main.versionmatches. The linker stamps-X main.versionfrom the tag, but the constant inagent/main.goand the fallback inui/src/components/product.tsare what a non-tagged build reports. Keeping them in step is part of the release, not an afterthought.- The pipeline is green on
main. Everything on The test suites has run. - It has been driven in a browser against the live bench. That is the definition of
done in
AGENTS.md, and no pipeline can do it for you.
git tag -a v1.2.3 -m "Wheelhouse OS 1.2.3"
git push origin v1.2.3What the tag sets off#
Two workflows run in parallel. Neither waits for the other.
ci.yml, after the tests pass#
- Builds the agent for
amd64andarm64. - Packages a
.debper architecture, plus a tarball per architecture, and copiesCHANGELOG.mdin beside them — the public mirror has no login and therefore no release page, so the notes have to be a file. - Writes a
SHA256SUMSover its owndist/, and signs it if a key is configured. - Creates the forge release with the changelog body and uploads every artefact.
- Runs
publish.sh <tag> /srv/wheelhouse-releases dist/*.
iso.yml, in about 30 to 60 minutes#
- Builds
ui/distand the amd64 agent, and packages the.debfrom its own checkout — a job token cannot read another private repository's releases. - Runs
build-iso.sh. - Inspects the finished image before publishing anything (Building the image).
- Attaches the image, its checksum, the corresponding-source record and the build-tooling
patch to the release, creating the release first if
ci.ymlhas not got there yet. - Runs the same
publish.sh.
publish.sh, in order#
.forgejo/workflows/publish.sh is what both jobs
call. It does six things:
- Copies the named files into
<releases-root>/<tag>/, after checking each is really a file — an unmatched glob arrives as its own literal, andcpwould otherwise report a missing file whose name looks like a pattern. - Deletes any signature already there.
SHA256SUMSis about to be replaced, and a signature that no longer matches what it covers is worse than no signature. - Runs
release-index.py --link-latest, which rewritesSHA256SUMSover every artefact in the tag directory, moveslatest, and writes the channel index. - Makes everything world-readable — after the checksums exist, not before, or the one file the release notes tell customers to fetch inherits the runner's umask.
- Signs
SHA256SUMSthroughinstall/sign.sh, whenRELEASE_SIGNING_KEYis set. - Re-runs
release-index.py --index-only, so the channel index recordssigned: true.--index-onlyreads the digests back out ofSHA256SUMSinstead of re-hashing: rewriting a file that has just been signed is exactly how a release acquires a signature that no longer matches what it covers.
release-index.py#
.forgejo/workflows/release-index.py writes
two things, with the standard library and no pip.
<tag>/SHA256SUMS#
One file over every artefact in the tag directory — the packages, the tarballs, the image,
the changelog, the corresponding-source record. It skips itself, its own signature, the
per-file .sha256 sidecars and the channel JSON, and it writes through a temporary file
and an atomic replace.
<channel>.json#
The channel is derived from the tag: a plain vX.Y.Z is stable, anything with a
pre-release suffix (v1.0.0-rc1) is beta.
{
"channel": "stable",
"version": "0.5.1",
"tag": "v0.5.1",
"base": "https://releases.rhymelikedi.me/v0.5.1/",
"notes": "…/CHANGELOG.md",
"checksums": "SHA256SUMS",
"signature": "SHA256SUMS.asc",
"signed": false,
"artifacts": [
{"name": "wheelhouse-0.5.1-amd64.iso", "kind": "iso", "arch": "amd64",
"size": 512483328, "sha256": "…"}
]
}version is the comparison key, base plus an artefact name forms the download URL,
sha256 is what to check, and signed says whether signature is real. Fields may be
added; a consumer must ignore what it does not know.
latest#
--link-latest points <root>/latest at the tag, through a temporary symlink and an
atomic os.replace, so a reader never sees latest missing. It refuses to move
backwards: re-running the pipeline for an old tag, which is a normal thing to do after
fixing a publish, would otherwise hand every customer last month's image. The one
exception is that a real release always supersedes a pre-release, so the first stable tag
takes latest off the release candidate that was standing in for it.
The release notes#
release-notes.py prints the body: the
version's changelog section, framed by a link to the tag's directory and the verification
instructions. It exists because the same twenty lines were inlined in two workflow files,
and a pair like that drifts. It lives beside the workflows because Forgejo only treats
*.yml in that directory as workflows, so a helper next to them is checked out with them.
Signing#
The verification half already works and needs no key material of ours:
curl -fsSLO https://releases.rhymelikedi.me/latest/SHA256SUMS
curl -fsSLO https://releases.rhymelikedi.me/latest/wheelhouse-<version>-amd64.iso
sha256sum --ignore-missing -c SHA256SUMS
# Once a key exists:
WHEELHOUSE_PUBKEY=./wheelhouse-release.asc bash install/sign.sh verify SHA256SUMSsign.sh verify imports the public key into a throwaway keyring, so verifying a download
never touches the caller's own. REQUIRE_SIGNATURE=1 makes it fail when nothing is
signed, which is what a script should set.
Where a release ends up#
| Place | Needs a login? | Carries |
|---|---|---|
The forge release at git.rhymelikedi.me | Yes — the repository is private | Every artefact and SHA256SUMS |
https://releases.rhymelikedi.me/<tag>/ | No | The same files |
https://releases.rhymelikedi.me/latest/ | No | A symlink following the newest tag |
https://releases.rhymelikedi.me/stable.json | No | The channel a customer can be told to watch |
If a publish goes wrong#
Re-running the tag's workflow is safe by design: publish.sh deletes the stale signature
before rewriting the checksums, release-index.py rescans the whole directory rather than
appending, and latest will not move backwards onto an older tag. What it does not do
is remove an artefact that should not have been published — that is a manual deletion on
the download host, followed by another run so the checksums and the index stop naming it.
See also#
- What CI does — every step of both workflows.
- Building the package · Building the image — what is being published.
- The test suites — what has to be green first.
- Working agreements — the definition of done behind a tag.
- Download and verify — the customer's side
of
SHA256SUMS, and why a checksum is not a signature. - Release history — the published record of what each tag changed.
- Channel index format — every field of
stable.json, enumerated. - Watching for releases — the channel index, from the operator's side.
- Upgrading the image — what a customer does with the result.
Checked against#
.forgejo/workflows/ci.yml ·
.forgejo/workflows/iso.yml ·
.forgejo/workflows/publish.sh ·
.forgejo/workflows/release-index.py ·
.forgejo/workflows/release-notes.py ·
install/sign.sh ·
CHANGELOG.md ·
docs/deploy.md ·
AGENTS.md