Gitea 28 adds job-index and job-total under strategy in the workflow it sends for a matrix job, and forgejo-runner's schema refuses them, so every matrix job dies before its first step (zegit image-gitea.yml run 23992, the v0.1.52 tag build). No forgejo-runner release accepts them (checked through 13.2.1). Build the runner from v12.11.1 at its commit with a one-file schema patch and two tests, one of them the real payload of that failed job; the build runs them. Bases are pinned by digest. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018wWyCiV27KmNSauYTdLyp7
133 lines
7.2 KiB
Docker
133 lines
7.2 KiB
Docker
# 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).
|
|
# 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
|
|
|
|
# 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
|