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>
177 lines
6.1 KiB
Bash
Executable file
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"
|