Skip to content
5 changes: 5 additions & 0 deletions .changeset/patch-fix-cli-proxy-latest-tag-alias.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 37 additions & 7 deletions actions/setup/sh/download_docker_images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ set +o histexpand
# When images include a digest pin (e.g. image:tag@sha256:abc), the script
# ensures the tag alias (image:tag) is created after pulling so that tools
# referencing images by tag (such as AWF with --skip-pull) can find them.
#
# The script also aliases the image as "image:latest" so that any downstream
# tool that references the image via the mutable ":latest" tag (regardless of
# which versioned tag was actually pulled) can still resolve it locally under
# --pull-never/--skip-pull semantics. This guards against tag mismatches
# between the version-pinned tag written here and a ":latest" reference used
# elsewhere (see gh-aw#50681).

set -euo pipefail

Expand All @@ -30,14 +37,37 @@ docker_pull_with_retry() {
if timeout 5m docker pull --quiet "$image" 2>&1; then
echo "Successfully pulled $image"

# When pulling with a digest pin (image:tag@sha256:...), Docker may not
# create the tag alias automatically. Ensure the tag exists so that
# downstream tools (e.g. AWF --skip-pull) can find the image by tag.
# When pulling with a digest pin, Docker may not create a digest-free
# alias automatically. Preserve the original base reference and add back
# its implicit ":latest" or explicit tag alias so downstream tools can
# resolve the image locally under --pull-never/--skip-pull semantics.
local tag_ref="$image"
local base_ref="$image"
if [[ "$image" == *"@sha256:"* ]]; then
local tag_ref="${image%%@sha256:*}"
if [[ "$tag_ref" == *":"* ]]; then
echo "Tagging digest-pinned image as $tag_ref"
docker tag "$image" "$tag_ref"
base_ref="${image%%@sha256:*}"
tag_ref="$base_ref"
if [[ "$base_ref" == *":"* ]]; then
echo "Tagging digest-pinned image as $base_ref"
docker tag "$image" "$base_ref"
else
local latest_ref="${base_ref}:latest"
echo "Tagging digest-pinned image as $latest_ref"
docker tag "$image" "$latest_ref"
tag_ref="$latest_ref"
fi
fi

# Only AWF images need a mutable ":latest" alias for local compose
# stacks, and only when the requested reference was version-pinned. This
# avoids races when unrelated repositories are pulled concurrently with
# multiple distinct tags.
if [[ "$base_ref" == ghcr.io/github/gh-aw-* && "$tag_ref" == *":"* ]]; then
local repo_ref="${tag_ref%%:*}"
local tag_part="${tag_ref##*:}"
if [[ "$tag_part" != "latest" ]]; then
local latest_ref="${repo_ref}:latest"
echo "Aliasing $tag_ref as $latest_ref"
docker tag "$tag_ref" "$latest_ref"
Comment on lines +68 to +70
fi
fi

Expand Down
79 changes: 68 additions & 11 deletions actions/setup/sh/download_docker_images_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,66 @@ else
fi
echo ""

# Test 3: Already cached images (should be fast)
echo -e "${YELLOW}Test 3: Already cached images${NC}"
# Test 3: AWF digest-pinned images restore local aliases without retagging unrelated latest refs
echo -e "${YELLOW}Test 3: Digest-pinned AWF alias handling${NC}"
WORKDIR=$(mktemp -d)
cleanup_mock_docker() {
rm -rf "$WORKDIR"
}
cat > "$WORKDIR/docker" <<'MOCK'
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' "$*" >> "$DOCKER_LOG"
case "$1" in
pull)
exit 0
;;
tag)
exit 0
;;
*)
echo "unexpected docker command: $*" >&2
exit 1
;;
esac
MOCK
chmod +x "$WORKDIR/docker"
DOCKER_LOG="$WORKDIR/docker.log"
export DOCKER_LOG
NODE_DIGEST="$(printf 'a%.0s' {1..64})"
CLI_PROXY_DIGEST="$(printf 'b%.0s' {1..64})"
if PATH="$WORKDIR:$PATH" bash "$DOWNLOAD_SCRIPT" \
"ghcr.io/github/gh-aw-node@sha256:${NODE_DIGEST}" \
"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44@sha256:${CLI_PROXY_DIGEST}" \
alpine:3.17 > /tmp/test3.log 2>&1; then
if grep -q 'tag ghcr.io/github/gh-aw-node@sha256:' "$DOCKER_LOG" \
&& grep -q 'ghcr.io/github/gh-aw-node:latest' "$DOCKER_LOG" \
&& grep -q 'tag ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44@sha256:' "$DOCKER_LOG" \
&& grep -q 'ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.44 ghcr.io/github/gh-aw-firewall/cli-proxy:latest' "$DOCKER_LOG"; then
echo -e "${GREEN}βœ“ PASS${NC}: AWF digest aliases restored correctly"
else
echo -e "${RED}βœ— FAIL${NC}: Missing expected AWF alias commands"
cat "$DOCKER_LOG"
exit 1
fi
if grep -q 'alpine:3.17 alpine:latest' "$DOCKER_LOG"; then
echo -e "${RED}βœ— FAIL${NC}: Unrelated image was incorrectly aliased to latest"
cat "$DOCKER_LOG"
exit 1
else
echo -e "${GREEN}βœ“ PASS${NC}: Unrelated repositories were not aliased to latest"
fi
else
echo -e "${RED}βœ— FAIL${NC}: Digest alias test failed"
cat /tmp/test3.log
exit 1
fi
echo ""

# Test 4: Already cached images (should be fast)
echo -e "${YELLOW}Test 4: Already cached images${NC}"
START_TIME=$(date +%s)
if bash "$DOWNLOAD_SCRIPT" alpine:3.19 alpine:3.18 > /tmp/test3.log 2>&1; then
if bash "$DOWNLOAD_SCRIPT" alpine:3.19 alpine:3.18 > /tmp/test4.log 2>&1; then
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo -e "${GREEN}βœ“ PASS${NC}: Cached images download succeeded (${DURATION}s)"
Expand All @@ -65,30 +121,30 @@ if bash "$DOWNLOAD_SCRIPT" alpine:3.19 alpine:3.18 > /tmp/test3.log 2>&1; then
fi
else
echo -e "${RED}βœ— FAIL${NC}: Cached images download failed"
cat /tmp/test3.log
cat /tmp/test4.log
exit 1
fi
echo ""

# Test 4: Invalid image (should fail gracefully)
echo -e "${YELLOW}Test 4: Invalid image (expected to fail)${NC}"
if bash "$DOWNLOAD_SCRIPT" "nonexistent-registry.invalid/fake-image:v999" > /tmp/test4.log 2>&1; then
# Test 5: Invalid image (should fail gracefully)
echo -e "${YELLOW}Test 5: Invalid image (expected to fail)${NC}"
if bash "$DOWNLOAD_SCRIPT" "nonexistent-registry.invalid/fake-image:v999" > /tmp/test5.log 2>&1; then
echo -e "${RED}βœ— FAIL${NC}: Should have failed for invalid image"
exit 1
else
echo -e "${GREEN}βœ“ PASS${NC}: Failed as expected for invalid image"
# Check for expected error message
if grep -q "Failed to download" /tmp/test4.log; then
if grep -q "Failed to download" /tmp/test5.log; then
echo -e "${GREEN}βœ“ PASS${NC}: Error message present"
else
echo -e "${YELLOW}⚠ WARNING${NC}: Expected error message format not found"
fi
fi
echo ""

# Test 5: Empty arguments (should handle gracefully)
echo -e "${YELLOW}Test 5: No images provided${NC}"
if bash "$DOWNLOAD_SCRIPT" > /tmp/test5.log 2>&1; then
# Test 6: Empty arguments (should handle gracefully)
echo -e "${YELLOW}Test 6: No images provided${NC}"
if bash "$DOWNLOAD_SCRIPT" > /tmp/test6.log 2>&1; then
echo -e "${GREEN}βœ“ PASS${NC}: Handled empty arguments gracefully"
else
# This might fail which is also acceptable behavior
Expand All @@ -102,3 +158,4 @@ echo "=========================================="

# Cleanup
rm -f /tmp/test*.log
cleanup_mock_docker
Loading