banger/scripts/publish-banger-release.sh
Thales Maciel 12f7a92bb4
publish-script: don't clobber COSIGN_PASSWORD with empty default
The previous form did

  COSIGN_PASSWORD="${COSIGN_PASSWORD:-}" cosign sign-blob ...

which set COSIGN_PASSWORD to "" when the caller hadn't exported one.
cosign sees an explicit empty password and tries to decrypt with
it instead of prompting interactively, so any real password-protected
offline key fails with "decryption failed".

Drop the prefix entirely. If COSIGN_PASSWORD is already in env, it
gets inherited normally; if it isn't, cosign prompts on the terminal
— which is the right UX for a maintainer running the publish script
locally with the offline private key.

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

199 lines
7.5 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"
# 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.
# If COSIGN_PASSWORD is set in the environment, cosign uses it.
# Otherwise cosign prompts on the terminal — which is what we want
# for a password-protected offline key. Don't pre-set it to empty:
# that suppresses the prompt and makes cosign try to decrypt with
# the empty password, failing with "decryption failed".
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"