Skip to content

Commit 52d2f61

Browse files
committed
Add caching mechanism to image sync with FORCE override
Includes: - Skip copying images if source and destination manifests match - Added `FORCE` option to bypass caching and re-copy images - Updated workflow and README to document caching behavior
1 parent 2b35f9b commit 52d2f61

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

.github/workflows/sync.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ on:
88
description: "Print actions without pushing"
99
type: boolean
1010
default: false
11+
force:
12+
description: "Re-copy even if the destination is already up to date"
13+
type: boolean
14+
default: false
1115
# Re-sync whenever the image list changes.
1216
push:
1317
branches: [main]
@@ -45,4 +49,5 @@ jobs:
4549
GHCR_USERNAME: ${{ github.actor }}
4650
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4751
DRY_RUN: ${{ inputs.dry_run || 'false' }}
52+
FORCE: ${{ inputs.force || 'false' }}
4853
run: bash scripts/sync.sh

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ images:
5555

5656
Commit to `main` and the workflow re-syncs automatically.
5757

58+
## Caching across runs
59+
60+
Before copying, the script compares the raw manifest of the source against the
61+
one already in GHCR. If they're identical the image is **skipped** — so a
62+
scheduled run only transfers tags that actually changed upstream, and unchanged
63+
tags cost a single cheap manifest lookup. (skopeo also never re-uploads layers
64+
that already exist at the destination, so even changed images reuse shared
65+
layers.)
66+
67+
Set `FORCE=true` (or tick **force** on the manual run) to re-copy everything
68+
regardless.
69+
5870
## Running locally
5971

6072
Requires `skopeo` and `yq` (v4, mikefarah).

scripts/sync.sh

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
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"
9798
log "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+
99111
failures=0
112+
skipped=0
100113
for 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
123142
done
124143

125144
if [[ "$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)"
127146
fi
128-
log "All images synced successfully"
147+
log "All images synced successfully ($skipped cached, $((count - skipped)) copied)"

0 commit comments

Comments
 (0)