From 417ab6070718210cd4eabe517f1fd380072d39cc Mon Sep 17 00:00:00 2001 From: Mathias Date: Tue, 25 Aug 2026 12:35:52 +0200 Subject: [PATCH] ci: call the shared build-image script instead of an inline copy (infra#309) Wave 2. The buildctl/skopeo step here was one of nineteen near-identical copies; 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. Labels, tags and build-args are inputs. Behaviour unchanged: same registry, same :sha/:latest/version tags, same revision and source labels. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011PL5XcG68SvQqz3vGVbWbf --- .gitea/build-image.sh | 121 ++++++++++++++++++++++++++++++++++++++++ .gitea/workflows/cd.yml | 62 ++++++-------------- 2 files changed, 139 insertions(+), 44 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 1addb20..2db6a40 100644 --- a/.gitea/workflows/cd.yml +++ b/.gitea/workflows/cd.yml @@ -58,53 +58,27 @@ jobs: REF="${{ github.ref }}" if [[ "$REF" == refs/tags/v* ]]; then echo "version-tag=${REF#refs/tags/}" >> "$GITHUB_OUTPUT" + echo "version=${REF#refs/tags/}" >> "$GITHUB_OUTPUT" + else + # The ldflags version stamp: git tag on a v* build, else short sha. + # Computed here in shell, not as a `${{ a || b }}` expression, so it + # cannot depend on how the runner treats an empty step output -- a + # wrong VERSION would be baked into the binary silently. + echo "version=${SHA}" >> "$GITHUB_OUTPUT" fi - name: Build and push to local registry - run: | - set -e - # infra#241: buildctl, not buildah. Rootless buildah in gitea-runner's - # hardened securityContext (capabilities drop ALL, - # allowPrivilegeEscalation false) cannot remap the UID/GID range - # newuidmap/newgidmap need on a fresh base-image pull, and the pod has - # no OCI runtime at all -- so this Dockerfile's `RUN apk add` / `RUN go - # build` could not execute there either. buildctl is a thin client - # that 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 stops being in the picture. Same pattern as - # mathias/infra's segment-embedder.yml. - BUILDKIT_HOST="unix:///run/buildkit/buildkitd.sock" - REGISTRY="localhost:5000" - REF="${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.sha-tag }}" - # Stamp the real build version: the git tag on a v* build, else the short sha. - VERSION="${{ steps.meta.outputs.version-tag }}" - # `if`, not `[ -z ] &&`: under `set -e` the && form exits the step - # whenever VERSION is already set. - if [ -z "$VERSION" ]; then VERSION="${{ steps.meta.outputs.sha-tag }}"; fi - 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="${VERSION}" \ - --opt label:org.opencontainers.image.revision="${{ github.sha }}" \ - --opt label:org.opencontainers.image.source="${{ github.repositoryUrl }}" \ - --output type=oci,dest="$TAR_FILE" - - # buildctl has no registry-push equivalent to `buildah push`; skopeo - # copies the OCI archive up, once per tag. - 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 - if [ -n "${{ steps.meta.outputs.version-tag }}" ]; then - skopeo copy --dest-tls-verify=false \ - oci-archive:"$TAR_FILE" \ - docker://${REGISTRY}/${{ env.IMAGE }}:${{ steps.meta.outputs.version-tag }} - fi - 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 }} + VERSION_TAG: ${{ steps.meta.outputs.version-tag }} + GIT_SHA: ${{ github.sha }} + IMAGE_SOURCE: ${{ github.repositoryUrl }} + BUILD_ARGS: VERSION=${{ steps.meta.outputs.version }} + run: bash .gitea/build-image.sh # No smoke test here. The old one ran `sudo k3s ctr run` against the host's # containerd: written for the Arch host runner, impossible in the