Files
nucleic/scripts/upload-r2.sh
T

85 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Publish a channel's Sparkle artifacts (the signed/notarized DMG[s] + the EdDSA-signed
# appcast) to the R2 bucket served at updates.nucleic.blakeslee.xyz. The appcast already
# carries the update signatures, so this is plain static hosting — a bucket object PUT.
#
# Object layout:
# <channel>/<file>.dmg DMGs, foldered by channel for tidy download links
# (e.g. beta/Nucleic-Beta-0.1.0.828.dmg)
# appcast-<channel>.xml Sparkle feed at the bucket root (build's baked-in SUFeedURL)
# <channel>/latest 302 → newest DMG, served by the cloud/nucleic-updates Worker
#
# scripts/upload-r2.sh {canary|beta|rc|stable}
#
# Env:
# NUCLEIC_R2_BUCKET target R2 bucket (default: nucleic-updates)
# NUCLEIC_FEED_BASE feed host, used for the printed verify URL AND the
# already-uploaded existence check (default: https://updates.nucleic.blakeslee.xyz)
# NUCLEIC_R2_FORCE re-upload every DMG even if already present (default 0)
# WRANGLER wrangler invocation (default: "npx --yes wrangler")
#
# Auth: wrangler must be logged in (`wrangler login`) or CLOUDFLARE_API_TOKEN set with R2 write.
set -euo pipefail
CHANNEL="${1:?usage: upload-r2.sh (canary|beta|rc|stable)}"
case "$CHANNEL" in
canary) SLUG="Nucleic-Canary" ;;
beta) SLUG="Nucleic-Beta" ;;
rc) SLUG="Nucleic-RC" ;;
stable) SLUG="Nucleic" ;;
*) echo "usage: $0 {canary|beta|rc|stable}" >&2; exit 2 ;;
esac
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUCKET="${NUCLEIC_R2_BUCKET:-nucleic-updates}"
FEED_BASE="${NUCLEIC_FEED_BASE:-https://updates.nucleic.blakeslee.xyz}"
WRANGLER="${WRANGLER:-npx --yes wrangler}"
APPCAST="$ROOT/dist/appcast-$CHANNEL.xml"
[ -f "$APPCAST" ] || { echo "upload-r2: $APPCAST missing — run generate-appcast.sh $CHANNEL first" >&2; exit 1; }
shopt -s nullglob
dmgs=("$ROOT/dist/${SLUG}-"*.dmg)
shopt -u nullglob
[ "${#dmgs[@]}" -gt 0 ] || { echo "upload-r2: no ${SLUG}-*.dmg in dist/ — build a release first" >&2; exit 1; }
put() { # <local-file> <key> <content-type> <cache-control>
echo " ↑ $2"
$WRANGLER r2 object put "$BUCKET/$2" --file "$1" \
--content-type "$3" --cache-control "$4" --remote
}
# already_uploaded <key> — true if the object is already served at FEED_BASE. DMGs are immutable
# and content-addressed by version, so once a key exists its bytes never change; a cheap HEAD
# (no body transfer) against the public host lets us skip re-PUTting it. Requires curl; if curl is
# absent or NUCLEIC_R2_FORCE=1, we don't skip (fall back to uploading).
already_uploaded() { # <key>
[ "${NUCLEIC_R2_FORCE:-0}" = "1" ] && return 1
command -v curl >/dev/null 2>&1 || return 1
curl -fsI -o /dev/null "$FEED_BASE/$1"
}
echo "▸ Uploading $CHANNEL → r2://$BUCKET"
# DMGs go under the channel folder (key "<channel>/<file>.dmg") for tidy, predictable download
# links — updates.nucleic.blakeslee.xyz/<channel>/<file>.dmg — matching the enclosure URLs
# generate-appcast.sh wrote (--download-url-prefix "$FEED_BASE/$CHANNEL/"). They're
# content-addressed by version → cache forever, so prior releases already in the bucket are skipped
# (they never change): only genuinely new DMGs upload, sparing egress + ops on every re-release.
for dmg in "${dmgs[@]}"; do
key="$CHANNEL/$(basename "$dmg")"
if already_uploaded "$key"; then
echo " = $key (already uploaded — skipping)"
continue
fi
put "$dmg" "$key" "application/octet-stream" "public, max-age=31536000, immutable"
done
# The appcast (the Sparkle feed) stays at the bucket root, where each build's baked-in SUFeedURL
# points it. It changes each release → short cache so updates surface quickly.
put "$APPCAST" "appcast-$CHANNEL.xml" "application/xml" "public, max-age=300"
echo "✓ Uploaded $CHANNEL to r2://$BUCKET"
echo " Verify: curl -I $FEED_BASE/appcast-$CHANNEL.xml"
echo " curl -I $FEED_BASE/$CHANNEL/$(basename "${dmgs[0]}")"
echo " curl -I $FEED_BASE/$CHANNEL/latest # → 302 to the newest DMG"