From c35c9699d478eee31d24a615f9c380a06d6871f5 Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 25 Aug 2026 11:29:53 +0200 Subject: [PATCH] ci: call the shared build-image script instead of an inline copy (infra#309) The buildctl/skopeo step here was one of nineteen near-identical copies across the estate. That duplication is why moving them off rootless buildah (infra#241) took eleven commits and then had to be rediscovered in both repo templates and the gitea-ci skill. The implementation now lives in .gitea/build-image.sh, byte-identical to mathias/infra's ci-templates/build-image.sh and held there by a drift gate. What used to be textual differences between copies -- labels, tags, build-args, build target -- are environment inputs. Behaviour is unchanged: same registry, same tags, same labels, same buildctl-against-host-buildkitd path. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf --- .gitea/build-image.sh | 121 ++++++++++++++++++++++++++++++++++++++++ .gitea/workflows/cd.yml | 42 +++----------- 2 files changed, 130 insertions(+), 33 deletions(-) create mode 100755 .gitea/build-image.sh diff --git a/.gitea/build-image.sh b/.gitea/build-image.sh new file mode 100755 index 0000000..d912c00 --- /dev/null +++ b/.gitea/build-image.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Build an OCI image with buildctl and push it to the local registry. +# +# CANONICAL COPY: mathias/infra ci-templates/build-image.sh (infra#309). +# Every repo carries a byte-identical copy at .gitea/build-image.sh, written by +# `task ci:sync` and held in place by the drift gate. DO NOT EDIT THE COPIES -- +# edit here, re-sync, and the gate will tell you which repos still disagree. +# +# Why this exists: this step was hand-copied into a dozen repos, so migrating it +# off rootless buildah (infra#241) meant eleven near-identical commits, eleven +# verifications, and three separate rediscoveries of the same fix in the two +# repo templates and the gitea-ci skill. The differences between the copies were +# incidental -- a label here, a build-arg there -- never intentional. Those are +# inputs now, not forks of the implementation. +# +# Why buildctl and not buildah: the gitea-runner pod runs with +# `capabilities: drop [ALL]` and `allowPrivilegeEscalation: false`, which +# unconditionally blocks the setuid-root exec that rootless buildah's uid/gid +# remapping needs, and the pod has no OCI runtime at all. buildctl is a thin +# client -- it hands the build to the host's buildkitd over a socket, so the +# privileged layer work happens on the host and this pod's securityContext is +# not in the picture. buildah is absent from the runner image as of baked-v14 +# and its absence is asserted at pod start (infra#308). +# +# Inputs, all via environment: +# +# IMAGE (required) image name, e.g. cobalt-dingo +# REGISTRY default localhost:5000 +# SHA_TAG default `git rev-parse --short HEAD` +# VERSION_TAG optional extra tag, e.g. v1.2.3 on a tag build +# PUSH_LATEST default 1; set 0 to skip the :latest tag +# CONTEXT default . -- build context directory +# DOCKERFILE_DIR default $CONTEXT -- directory holding the Dockerfile +# DOCKERFILE_NAME optional, when the file is not named `Dockerfile` +# TARGET optional, multi-stage build target (--opt target=) +# BUILD_ARGS optional, space-separated K=V pairs +# BUILD_SECRETS optional, space-separated buildkit secret specs, each +# `id=NAME,src=PATH`. Pass the SPEC only -- the caller +# writes the secret to PATH itself. A secret VALUE must +# never be passed here: it would land in this script's +# argv and therefore in the CI log. +# EXTRA_LABELS optional, space-separated K=V pairs +# GIT_SHA optional, stamped as org.opencontainers.image.revision +# IMAGE_SOURCE optional, stamped as org.opencontainers.image.source +# +# Note on shell style below: every conditional is a full `if` block, never +# `[ -n "$X" ] && cmd`. Under `set -e` the && form exits the script whenever the +# test is false, which is a real bug this estate has already shipped once. + +set -euo pipefail + +if [ -z "${IMAGE:-}" ]; then + echo "build-image: IMAGE is required" >&2 + exit 2 +fi + +REGISTRY="${REGISTRY:-localhost:5000}" +BUILDKIT_HOST="${BUILDKIT_HOST:-unix:///run/buildkit/buildkitd.sock}" +CONTEXT="${CONTEXT:-.}" +DOCKERFILE_DIR="${DOCKERFILE_DIR:-$CONTEXT}" +PUSH_LATEST="${PUSH_LATEST:-1}" +VERSION_TAG="${VERSION_TAG:-}" + +if [ -z "${SHA_TAG:-}" ]; then + SHA_TAG="$(git rev-parse --short HEAD)" +fi + +REF="${REGISTRY}/${IMAGE}:${SHA_TAG}" + +TAR_FILE="$(mktemp)" +trap 'rm -f "$TAR_FILE"' EXIT + +args=( + --frontend dockerfile.v0 + --local "context=${CONTEXT}" + --local "dockerfile=${DOCKERFILE_DIR}" +) + +if [ -n "${DOCKERFILE_NAME:-}" ]; then + args+=(--opt "filename=${DOCKERFILE_NAME}") +fi +if [ -n "${TARGET:-}" ]; then + args+=(--opt "target=${TARGET}") +fi +for spec in ${BUILD_SECRETS:-}; do + # Opaque id=NAME,src=PATH -- buildkit reads the file, nothing is echoed. + args+=(--secret "${spec}") +done +if [ -n "${GIT_SHA:-}" ]; then + args+=(--opt "label:org.opencontainers.image.revision=${GIT_SHA}") +fi +if [ -n "${IMAGE_SOURCE:-}" ]; then + args+=(--opt "label:org.opencontainers.image.source=${IMAGE_SOURCE}") +fi +for kv in ${BUILD_ARGS:-}; do + args+=(--opt "build-arg:${kv}") +done +for kv in ${EXTRA_LABELS:-}; do + args+=(--opt "label:${kv}") +done + +echo "build-image: building ${REF}" +buildctl --addr "$BUILDKIT_HOST" build "${args[@]}" --output "type=oci,dest=${TAR_FILE}" + +# buildctl has no registry-push equivalent to `buildah push`, so skopeo copies +# the OCI archive up -- once per tag, from the same archive, so every tag points +# at bit-identical content. +push_tag() { + echo "build-image: pushing ${1}" + skopeo copy --dest-tls-verify=false "oci-archive:${TAR_FILE}" "docker://${1}" +} + +push_tag "$REF" +if [ "$PUSH_LATEST" = "1" ]; then + push_tag "${REGISTRY}/${IMAGE}:latest" +fi +if [ -n "$VERSION_TAG" ]; then + push_tag "${REGISTRY}/${IMAGE}:${VERSION_TAG}" +fi + +echo "✓ build-image: ${REF} pushed" diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml index fa62def..13126df 100644 --- a/.gitea/workflows/cd.yml +++ b/.gitea/workflows/cd.yml @@ -142,39 +142,15 @@ jobs: echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - name: Build and push to local registry - run: | - set -euo pipefail - # infra#241: rootless buildah cannot build this Dockerfile in the - # gitea-runner pod — it drops ALL capabilities and sets - # allowPrivilegeEscalation:false (deliberate, it holds CI secrets), so - # newuidmap/newgidmap cannot remap uids and `RUN apk add` dies with - # "exec: no command". Every other repo on this runner has already - # moved: dispatch, tapir, brain-ingest-ntfy, parallax, macro-research, - # cobalt-dingo, agentsquad (infra#233), segment-embedder (infra#243). - # - # buildctl talks to koala's host buildkitd over the socket the runner - # pod already mounts, so the privileged work happens on the host and - # this pod's posture is irrelevant to whether the build succeeds. - # buildctl cannot push, so it writes an OCI archive that skopeo copies. - BUILDKIT_HOST="unix:///run/buildkit/buildkitd.sock" - REGISTRY="localhost:5000" - REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" - TAR_FILE="$(mktemp)" - trap 'rm -f "$TAR_FILE"' EXIT - - buildctl --addr "$BUILDKIT_HOST" build \ - --frontend dockerfile.v0 \ - --local context=. \ - --local dockerfile=. \ - --opt build-arg:VERSION="${{ steps.meta.outputs.version }}" \ - --opt label:org.opencontainers.image.revision="${{ github.sha }}" \ - --output type=oci,dest="$TAR_FILE" - - skopeo copy --dest-tls-verify=false \ - oci-archive:"$TAR_FILE" docker://"${REF}" - skopeo copy --dest-tls-verify=false \ - oci-archive:"$TAR_FILE" docker://"${REGISTRY}/${{ env.IMAGE }}:latest" - echo "✓ Image pushed to ${REF}" + # infra#309: implementation lives in .gitea/build-image.sh, a + # byte-identical copy of mathias/infra's ci-templates/build-image.sh. + # Do not edit it here -- edit the canonical file, run `task ci:sync`, + # and infra's drift gate names any repo still carrying an old copy. + env: + SHA_TAG: ${{ steps.meta.outputs.sha-tag }} + GIT_SHA: ${{ github.sha }} + BUILD_ARGS: VERSION=${{ steps.meta.outputs.version }} + run: bash .gitea/build-image.sh deploy: name: Deploy via GitOps