Files
spinifex/scripts/publish-system-image.sh
T
Josh Siviour 7bcd365549 ci(eks-ami): build on self-hosted ci-single runner (#326)
* ci(eks-ami): build on self-hosted ci-single-1 runner

GitHub-hosted ubuntu-24.04 cannot run the libguestfs appliance network:
passt fails (PID file denied under its AppArmor confine / privilege drop)
and the SLIRP fallback has no outbound, so virt-customize --network dies.
The self-hosted ci-single-1 runner has /dev/kvm and real networking, so
the existing libguestfs build works unmodified.

* fix(build-system-image): add --no-cache, force fresh build on publish

The <10min cached-raw skip avoids duplicate work for concurrent e2e
jobs, but on the persistent publish runner it could republish a stale
image from a prior or concurrent run. Add --no-cache to bypass the skip
and have publish-system-image.sh pass it on every build.

* ci(eks-ami): target ci-single label (no ci-single-1 label exists)

The self-hosted runners are named ci-single/-2/-3 but all share the
single label 'ci-single'; runs-on matches labels, not names, so
ci-single-1 never schedules.

* ci(eks-ami): disable Go cache on self-hosted runner

setup-go's cache-save (Post Setup Go) re-uploaded the Go cache to the
Actions backend on every run, taking ~2.5min vs the 36s build. The
self-hosted runner already keeps the cache on local disk, so cache:false.
2026-06-09 16:58:04 +10:00

150 lines
4.7 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# publish-system-image.sh — Publish a built system AMI to Cloudflare R2
#
# Converts the raw image produced by build-system-image.sh into a compressed
# qcow2, writes a .sha256 sidecar (coreutils format, matched by the spx catalog
# checksum verifier), and uploads both to the R2 bucket that backs
# iso.mulgadc.com/system-ami/. The matching catalog entry in
# spinifex/spinifex/utils/images.go then lets operators run:
#
# spx admin images import --name <catalog-key>
#
# Object name is <AMI_NAME or IMAGE_NAME>-<arch>.qcow2 — this MUST match the
# basename of the catalog entry's URL.
#
# Requirements: qemu-img, sha256sum, aws (CLI; R2 speaks the S3 API)
# Usage: ./scripts/publish-system-image.sh <manifest.conf> [--build] [--dry-run]
# --build Run build-system-image.sh first (otherwise reuse the cached raw)
# --dry-run Print the convert/checksum/upload steps without uploading
#
# Env (required unless --dry-run):
# R2_ENDPOINT https://<account>.r2.cloudflarestorage.com
# R2_BUCKET defaults to "spinifex-iso" (backs iso.mulgadc.com)
# AWS_ACCESS_KEY_ID R2 token access key
# AWS_SECRET_ACCESS_KEY R2 token secret
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
usage() {
echo "Usage: $0 <manifest.conf> [--build] [--dry-run]"
exit 1
}
if [[ $# -lt 1 || "$1" == "-h" || "$1" == "--help" ]]; then
usage
fi
MANIFEST="$1"
shift
if [[ ! -f "$MANIFEST" ]]; then
echo "ERROR: Manifest not found: $MANIFEST"
exit 1
fi
DO_BUILD=false
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--build) DO_BUILD=true ;;
--dry-run) DRY_RUN=true ;;
*) echo "ERROR: unknown flag: $arg"; usage ;;
esac
done
# shellcheck source=/dev/null
source "$MANIFEST"
DISTRO="${DISTRO:-alpine}"
ARCH="x86_64" # build-system-image.sh builds x86_64 only today
R2_BUCKET="${R2_BUCKET:-spinifex-iso}" # iso.mulgadc.com is served from this bucket
if [[ -z "${IMAGE_NAME:-}" ]]; then
echo "ERROR: Manifest missing required field: IMAGE_NAME"
exit 1
fi
# Mirror build-system-image.sh's derived paths so we reuse its cached raw.
BUILD_DIR="/tmp/${IMAGE_NAME}-image-build"
RAW_IMAGE="${BUILD_DIR}/${IMAGE_NAME}-${DISTRO}.raw"
OBJECT_BASE="${AMI_NAME:-$IMAGE_NAME}-${ARCH}"
QCOW2_OUT="${BUILD_DIR}/${OBJECT_BASE}.qcow2"
SHA_OUT="${QCOW2_OUT}.sha256"
OBJECT_QCOW2="${OBJECT_BASE}.qcow2"
OBJECT_SHA="${OBJECT_BASE}.qcow2.sha256"
for tool in qemu-img sha256sum; do
if ! command -v "$tool" &>/dev/null; then
echo "ERROR: $tool not found"
exit 1
fi
done
if [[ "$DRY_RUN" == false ]] && ! command -v aws &>/dev/null; then
echo "ERROR: aws CLI not found (required for R2 upload; use --dry-run to skip)"
exit 1
fi
if [[ "$DO_BUILD" == true ]]; then
echo "=== Building ${IMAGE_NAME} ==="
# --no-cache: publishing must never reuse a stale raw left by a prior or
# concurrent build on a persistent runner.
"${SCRIPT_DIR}/build-system-image.sh" "$MANIFEST" --no-cache
fi
if [[ ! -f "$RAW_IMAGE" ]]; then
echo "ERROR: Raw image not found: $RAW_IMAGE"
echo " Run with --build, or 'make build-system-image IMAGE=...' first."
exit 1
fi
if [[ "$DRY_RUN" == false ]]; then
for var in R2_ENDPOINT AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
if [[ -z "${!var:-}" ]]; then
echo "ERROR: $var must be set (or use --dry-run)"
exit 1
fi
done
fi
echo "=== Publish System Image ==="
echo " Manifest: ${MANIFEST}"
echo " Raw: ${RAW_IMAGE}"
echo " Object: s3://${R2_BUCKET}/system-ami/${OBJECT_QCOW2}"
echo ""
run() {
echo "+ $*"
if [[ "$DRY_RUN" == false ]]; then
"$@"
fi
}
# Compressed qcow2 keeps the transfer small (consistent with the GPU images).
run qemu-img convert -c -f raw -O qcow2 "$RAW_IMAGE" "$QCOW2_OUT"
# Sidecar in coreutils format: "<hex> <basename>". The spx catalog verifier
# matches the entry by the downloaded file's basename, so write the basename
# the catalog URL resolves to, not the local build path.
if [[ "$DRY_RUN" == false ]]; then
( cd "$(dirname "$QCOW2_OUT")" && sha256sum "$(basename "$QCOW2_OUT")" > "$(basename "$SHA_OUT")" )
echo " sha256: $(cut -d' ' -f1 "$SHA_OUT")"
else
echo "+ sha256sum ${OBJECT_QCOW2} > ${OBJECT_SHA}"
fi
S3_PREFIX="s3://${R2_BUCKET}/system-ami"
run aws s3 cp --endpoint-url "${R2_ENDPOINT:-<R2_ENDPOINT>}" "$QCOW2_OUT" "${S3_PREFIX}/${OBJECT_QCOW2}"
run aws s3 cp --endpoint-url "${R2_ENDPOINT:-<R2_ENDPOINT>}" "$SHA_OUT" "${S3_PREFIX}/${OBJECT_SHA}"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "=== Dry run complete (no upload) ==="
else
echo "=== Published ==="
echo " https://iso.mulgadc.com/system-ami/${OBJECT_QCOW2}"
echo " https://iso.mulgadc.com/system-ami/${OBJECT_SHA}"
fi