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 (#1) made that survivable; this removes the manual step.
Why GitHub and not our own Gitea Actions. 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. Rootless buildah needs newuidmap/user namespaces for its RUN steps, so building there would mean weakening the posture the hardening established.
Why ghcr.io and not the Gitea registry. Cloudflare's body cap is what produced the 413 in the first place. The cluster already pulls zegit's own images from ghcr.io through an existing github-registry-credentials pull secret, so this reuses a trusted path rather than inventing one.
A push to master touching the Dockerfile validates both architectures still build but publishes nothing; only an explicit workflow_dispatch names a tag and pushes.
Assumptions & Decisions
A branch push must not publish. Deriving a tag from a branch would silently mint versions, and this tag encodes the upstream runner version plus a ci revision (12.11.1-ci4) that only a human can decide.
The final step reads the published index back and fails if either arch is missing. A single-arch image under this tag would CrashLoop on half the nodes — forgejo-runner runs on arm64, zegit-runner on amd64, neither pins a nodeSelector.
provenance: false, sbom: false — attestations turn a single-platform build into an OCI index, which is what broke docker manifest create by hand.
Action SHAs match the pins zegit already uses, verified to exist, rather than introducing a second set of versions to track.
Rejected: a privileged in-cluster builder. It works, but adds privileged surface to runners that were deliberately hardened.
Rejected: vendoring the Dockerfile into zegit-zoo/zegit to reuse its workflows. Avoids a repo, but makes the runner toolchain's provenance opaque.
push-image.sh stays for hand-building.
Follow-up, not in this PR: once a tag is published to ghcr.io, flux-config's deployment-zegit-runner.yaml and deployment-forgejo-runner.yaml need their image switched from gitea.langenoja.se/... to ghcr.io/zegit-zoo/zegit-forgejo-runner:<tag>.
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` (#1) made that survivable; this removes the manual step.
**Why GitHub and not our own Gitea Actions.** 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`. Rootless buildah needs `newuidmap`/user namespaces for its `RUN` steps, so building there would mean weakening the posture the hardening established.
**Why ghcr.io and not the Gitea registry.** Cloudflare's body cap is what produced the 413 in the first place. The cluster already pulls zegit's own images from ghcr.io through an existing `github-registry-credentials` pull secret, so this reuses a trusted path rather than inventing one.
A push to `master` touching the Dockerfile **validates** both architectures still build but publishes nothing; only an explicit `workflow_dispatch` names a tag and pushes.
## Assumptions & Decisions
- A branch push must not publish. Deriving a tag from a branch would silently mint versions, and this tag encodes the upstream runner version plus a ci revision (`12.11.1-ci4`) that only a human can decide.
- The final step reads the published index back and fails if either arch is missing. A single-arch image under this tag would CrashLoop on half the nodes — `forgejo-runner` runs on arm64, `zegit-runner` on amd64, neither pins a `nodeSelector`.
- `provenance: false, sbom: false` — attestations turn a single-platform build into an OCI index, which is what broke `docker manifest create` by hand.
- Action SHAs match the pins zegit already uses, verified to exist, rather than introducing a second set of versions to track.
- Rejected: a privileged in-cluster builder. It works, but adds privileged surface to runners that were deliberately hardened.
- Rejected: vendoring the Dockerfile into `zegit-zoo/zegit` to reuse its workflows. Avoids a repo, but makes the runner toolchain's provenance opaque.
- `push-image.sh` stays for hand-building.
**Follow-up, not in this PR:** once a tag is published to ghcr.io, flux-config's `deployment-zegit-runner.yaml` and `deployment-forgejo-runner.yaml` need their image switched from `gitea.langenoja.se/...` to `ghcr.io/zegit-zoo/zegit-forgejo-runner:<tag>`.
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.
markus
merged commit f2b9661be2 into master2026-08-20 12:46:04 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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(#1) made that survivable; this removes the manual step.Why GitHub and not our own Gitea Actions. The runners cannot build images: their labels are
ubuntu-latest:host— host mode, no job containers — and the container drops ALL capabilities withallowPrivilegeEscalation: false. Rootless buildah needsnewuidmap/user namespaces for itsRUNsteps, so building there would mean weakening the posture the hardening established.Why ghcr.io and not the Gitea registry. Cloudflare's body cap is what produced the 413 in the first place. The cluster already pulls zegit's own images from ghcr.io through an existing
github-registry-credentialspull secret, so this reuses a trusted path rather than inventing one.A push to
mastertouching the Dockerfile validates both architectures still build but publishes nothing; only an explicitworkflow_dispatchnames a tag and pushes.Assumptions & Decisions
12.11.1-ci4) that only a human can decide.forgejo-runnerruns on arm64,zegit-runneron amd64, neither pins anodeSelector.provenance: false, sbom: false— attestations turn a single-platform build into an OCI index, which is what brokedocker manifest createby hand.zegit-zoo/zegitto reuse its workflows. Avoids a repo, but makes the runner toolchain's provenance opaque.push-image.shstays for hand-building.Follow-up, not in this PR: once a tag is published to ghcr.io, flux-config's
deployment-zegit-runner.yamlanddeployment-forgejo-runner.yamlneed their image switched fromgitea.langenoja.se/...toghcr.io/zegit-zoo/zegit-forgejo-runner:<tag>.