This image is a TOOLCHAIN, so "it built" is not the property worth checking — "it still contains what the repo's gates shell out to" is. It exists because the hand-built image had drifted from this Dockerfile: the deployed ci3 carried gcc, musl-dev, python3 and py3-yaml, the Dockerfile did not, and rebuilding from the Dockerfile silently dropped them. Gitea CI then failed at "credentials-coverage: need python3" without reaching the tests at all. Nothing in the pipeline objected, because every check asked whether the image built, not what was in it. Asserts python3 with a working yaml import, gcc, node, bash, make, curl, tar and git-http-backend, on BOTH architectures — a package that resolves on amd64 but not arm64 would otherwise reach half the nodes unnoticed. Verified the check can actually FAIL, which is the only property that makes it worth having: against the bare forgejo/runner:12.11.1 it reports python3+yaml, gcc, node and git-http-backend all MISSING; with the packages installed it reports 3.12.14 and ok for the rest.
138 lines
6.1 KiB
YAML
138 lines
6.1 KiB
YAML
name: build image
|
|
|
|
# Publishes the runner toolchain image to ghcr.io.
|
|
#
|
|
# Why GitHub rather than our own Gitea Actions: the runners are host mode
|
|
# (`ubuntu-latest:host`, no job containers) and their container drops ALL
|
|
# capabilities with allowPrivilegeEscalation:false. Rootless buildah needs
|
|
# newuidmap/user namespaces for its RUN steps, so building images there would
|
|
# mean weakening the posture that hardening established. GitHub runners have
|
|
# docker + buildx + QEMU already.
|
|
#
|
|
# Why ghcr.io rather than the Gitea registry: gitea.langenoja.se sits behind
|
|
# Cloudflare, whose request-body cap returns 413 on a registry layer. The
|
|
# cluster already pulls zegit's own images from ghcr.io and the runner
|
|
# deployments already carry a github-registry-credentials pull secret, so this
|
|
# reuses an established, trusted path instead of inventing one.
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
tag:
|
|
description: 'Image tag, e.g. 12.11.1-ci4 (upstream runner version + ci revision)'
|
|
required: true
|
|
type: string
|
|
push:
|
|
branches: [master]
|
|
paths:
|
|
- 'Dockerfile'
|
|
- '.github/workflows/build-image.yml'
|
|
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
env:
|
|
IMAGE: ghcr.io/${{ github.repository }}
|
|
|
|
jobs:
|
|
build:
|
|
# GitHub-only: the Gitea mirror has no ghcr.io and no GITHUB_TOKEN.
|
|
if: ${{ github.server_url == 'https://github.com' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
steps:
|
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
|
|
# arm64 is not optional: forgejo-runner runs on arm64 nodes and
|
|
# zegit-runner on amd64, neither deployment sets a nodeSelector, and both
|
|
# use this one tag.
|
|
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
|
|
|
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# A master push VALIDATES the Dockerfile still builds on both arches but
|
|
# publishes nothing — only an explicit dispatch names a tag and pushes.
|
|
# Guessing a tag from a branch push would silently mint versions.
|
|
- id: meta
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
echo "push=true" >> "$GITHUB_OUTPUT"
|
|
echo "tags=${IMAGE}:${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "push=false" >> "$GITHUB_OUTPUT"
|
|
echo "tags=${IMAGE}:ci-validation" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
|
with:
|
|
context: .
|
|
platforms: linux/amd64,linux/arm64
|
|
push: ${{ steps.meta.outputs.push }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
provenance: false
|
|
sbom: false
|
|
|
|
# Read the published index back rather than trusting the push's exit
|
|
# code, and fail if either architecture is absent — a single-arch image
|
|
# under this tag would CrashLoop on half the nodes.
|
|
- name: Verify both architectures were published
|
|
if: ${{ steps.meta.outputs.push == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
out=$(docker buildx imagetools inspect "${IMAGE}:${{ inputs.tag }}")
|
|
echo "$out"
|
|
for want in amd64 arm64; do
|
|
echo "$out" | grep -q "linux/$want" || { echo "::error::published image is missing linux/$want"; exit 1; }
|
|
done
|
|
echo "✓ ${IMAGE}:${{ inputs.tag }} carries linux/amd64 and linux/arm64"
|
|
|
|
# The image is a TOOLCHAIN, so "it built" is not the property that matters
|
|
# — "it still contains what the gates shell out to" is. This exists because
|
|
# a hand-built image had drifted from this Dockerfile: the deployed ci3
|
|
# carried gcc, musl-dev, python3 and py3-yaml, the Dockerfile did not, and
|
|
# rebuilding from the Dockerfile silently dropped them. CI runs then failed
|
|
# at "credentials-coverage: need python3" instead of running the tests.
|
|
#
|
|
# Asserted on BOTH architectures, because a package that resolves on amd64
|
|
# and not on arm64 would otherwise reach half the nodes unnoticed.
|
|
- name: Verify the image still contains the toolchain the gates need
|
|
if: ${{ steps.meta.outputs.push == 'true' }}
|
|
run: |
|
|
set -euo pipefail
|
|
REF="${IMAGE}:${{ inputs.tag }}"
|
|
fail=0
|
|
for plat in linux/amd64 linux/arm64; do
|
|
echo "── $plat"
|
|
out=$(docker run --rm --platform "$plat" --entrypoint sh "$REF" -c '
|
|
set -u
|
|
py=$(python3 -c "import yaml,sys;print(sys.version.split()[0])" 2>/dev/null) \
|
|
&& echo "python3+yaml=$py" || echo "python3+yaml=MISSING"
|
|
command -v gcc >/dev/null && echo "gcc=ok" || echo "gcc=MISSING"
|
|
command -v node >/dev/null && echo "node=ok" || echo "node=MISSING"
|
|
command -v bash >/dev/null && echo "bash=ok" || echo "bash=MISSING"
|
|
command -v make >/dev/null && echo "make=ok" || echo "make=MISSING"
|
|
command -v curl >/dev/null && echo "curl=ok" || echo "curl=MISSING"
|
|
command -v tar >/dev/null && echo "tar=ok" || echo "tar=MISSING"
|
|
[ -x "$(git --exec-path)/git-http-backend" ] \
|
|
&& echo "git-http-backend=ok" || echo "git-http-backend=MISSING"
|
|
' 2>&1) || { echo "::error::could not run $REF on $plat"; fail=1; continue; }
|
|
echo "$out"
|
|
if echo "$out" | grep -q MISSING; then
|
|
echo "$out" | grep MISSING | while read -r l; do
|
|
echo "::error::$plat is missing a required tool: $l"
|
|
done
|
|
fail=1
|
|
fi
|
|
done
|
|
[ "$fail" = 0 ] || {
|
|
echo "::error::The published image lost a tool the repo's gates depend on. Restore it in the Dockerfile — do not weaken this check."
|
|
exit 1
|
|
}
|
|
echo "✓ toolchain intact on both architectures"
|