#!/bin/bash # push-oci-artifact.sh — Publish a KubeSolo OS update artifact to an OCI registry. # # Produces the artifact format consumed by `kubesolo-update --registry`: # # /:- per-arch manifest, layers: # * vmlinuz (Image on arm64) → application/vnd.kubesolo.os.kernel.v1+octet-stream # * kubesolo-os.gz → application/vnd.kubesolo.os.initramfs.v1+gzip # annotations: # io.kubesolo.os.version # io.kubesolo.os.channel # io.kubesolo.os.architecture # io.kubesolo.os.min_compatible_version (optional) # # After running this for each architecture, combine the per-arch tags into a # multi-arch index with `oras manifest index create` (see end of script). # # Requires: oras (>= 1.2), curl, jq. # # Usage: # build/scripts/push-oci-artifact.sh \ # --registry ghcr.io/portainer/kubesolo-os \ # --arch amd64 \ # --channel stable \ # [--min-compatible-version v0.2.0] # # Authentication: oras reads ~/.docker/config.json. In CI, run # `oras login ghcr.io -u USER -p TOKEN` before invoking this script # (or set DOCKER_CONFIG to a directory with config.json). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" VERSION="$(cat "$PROJECT_ROOT/VERSION")" OUTPUT_DIR="$PROJECT_ROOT/output" CACHE_DIR="$PROJECT_ROOT/build/cache" REGISTRY="" ARCH="" CHANNEL="stable" MIN_COMPATIBLE_VERSION="" RELEASE_NOTES="" while [ $# -gt 0 ]; do case "$1" in --registry) REGISTRY="$2"; shift 2 ;; --arch) ARCH="$2"; shift 2 ;; --channel) CHANNEL="$2"; shift 2 ;; --min-compatible-version) MIN_COMPATIBLE_VERSION="$2"; shift 2 ;; --release-notes) RELEASE_NOTES="$2"; shift 2 ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac done if [ -z "$REGISTRY" ] || [ -z "$ARCH" ]; then echo "Usage: $0 --registry REGISTRY/REPO --arch (amd64|arm64) [--channel stable] [--min-compatible-version vX.Y.Z]" >&2 exit 1 fi if ! command -v oras >/dev/null 2>&1; then echo "ERROR: oras CLI not found. Install from https://oras.land/docs/installation/" >&2 echo " or apt-get install oras (Ubuntu 24.04+)" >&2 exit 1 fi # Locate the artifacts. For arm64 the kernel is "Image"; everywhere else it's # "vmlinuz". Initramfs is always kubesolo-os.gz. case "$ARCH" in amd64) KERNEL="$CACHE_DIR/custom-kernel/vmlinuz" [ -f "$KERNEL" ] || KERNEL="$OUTPUT_DIR/vmlinuz" KERNEL_BASENAME="vmlinuz" ;; arm64) KERNEL="$CACHE_DIR/kernel-arm64-generic/Image" KERNEL_BASENAME="vmlinuz" # we publish under the vmlinuz name regardless; # the consumer looks up by media type, not filename. ;; *) echo "ERROR: unsupported --arch $ARCH (use amd64 or arm64)" >&2 exit 1 ;; esac INITRAMFS="$PROJECT_ROOT/build/rootfs-work/kubesolo-os.gz" if [ ! -f "$KERNEL" ]; then echo "ERROR: kernel not found at $KERNEL" >&2 echo " Run 'make kernel' (amd64) or 'make kernel-arm64' (arm64) first." >&2 exit 1 fi if [ ! -f "$INITRAMFS" ]; then echo "ERROR: initramfs not found at $INITRAMFS" >&2 echo " Run 'make initramfs' or 'make rootfs-arm64' first." >&2 exit 1 fi # Stage files in a temp dir so the basenames in the manifest are clean. STAGE="$(mktemp -d)" trap 'rm -rf "$STAGE"' EXIT cp "$KERNEL" "$STAGE/$KERNEL_BASENAME" cp "$INITRAMFS" "$STAGE/kubesolo-os.gz" KERNEL_MEDIA="application/vnd.kubesolo.os.kernel.v1+octet-stream" INITRD_MEDIA="application/vnd.kubesolo.os.initramfs.v1+gzip" REF="${REGISTRY}:${VERSION}-${ARCH}" CHANNEL_REF="${REGISTRY}:${CHANNEL}-${ARCH}" echo "==> Pushing ${REF}" echo " kernel: $KERNEL ($(du -h "$KERNEL" | cut -f1))" echo " initramfs: $INITRAMFS ($(du -h "$INITRAMFS" | cut -f1))" ORAS_ANNOTATIONS=( --annotation "io.kubesolo.os.version=${VERSION}" --annotation "io.kubesolo.os.channel=${CHANNEL}" --annotation "io.kubesolo.os.architecture=${ARCH}" ) if [ -n "$MIN_COMPATIBLE_VERSION" ]; then ORAS_ANNOTATIONS+=(--annotation "io.kubesolo.os.min_compatible_version=${MIN_COMPATIBLE_VERSION}") fi if [ -n "$RELEASE_NOTES" ]; then ORAS_ANNOTATIONS+=(--annotation "io.kubesolo.os.release_notes=${RELEASE_NOTES}") fi ORAS_ANNOTATIONS+=(--annotation "io.kubesolo.os.release_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)") # oras push: --artifact-type sets the manifest artifactType field; # file:type syntax sets per-layer media types. (cd "$STAGE" && oras push "$REF" \ --artifact-type "application/vnd.kubesolo.os.update.v1+json" \ "${ORAS_ANNOTATIONS[@]}" \ "${KERNEL_BASENAME}:${KERNEL_MEDIA}" \ "kubesolo-os.gz:${INITRD_MEDIA}") # Also tag as - so the manifest-index step can reference it # stably across patch releases. echo "==> Tagging ${CHANNEL_REF}" oras tag "$REF" "${CHANNEL}-${ARCH}" echo "" echo "==> Published:" echo " ${REF}" echo " ${CHANNEL_REF}" echo "" echo "To combine multi-arch into the channel index, run after both arches are pushed:" echo "" echo " oras manifest index create ${REGISTRY}:${CHANNEL} \\" echo " ${REGISTRY}:${CHANNEL}-amd64,platform=linux/amd64 \\" echo " ${REGISTRY}:${CHANNEL}-arm64,platform=linux/arm64" echo ""