From a23de297e0708dff47bfd259f7b1b1468c4a112d Mon Sep 17 00:00:00 2001 From: Paris Yu Date: Sat, 26 Sep 2026 20:42:04 +0800 Subject: [PATCH] ci: mirror published multi-arch images to GHCR After the Docker Hub manifests are promoted and digest-verified, copy the four promoted tags to ghcr.io/ with 'docker buildx imagetools create'. Registry-to-registry manifest copy keeps GHCR tags on the exact same digests as Docker Hub -- same workflow, same tags, same digests, as requested in the issue -- with no rebuild lane and no new secrets (GITHUB_TOKEN + packages: write). Each mirrored tag is digest-checked against the just-verified Docker Hub manifest so a silently diverged mirror fails the release. Architecture-suffixed staging tags stay Docker-Hub-only; attestations keep their index.docker.io subject names and remain verifiable against the shared digests. Fixes #224 --- .github/workflows/docker-release.yml | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 4e975a080e63a..0c75b9c0da692 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -18,6 +18,7 @@ permissions: id-token: write attestations: write artifact-metadata: write + packages: write concurrency: group: docker-release @@ -200,6 +201,13 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Install Syft uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 with: @@ -348,6 +356,41 @@ jobs: fi echo "SILO_DISTROLESS_DIGEST=${DISTROLESS_RELEASE_DIGEST}" >> "${GITHUB_ENV}" + # Mirror the published multi-architecture manifests to GHCR. + # `imagetools create` copies the existing manifests and blobs + # registry-to-registry, so the GHCR tags resolve to the exact same + # digests as Docker Hub -- no rebuild, no drift. Architecture-suffixed + # staging tags stay Docker-Hub-only; only the four promoted tags are + # mirrored. Attestations keep their index.docker.io subject names; + # verification against the mirrored digests still works by passing the + # Docker Hub subject registry explicitly. + - name: Mirror multi-architecture manifests to GHCR + run: | + set -euo pipefail + for tag in "${RELEASE_TAG}" "latest" "${RELEASE_TAG}-distroless" "distroless"; do + echo "Mirroring pgsty/silo:${tag} -> ghcr.io/${GITHUB_REPOSITORY,,}:${tag}" + docker buildx imagetools create \ + --tag "ghcr.io/${GITHUB_REPOSITORY,,}:${tag}" \ + "pgsty/silo:${tag}" + done + # The mirrored manifests must resolve to the same digests that were + # just verified on Docker Hub; anything else means the mirror lane + # silently diverged. + for pair in \ + "${SILO_IMAGE_DIGEST} ${RELEASE_TAG}" \ + "${SILO_IMAGE_DIGEST} latest" \ + "${SILO_DISTROLESS_DIGEST} ${RELEASE_TAG}-distroless" \ + "${SILO_DISTROLESS_DIGEST} distroless"; do + set -- ${pair} + want_digest="${1}" + tag="${2}" + got_digest="$(docker buildx imagetools inspect "ghcr.io/${GITHUB_REPOSITORY,,}:${tag}" --format '{{json .Manifest.Digest}}' | tr -d '"')" + if [ "${got_digest}" != "${want_digest}" ]; then + echo "ghcr.io ${tag} resolved to ${got_digest}, want ${want_digest}" >&2 + exit 1 + fi + done + - name: Generate architecture image SBOMs env: AMD64_DIGEST: ${{ steps.build-amd64.outputs.digest }}