diff --git a/AGENTS.md b/AGENTS.md index 5e15ebf..8050f32 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,6 +25,7 @@ Always run `make build` before commit. - `./build/bin/banger image promote ` copies an unmanaged image into daemon-owned managed artifacts. - `scripts/make-generic-kernel.sh` builds a Firecracker-optimized vmlinux from upstream sources. `scripts/publish-kernel.sh ` publishes it to the kernel catalog. - `scripts/publish-golden-image.sh` rebuilds + publishes the golden image bundle and patches the image catalog. +- `scripts/publish-banger-release.sh ` cuts a banger release. Full runbook in `docs/release-process.md`. ## Image Model diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000..510ac06 --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,189 @@ +# 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.