From f0922508a896609298001f96dae388e05677a604 Mon Sep 17 00:00:00 2001 From: agent Date: Thu, 20 Aug 2026 14:44:59 +0200 Subject: [PATCH] ci: build and publish the image on GitHub to ghcr.io MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publishing this image by hand kept hitting the same four walls — Cloudflare's 413, Docker parsing a bare MagicDNS name as a Docker Hub namespace, a PAT missing write:package, and multi-arch. push-image.sh made that survivable; this removes the manual step entirely. It builds on GitHub rather than our own Gitea Actions because the runners cannot build images: their labels are `ubuntu-latest:host` (host mode, no job containers) and the container drops ALL capabilities with allowPrivilegeEscalation:false, while rootless buildah needs newuidmap/user namespaces for RUN steps. Building there would mean weakening the posture the hardening established. It publishes to ghcr.io rather than the Gitea registry because Cloudflare's body cap is what caused the 413 in the first place, and because the cluster already pulls zegit's own images from ghcr.io through an existing github-registry-credentials pull secret — an established path rather than a new one. A master push validates that both arches still build but publishes nothing; only an explicit dispatch names a tag and pushes, so a branch push cannot silently mint a version. The final step reads the published index back and fails if either architecture is absent, since a single-arch image under this tag would CrashLoop on half the nodes. Action SHAs match the pins zegit already uses, rather than introducing a second set of versions to track. --- .github/workflows/build-image.yml | 93 +++++++++++++++++++++++++++++++ README.md | 21 +++++++ 2 files changed, 114 insertions(+) create mode 100644 .github/workflows/build-image.yml diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..543bbe0 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,93 @@ +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" diff --git a/README.md b/README.md index 9ec1f51..1948f44 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,24 @@ Two traps the script exists to prevent: pushing to the bare MagicDNS name namespace**, since a registry host is only recognised when it contains a dot or colon; and `docker push $_` pushes `.`, because `$_` is the last ARGUMENT of the build command, not its tag. + +### Automated builds + +`.github/workflows/build-image.yml` builds and publishes to +**ghcr.io/zegit-zoo/zegit-forgejo-runner** on GitHub. + +Run it from the Actions tab (**build image** → Run workflow) with the tag, e.g. +`12.11.1-ci4`. A push to `master` that touches the Dockerfile only VALIDATES +that both architectures still build — it publishes nothing, because guessing a +tag from a branch push would silently mint versions. + +It builds on GitHub rather than our own Gitea Actions because the runners are +host mode (`ubuntu-latest:host`, no job containers) and drop ALL capabilities +with `allowPrivilegeEscalation: false`; rootless buildah needs user namespaces +for its RUN steps, so building there would mean weakening that posture. It +publishes to ghcr.io rather than the Gitea registry because +`gitea.langenoja.se` sits behind Cloudflare, whose body cap returns **413** on a +registry layer — and the cluster already pulls zegit's own images from ghcr.io +with an existing `github-registry-credentials` pull secret. + +`push-image.sh` remains for building by hand when you need it. -- 2.54.0