banger/scripts/publish-banger-release.sh
Thales Maciel fae28e3d8b
update: docs + publish script for the self-update feature
README gets a top-level Updating section; docs/privileges.md gains
a step-by-step trust-model writeup of `banger update`. The new
scripts/publish-banger-release.sh drives the manual release cut:
build, tar, sha256sum, cosign sign-blob, verify against the embedded
public key, jq-merge into manifest.json, rclone upload to the R2
bucket. Refuses outright if the embedded key is still the placeholder
so we can't accidentally publish an unverifiable release. Also folds
in gofmt drift accumulated across the updater package and a few
sibling files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:43:46 -03:00

177 lines
6.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# publish-banger-release.sh
#
# Cut and publish a banger release tarball + SHA256SUMS + cosign
# signature to the R2 bucket consumed by `banger update`.
#
# Usage:
# scripts/publish-banger-release.sh v0.1.0
#
# Environment overrides:
# COSIGN_KEY path to the cosign private key (default: cosign.key)
# RCLONE_REMOTE rclone remote name (default: releases)
# BUCKET_PATH object-key prefix in the bucket (default: banger)
# BASE_URL public URL prefix for objects (default: https://releases.thaloco.com)
# SKIP_UPLOAD set to 1 to stage everything locally without rclone upload
#
# Prerequisites:
# * cosign in PATH (https://github.com/sigstore/cosign)
# * rclone in PATH, configured with a remote named ${RCLONE_REMOTE}
# pointing at the R2 bucket served at ${BASE_URL}.
# * A cosign keypair already generated. The public key MUST already
# be embedded in internal/updater/verify_signature.go's
# BangerReleasePublicKey constant — running this script with a
# placeholder key would publish a release no installed banger can
# verify.
#
# Output (under build/release/<version>/):
# banger-<version>-linux-amd64.tar.gz
# SHA256SUMS
# SHA256SUMS.sig
# manifest.json (the freshly-mutated copy uploaded to the bucket)
set -euo pipefail
log() { printf '[publish-banger-release] %s\n' "$*" >&2; }
die() { log "$*"; exit 1; }
if [[ $# -lt 1 ]]; then
die "usage: $0 <version> (e.g. $0 v0.1.0)"
fi
VERSION="$1"
case "$VERSION" in
v*.*.*) ;;
*) die "version must look like vX.Y.Z, got $VERSION" ;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
COSIGN_KEY="${COSIGN_KEY:-cosign.key}"
RCLONE_REMOTE="${RCLONE_REMOTE:-releases}"
BUCKET_PATH="${BUCKET_PATH:-banger}"
BASE_URL="${BASE_URL:-https://releases.thaloco.com}"
SKIP_UPLOAD="${SKIP_UPLOAD:-0}"
command -v cosign >/dev/null || die "cosign not in PATH"
command -v rclone >/dev/null || die "rclone not in PATH"
command -v sha256sum >/dev/null || die "sha256sum not in PATH"
command -v jq >/dev/null || die "jq not in PATH"
[[ -f "$COSIGN_KEY" ]] || die "cosign key not found at $COSIGN_KEY (override with COSIGN_KEY=...)"
cd "$REPO_ROOT"
OUT_DIR="$REPO_ROOT/build/release/$VERSION"
TARBALL_NAME="banger-$VERSION-linux-amd64.tar.gz"
TARBALL_PATH="$OUT_DIR/$TARBALL_NAME"
log "preparing $OUT_DIR"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
log "building binaries with version=$VERSION"
COMMIT="$(git rev-parse HEAD)"
BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
LDFLAGS="-X banger/internal/buildinfo.Version=$VERSION \
-X banger/internal/buildinfo.Commit=$COMMIT \
-X banger/internal/buildinfo.BuiltAt=$BUILT_AT"
BUILD_STAGE="$OUT_DIR/stage"
mkdir -p "$BUILD_STAGE"
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/banger" ./cmd/banger
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/bangerd" ./cmd/bangerd
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags "$LDFLAGS" -o "$BUILD_STAGE/banger-vsock-agent" \
./cmd/banger-vsock-agent
log "tarring → $TARBALL_PATH"
# -C into the stage dir so the tarball's root entries are bare
# basenames (banger, bangerd, banger-vsock-agent) — the updater's
# StageTarball validator rejects anything else.
tar -czf "$TARBALL_PATH" -C "$BUILD_STAGE" \
banger bangerd banger-vsock-agent
log "computing SHA256SUMS"
(
cd "$OUT_DIR"
sha256sum "$TARBALL_NAME" > SHA256SUMS
cat SHA256SUMS
) >&2
log "cosign sign-blob → SHA256SUMS.sig"
COSIGN_PASSWORD="${COSIGN_PASSWORD:-}" \
cosign sign-blob --yes \
--key "$COSIGN_KEY" \
--output-signature "$OUT_DIR/SHA256SUMS.sig" \
"$OUT_DIR/SHA256SUMS"
log "verifying signature against the embedded public key"
EMBEDDED_PUB="$OUT_DIR/embedded-pubkey.pem"
awk '/BEGIN PUBLIC KEY/,/END PUBLIC KEY/' \
"$REPO_ROOT/internal/updater/verify_signature.go" \
| grep -v '"' | grep -v '^//' \
> "$EMBEDDED_PUB"
if grep -q PLACEHOLDER "$EMBEDDED_PUB"; then
die "BangerReleasePublicKey is the placeholder in verify_signature.go; replace it with cosign.pub before publishing"
fi
cosign verify-blob \
--key "$EMBEDDED_PUB" \
--signature "$OUT_DIR/SHA256SUMS.sig" \
"$OUT_DIR/SHA256SUMS"
# Build the manifest. Pull the existing manifest from the bucket so
# we don't lose previous release entries, append this one, bump
# latest_stable, write back.
log "fetching existing manifest"
PREV_MANIFEST="$OUT_DIR/manifest.previous.json"
if curl -fsSL "$BASE_URL/$BUCKET_PATH/manifest.json" -o "$PREV_MANIFEST" 2>/dev/null; then
log " found previous manifest"
else
log " no previous manifest (first release); seeding"
printf '{"schema_version":1,"latest_stable":"","releases":[]}' > "$PREV_MANIFEST"
fi
NEW_MANIFEST="$OUT_DIR/manifest.json"
RELEASED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
jq --arg version "$VERSION" \
--arg tarball_url "$BASE_URL/$BUCKET_PATH/$VERSION/$TARBALL_NAME" \
--arg sums_url "$BASE_URL/$BUCKET_PATH/$VERSION/SHA256SUMS" \
--arg sig_url "$BASE_URL/$BUCKET_PATH/$VERSION/SHA256SUMS.sig" \
--arg released_at "$RELEASED_AT" \
'
.schema_version = 1
| .latest_stable = $version
| .releases = (
(.releases // [])
| map(select(.version != $version))
| . + [{
"version": $version,
"tarball_url": $tarball_url,
"sha256sums_url": $sums_url,
"sha256sums_sig_url": $sig_url,
"released_at": $released_at
}]
)
' "$PREV_MANIFEST" > "$NEW_MANIFEST"
log "manifest:"
jq '.' "$NEW_MANIFEST" >&2
if [[ "$SKIP_UPLOAD" == "1" ]]; then
log "SKIP_UPLOAD=1, not uploading. Artifacts staged under $OUT_DIR"
exit 0
fi
log "uploading to $RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
rclone copy "$TARBALL_PATH" "$RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
rclone copy "$OUT_DIR/SHA256SUMS" "$RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
rclone copy "$OUT_DIR/SHA256SUMS.sig" "$RCLONE_REMOTE:$BUCKET_PATH/$VERSION/"
log "uploading manifest"
rclone copy "$NEW_MANIFEST" "$RCLONE_REMOTE:$BUCKET_PATH/"
log "done. verify with:"
log " curl -fsSL $BASE_URL/$BUCKET_PATH/manifest.json | jq ."
log " banger update --check"