Files
MusicBot/scripts/build-and-publish-unraid.sh
Miles WardandClaude Fable 5.1 6fa37315d8
Build and Test / Build and Test (push) Successful in 1m12s
publish: force gzip on every layer so no blob exceeds Cloudflare's 100 MB
bitnami/minideb ships its base layer as an uncompressed 133 MB tar; with
the containerd image store docker push sends it as-is and Cloudflare in
front of Gitea answers 413. Build with force-compression=true (base layer
becomes ~43 MB) and document why.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-09-17 19:27:05 -04:00

145 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# build-and-publish-unraid.sh — build the $uicide Music Bot image and push it
# to the Gitea container registry, ready to pull from an Unraid template.
#
# Run this on any host that can reach Gitea and has Docker. It clones a fresh
# copy rather than building the working tree, so what ships is exactly what is
# committed.
#
# Inputs (env vars, all optional except GITEA_TOKEN):
# GITEA_TOKEN personal access token with `write:package` scope
# GITEA_HOST default: gitea.milesward.dev
# GITEA_OWNER default: mward4
# GITEA_REPO default: MusicBot
# IMAGE_NAME default: musicbot
# IMAGE_TAG default: 0.7.1 (also pushed as :latest)
# PLATFORM default: linux/amd64 (Unraid is x86_64)
# GIT_REF default: v0.7.1 (tag/branch to build)
# WORKDIR default: /tmp/musicbot-build (removed on exit)
#
# After this, set the Unraid template's Repository to:
# <GITEA_HOST>/<owner>/musicbot:<tag> (or :latest)
set -euo pipefail
GITEA_HOST=${GITEA_HOST:-gitea.milesward.dev}
GITEA_OWNER=${GITEA_OWNER:-mward4}
GITEA_REPO=${GITEA_REPO:-MusicBot}
IMAGE_NAME=${IMAGE_NAME:-musicbot}
IMAGE_TAG=${IMAGE_TAG:-0.7.1}
PLATFORM=${PLATFORM:-linux/amd64}
GIT_REF=${GIT_REF:-v0.7.1}
WORKDIR=${WORKDIR:-/tmp/musicbot-build}
# OCI distribution rejects uppercase repository paths, so the image name is
# lowercased independently of the repo name.
IMAGE_NAME="$(printf '%s' "$IMAGE_NAME" | tr '[:upper:]' '[:lower:]')"
FULL_TAG="$GITEA_HOST/$GITEA_OWNER/$IMAGE_NAME:$IMAGE_TAG"
LATEST_TAG="$GITEA_HOST/$GITEA_OWNER/$IMAGE_NAME:latest"
if [[ -z "${GITEA_TOKEN:-}" ]]; then
echo "ERROR: set GITEA_TOKEN to a token with write:package scope." >&2
echo " Gitea -> Settings -> Applications -> Generate New Token." >&2
exit 2
fi
for tool in git docker; do
command -v "$tool" >/dev/null 2>&1 \
|| { echo "ERROR: '$tool' not found in PATH" >&2; exit 2; }
done
# A private docker config so the token never lands in ~/.docker/config.json.
# The trap wipes it (and the checkout) on any exit path.
#
# The auth is written into the file directly rather than via `docker login`.
# With an empty config the CLI falls back to the platform credential helper,
# and on macOS `docker-credential-osxkeychain` shares its keychain item with
# git's `osxkeychain` helper: `login` then fails with -25299 if git already
# stored the token, and `logout` deletes the credential git was using. A
# config that already contains an `auths` entry skips the helper entirely.
DOCKER_CONFIG="$(mktemp -d)"
export DOCKER_CONFIG
trap 'rm -rf "$DOCKER_CONFIG" "$WORKDIR"' EXIT
# CLI plugins (buildx, needed for --platform/--provenance) are looked up under
# $DOCKER_CONFIG/cli-plugins, so the private config would otherwise fall back
# to the legacy builder. Point it at the user's plugins and at Homebrew's.
if [[ -d "$HOME/.docker/cli-plugins" ]]; then
ln -s "$HOME/.docker/cli-plugins" "$DOCKER_CONFIG/cli-plugins"
fi
for extra in /opt/homebrew/lib/docker/cli-plugins /usr/local/lib/docker/cli-plugins; do
if [[ -d "$extra" ]]; then
printf '{"cliPluginsExtraDirs":["%s"]}' "$extra" > "$DOCKER_CONFIG/config.json"
break
fi
done
# Colima keeps its socket outside /var/run; an isolated DOCKER_CONFIG has no
# `currentContext`, so point at the daemon explicitly when one is not set.
if [[ -z "${DOCKER_HOST:-}" && -S "$HOME/.colima/default/docker.sock" ]]; then
export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"
fi
docker buildx version >/dev/null 2>&1 \
|| { echo "ERROR: docker buildx plugin not found (needed for --platform builds)." >&2; exit 2; }
echo
echo "==> Workspace : $WORKDIR"
echo "==> Gitea host : $GITEA_HOST"
echo "==> Building ref : $GIT_REF"
echo "==> Image to push : $FULL_TAG"
echo "==> Platform : $PLATFORM"
echo
# Step 1: shallow clone of the exact ref.
rm -rf "$WORKDIR"
git -c "http.extraHeader=Authorization: token $GITEA_TOKEN" \
clone --depth 1 --branch "$GIT_REF" \
"https://$GITEA_HOST/$GITEA_OWNER/$GITEA_REPO.git" "$WORKDIR"
cd "$WORKDIR"
GIT_SHA="$(git rev-parse HEAD)"
COMMIT_TIME="$(git log -1 --format=%cI)"
# Step 2: build. --pull so a stale cached base image doesn't bake in a CVE
# that upstream has already fixed. --provenance/--sbom off because Unraid's
# Docker cannot load the attestation manifests BuildKit attaches by default.
#
# The Maven stage runs on the builder's native architecture (the JAR is
# architecture-neutral); only the jlink and runtime stages run under emulation
# when cross-building linux/amd64 on an arm64 host. BUILD_TIMESTAMP pins the
# JAR's reproducible-build timestamp to the commit, as upstream's CI does.
#
# force-compression matters: bitnami/minideb publishes its base layer as an
# UNCOMPRESSED 133 MB tar, a daemon with the containerd image store pushes
# blobs exactly as stored, and Gitea sits behind Cloudflare, which rejects any
# request body over 100 MB with a 413. Recompressing every layer with gzip
# brings that layer to ~43 MB; no layer of this image is then over 100 MB.
docker buildx build \
--pull \
--platform "$PLATFORM" \
--provenance=false --sbom=false \
--build-arg "GIT_SHA=$GIT_SHA" \
--build-arg "VERSION=$IMAGE_TAG" \
--build-arg "BUILD_TIMESTAMP=$COMMIT_TIME" \
--tag "$FULL_TAG" \
--tag "$LATEST_TAG" \
--output type=docker,compression=gzip,force-compression=true \
.
# Step 3: registry auth, written straight into the private config (see above).
EXTRA_DIRS="$(sed -n 's/.*"cliPluginsExtraDirs":\(\[[^]]*\]\).*/\1/p' "$DOCKER_CONFIG/config.json" 2>/dev/null || true)"
printf '{"cliPluginsExtraDirs":%s,"auths":{"%s":{"auth":"%s"}}}' "${EXTRA_DIRS:-[]}" "$GITEA_HOST" \
"$(printf '%s:%s' "$GITEA_OWNER" "$GITEA_TOKEN" | base64 | tr -d '\n')" \
> "$DOCKER_CONFIG/config.json"
# Step 4: push both tags. The trap removes the config, so there is no logout.
docker push "$FULL_TAG"
docker push "$LATEST_TAG"
echo
echo "==> Done. Pull from an Unraid template with:"
echo " $FULL_TAG"
echo " $LATEST_TAG"
echo