name: Publish System AMIs # Builds the system AMIs (libguestfs, on the runner) and publishes each # compressed qcow2 + .sha256 sidecar to the Cloudflare R2 bucket that backs # iso.mulgadc.com/system-ami/. Six images ship: # spinifex-eks-node Alpine EKS K3s node (server|agent, role at boot) # spinifex-ecs-node Alpine ECS container instance # spinifex-eks-node-gpu Ubuntu EKS K3s GPU worker (NVIDIA driver + toolkit) # spinifex-ecs-node-gpu Ubuntu ECS GPU container instance # spinifex-rds-postgres Alpine RDS PostgreSQL instance # spinifex-rds-mariadb Alpine RDS MariaDB instance # Operators then install them on any cluster with: # spx admin images import --name spinifex-eks-node # spx admin images import --name spinifex-ecs-node # spx admin images import --name spinifex-eks-node-gpu # spx admin images import --name spinifex-ecs-node-gpu # spx admin images import --name spinifex-rds-postgres # spx admin images import --name spinifex-rds-mariadb # # Publishing overwrites the mirror artifact, so it runs only from release.yml # once a tag has passed its gates, with siblings at the tags go.mod requires. # The `variant` input scopes which images build — the Ubuntu GPU legs are 16G # and much slower (NVIDIA driver download + DKMS build). on: workflow_call: inputs: variant: description: "Which images to build (all|cpu|gpu|rds)" type: string default: all dry_run: description: "Build + convert only; skip the R2 upload" type: boolean default: false permissions: contents: read concurrency: group: publish-eks-ami cancel-in-progress: false jobs: matrix: name: Resolve image matrix runs-on: [self-hosted, ci-single] outputs: images: ${{ steps.gen.outputs.images }} steps: - name: Select images for variant id: gen env: VARIANT: ${{ inputs.variant }} run: | set -euo pipefail cpu='{"name":"spinifex-eks-node","manifest":"scripts/images/eks-node/manifest.conf"},{"name":"spinifex-ecs-node","manifest":"scripts/images/ecs-agent/manifest.conf"}' gpu='{"name":"spinifex-eks-node-gpu","manifest":"scripts/images/eks-node-gpu/manifest.conf"},{"name":"spinifex-ecs-node-gpu","manifest":"scripts/images/ecs-agent-gpu/manifest.conf"}' rds='{"name":"spinifex-rds-postgres","manifest":"scripts/images/rds-postgres/manifest.conf"},{"name":"spinifex-rds-mariadb","manifest":"scripts/images/rds-mariadb/manifest.conf"}' case "${VARIANT}" in cpu) items="${cpu}" ;; gpu) items="${gpu}" ;; rds) items="${rds}" ;; *) items="${cpu},${gpu},${rds}" ;; esac echo "images={\"include\":[${items}]}" >> "$GITHUB_OUTPUT" publish: name: Build + publish ${{ matrix.name }} needs: matrix runs-on: [self-hosted, ci-single] strategy: fail-fast: false # ci-single is one runner and each GPU build needs ~16G of scratch disk; # serialise the legs so they never contend for /_temp or the mirror. max-parallel: 1 matrix: ${{ fromJSON(needs.matrix.outputs.images) }} steps: - name: Checkout Spinifex uses: actions/checkout@v7.0.1 with: path: spinifex - name: Resolve sibling release tags from go.mod id: siblings run: bash spinifex/.github/resolve-release-sibling-refs.sh spinifex/go.mod - name: Checkout Viperblock uses: actions/checkout@v7.0.1 with: repository: mulgadc/viperblock ref: ${{ steps.siblings.outputs.viperblock }} path: viperblock - name: Checkout Predastore uses: actions/checkout@v7.0.1 with: repository: mulgadc/predastore ref: ${{ steps.siblings.outputs.predastore }} path: predastore - name: Checkout Northstar uses: actions/checkout@v7.0.1 with: repository: mulgadc/northstar ref: ${{ steps.siblings.outputs.northstar }} path: northstar - name: Checkout Bluebottle uses: actions/checkout@v7.0.1 with: repository: mulgadc/bluebottle ref: ${{ steps.siblings.outputs.bluebottle }} path: bluebottle - name: Setup Go uses: actions/setup-go@v7.0.0 with: go-version-file: "spinifex/go.mod" # Self-hosted runner: the Go module + build cache already persists on # local disk between runs. cache: false - name: Setup Go Workspace # Idempotent: the ci-single runner reuses one workspace across matrix # legs and runs, and checkout never scrubs the root go.work. Remove any # stale file so each leg re-inits cleanly instead of failing on # "go.work already exists". run: | rm -f go.work go.work.sum go work init ./spinifex ./viperblock ./predastore ./northstar ./bluebottle - name: Install build + upload tooling run: | set -euo pipefail sudo apt-get update -qq sudo apt-get install -y --no-install-recommends libguestfs-tools qemu-utils if ! command -v aws &>/dev/null; then curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o /tmp/awscliv2.zip unzip -q /tmp/awscliv2.zip -d /tmp/aws-install sudo /tmp/aws-install/aws/install rm -rf /tmp/awscliv2.zip /tmp/aws-install fi - name: Build + publish ${{ matrix.name }} to R2 working-directory: spinifex env: AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_SECRET_ACCESS_KEY }} R2_ENDPOINT: ${{ secrets.CF_R2_ENDPOINT_URL }} R2_BUCKET: spinifex-iso DRY_RUN: ${{ inputs.dry_run }} MANIFEST: ${{ matrix.manifest }} # Ubuntu GPU images are 16G — a RAM-backed /tmp cannot hold the qcow2 + # expanded raw, so point the build scratch at the runner's on-disk # _temp. Harmless for the small Alpine legs too. SYSTEM_IMAGE_BUILD_DIR: ${{ runner.temp }}/system-image-build run: | set -euo pipefail # libguestfs needs to read a host kernel; Ubuntu ships it mode 0600. # direct backend = no libvirt on the runner. export LIBGUESTFS_BACKEND=direct sudo chmod 0644 /boot/vmlinuz-* || true mkdir -p "${SYSTEM_IMAGE_BUILD_DIR}" FLAGS=(--build) if [[ "${DRY_RUN}" == "true" ]]; then FLAGS+=(--dry-run) fi ./scripts/publish-system-image.sh "${MANIFEST}" "${FLAGS[@]}"