Downloading and verifying releases¶
A script, an Ansible role or a Dockerfile that installs the agent needs four things: which version is current, the URL of the right file, its checksum, and its signature. Each release publishes a manifest that holds them, so none of this means reading the GitHub page.
The addresses¶
Everything is served from https://packages.senhub.io, the site that
also hosts the package repositories.
| What | Address |
|---|---|
| Newest stable release | https://packages.senhub.io/releases/stable/latest.json |
| Newest beta | https://packages.senhub.io/releases/beta/latest.json |
| One release, forever | https://packages.senhub.io/releases/<version>/manifest.json |
| Every release, per channel | https://packages.senhub.io/releases/index.json |
| The schema | https://packages.senhub.io/releases/manifest.schema.json |
<version> is the release tag, without a v (0.6.2, 0.6.2-beta.1).
latest.json is a copy of the manifest of the highest version of its
channel. A beta never appears in stable, and beta follows betas only:
a machine that wants "the newest beta, or the final release once it is
out" reads both and picks the higher version.
Why not GitHub's /releases/latest/download/
That address ignores pre-releases, knows nothing of channels, and can only redirect to one file name, so it cannot tell you a checksum or a signature, nor that the Windows files are still waiting to be signed. The manifest carries all of that and is the same for a script and for a person.
What a manifest holds¶
{
"schema_version": 1,
"version": "0.6.2-beta.1",
"channel": "beta",
"date": "2026-10-06T08:05:15Z",
"commit": "aac44564b4536f978a79d63d4492ebe52a53c408",
"complete": false,
"signing_pending": ["senhub-agent-windows-amd64.zip", "senhub-agent-oss-windows-amd64.zip"],
"artifacts": [
{
"name": "senhub-agent-linux-amd64.zip",
"os": "linux", "arch": "amd64", "edition": "full", "kind": "zip",
"url": "https://github.com/senhub-io/senhub-agent/releases/download/0.6.2-beta.1/senhub-agent-linux-amd64.zip",
"sha256": "f8ecc56d046e4e6587b1c6422c64ff2ff2286c1235d069919c91e24ddebca957",
"size": 28994471,
"minisig_url": "https://github.com/senhub-io/senhub-agent/releases/download/0.6.2-beta.1/senhub-agent-linux-amd64.zip.minisig"
}
],
"images": [
{
"repo": "ghcr.io/senhub-io/senhub-agent",
"edition": "full",
"tag": "0.6.2",
"digest": "sha256:..."
}
]
}
| Field | Meaning |
|---|---|
version, channel |
The release tag, and stable or beta. |
date |
When the release was published, UTC. |
commit |
The commit of the open source repository the release is tagged at. |
complete |
true when every ZIP and MSI carries its signature and, on a stable release, both MSIs are there. |
signing_pending |
The ZIPs and MSIs that have no signature yet. |
artifacts[].kind |
zip (the archive the auto-updater reads), msi, deb, rpm. |
artifacts[].edition |
full or oss. |
artifacts[].os, arch |
linux or windows; amd64 or arm64 (an RPM built for x86_64 or aarch64 is listed as amd64 or arm64). |
artifacts[].url |
The GitHub release asset. |
artifacts[].sha256, size |
Checksum and size in bytes, as GitHub computed them at upload. |
artifacts[].minisig_url |
The detached signature, or null. |
images[] |
The container images of this exact version, with the digest to pull by. |
charts[] |
The Helm chart of this exact version (repo, version, digest), published as an OCI artifact; empty when it is not published yet. |
Three things worth knowing:
- A Windows file has no signature until the signing round has run.
The manifest published with a release reads
"complete": false; it is rebuilt when the Windows ZIPs are signed and again when the MSIs are. Do not install a Windows file from a manifest that is notcomplete. .deband.rpmhave"minisig_url": nullby design. They are signed with the repository key (https://packages.senhub.io/gpg.key); install them from the package repositories, where the package manager does the verification.- There is no
min_upgrade_from. The agent has no rule that refuses an upgrade from an older version, so the manifest does not pretend to know of a minimum. If one appears, it will be a new field of the same schema version.
The schema may gain fields without a version change: ignore the ones you
do not know. A rename, a removal or a change of meaning raises
schema_version.
The images and charts lists are filled by the release itself: the release publishes both images and the Helm chart, waits for them, then builds the manifest.
What is published, and when¶
Cutting a release publishes everything, in this order, with no step to
remember: the ZIPs and their signatures, the .deb and .rpm packages and
repositories, the container images of both editions (and the minor tag of
a stable release), the Helm chart, then the manifest. The only manual step left is the
Authenticode signing of the Windows files; the manifest is rebuilt after
the Windows ZIPs are signed and again after the MSIs, and says
"complete": false until then.
If a step fails (a package build, an image scan), the run fails and names it, and the manifest still lists what exists. Running the "Publish release manifest" workflow again for the tag completes it.
Finding and verifying the right file¶
The public key below is the one the agent embeds to verify its own updates. It is the same for every release.
CHANNEL=stable # or: beta
EDITION=oss # or: full
OS=linux ARCH=amd64
KEY=RWRlfkyeLpjI0MjTSfuvT/bDNHHaVJhRirQN8Z8LTAM+n4LKVbpjrlRh
manifest=$(curl -fsSL "https://packages.senhub.io/releases/$CHANNEL/latest.json")
# Refuse a release whose signing round is not finished.
[ "$(jq -r .complete <<<"$manifest")" = true ] || { echo "release not complete yet" >&2; exit 1; }
file=$(jq -c --arg e "$EDITION" --arg o "$OS" --arg a "$ARCH" \
'.artifacts[] | select(.kind == "zip" and .edition == $e and .os == $o and .arch == $a)' <<<"$manifest")
name=$(jq -r .name <<<"$file")
curl -fsSLo "$name" "$(jq -r .url <<<"$file")"
curl -fsSLo "$name.minisig" "$(jq -r .minisig_url <<<"$file")"
echo "$(jq -r .sha256 <<<"$file") $name" | sha256sum -c - # shasum -a 256 -c - on macOS
minisign -Vm "$name" -P "$KEY"
The checksum catches a corrupted download; the signature proves the file was built and signed by SenHub. Do both: the manifest and the file come from different hosts, and the signature is what ties them.
To stay on one minor line, take the newest of it from the index:
curl -fsSL https://packages.senhub.io/releases/index.json \
| jq -r '.channels.stable.releases[] | select(.version | startswith("0.6.")) | .version' | head -n 1
(the list is newest first).
To pin a release in an Ansible role or a Dockerfile, read
https://packages.senhub.io/releases/<version>/manifest.json instead of
latest.json: it never changes except to gain a signature or an image.
Container image tags¶
Images are published to ghcr.io/senhub-io/senhub-agent (full edition)
and ghcr.io/senhub-io/senhub-agent-oss (open source edition).
| Tag | Moves? | Use it for |
|---|---|---|
0.6.2 |
Never | A reproducible deployment. |
0.6 |
To each new patch of that minor line, stable releases only | Following the fixes of one minor line. |
0.6.2-beta.1 |
Never | Testing a beta. A beta never moves 0.6. |
There is no latest tag: it would cross minor lines without notice.
For a deployment that must be immune even to a moved tag, pull by the
digest that the manifest lists under images.
Helm chart¶
The chart is published to oci://ghcr.io/senhub-io/charts/senhub-agent
by the release, with the release version as its version (0.6.2,
0.6.2-beta.1). A beta is only installed with an explicit --version.
See Kubernetes (Helm).