diff --git a/.github/workflows/mirror-official.yml b/.github/workflows/mirror-official.yml new file mode 100644 index 0000000..7d4f9e0 --- /dev/null +++ b/.github/workflows/mirror-official.yml @@ -0,0 +1,73 @@ +name: mirror official + +# Copies tags of the official `plone` image (docker.io/library/plone) into +# plone/plone and GHCR, by digest, without rebuilding them. The list of tags +# and their pinned digests is mirror/official-tags.txt. +# +# Why copy rather than build: the 4.3 - 5.2 images were built and published by +# Docker Official Images from this repository's own Dockerfiles, and those +# Dockerfiles can no longer be rebuilt faithfully: their Debian releases have +# left deb.debian.org, so a rebuild today either fails at apt-get or produces a +# different image. A copy is the image that was released. +# +# Pull requests only verify that every source tag still resolves to its pinned +# digest. Publishing is manual: run this workflow from `main`. + +on: + pull_request: + paths: + - 'mirror/**' + - '.github/workflows/mirror-official.yml' + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + mirror: + runs-on: ubuntu-latest + env: + # Pinned on purpose, as in legacy-build.yml: deriving it from + # github.repository would change the image name when the repository is + # renamed (see #209). + GHCR_IMAGE: ghcr.io/plone/plone.docker + DOCKERHUB_IMAGE: docker.io/plone/plone + PUBLISH: ${{ github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' && '1' || '0' }} + # Secrets cannot be referenced from an `if:` condition; see + # legacy-build.yml for the same pattern. + HAS_DOCKERHUB: ${{ secrets.DOCKERHUB_USERNAME != '' && secrets.DOCKERHUB_TOKEN != '' }} + steps: + - uses: actions/checkout@v7 + + - uses: docker/setup-buildx-action@v4 + + - name: Login to GHCR + if: env.PUBLISH == '1' + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + if: env.PUBLISH == '1' && env.HAS_DOCKERHUB == 'true' + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # Unlike legacy-build.yml, a missing Docker Hub login fails the run + # instead of falling back to GHCR only: Docker Hub is the target that + # matters here, and a green run that skipped it would read as done. + - name: Mirror + run: | + if [ "${PUBLISH}" = "1" ] && [ "${HAS_DOCKERHUB}" != "true" ]; then + echo "::error::DOCKERHUB_USERNAME/DOCKERHUB_TOKEN not set - cannot publish to ${DOCKERHUB_IMAGE}." + exit 1 + fi + TARGETS="${DOCKERHUB_IMAGE} ${GHCR_IMAGE}" ./mirror/mirror-official.sh diff --git a/CHANGES.md b/CHANGES.md index 23e466c..e5d92ca 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,6 @@ # Changelog +- Publish Plone 5.2.14 as `plone/plone:5.2.14` (and GHCR), copied by digest from the official `plone:5.2.14`. `plone/plone:latest`, `5`, `5.2` and `python38` now point at 5.2.14 instead of 5.2.4. Refs #210 + [@ericof] - Add Plone 1.0 image to the legacy matrix, completing it. Refs #197 [@ericof] - Add Plone 2.0 image to the legacy matrix. Refs #196 diff --git a/README.md b/README.md index 8ebfc7a..13ff2af 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,19 @@ alone do not say which: | Rows | Registry | Pull with | |---|---|---| | 4.3 - 5.2 | the official `plone` image | `docker pull plone:5.2.14` | +| 5.2.14 (Debian) | also `plone/plone` and GHCR | `docker pull plone/plone:5.2.14`
`docker pull ghcr.io/plone/plone.docker:5.2.14` | | `legacy/` | `plone/plone` and GHCR | `docker pull plone/plone:4.2`
`docker pull ghcr.io/plone/plone.docker:4.2` | -`plone/plone` also carries its own 4.3 - 5.2 tags, but those are a mirror last -pushed in **March 2021** and are older than the official image's. For anything -4.3 and above, prefer `plone:`. +The 5.2.14 tags on `plone/plone` and GHCR are copies of the official image, made +by digest rather than rebuilt: `plone/plone:5.2.14` *is* `plone:5.2.14`. The +tags copied, and the digests they are pinned to, are listed in +[`mirror/official-tags.txt`](mirror/official-tags.txt); the +[`mirror official`](.github/workflows/mirror-official.yml) workflow publishes +them. + +`plone/plone`'s other 4.3 - 5.2 tags come from an earlier mirror, last pushed in +**March 2021**, and lag behind the official image. For any other 4.3 - 5.2 tag, +prefer `plone:`. Each legacy series publishes four tags — the full version and the series, each with a `-demo` counterpart that ships a Plone site already created. diff --git a/mirror/mirror-official.sh b/mirror/mirror-official.sh new file mode 100755 index 0000000..546ec5c --- /dev/null +++ b/mirror/mirror-official.sh @@ -0,0 +1,62 @@ +#!/bin/sh +# Copy tags of the official `plone` image into our own repositories. +# +# ./mirror/mirror-official.sh # verify only: nothing is pushed +# PUBLISH=1 ./mirror/mirror-official.sh # verify, copy, verify the copies +# +# Reads ` ` pairs from mirror/official-tags.txt. Every source tag +# must still resolve to its pinned digest before anything is copied, and every +# copy must come out with that same digest, so a mirrored tag is provably the +# official image and not a rebuild of it. +# +# The copy is `docker buildx imagetools create` from the pinned digest: a +# registry-side manifest copy, with no pull and no build, that keeps every +# platform of the multi-arch image. +set -eu + +SOURCE="${SOURCE:-docker.io/library/plone}" +TARGETS="${TARGETS:-docker.io/plone/plone ghcr.io/plone/plone.docker}" +TAGS_FILE="${TAGS_FILE:-$(dirname "$0")/official-tags.txt}" +PUBLISH="${PUBLISH:-0}" + +digest_of() { + docker buildx imagetools inspect --format '{{.Manifest.Digest}}' "$1" +} + +PAIRS=$(grep -vE '^[[:space:]]*(#|$)' "${TAGS_FILE}") +COUNT=$(printf '%s\n' "${PAIRS}" | wc -l | tr -d ' ') +if [ -z "${PAIRS}" ]; then + echo "FAIL: no tags listed in ${TAGS_FILE}" >&2 + exit 1 +fi + +echo "=== verifying ${COUNT} source tag(s) in ${SOURCE} ===" +printf '%s\n' "${PAIRS}" | while read -r tag digest; do + actual=$(digest_of "${SOURCE}:${tag}") + if [ "${actual}" != "${digest}" ]; then + echo "FAIL: ${SOURCE}:${tag} is ${actual}, pinned ${digest}" >&2 + exit 1 + fi + echo "OK: ${SOURCE}:${tag} = ${digest}" +done + +if [ "${PUBLISH}" != "1" ]; then + echo "NOTE: PUBLISH is not 1 - verified only, nothing copied" + exit 0 +fi + +for target in ${TARGETS}; do + echo "=== copying ${COUNT} tag(s) to ${target} ===" + printf '%s\n' "${PAIRS}" | while read -r tag digest; do + docker buildx imagetools create \ + --tag "${target}:${tag}" "${SOURCE}@${digest}" + actual=$(digest_of "${target}:${tag}") + if [ "${actual}" != "${digest}" ]; then + echo "FAIL: ${target}:${tag} came out as ${actual}, expected ${digest}" >&2 + exit 1 + fi + echo "OK: ${target}:${tag} = ${digest}" + done +done + +echo "MIRROR PASS: ${COUNT} tag(s) x $(echo "${TARGETS}" | wc -w | tr -d ' ') target(s)" diff --git a/mirror/official-tags.txt b/mirror/official-tags.txt new file mode 100644 index 0000000..a508fed --- /dev/null +++ b/mirror/official-tags.txt @@ -0,0 +1,21 @@ +# Tags copied from the official `plone` image (docker.io/library/plone) into +# plone/plone and GHCR by mirror/mirror-official.sh. +# +# One ` ` pair per line. The digest is the official image's +# manifest list for that tag, pinned so the copy is exactly what was reviewed: +# if the official tag ever moves, the mirror fails instead of following it. +# +# Resolve a digest with: +# docker buildx imagetools inspect --format '{{.Manifest.Digest}}' plone: + +# Plone 5.2.14, Python 3.8, Debian: built by Docker Official Images from +# 5.2/5.2.14/debian at plone/plone.docker@a3a9c7e (2024-09-27). Same tag set as +# the official image; the floating tags previously pointed at 5.2.4. +5.2.14 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +5.2.14-python38 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +5.2 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +5.2-python38 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +5 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +5-python38 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +python38 sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b +latest sha256:e73ae6e9864e43d4d49169f9fda09012d48c7720e8e204a8b0304b764600923b