# Custom toolchain image for the self-hosted Gitea Actions (Forgejo) runner. # # The cluster runs act_runner in *host mode* (docker disabled), so workflow steps # execute INSIDE this image. The stock forgejo/runner image is minimal and lacks # node (needed by JS actions: actions/checkout, setup-go, ...), git, make, etc. # The runner pod is non-root with all caps dropped, so it cannot apt/apk-add at # runtime -- the tools must be baked in here. # # PUBLISHING IS AUTOMATED. Run the "build image" workflow on GitHub with a tag # (e.g. 12.11.1-ci4); it builds both architectures and pushes to # ghcr.io/zegit-zoo/zegit-forgejo-runner. ./push-image.sh remains for building # by hand. README.md explains why the registry moved off Gitea (Cloudflare's # request-body cap returns 413 on a registry layer) and why the build cannot run # on our own runners (host mode, all capabilities dropped). # # Then in flux-config set the image on BOTH deployment-zegit-runner and # deployment-forgejo-runner, digest-pinned. # # The image is NOT public. Pulls need a registry credential in # forgejo-runner-system -- github-registry-credentials for ghcr.io. An earlier # version of this comment claimed the package was public and needed no secret; # that was wrong for the Gitea registry too, which is private. # The runner binary is built from upstream v12.11.1 with one patch: Gitea 28 adds # job-index and job-total under `strategy` in the workflow it sends for a matrix # job, and the stock schema refuses them ("Unknown Property job-total ... the # workflow file is not usable"), so every matrix job dies before its first step. # No forgejo-runner release accepts them (checked through 13.2.1). Drop this stage # when upstream does, or when the runner is replaced. FROM --platform=$BUILDPLATFORM data.forgejo.org/oci/golang:1.25-alpine3.23@sha256:27b3069b25647beacd3745b0b2c83e2f9b8d3935455a32cd4531236ee1c960e0 AS runner-build ARG TARGETOS ARG TARGETARCH RUN apk add --no-cache git ADD --keep-git-dir=false https://code.forgejo.org/forgejo/runner.git#8e01d43cd27e0732a1cff664996a92739472d42b /srv COPY patches/ /patches/ WORKDIR /srv RUN git apply --verbose /patches/*.patch # The patch's own tests, so a build cannot ship a patch that stopped working. RUN go test -count=1 ./act/schema/ ./internal/app/run/ -run 'StrategyJobIndexAndTotal|Gitea28MatrixJob' RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" go build -trimpath -tags 'netgo osusergo' \ -ldflags '-s -w -X code.forgejo.org/forgejo/runner/v12/internal/pkg/ver.version=v12.11.1' \ -o /out/forgejo-runner . FROM code.forgejo.org/forgejo/runner:12.11.1@sha256:a1a2885261a0b8337d4791dd6949651117206741ba66423c24a57c107a64d4a8 USER root # nodejs -> JS actions runtime (actions/checkout, actions/setup-go, ...) # git -> checkout # git-daemon -> provides git-http-backend, which Alpine splits OUT of the `git` # package (git alone ships git-remote-http/https but NOT the CGI # backend). Without it, internal/forgeprobe's smart-HTTP tests died # with "fork/exec git-http-backend: no such file or directory" and # reported three FAILURES for a missing tool — which turned Gitea CI # red on every zegit master commit while GitHub stayed green, for # hours, unnoticed. # bash -> `shell: bash` steps and many action entrypoints # coreutils-> GNU userland (alpine busybox tools trip up some scripts) # 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 # openssl -> the release tag gate verifies the Control Plane's SIGNED quorum # verdict (Ed25519 DSSE) with `openssl pkeyutl -verify -rawin`. # Absent, that command failed "command not found" and the gate's # `|| { echo ...; exit 1; }` reported it as "Quorum verdict # signature FAILED verification" -- a TOOLING gap narrated as a # SECURITY verdict, pointing the investigation at a perfectly # valid signed approval instead of at this image. It blocked # v0.0.134 and v0.0.135 on 2026-08-31. # jq -> the same story one step earlier: the gate parsed that envelope # with `jq -e`, and a missing jq was reported as "not a signed # DSSE envelope". jq is the reflex tool for JSON in shell steps, # so its absence keeps resurfacing as a misleading failure in # whichever step reaches for it next. # 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 # # chromium, font-noto -> the browser gates (2026-09-08): zegit's `browser # tests` (chromedp + a CDP virtual authenticator over the WebAuthn # ceremony pages) and `lighthouse` (treosh/lighthouse-ci-action # via chrome-launcher) were github.com-only jobs, so when `ci` # was routed to this runner on 2026-08-31 they stopped running # anywhere. chromedp finds `chromium`/`chromium-browser` on PATH; # chrome-launcher honours CHROME_PATH. font-noto so text renders # (an empty font set makes layout/contrast audits meaningless). # openssh-keygen -> `ssh-keygen`: zegit's SSHSIG oracle test # (TestSSHSIG_ChecksumsVerifyWithSSHKeygen_441) verifies release-checksum # signatures with `ssh-keygen -Y verify` and fails under CI without it, # so the v0.1.56 cut was refused on this runner (zegit#575). # postgresql-client -> `psql`, so the lighthouse job can CREATE/DROP its own # database on the runner's ci-postgres (ZEGIT_TESTPG_DSN) instead # of migrating the shared maintenance database from under the # integration suite's template. # # 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. RUN apk add --no-cache \ nodejs \ git \ git-daemon \ bash \ coreutils \ curl \ tar \ make \ ca-certificates \ gcc \ musl-dev \ python3 \ py3-yaml \ openssl \ jq \ chromium \ font-noto \ postgresql-client \ openssh-keygen # ssh-keygen signs and verifies an SSHSIG in namespace "file", the form zegit's # test uses, so a build without a working ssh-keygen fails here, not in CI. RUN set -eu; d=$(mktemp -d); \ ssh-keygen -q -t ed25519 -N '' -f "$d/k"; \ echo smoke > "$d/f"; \ ssh-keygen -q -Y sign -n file -f "$d/k" "$d/f"; \ printf 'smoke %s\n' "$(cat "$d/k.pub")" > "$d/allowed"; \ ssh-keygen -Y verify -f "$d/allowed" -I smoke -n file -s "$d/f.sig" < "$d/f"; \ rm -rf "$d" # CVE-2026-111497 (brotli decoder out-of-bounds write, CISA KEV, due 2026-11-10): the # forgejo/runner:12.11.1 base ships brotli-libs 1.2.0-r0, and `apk add` above never # upgrades a package the base already has. git-remote-http and curl link it through # libcurl. Upgrade it explicitly and fail the build if it is still behind, so a base # bump can never quietly bring the vulnerable version back. Drop this once the upstream # runner base ships brotli-libs >= 1.2.1. RUN set -eux; \ apk upgrade --no-cache brotli-libs; \ v="$(apk list --installed brotli-libs | sed -n 's/^brotli-libs-\([0-9][^ ]*\) .*/\1/p')"; \ echo "brotli-libs ${v}"; \ [ "$(apk version -t "${v}" 1.2.1-r0)" != "<" ] || { echo "brotli-libs ${v} is older than 1.2.1-r0 (CVE-2026-111497)"; exit 1; } # The patched binary replaces the stock one (see the runner-build stage). COPY --from=runner-build /out/forgejo-runner /bin/forgejo-runner # Match the runner pod's runAsUser (1000); keep the image non-root by default. USER 1000