Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions .github/workflows/mirror-official.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`<br>`docker pull ghcr.io/plone/plone.docker:5.2.14` |
| `legacy/` | `plone/plone` and GHCR | `docker pull plone/plone:4.2`<br>`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:<tag>`.
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:<tag>`.

Each legacy series publishes four tags — the full version and the series, each
with a `-demo` counterpart that ships a Plone site already created.
Expand Down
62 changes: 62 additions & 0 deletions mirror/mirror-official.sh
Original file line number Diff line number Diff line change
@@ -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 `<tag> <digest>` 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)"
21 changes: 21 additions & 0 deletions mirror/official-tags.txt
Original file line number Diff line number Diff line change
@@ -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 `<tag> <digest>` 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:<tag>

# 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
Loading