#!/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//): # banger--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 (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" # Flag rationale (cosign v3.x): # --use-signing-config=false bypasses the new signing-config flow that # otherwise insists on bundle output + Rekor. # --tlog-upload=false skip the public transparency log; banger's # trust model is "embedded public key", not # "Rekor lookup", so the log adds nothing. # --new-bundle-format=false emit a bare base64 ASN.1 DER signature, # which is what internal/updater consumes # via crypto/ecdsa.VerifyASN1. # These flags also work on cosign v2.x, so the script is forward- and # backward-compatible across the v2→v3 boundary. COSIGN_PASSWORD="${COSIGN_PASSWORD:-}" \ cosign sign-blob --yes \ --key "$COSIGN_KEY" \ --use-signing-config=false \ --tlog-upload=false \ --new-bundle-format=false \ --output-signature "$OUT_DIR/SHA256SUMS.sig" \ "$OUT_DIR/SHA256SUMS" log "verifying signature against the embedded public key" EMBEDDED_PUB="$OUT_DIR/embedded-pubkey.pem" # verify_signature.go embeds the PEM inside a Go raw-string literal, so the # BEGIN line is prefixed with `var ... = ` + backtick and the END line has a # trailing backtick. Strip those so the result is a clean PEM. sed -n '/-----BEGIN PUBLIC KEY-----/,/-----END PUBLIC KEY-----/p' \ "$REPO_ROOT/internal/updater/verify_signature.go" \ | sed -E 's/.*(-----BEGIN PUBLIC KEY-----)/\1/; s/(-----END PUBLIC KEY-----).*/\1/' \ > "$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" \ --insecure-ignore-tlog \ --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"