Wheelhouse docs

Channel index format#

Each channel is one small JSON document at the root of the download host, regenerated whenever a tag publishes. It exists so a person, a script or a monitoring check can ask "is there a newer release" without scraping a page.

https://releases.rhymelikedi.me/stable.json
https://releases.rhymelikedi.me/beta.json

The document#

json
{
  "channel": "stable",
  "version": "0.5.1",
  "tag": "v0.5.1",
  "released": "2026-09-01T12:00:00Z",
  "base": "https://releases.rhymelikedi.me/v0.5.1/",
  "notes": "https://releases.rhymelikedi.me/v0.5.1/CHANGELOG.md",
  "checksums": "SHA256SUMS",
  "signature": "SHA256SUMS.asc",
  "signed": false,
  "artifacts": [
    {"name": "wheelhouse-0.5.1-amd64.iso", "kind": "iso", "arch": "amd64",
     "size": 512483328, "sha256": "…"},
    {"name": "wheelhouse-agent_0.5.1_amd64.deb", "kind": "deb", "arch": "amd64",
     "size": 9911244, "sha256": "…"}
  ]
}
FieldTypeMeaning
channelstringstable or beta.
versionstringThe tag without its leading v. This is the comparison key.
tagstringThe git tag, e.g. v0.5.1.
releasedstringWhen the index was written, UTC, %Y-%m-%dT%H:%M:%SZ.
basestringThe directory URL. base + an artefact's name is the download URL.
notesstring or nullThe release notes file, absolute. Null when the tag directory has none.
checksumsstringAlways SHA256SUMS, relative to base.
signaturestringAlways SHA256SUMS.asc, relative to base.
signedboolWhether that signature file actually exists.
artifactsarrayEvery published file in the tag directory.

Each artefact:

FieldMeaning
nameThe file name, relative to base.
kindiso, deb, qcow2, tarball, sources, notes, or file for anything else.
archamd64 or arm64, matched as a substring of the file name; null when the name says neither.
sizeBytes.
sha256Hex digest.

Fields are stable. Unknown fields may be added; a consumer must ignore what it does not recognise.

Building a download URL#

bash
curl -fsS https://releases.rhymelikedi.me/stable.json |
python3 -c '
import json,sys
i = json.load(sys.stdin)
for a in i["artifacts"]:
    if a["kind"] == "iso" and a["arch"] == "amd64":
        print(i["base"] + a["name"], a["sha256"])
'

Which channel a tag lands in#

Derived from the tag, not chosen by hand:

TagChannel
v1.2.3stable
v1.2.3-rc1, v1.2.3+build4beta
Anything that is not vX.Y.Z[-suffix]Refused: the publisher exits rather than guessing.

A channel is never moved backwards. Re-running the publisher for an old tag notices that the current index names a newer version and leaves it alone, so a re-run of last month's tag does not tell every router that the newest release is last month's. --force overrides that.

The index is written to a temporary file and renamed into place, so a reader never sees a half-written document, and it is left at mode 0644.

What is published beside it#

Both publish jobs — the .deb from the CI workflow and the ISO from the image workflow — call the same script after copying their files in, and it rescans the whole tag directory each time. Whichever job finishes last therefore leaves a complete index and a complete SHA256SUMS; the jobs do not have to know about each other.

SHA256SUMS covers every artefact in the directory. It deliberately does not cover itself, its own signature, .json files, or the per-file .sha256 sidecars it supersedes.

Verifying a download#

bash
curl -fsSLO https://releases.rhymelikedi.me/latest/SHA256SUMS
curl -fsSLO https://releases.rhymelikedi.me/latest/wheelhouse-0.5.1-amd64.iso
sha256sum --ignore-missing -c SHA256SUMS

Two places, on purpose#

WhereWhat
The forge releaseEvery artefact and SHA256SUMS. The forge is private and needs a login.
https://releases.rhymelikedi.me/<tag>/The same files, public. /latest/ follows the newest tag.

A checksum and the artefact it attests to do not have to come from the same place.

Polling for a new version#

bash
#!/bin/sh
# Exits 0 when the channel names a version newer than the one given.
have=$1
want=$(curl -fsS https://releases.rhymelikedi.me/stable.json |
       python3 -c 'import json,sys; print(json.load(sys.stdin)["version"])')
[ "$have" = "$want" ] || echo "newer release available: $want"

Compare on version, not on released: the timestamp is when the index was written, which a re-publish changes.

See also#

Checked against#

.forgejo/workflows/release-index.py (main, artefacts, kind_of, arch_of, parse_version, KINDS, SKIP_SUFFIXES), .forgejo/workflows/publish.sh, install/sign.sh, docs/deploy.md "Releases and update channels", docs/upgrade.md.

Updated 2026-09-02 releases channels downloads checksums