Wheelhouse docs

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#

  1. 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 &lt;version&gt;".
  2. main.version matches. The linker stamps -X main.version from the tag, but the constant in agent/main.go and the fallback in ui/src/components/product.ts are what a non-tagged build reports. Keeping them in step is part of the release, not an afterthought.
  3. The pipeline is green on main. Everything on The test suites has run.
  4. 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.
bash
git tag -a v1.2.3 -m "Wheelhouse OS 1.2.3"
git push origin v1.2.3

What the tag sets off#

Two workflows run in parallel. Neither waits for the other.

ci.yml, after the tests pass#

  1. Builds the agent for amd64 and arm64.
  2. Packages a .deb per architecture, plus a tarball per architecture, and copies CHANGELOG.md in beside them — the public mirror has no login and therefore no release page, so the notes have to be a file.
  3. Writes a SHA256SUMS over its own dist/, and signs it if a key is configured.
  4. Creates the forge release with the changelog body and uploads every artefact.
  5. Runs publish.sh <tag> /srv/wheelhouse-releases dist/*.

iso.yml, in about 30 to 60 minutes#

  1. Builds ui/dist and the amd64 agent, and packages the .deb from its own checkout — a job token cannot read another private repository's releases.
  2. Runs build-iso.sh.
  3. Inspects the finished image before publishing anything (Building the image).
  4. Attaches the image, its checksum, the corresponding-source record and the build-tooling patch to the release, creating the release first if ci.yml has not got there yet.
  5. Runs the same publish.sh.

publish.sh, in order#

.forgejo/workflows/publish.sh is what both jobs call. It does six things:

  1. Copies the named files into <releases-root>/<tag>/, after checking each is really a file — an unmatched glob arrives as its own literal, and cp would otherwise report a missing file whose name looks like a pattern.
  2. Deletes any signature already there. SHA256SUMS is about to be replaced, and a signature that no longer matches what it covers is worse than no signature.
  3. Runs release-index.py --link-latest, which rewrites SHA256SUMS over every artefact in the tag directory, moves latest, and writes the channel index.
  4. 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.
  5. Signs SHA256SUMS through install/sign.sh, when RELEASE_SIGNING_KEY is set.
  6. Re-runs release-index.py --index-only, so the channel index records signed: true. --index-only reads the digests back out of SHA256SUMS instead 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.

json
{
  "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:

bash
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 SHA256SUMS

sign.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#

PlaceNeeds a login?Carries
The forge release at git.rhymelikedi.meYes — the repository is privateEvery artefact and SHA256SUMS
https://releases.rhymelikedi.me/<tag>/NoThe same files
https://releases.rhymelikedi.me/latest/NoA symlink following the newest tag
https://releases.rhymelikedi.me/stable.jsonNoThe 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#

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

Updated 2026-09-02 development release versioning signing