Skip to content

Release s390x

Release s390x #45

# Hardened QEMU s390x release workflow for the image-build family.
# Build the s390x leg under QEMU on ubuntu-latest, then combine it with the upstream amd64/arm64
# legs (pinned BY DIGEST) into one published manifest. workflow_dispatch-only.
#
# Per-fork: this file is committed to the fork's DEFAULT branch (so workflow_dispatch resolves — C2)
# under the fork's own filename (base=release-s390x.yaml, coredns=release-s390x.yml). IMAGE_NAME /
# UPSTREAM_IMAGE below must be set to the fork's image when aligning.
#
# Hardening vs the original:
# C3 manifest legs are DETECTED from the upstream manifest list, not hardcoded amd64+arm64
# H2 upstream legs pulled BY DIGEST and the digest is echoed for the verifier to assert
# M3 Go build-arg derivation FAILS CLOSED if the tag doesn't match vX.Y.ZbN
# M4 the combined tag is treated as immutable — overwrite requires force=true
# M5 workflow_dispatch-only (no push:tags double-fire); no GitHub Release job
name: Release s390x
on:
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g., v1.24.12b1)"
required: true
type: string
force:
description: "Overwrite the combined tag if it already exists (treat as immutable otherwise)"
required: false
default: false
type: boolean
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/hardened-build-base # rendered per-fork by apply-s390x-patch.sh
UPSTREAM_IMAGE: rancher/hardened-build-base # rendered per-fork by apply-s390x-patch.sh
jobs:
build-s390x:
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.get-tag.outputs.tag }}
go_version: ${{ steps.get-tag.outputs.go_version }}
steps:
# Build the s390x leg from the per-version build branch, NOT the dispatch ref and NOT the bare
# tag. The workflow is dispatched on the default branch (so workflow_dispatch resolves it), but
# the source must be fix/<tag>-s390x = the tag's tree (correct component/trivy versions, matching
# the upstream amd64/arm64 legs) + the injected s390x Dockerfile patch. apply-s390x-patch.sh
# creates and pushes this branch before dispatch.
- name: Checkout (s390x-patched build branch off the tag)
uses: actions/checkout@v4
with:
ref: fix/${{ inputs.tag }}-s390x
- name: Get tag + validate Go version (M3 fail-closed)
id: get-tag
env:
TAG: ${{ inputs.tag }} # via env, never inlined into the shell (injection-safe)
run: |
set -euo pipefail
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
# vX.Y.ZbN -> X.Y.Z ; refuse to pass an unmatched raw tag through as the build-arg.
GO_VERSION="$(printf '%s' "$TAG" | sed -nE 's/^v([0-9]+\.[0-9]+\.[0-9]+)b[0-9]+$/\1/p')"
if [ -z "$GO_VERSION" ]; then
echo "::error::Cannot derive Go version from tag '$TAG' (expected vX.Y.ZbN). Aborting."
exit 1
fi
echo "go_version=$GO_VERSION" >> "$GITHUB_OUTPUT"
echo "Tag: $TAG, Go: $GO_VERSION"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push s390x image
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/s390x
push: true
build-args: |
GOLANG_VERSION=${{ steps.get-tag.outputs.go_version }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.get-tag.outputs.tag }}-s390x
create-manifest:
runs-on: ubuntu-latest
needs: build-s390x
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: M4 — refuse to clobber an existing combined tag unless force
env:
TAG: ${{ needs.build-s390x.outputs.tag }}
FORCE: ${{ inputs.force }}
run: |
set -euo pipefail
IMG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG"
if docker buildx imagetools inspect "$IMG" >/dev/null 2>&1; then
if [ "$FORCE" != "true" ]; then
echo "::error::$IMG already exists and force=false (tags are immutable). Re-run with force=true to overwrite."
exit 1
fi
echo "::warning::$IMG exists; force=true → overwriting."
fi
# H2/C3/M2: combine the s390x leg with the upstream legs referenced DIRECTLY by digest.
# No pull/retag/push roundtrip — imagetools copies the digest-pinned upstream manifests into
# the target repo, so the published per-arch digests are guaranteed to match what the verifier
# recorded at preflight. Legs are detected dynamically (not hardcoded amd64+arm64).
- name: Create and push multi-arch manifest (digest-pinned legs)
env:
TAG: ${{ needs.build-s390x.outputs.tag }}
UP: ${{ env.UPSTREAM_IMAGE }}
run: |
set -euo pipefail
IMG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
RAW="$(docker buildx imagetools inspect --raw "$UP:$TAG")"
mapfile -t LEGS < <(echo "$RAW" | jq -r --arg up "$UP" '.manifests[]
| select(.platform.os=="linux" and .platform.architecture!="unknown")
| "\($up)@\(.digest)"')
if [ "${#LEGS[@]}" -eq 0 ]; then
echo "::error::no real linux upstream legs detected for $UP:$TAG — refusing to publish s390x-only"
exit 1
fi
echo "upstream legs:"; printf ' %s\n' "${LEGS[@]}"
docker buildx imagetools create -t "$IMG:$TAG" "$IMG:$TAG-s390x" "${LEGS[@]}"
echo "Created: $IMG:$TAG"
- name: Verify manifest
env:
TAG: ${{ needs.build-s390x.outputs.tag }}
run: |
set -euo pipefail
IMG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
docker buildx imagetools inspect "$IMG:$TAG"
docker buildx imagetools inspect --raw "$IMG:$TAG" \
| jq -e '[.manifests[].platform.architecture] | index("s390x") != null' >/dev/null \
|| { echo "::error::s390x leg missing from combined manifest"; exit 1; }