How VayuPress is released
Last reviewed: 2026-10-17, against VayuPress 3.18.7
A release is one push to main that changes .release-version. Nobody tags
by hand and nobody uploads a file. The workflow builds, signs, attaches and
publishes, and every install's updater takes it from there.
Version numbers: micro by default, roll over at 99
Every release bumps the third segment, however large the change:
v3.18.6 → v3.18.7. Size never decides the bump; only the rollover does.
Each segment counts 0–99:
- micro at 99 → the next release bumps the minor and resets micro:
v3.13.99→v3.14.0; - minor at 99 as micro rolls over → the next release bumps the major and
resets both:
v3.99.99→v4.0.0.
So a version says when, not how much. What a release changes, and whether it changes anything an operator must act on, is in its CHANGELOG section and its Upgrade Notes. A stable API route never breaks inside a major version whatever the number does (compatibility/api-contracts.md).
A version with a hyphen (v3.19.0-rc.1) is published as a pre-release. The
console offers it only to an operator who asks for pre-releases.
The release commit
Three files change in one commit, so they always agree:
| File | Holds |
|---|---|
.release-version |
vX.Y.Z; the push of this file starts the workflow |
cmd/vayupress/main.go |
var Version = "X.Y.Z" (no v) |
CHANGELOG.md |
A ## [X.Y.Z] — YYYY-MM-DD section; the [Unreleased] notes move into it |
scripts/release-consistency.sh compares the three on every push (CI job
Release Metadata Consistency). A version file ahead of the changelog fails
the build, so a version is never announced without its notes, or bumped
without being shipped.
What the workflow does
.github/workflows/tag-release.yml, on a push to main that touches
.release-version:
- Reads the version and refuses one that is not
vX.Y.Z. - Skips if that release is already published, so a re-run is harmless.
- Builds with the Go version
go.modpins: CGO for SQLite, linked statically (-linkmode external -extldflags '-static'),-trimpath, with-X main.Version. Onelinux/amd64binary with libc linked in, so it does not depend on the host's. - Signs it twice. Cosign signs keyless, through GitHub's OIDC identity
for this workflow, with an RFC 3161 timestamp from Sigstore's timestamp
authority, and the entry is recorded in the Rekor transparency log. The Ed25519 release key signs it too (
scripts/release-sign). The workflow refuses to publish a build that pins a key without the key's secret, because every install would refuse that release and no later one could rescue them. - Writes and signs a CycloneDX SBOM.
- Packages and signs the provisioning helpers, the VayuShield helper agent and the self-hosted marketing site.
- Lifts the version's CHANGELOG section into the release body
(
scripts/changelog-section.sh). - Checks that no attachment sorts before
vayupress. Installs older than the asset-selection fix take the first installable file. A ZIP that sorted first was once written over a running binary. - Creates the tag and publishes the release. Releases are immutable once published.
The assets of every release:
| Asset | |
|---|---|
vayupress |
The binary |
vayupress.sha256 |
Its checksum |
vayupress.cosign.bundle |
The Sigstore signature, certificate and Rekor entry |
vayupress.sig |
The Ed25519 signature |
vayupress-vX.Y.Z.sbom.json (+ .cosign.bundle) |
The SBOM |
vayuprovision-helpers.tar.gz, vayushield-agent.tar.gz (+ .sha256, .cosign.bundle) |
The root helper scripts and the shield agent |
vayupress-selfhosted-site.zip (+ .sha256) |
The project site |
What an install checks before it runs any of this is in UPGRADING.md.
Verifying a release yourself
cosign verify-blob vayupress \
--bundle vayupress.cosign.bundle \
--certificate-identity https://github.com/johalputt/VayuPress/.github/workflows/tag-release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c vayupress.sha256
The identity and issuer are the ones the updater pins
(update.ReleaseSignerIdentity, update.ReleaseSignerIssuer): a release
signed from any other workflow, or from any branch but main, is refused. A
test fails if this page and the code disagree.
Before the release commit
A release is cut only from a tree that has passed, on that tree:
- the Go gates:
gofmt,go vet,go build, unit, race and integration tests, staticcheck, golangci-lint, gosec, govulncheck, the binary-size budget; - the browser suite against a built binary (Playwright, Chromium);
- a review of everything under
[Unreleased]as an attacker would read it, with each finding fixed or recorded before the version is bumped.
Then the version is bumped and the commit is pushed. Every gate CI runs, and what each does not prove, is in QUALITY-GATES.md.
A fix that cannot wait
There is no separate hotfix branch: the fix is committed to main, gated the
same way, and released with the next micro number. A vulnerability is handled
privately first, by the process in SECURITY.md, and
disclosed with the release that fixes it.