# 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 | `<version>/banger-<version>-linux-amd64.tar.gz` | `banger`, `bangerd`, `banger-vsock-agent` at the root, no subdirs |
| Hashes | `<version>/SHA256SUMS` | One line for the tarball, GNU `sha256sum` format |
| Signature | `<version>/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 <name>` 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.
