# Release process Maintainer-facing runbook for cutting and publishing a new banger release. End users don't need any of this — they pick up new releases through `banger update` or the curl-piped `install.sh`. ## What ships in a release Each release publishes four objects to the R2 bucket served at `https://releases.thaloco.com/banger/`: | Object | Path | Notes | |---|---|---| | Tarball | `/banger--linux-amd64.tar.gz` | `banger`, `bangerd`, `banger-vsock-agent` at the root, no subdirs | | Hashes | `/SHA256SUMS` | One line for the tarball, GNU `sha256sum` format | | Signature | `/SHA256SUMS.sig` | base64-encoded ASN.1 ECDSA cosign-blob signature over `SHA256SUMS` | | Manifest | `manifest.json` (bucket root) | Describes every published release; `latest_stable` points at the most recent | `install.sh` lives at the bucket root too (unversioned) so the `curl … | bash` URL stays stable across releases. ## Trust model recap Every release is cosign-signed. The public key is pinned in two places that MUST stay in sync: - `internal/updater/verify_signature.go` — `BangerReleasePublicKey` used by `banger update`. - `scripts/install.sh` — embedded copy used by the curl-piped installer before any banger binary is on disk. `scripts/publish-banger-release.sh` aborts the upload if the two copies diverge — that's the only mechanism keeping them coupled, so don't edit either alone. The signed payload is `SHA256SUMS`, which in turn covers the tarball. Verification uses the Go standard library (`crypto/ecdsa.VerifyASN1`) on the update path and `openssl dgst -verify` on the install-script path. cosign is needed only for **signing**. ## Pre-flight checklist Run these before tagging or publishing: 1. **`make smoke`** — the full systemd-driven scenario suite must be green. The smoke harness exercises the real install + update path end to end; if it's red, do not cut. 2. **CHANGELOG entry.** Add a `## [vX.Y.Z] - YYYY-MM-DD` section under `## [Unreleased]` describing what changed. Use the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) sub-headings (`### Added`, `### Fixed`, `### Notes`). 3. **Bump the link table** at the bottom of `CHANGELOG.md`: ```markdown [Unreleased]: …/compare/vX.Y.Z...HEAD [vX.Y.Z]: …/releases/tag/vX.Y.Z ``` 4. **Note unit-file changes loudly** in the CHANGELOG entry. `banger update` swaps binaries only — it does NOT rewrite `/etc/systemd/system/bangerd*.service`. If this release changed `renderSystemdUnit` / `renderRootHelperSystemdUnit`, the entry must tell existing-install users to run `sudo banger system install` once after updating to pick up the new units. v0.1.4 and v0.1.6 are reference examples. Commit the CHANGELOG change, push to `main`, and confirm CI is green. ## Cutting the release Order matters: publish first, then tag. 1. **Run the publish script:** ```sh scripts/publish-banger-release.sh vX.Y.Z ``` The script: - Builds `banger`, `bangerd`, `banger-vsock-agent` with `-ldflags` baking the version, the current commit SHA, and a UTC build timestamp into `internal/buildinfo`. - Tarballs the three binaries (bare basenames at the tar root — `internal/updater/StageTarball` rejects anything else). - Computes `SHA256SUMS`, signs it with `cosign sign-blob` (no transparency log, no bundle format — banger verifies the bare ASN.1 DER signature directly). - Verifies the signature against the public key extracted from `internal/updater/verify_signature.go`, then diffs that against the public key embedded in `scripts/install.sh`. Either failure aborts before upload. - Pulls the existing `manifest.json` from the bucket, appends the new release entry, points `latest_stable` at it, and uploads everything via rclone. - Uploads `scripts/install.sh` to the bucket root so the curl-piped installer stays current. 2. **Tag and push:** ```sh git tag vX.Y.Z git push --tags ``` Tagging happens AFTER publishing so the tag only exists if the release actually shipped. 3. **Verify from a clean machine:** ```sh curl -fsSL https://releases.thaloco.com/banger/manifest.json | jq .latest_stable curl -fsSL https://releases.thaloco.com/banger/install.sh | head -20 banger update --check # on an existing install ``` ## Verification releases If a release fixes anything in the update flow itself — `runUpdate` (`internal/cli/commands_update.go`), the systemd unit templates, or the helper/daemon restart sequencing — cut a follow-up no-op verification release immediately. The reason: `banger update` runs the OLD binary as the driver of the swap. A fix in vN can't be observed end-to-end on a vN-1 host updating to vN, because vN-1 is still in the driver seat. vN+1 with no functional changes lets a host on vN update to it and observe the fix live with vN as the driver. Examples in CHANGELOG.md: v0.1.3 follows v0.1.2's update-flow fix; v0.1.5 follows v0.1.4's daemon-restart fix. The verification-release CHANGELOG section is short and explicit: > No functional changes. Verification release for vN: … ## Patch vs minor banger follows [SemVer](https://semver.org/spec/v2.0.0.html). For v0.1.x, the practical contract: - **Patch (v0.1.x):** bug fixes, internal refactors, anything that doesn't change the exposed API/CLI behavior. - **Minor (v0.2.x):** any change to the **exposed API behavior or contract**. The vsock guest-agent protocol is the canonical example — a minor bump means existing VMs created against the older minor need to be re-pulled. Other minor-trigger changes: removing a CLI flag, changing a stable RPC method's request/response shape, breaking the on-disk store schema in a non-forward-compatible way. If in doubt, prefer the higher bump. Patch releases that turn out to have broken a contract are the worst-of-both: users update without warning, then break. ## Sibling catalogs Kernel and golden-image releases ship through the same gate. The `internal/kernelcat/catalog.json` and `internal/imagecat/catalog.json` manifests are `go:embed`-ed at build time, so a new entry only reaches users when banger itself is re-released. In practice: 1. Run `scripts/publish-kernel.sh ` or `scripts/publish-golden-image.sh …` to upload the artefact and patch the appropriate `catalog.json` in the working tree. 2. Commit the catalog change with whatever banger fix or feature it's landing alongside. 3. Cut a banger release the normal way; the new catalog entry ships with the next `banger` binary. The kernel and image catalogs each have their own R2 bucket (`kernels.thaloco.com`, `images.thaloco.com`) so versioning of the artefacts is independent of banger's release cadence — but **discoverability** is gated by the banger release that embeds the catalog pointer. ## When something goes wrong mid-release - **Signature verification fails locally** in `publish-banger-release.sh`: confirm `internal/updater/verify_signature.go` contains the same public key as `cosign.pub` in the repo root. If the script reports drift between `verify_signature.go` and `install.sh`, run `diff` between the two `BEGIN PUBLIC KEY` blocks and resolve before rerunning. - **rclone upload fails partway through:** the script uploads tarball, hashes, signature, and manifest in that order. Re-running is safe; rclone will overwrite. Until the manifest is uploaded, no client sees the new release — so a partial upload is invisible. - **Manifest already names the version** (re-cutting): the publish script's `jq` filter dedupes by `version`, so re-running with the same `vX.Y.Z` cleanly replaces the entry. - **Already tagged but the release is bad:** delete the tag locally AND on the remote (`git push --delete origin vX.Y.Z`), revert the CHANGELOG entry, fix the bug, and start the cycle over with a fresh patch number. Do NOT re-use the version — installed clients have already cached its `SHA256SUMS` against the manifest.