Files
haproxy-manager-base/.gitea/workflows/mirror-base-image.yaml
T
shadowdaoandClaude Sonnet 5.5 905b2894b6 fix(ci): mirror base images as whole multi-arch indexes, with assertions
mirror-base-image.yaml pulled the upstream index digest on an amd64 runner and
re-pushed it, publishing only the amd64 manifest; the python/golang mirrors
had no arm64 variant. Replace pull/tag/push with a registry-to-registry
`imagetools create` of the digest-pinned index via .gitea/scripts/
mirror-index-copy.sh: copy to <tag>-staging, assert linux/amd64+linux/arm64
are listed and the index digest equals the Docker Hub digest, then move the
real tag and re-assert. A failed assertion never touches the real tag.
Adds a local-registry test (.gitea/scripts/test-mirror-index-copy.sh).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
2026-11-11 16:40:22 -08:00

93 lines
4.5 KiB
YAML

name: Mirror base images
run-name: weekly base-image mirror
# Copies each declared base image (the whole multi-arch INDEX, not one
# platform) from upstream to the in-house registry, so any of our images that FROM these don't depend on docker.io's
# Cloudflare R2 blob storage being reachable. The 2026-05-12 Cloudflare
# incident motivated this for python:3.12-slim and again for golang:1.25
# when the coraza-spoa build hit the same blob-fetch failure.
#
# Adding a new mirror = add one entry to the matrix below. The destination
# tag is always cloud-hosting-platform/<image>:<tag>, matching upstream.
on:
schedule:
# Mondays 06:00 UTC — outside customer peak hours and well before the
# typical Tuesday/Thursday push cycles. workflow_dispatch lets us trigger
# manually from the Gitea UI when upstream publishes patches.
- cron: '0 6 * * 1'
workflow_dispatch:
jobs:
Mirror-Base:
runs-on: ubuntu-latest
strategy:
# fail-fast=false so one image's upstream being down doesn't block the
# others from refreshing.
fail-fast: false
matrix:
# hub: the Docker Hub IMAGE:TAG whose digest is resolved DIRECTLY at
# registry-1.docker.io (resolve-base-image.sh expects no docker.io/
# prefix; official images resolve as library/<image>).
image:
- { hub: 'python:3.12-slim', dst_path: 'cloud-hosting-platform/python', tag: '3.12-slim' }
- { hub: 'golang:1.25', dst_path: 'cloud-hosting-platform/golang', tag: '1.25' }
steps:
# Only for .gitea/scripts/resolve-base-image.sh.
- name: Checkout
uses: actions/checkout@v4
# Supply chain: ask Docker Hub itself (over HTTPS, never via a mirror) which
# digest the tag points at right now, then pull exactly that digest. What
# lands in the in-house registry is therefore content-addressed to the
# digest Docker Hub reported - not whatever a `docker pull TAG` on the
# runner happened to resolve through any configured registry mirror.
# If the lookup fails the job FAILS and last week's mirror stays in place.
- name: Resolve ${{ matrix.image.hub }} digest (Docker Hub, direct)
id: base
env:
HUB_TAG: ${{ matrix.image.hub }}
run: |
set -euo pipefail
ref=$(.gitea/scripts/resolve-base-image.sh "$HUB_TAG")
[[ "$ref" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]] || { echo "::error::bad digest reference for ${HUB_TAG}"; exit 1; }
echo "base-image: ${HUB_TAG} -> ${ref} (digest from registry-1.docker.io)"
echo "ref=${ref}" >> "$GITHUB_OUTPUT"
# imagetools needs buildx; same setup as the build workflows.
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
# The registry token is written ONLY by this trusted x86 runner step
# (unchanged from before; no new secret exposure).
- name: Login to in-house registry
uses: docker/login-action@v3
with:
registry: repo.anhonesthost.net
username: ${{ secrets.CI_USER }}
password: ${{ secrets.CI_TOKEN }}
# Registry-to-registry copy of the WHOLE upstream index (all platforms,
# incl. linux/arm64). The old pull/tag/push re-published only the runner's
# amd64 manifest. mirror-index-copy.sh stages to <tag>-staging, asserts
# amd64+arm64 present and index digest == the Docker-Hub-reported digest,
# and only then moves the real tag, so a bad copy never replaces a good
# multi-arch mirror. Logic + tests: .gitea/scripts/mirror-index-copy.sh,
# .gitea/scripts/test-mirror-index-copy.sh.
- name: Copy index by digest, assert platforms + digest ${{ matrix.image.hub }}
# Values reach the script via env, never via ${{ }} inside run:.
env:
REF: ${{ steps.base.outputs.ref }}
DST_PATH: ${{ matrix.image.dst_path }}
DST_TAG: ${{ matrix.image.tag }}
run: |
set -euo pipefail
[[ "$REF" =~ ^[a-z0-9][a-z0-9._/-]*@sha256:[0-9a-f]{64}$ ]] || { echo "::error::bad digest reference '${REF}'"; exit 1; }
[[ "$DST_PATH" =~ ^[a-z0-9][a-z0-9._/-]*$ && "$DST_TAG" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || { echo "::error::bad destination"; exit 1; }
name="${REF%@*}"; digest="${REF#*@}"
[[ "$name" == */* ]] || name="library/${name}" # official images
.gitea/scripts/mirror-index-copy.sh \
"docker.io/${name}@${digest}" \
"repo.anhonesthost.net/${DST_PATH}:${DST_TAG}"