Compare commits

...
Author SHA1 Message Date
agent 5427ccbad2 fix: restore gcc, musl-dev, python3 and py3-yaml to the image
REGRESSION, mine. The committed Dockerfile had drifted from the image actually
in service: the running ci3 image contained these four packages, the Dockerfile
did not, and I rebuilt ci4 from the Dockerfile. The rollout therefore removed
tools zegit's CI depends on.

Proof the deployed ci3 had them: its Gitea runs got PAST `make credentials-check`
— which shells out to python3 — and failed later at cover-check. The ci4 run
fails earlier, at `credentials-coverage: need python3`.

What each is for, so the next rebuild does not drop them again:
  python3, py3-yaml  the repo's own gates are Python — credentials-coverage.sh,
                     validate-admission.sh and feature-lint.sh all shell out to
                     it, and credentials-coverage parses manifests with yaml
  gcc, musl-dev      cgo, which `go test -race` in the Makefile test target
                     requires: a C compiler plus libc headers (musl on Alpine)

Verified on the real base image rather than assumed — on
code.forgejo.org/forgejo/runner:12.11.1, `apk add git git-daemon gcc musl-dev
python3 py3-yaml` yields Python 3.12.14, a working `import yaml`, gcc 15.2.0,
and git-http-backend on the git exec path.

The deeper lesson is that a hand-built image can drift from its Dockerfile
silently, and nothing notices until someone rebuilds. Publishing via CI (#2)
is what stops that recurring, since every future image comes from the
committed file.
2026-08-20 15:53:23 +02:00
markus f2b9661be2 Merge pull request 'ci: build and publish the image on GitHub to ghcr.io' (#2) from claude/ci/build-on-github into master
build image / build (push) Skipped
Reviewed-on: #2
2026-08-20 12:46:03 +00:00
agent f0922508a8 ci: build and publish the image on GitHub to ghcr.io
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.
2026-08-20 14:44:59 +02:00
markus 8c356b7e95 Merge pull request 'fix: add git-daemon so git-http-backend exists, plus a publish script' (#1) from claude/fix/git-http-backend into master
Reviewed-on: #1
2026-08-20 12:25:29 +00:00
3 changed files with 125 additions and 1 deletions

No files matched your search

+93
View File
@@ -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"
+11 -1
View File
@@ -33,6 +33,12 @@ USER root
# curl,tar -> setup-go / gitleaks download + extract their own arch-specific binaries
# make -> zegit Makefile targets
# ca-certificates -> trusted roots for HTTPS to gitea/github/goproxy
# gcc,musl-dev -> C toolchain for cgo; `go test -race` (the Makefile test
# target) requires cgo, which needs a C compiler + libc headers
# (musl on Alpine)
# python3,py3-yaml -> the repo's own gates are Python: credentials-coverage.sh,
# validate-admission.sh and feature-lint.sh all shell out to
# python3, and credentials-coverage parses manifests with yaml
#
# Go is intentionally NOT baked in: actions/setup-go fetches the pinned version
# per-run (node + tar are present), so the image needs no rebuild on Go bumps.
@@ -45,7 +51,11 @@ RUN apk add --no-cache \
curl \
tar \
make \
ca-certificates
ca-certificates \
gcc \
musl-dev \
python3 \
py3-yaml
# Match the runner pod's runAsUser (1000); keep the image non-root by default.
USER 1000
+21
View File
@@ -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.