@@ -274,11 +274,12 @@ jobs:
274274 path : ${{ env.MANIFEST_SUMMARY_FILE }}
275275 if-no-files-found : warn
276276
277- # Mirror the GHCR multi-arch manifests to Docker Hub. This runs as a single
278- # serial job (NOT inside the manifest matrix): all OS families target the same
279- # Docker Hub repo, and concurrent blob uploads to one repo trip Docker Hub's
280- # rate limits, so tags are copied one at a time with retries. The copy is
281- # registry-to-registry (no pull, no rebuild). Skipped when the
277+ # Mirror the GHCR multi-arch manifests to Docker Hub. Copied with skopeo, not
278+ # `docker buildx imagetools`: the latter's blob-upload finalize is rejected by
279+ # Docker Hub's registry with "400 Bad Request", whereas skopeo's streaming
280+ # upload is accepted. The copy is registry-to-registry (no pull, no rebuild)
281+ # and `--all` carries every architecture in the manifest list. Tags are copied
282+ # one at a time (sequential) with skopeo's own retry. Skipped when the
282283 # DOCKERHUB_USERNAME secret is absent (e.g. on forks).
283284 dockerhub_mirror :
284285 name : Mirror images to Docker Hub
@@ -287,7 +288,7 @@ jobs:
287288 runs-on : ubuntu-latest
288289 env :
289290 DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}
290- DOCKERHUB_REPO : ${{ vars.DOCKERHUB_REPO || 'docker.io/ maureeungaro/gemc' }}
291+ DOCKERHUB_REPO : ${{ vars.DOCKERHUB_REPO || 'maureeungaro/gemc' }}
291292 steps :
292293 - name : Checkout repository
293294 uses : actions/checkout@v6
@@ -310,11 +311,13 @@ jobs:
310311 username : ${{ secrets.DOCKERHUB_USERNAME }}
311312 password : ${{ secrets.DOCKERHUB_TOKEN }}
312313
313- - name : Set up Buildx
314+ - name : Install skopeo
314315 if : ${{ env.DOCKERHUB_USERNAME != '' }}
315- uses : docker/setup-buildx-action@v4
316+ run : |
317+ sudo apt-get update
318+ sudo apt-get install -y skopeo
316319
317- - name : Mirror all tags (sequential, with retry )
320+ - name : Mirror all tags (sequential)
318321 if : ${{ env.DOCKERHUB_USERNAME != '' }}
319322 shell : bash
320323 env :
@@ -323,26 +326,23 @@ jobs:
323326 set -uo pipefail
324327 source ci/tags_config.sh
325328
326- copy_with_retry() {
327- local src=$1 dst=$2 n=0 max=5
328- until docker buildx imagetools create -t "$dst" "$src"; do
329- n=$((n + 1))
330- if [ "$n" -ge "$max" ]; then
331- echo "::error::failed to mirror $src -> $dst after $max attempts"
332- return 1
333- fi
334- echo "retry $n/$max for $dst; backing off $((n * 15))s"
335- sleep $((n * 15))
336- done
337- echo "mirrored $src -> $dst"
338- }
329+ # skopeo reads the credentials written by the docker login steps above.
330+ AUTHFILE="$HOME/.docker/config.json"
339331
340332 rc=0
341333 for gemcv in $(get_gemc_tags); do
342334 for pair in "${OS_VERSIONS[@]}"; do
343335 os="${pair%%=*}"; ver="${pair#*=}"
344336 tag="${gemcv}-${os}-${ver}"
345- copy_with_retry "${SRC_IMAGE}:${tag}" "${DOCKERHUB_REPO}:${tag}" || rc=1
337+ echo "Mirroring ${SRC_IMAGE}:${tag} -> ${DOCKERHUB_REPO}:${tag}"
338+ if skopeo copy --all --retry-times 5 --authfile "$AUTHFILE" \
339+ "docker://${SRC_IMAGE}:${tag}" \
340+ "docker://${DOCKERHUB_REPO}:${tag}"; then
341+ echo "mirrored ${DOCKERHUB_REPO}:${tag}"
342+ else
343+ echo "::error::failed to mirror ${SRC_IMAGE}:${tag} -> ${DOCKERHUB_REPO}:${tag}"
344+ rc=1
345+ fi
346346 done
347347 done
348348 exit "$rc"
@@ -356,7 +356,7 @@ jobs:
356356 runs-on : ubuntu-latest
357357 env :
358358 DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}
359- DOCKERHUB_REPO : ${{ vars.DOCKERHUB_REPO || 'docker.io/ maureeungaro/gemc' }}
359+ DOCKERHUB_REPO : ${{ vars.DOCKERHUB_REPO || 'maureeungaro/gemc' }}
360360 steps :
361361 - name : Checkout repository
362362 uses : actions/checkout@v6
0 commit comments