77# GHCR_NAMESPACE Destination namespace (default: ghcr.io/openprojectx)
88# IMAGES_FILE Path to the image list (default: images.yaml)
99# DRY_RUN If "true", print actions instead of running skopeo
10+ # FORCE If "true", re-copy even when the destination is up to date
1011# GHCR_USERNAME Username for skopeo login (optional; login skipped if unset)
1112# GHCR_TOKEN Token/password for login (optional; login skipped if unset)
1213#
@@ -96,7 +97,19 @@ count="$(yq '.images | length' "$IMAGES_FILE")"
9697[[ " $count " =~ ^[0-9]+$ && " $count " -gt 0 ]] || die " no images found in $IMAGES_FILE "
9798log " Found $count image(s) to mirror into $GHCR_NAMESPACE "
9899
100+ # Return success when the destination already holds the exact same manifest
101+ # as the source, so the image can be skipped (a cross-run "pull cache"). Set
102+ # FORCE=true to always re-copy. The raw manifests are compared byte-for-byte,
103+ # which works for both single images and multi-arch manifest lists.
104+ already_mirrored () {
105+ local source=" $1 " dest=" $2 " src_raw dst_raw
106+ dst_raw=" $( skopeo inspect --raw " docker://$dest " 2> /dev/null) " || return 1
107+ src_raw=" $( skopeo inspect --raw " docker://$source " 2> /dev/null) " || return 1
108+ [[ -n " $src_raw " && " $src_raw " == " $dst_raw " ]]
109+ }
110+
99111failures=0
112+ skipped=0
100113for i in $( seq 0 $(( count - 1 )) ) ; do
101114 source=" $( yq -r " .images[$i ].source" " $IMAGES_FILE " ) "
102115 target=" $( yq -r " .images[$i ].target // \"\" " " $IMAGES_FILE " ) "
@@ -116,13 +129,19 @@ for i in $(seq 0 $((count - 1))); do
116129 continue
117130 fi
118131
132+ if [[ " ${FORCE:- false} " != " true" ]] && already_mirrored " $source " " $dest " ; then
133+ echo " cache hit: destination already up to date, skipping"
134+ skipped=$(( skipped + 1 ))
135+ continue
136+ fi
137+
119138 if ! skopeo copy " ${copy_args[@]} " " docker://$source " " docker://$dest " ; then
120139 warn " failed to copy $source "
121140 failures=$(( failures + 1 ))
122141 fi
123142done
124143
125144if [[ " $failures " -gt 0 ]]; then
126- die " $failures image(s) failed to sync"
145+ die " $failures image(s) failed to sync ( $skipped cached, $(( count - failures - skipped )) copied) "
127146fi
128- log " All images synced successfully"
147+ log " All images synced successfully ( $skipped cached, $(( count - skipped )) copied) "
0 commit comments