From 90ebcfaadcffbfc5a0d35702a05a661921a54e51 Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Thu, 24 Sep 2026 11:22:40 -0700 Subject: [PATCH 1/2] Mirror the MinIO CI image to GHCR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MinIO has withdrawn anonymous public access to its server image: first from Docker Hub (~13 Sept), then from quay.io (~24 Sept). Both registries now answer 401 to anonymous manifest requests for every tag, and dl.min.io answers 410. Any Dockerfile doing `FROM /minio/minio` fails before tests run, which is every tox-docker job in circulation, library-registry and virtual-library-card. Pinning an older tag does not help (the whole repository is gated, not just :latest), and neither does a local cache (tox_docker passes pull=True on every build, forcing a fresh pull). This adds a mirror published as ghcr.io/thepalaceproject/palace-ci-minio, built from MinIO's GitHub release assets — the one channel still served anonymously. The binaries are byte-for-byte identical to the ones inside the last upstream image CI used, verified by comparing their sha256 against /usr/bin/minio and /usr/bin/mc extracted from quay.io/minio/minio:latest. The image is a bare passthrough with no Palace configuration, so the three consuming repos keep their own differing MinIO setups and change only a FROM line. It is pinned to an immutable release tag and publishes no `latest`. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-minio-mirror.yml | 101 +++++++++++++++++++++++ docker/Dockerfile.minio.mirror | 95 +++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 .github/workflows/build-minio-mirror.yml create mode 100644 docker/Dockerfile.minio.mirror diff --git a/.github/workflows/build-minio-mirror.yml b/.github/workflows/build-minio-mirror.yml new file mode 100644 index 0000000000..d9a10ab019 --- /dev/null +++ b/.github/workflows/build-minio-mirror.yml @@ -0,0 +1,101 @@ +name: Build MinIO Mirror + +# Publishes ghcr.io/thepalaceproject/palace-ci-minio, the MinIO image used by the test suites of +# circulation, library-registry and virtual-library-card. MinIO withdrew anonymous public access +# to its own image (Docker Hub, then quay.io), so we host a copy built from its official GitHub +# release binaries. See docker/Dockerfile.minio.mirror for the full rationale. +# +# The image is pinned to a single upstream release and its content is fully determined by the +# checksums in that Dockerfile, so there is no scheduled rebuild: running this again produces the +# same image. It runs only when the Dockerfile or this workflow changes, or on demand. +# +# One-time manual step: GHCR packages are created private. After the first successful run, set the +# package visibility to public (Org -> Packages -> palace-ci-minio -> Package settings), matching +# the other Palace images. Without that, every developer and CI job would need a docker login. + +on: + push: + branches: + - main + paths: + - .github/workflows/build-minio-mirror.yml + - docker/Dockerfile.minio.mirror + workflow_dispatch: + +concurrency: + group: build-minio-mirror-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + build: + name: Build & Push MinIO Mirror + runs-on: ubuntu-24.04 + timeout-minutes: 30 + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # See comment here: https://github.com/actions/runner-images/issues/1187#issuecomment-686735760 + - name: Disable network offload + run: sudo ethtool -K eth0 tx off rx off + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # The Dockerfile is the single source of truth for which upstream release we mirror, so the + # tag is read back out of it rather than duplicated here where the two could drift apart. + - name: Determine image and tag + id: image + run: | + set -euo pipefail + image="ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/palace-ci-minio" + tag=$(sed -n 's/^ARG MINIO_RELEASE=\(.*\)$/\1/p' docker/Dockerfile.minio.mirror | head -1) + if [[ -z "$tag" ]]; then + echo "::error::Could not read MINIO_RELEASE from docker/Dockerfile.minio.mirror" + exit 1 + fi + echo "Publishing $image:$tag" + echo "image=$image" >> "$GITHUB_OUTPUT" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + # The mirror contains no RUN instructions, so both architectures cross-build on this single + # amd64 runner with no QEMU emulation and no per-arch runner matrix. + - name: Build and push mirror image + uses: docker/build-push-action@v7 + with: + context: . + file: ./docker/Dockerfile.minio.mirror + target: minio + platforms: linux/amd64,linux/arm64 + push: true + # Deliberately no `latest` tag: consumers pin this exact release. Following a moving + # upstream tag is part of how we ended up needing this mirror. + tags: ${{ steps.image.outputs.image }}:${{ steps.image.outputs.tag }} + + - name: Verify published image + run: | + set -euo pipefail + ref="${{ steps.image.outputs.image }}:${{ steps.image.outputs.tag }}" + docker buildx imagetools inspect "$ref" + # Confirm both architectures actually made it into the published manifest list. + platforms=$(docker buildx imagetools inspect "$ref" --raw \ + | jq -r '.manifests[] | select(.platform.os != "unknown") | "\(.platform.os)/\(.platform.architecture)"') + echo "Published platforms: $platforms" + for platform in linux/amd64 linux/arm64; do + grep -qx "$platform" <<< "$platforms" \ + || { echo "::error::$platform missing from $ref"; exit 1; } + done + # Smoke-test the runner's native architecture. + docker run --rm --entrypoint /usr/bin/minio "$ref" --version diff --git a/docker/Dockerfile.minio.mirror b/docker/Dockerfile.minio.mirror new file mode 100644 index 0000000000..d0ebdac570 --- /dev/null +++ b/docker/Dockerfile.minio.mirror @@ -0,0 +1,95 @@ +# syntax=docker/dockerfile:1.7 +# +# Mirror of the upstream MinIO server image, published as +# ghcr.io/thepalaceproject/palace-ci-minio. +# +# Why this exists +# --------------- +# MinIO withdrew anonymous public access to its server image: first from Docker Hub +# (~13 Sept 2026), then from quay.io (~24 Sept 2026). Both registries now answer 401 to +# anonymous manifest requests for every tag, and the binary download host (dl.min.io) answers +# 410. Every Palace repo that ran `FROM /minio/minio` in its test setup therefore +# fails before its test suite starts. Pinning an older tag or relying on a local Docker cache +# does not help: the whole repository is gated, and tox-docker passes `pull=True` on every +# build, forcing a fresh pull each time. +# +# The one channel MinIO still serves anonymously is GitHub release assets, so this image is +# assembled from the official release binaries rather than pulled and re-tagged. The binaries +# below are byte-for-byte identical to the ones inside the last upstream image CI used +# (quay.io/minio/minio:latest, labelled RELEASE.2025-09-07T16-13-09Z) — verified by comparing +# their sha256 against /usr/bin/minio and /usr/bin/mc extracted from that image. +# +# Scope +# ----- +# This is a bare passthrough. It contains no Palace configuration: no credentials, no buckets, +# no entrypoint script. The repos that consume it (circulation, library-registry, +# virtual-library-card) each configure MinIO differently and keep doing so in their own +# Dockerfiles — they change only their `FROM` line. Baking configuration in here would fork +# the image between repos immediately. +# +# This mirror is a bridge, not a destination; see the retirement ticket for replacing MinIO +# outright. Do not add features to it. +# +# Updating +# -------- +# The release strings and their checksums are a matched set. If you bump a version you MUST +# also replace the matching `--checksum=` value, or the build will fail (by design). +# Checksums come from the `.sha256sum` asset published alongside each binary. + +ARG BASE_IMAGE=registry.access.redhat.com/ubi9/ubi-minimal:9.6 + +# MinIO publishes one binary per platform rather than a multi-arch artifact, and each has its +# own checksum, so the download is split into a per-architecture stage that `fetch` selects +# from using TARGETARCH. This keeps checksum verification native to BuildKit's ADD. +FROM ${BASE_IMAGE} AS fetch-amd64 +ARG MINIO_RELEASE=RELEASE.2025-09-07T16-13-09Z +ARG MC_RELEASE=RELEASE.2025-08-13T08-35-41Z +ADD --chmod=755 --checksum=sha256:7c5bd8512c6e966455b1d198209358b2d191c77a83ab377c4073281065fb855f \ + https://github.com/minio/minio/releases/download/${MINIO_RELEASE}/minio.linux-amd64.${MINIO_RELEASE} \ + /staging/minio +ADD --chmod=755 --checksum=sha256:01f866e9c5f9b87c2b09116fa5d7c06695b106242d829a8bb32990c00312e891 \ + https://github.com/minio/mc/releases/download/${MC_RELEASE}/mc.linux-amd64.${MC_RELEASE} \ + /staging/mc + +FROM ${BASE_IMAGE} AS fetch-arm64 +ARG MINIO_RELEASE=RELEASE.2025-09-07T16-13-09Z +ARG MC_RELEASE=RELEASE.2025-08-13T08-35-41Z +ADD --chmod=755 --checksum=sha256:5c83cd2cf151717ba0243f73e1c7802ff36e272b67144bdd7f1f7d684fd6f03d \ + https://github.com/minio/minio/releases/download/${MINIO_RELEASE}/minio.linux-arm64.${MINIO_RELEASE} \ + /staging/minio +ADD --chmod=755 --checksum=sha256:14c8c9616cfce4636add161304353244e8de383b2e2752c0e9dad01d4c27c12c \ + https://github.com/minio/mc/releases/download/${MC_RELEASE}/mc.linux-arm64.${MC_RELEASE} \ + /staging/mc + +FROM fetch-${TARGETARCH} AS fetch + +FROM ${BASE_IMAGE} AS minio +ARG MINIO_RELEASE=RELEASE.2025-09-07T16-13-09Z +ARG MC_RELEASE=RELEASE.2025-08-13T08-35-41Z + +LABEL org.opencontainers.image.title="palace-ci-minio" \ + org.opencontainers.image.description="Unmodified MinIO server build, mirrored for Palace CI because upstream withdrew anonymous registry access." \ + org.opencontainers.image.version="${MINIO_RELEASE}" \ + org.opencontainers.image.source="https://github.com/ThePalaceProject/circulation" \ + org.opencontainers.image.vendor="The Palace Project" \ + io.palace.minio.release="${MINIO_RELEASE}" \ + io.palace.mc.release="${MC_RELEASE}" + +COPY --from=fetch /staging/minio /usr/bin/minio +COPY --from=fetch /staging/mc /usr/bin/mc + +# Matches the upstream image: MinIO reads credentials from these files when the corresponding +# environment variables are not set, and `mc` needs a writable config dir. +ENV MINIO_ROOT_USER_FILE=access_key \ + MINIO_ROOT_PASSWORD_FILE=secret_key \ + MINIO_KMS_SECRET_KEY_FILE=kms_master_key \ + MINIO_CONFIG_ENV_FILE=config.env \ + MC_CONFIG_DIR=/tmp/.mc + +# Declaring the volume creates the mount point, so no RUN is needed here. Keeping this stage +# free of RUN instructions means the image cross-builds for every architecture without QEMU. +VOLUME ["/data"] +EXPOSE 9000 9001 + +# A plain, unconfigured server. Consumers override this with their own CMD/ENTRYPOINT. +CMD ["minio", "server", "/data", "--address", ":9000", "--console-address", ":9001"] From 3e5fce4f9992a4d334ab568f105a7bcbe5661beb Mon Sep 17 00:00:00 2001 From: Daniel Bernstein Date: Thu, 24 Sep 2026 11:26:28 -0700 Subject: [PATCH 2/2] Validate the MinIO mirror build on pull requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow previously only ran on pushes to main, so a broken Dockerfile or workflow would surface after merge — precisely when the image needs to publish. Building on PRs that touch either file catches that in review instead. Pull request runs build both architectures and verify the pinned checksums, then discard the result: the registry login, the push and the published-manifest check are all skipped, so nothing is written to GHCR from a PR. Co-Authored-By: Claude Opus 5 --- .github/workflows/build-minio-mirror.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-minio-mirror.yml b/.github/workflows/build-minio-mirror.yml index d9a10ab019..d8d745b74e 100644 --- a/.github/workflows/build-minio-mirror.yml +++ b/.github/workflows/build-minio-mirror.yml @@ -20,15 +20,21 @@ on: paths: - .github/workflows/build-minio-mirror.yml - docker/Dockerfile.minio.mirror + # Build (but do not push) on PRs that touch the mirror, so a broken Dockerfile or workflow is + # caught in review rather than on main, where the failure would leave the image unpublished. + pull_request: + paths: + - .github/workflows/build-minio-mirror.yml + - docker/Dockerfile.minio.mirror workflow_dispatch: concurrency: - group: build-minio-mirror-${{ github.ref_name }} + group: build-minio-mirror-${{ github.ref_name }}-${{ github.event_name }} cancel-in-progress: true jobs: build: - name: Build & Push MinIO Mirror + name: Build MinIO Mirror runs-on: ubuntu-24.04 timeout-minutes: 30 permissions: @@ -48,6 +54,7 @@ jobs: uses: docker/setup-buildx-action@v4 - name: Login to GitHub Container Registry + if: github.event_name != 'pull_request' uses: docker/login-action@v4.6.0 with: registry: ghcr.io @@ -72,19 +79,22 @@ jobs: # The mirror contains no RUN instructions, so both architectures cross-build on this single # amd64 runner with no QEMU emulation and no per-arch runner matrix. - - name: Build and push mirror image + - name: Build mirror image uses: docker/build-push-action@v7 with: context: . file: ./docker/Dockerfile.minio.mirror target: minio platforms: linux/amd64,linux/arm64 - push: true + # On a pull request this builds both architectures and verifies the pinned checksums, + # then throws the result away. Only main and manual runs publish. + push: ${{ github.event_name != 'pull_request' }} # Deliberately no `latest` tag: consumers pin this exact release. Following a moving # upstream tag is part of how we ended up needing this mirror. tags: ${{ steps.image.outputs.image }}:${{ steps.image.outputs.tag }} - name: Verify published image + if: github.event_name != 'pull_request' run: | set -euo pipefail ref="${{ steps.image.outputs.image }}:${{ steps.image.outputs.tag }}"