First entry in the image catalog. Verified end-to-end: - https://images.thaloco.com/debian-bookworm-x86_64.tar.zst reachable - sha256 071495e6... matches - bundle unpacks to rootfs.ext4 (4 GiB) + manifest.json with the expected name/distro/arch/kernel_ref. publish-golden-image.sh tweaks: - default RCLONE_REMOTE from 'r2' to 'banger-images' (matches the rclone config actually in use here). - rclone copyto now passes --s3-no-check-bucket and --no-check-dest so scoped R2 tokens without HeadBucket/HeadObject permission still upload cleanly. To use: restart bangerd so it picks up the new embedded catalog, then `banger image pull debian-bookworm`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
4.6 KiB
Bash
Executable file
150 lines
4.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# publish-golden-image.sh
|
|
#
|
|
# Build the banger golden-image bundle, upload it to R2, and patch
|
|
# internal/imagecat/catalog.json with the resulting URL + sha256 +
|
|
# size. Mirrors publish-kernel.sh for kernelcat.
|
|
#
|
|
# Usage:
|
|
# scripts/publish-golden-image.sh [--name <n>] [--kernel-ref <k>] \
|
|
# [--distro <d>] [--arch <a>] [--description "..."] \
|
|
# [--size <spec>] [--platform <p>] [--skip-upload]
|
|
#
|
|
# Environment overrides:
|
|
# RCLONE_REMOTE rclone remote to upload through (default: banger-images)
|
|
# RCLONE_BUCKET R2 bucket name (default: banger-images)
|
|
# BASE_URL public URL prefix for the bucket (default: https://images.thaloco.com)
|
|
|
|
set -euo pipefail
|
|
|
|
log() { printf '[publish-golden-image] %s\n' "$*" >&2; }
|
|
die() { log "$*"; exit 1; }
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
CATALOG_FILE="$REPO_ROOT/internal/imagecat/catalog.json"
|
|
|
|
RCLONE_REMOTE="${RCLONE_REMOTE:-banger-images}"
|
|
RCLONE_BUCKET="${RCLONE_BUCKET:-banger-images}"
|
|
BASE_URL="${BASE_URL:-https://images.thaloco.com}"
|
|
|
|
NAME="debian-bookworm"
|
|
KERNEL_REF="generic-6.12"
|
|
DISTRO="debian"
|
|
ARCH="x86_64"
|
|
DESCRIPTION=""
|
|
SIZE=""
|
|
PLATFORM="linux/amd64"
|
|
SKIP_UPLOAD=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) NAME="${2:-}"; shift 2;;
|
|
--kernel-ref) KERNEL_REF="${2:-}"; shift 2;;
|
|
--distro) DISTRO="${2:-}"; shift 2;;
|
|
--arch) ARCH="${2:-}"; shift 2;;
|
|
-d|--description) DESCRIPTION="${2:-}"; shift 2;;
|
|
--size) SIZE="${2:-}"; shift 2;;
|
|
--platform) PLATFORM="${2:-}"; shift 2;;
|
|
--skip-upload) SKIP_UPLOAD=1; shift;;
|
|
-h|--help)
|
|
sed -n '2,/^set -euo/p' "$0" | sed 's/^# \?//' | sed '$d'
|
|
exit 0
|
|
;;
|
|
*) die "unknown option: $1";;
|
|
esac
|
|
done
|
|
|
|
for tool in jq sha256sum stat; do
|
|
command -v "$tool" >/dev/null 2>&1 || die "missing required tool: $tool"
|
|
done
|
|
[[ -f "$CATALOG_FILE" ]] || die "catalog file not found: $CATALOG_FILE"
|
|
if [[ "$SKIP_UPLOAD" -ne 1 ]]; then
|
|
for tool in rclone curl; do
|
|
command -v "$tool" >/dev/null 2>&1 || die "missing required tool: $tool"
|
|
done
|
|
fi
|
|
|
|
TARBALL_NAME="${NAME}-${ARCH}.tar.zst"
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
OUT="$STAGE/$TARBALL_NAME"
|
|
|
|
log "building bundle via make-golden-bundle.sh"
|
|
SIZE_FLAG=()
|
|
[[ -n "$SIZE" ]] && SIZE_FLAG=(--size "$SIZE")
|
|
"$SCRIPT_DIR/make-golden-bundle.sh" \
|
|
--name "$NAME" \
|
|
--kernel-ref "$KERNEL_REF" \
|
|
--distro "$DISTRO" \
|
|
--arch "$ARCH" \
|
|
--description "$DESCRIPTION" \
|
|
--platform "$PLATFORM" \
|
|
"${SIZE_FLAG[@]}" \
|
|
--out "$OUT"
|
|
|
|
SHA256="$(sha256sum "$OUT" | awk '{print $1}')"
|
|
SIZE_BYTES="$(stat -c '%s' "$OUT")"
|
|
HUMAN="$(numfmt --to=iec --suffix=B "$SIZE_BYTES" 2>/dev/null || echo "${SIZE_BYTES}B")"
|
|
log "bundle ready: $TARBALL_NAME ($HUMAN, sha256 $SHA256)"
|
|
|
|
if [[ "$SKIP_UPLOAD" -eq 1 ]]; then
|
|
KEEP="$REPO_ROOT/dist/$TARBALL_NAME"
|
|
mkdir -p "$(dirname "$KEEP")"
|
|
cp -f "$OUT" "$KEEP"
|
|
log "--skip-upload set; catalog not patched"
|
|
log "bundle kept at: $KEEP"
|
|
exit 0
|
|
fi
|
|
|
|
log "uploading to $RCLONE_REMOTE:$RCLONE_BUCKET/$TARBALL_NAME"
|
|
# --s3-no-check-bucket skips the HeadBucket preflight; --no-check-dest
|
|
# skips the HeadObject preflight. Both fail with 403 on R2 tokens that
|
|
# only have PutObject + GetObject but not Head* — a common scoped-token
|
|
# setup.
|
|
rclone copyto \
|
|
--s3-no-check-bucket \
|
|
--no-check-dest \
|
|
"$OUT" "$RCLONE_REMOTE:$RCLONE_BUCKET/$TARBALL_NAME"
|
|
|
|
URL="$BASE_URL/$TARBALL_NAME"
|
|
log "verifying $URL is reachable"
|
|
HEAD_STATUS="$(curl -fsSI -o /dev/null -w '%{http_code}' "$URL" || true)"
|
|
if [[ "$HEAD_STATUS" != "200" ]]; then
|
|
die "uploaded tarball is not publicly reachable at $URL (HTTP $HEAD_STATUS); check bucket public-access config"
|
|
fi
|
|
|
|
log "patching $CATALOG_FILE"
|
|
NEW_ENTRY="$(jq -n \
|
|
--arg name "$NAME" \
|
|
--arg distro "$DISTRO" \
|
|
--arg arch "$ARCH" \
|
|
--arg kref "$KERNEL_REF" \
|
|
--arg url "$URL" \
|
|
--arg sha "$SHA256" \
|
|
--argjson size "$SIZE_BYTES" \
|
|
--arg desc "$DESCRIPTION" \
|
|
'{
|
|
name: $name,
|
|
distro: $distro,
|
|
arch: $arch,
|
|
kernel_ref: $kref,
|
|
tarball_url: $url,
|
|
tarball_sha256: $sha,
|
|
size_bytes: $size,
|
|
description: $desc
|
|
} | with_entries(select(.value != null and .value != ""))')"
|
|
|
|
CATALOG_TMP="$(mktemp)"
|
|
jq --arg name "$NAME" --argjson new "$NEW_ENTRY" '
|
|
.version = (.version // 1)
|
|
| .entries = (((.entries // []) | map(select(.name != $name))) + [$new])
|
|
| .entries |= sort_by(.name)
|
|
' "$CATALOG_FILE" > "$CATALOG_TMP"
|
|
mv "$CATALOG_TMP" "$CATALOG_FILE"
|
|
|
|
log "done"
|
|
log "next steps:"
|
|
log " git diff -- $CATALOG_FILE"
|
|
log " git add $CATALOG_FILE && git commit -m 'imagecat: publish $NAME'"
|
|
log " make build # rebuild banger so the new catalog is embedded"
|