From 55398ba262c5c75891f89ad67505b7c05b644f65 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Sun, 11 Oct 2026 11:17:35 -0400 Subject: [PATCH] fix(ci): probe default images by digest, not tag@digest (D-0264) docker manifest inspect repo:tag@digest fetches the tag and then checks the result against the digest. When upstream moves a floating tag (python:3.12-slim, mailpit:latest) the check fails with "manifest verification failed for digest" even though the pinned digest still exists and still pulls; a pull of repo:tag@digest ignores the tag. Probe repo@digest instead, fail loudly on a lock entry with no digest, and probe the mirror by digest too. Reproduced locally: the tag@digest form returns rc=1 for python 05cda977, the repo@digest form returns rc=0; the fake 1111... digest still fails. Refs #602 --- .github/workflows/default-images-pullable.yml | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/default-images-pullable.yml b/.github/workflows/default-images-pullable.yml index 0811b3ad3..66625a204 100644 --- a/.github/workflows/default-images-pullable.yml +++ b/.github/workflows/default-images-pullable.yml @@ -67,8 +67,8 @@ jobs: # The list comes from the image lock (internal/compose/images.lock.json) # itself, so a new pin is covered the moment it lands and there is no - # second list. Each ref is repository:version@digest, so the gate also - # proves the pinned digest still exists upstream. + # second list. Each ref is repository:version@digest; the gate probes + # repository@digest, which proves the pinned digest still exists. go run ./tools/imagelist -platforms > /tmp/default-images.tsv if [ ! -s /tmp/default-images.tsv ]; then echo "::error::imagelist produced no images — the generator or the map is broken" @@ -85,9 +85,31 @@ jobs: while IFS="$(printf '\t')" read -r service image platforms; do [ -n "${image}" ] || continue - echo "Probing ${service} -> ${image}" - - if ! manifest_json=$(docker manifest inspect "${image}" 2>&1); then + # Probe by digest alone. `docker manifest inspect repo:tag@digest` + # fetches the TAG and then checks the result against the digest, so + # it fails ("manifest verification failed for digest") every time + # upstream moves a floating tag such as python:3.12-slim or + # mailpit:latest, even though the pinned digest still pulls. A pull + # of repo:tag@digest ignores the tag, so the probe must as well. + # Tag drift is the weekly pin-bump PR's job, not this gate's. + digest="${image##*@}" + if [ "${digest}" = "${image}" ]; then + failed=1 + echo "FAIL ${service} ${image} has no @digest pin" + echo "::error title=${service} image is not pinned::${image} has no @digest; re-run go run ./tools/imagelock -resolve" + continue + fi + name="${image%@*}" + last="${name##*/}" + case "${name}" in + */*) repo="${name%/*}/${last%%:*}" ;; + *) repo="${last%%:*}" ;; + esac + probe_ref="${repo}@${digest}" + + echo "Probing ${service} -> ${probe_ref}" + + if ! manifest_json=$(docker manifest inspect "${probe_ref}" 2>&1); then failed=1 echo "FAIL ${service} ${image}" echo "::error title=${service} image is not pullable::${image} — ${manifest_json}" @@ -114,8 +136,7 @@ jobs: done if [ "$IMAGE_MIRROR_PROBE" = "true" ] && [[ "$service" != plugin/* ]]; then - version_digest="${image#*:}" - mirror="docker.io/nself/${service}:${version_digest}" + mirror="docker.io/nself/${service}@${digest}" echo "Probing mirror -> ${mirror}" if ! docker manifest inspect "${mirror}" >/dev/null 2>&1; then failed=1