fix(ci): mirror index copy works on Gitea (pinned buildx, digest assertion via raw bytes) [skip ci]
Run 5156 failed copying python:3.12-slim / golang:1.25 into Gitea: `PUT .../manifests/<tag>-staging: 404`, leaving a stray single-manifest <tag>-staging version. Reproduced on real Gitea with scratch packages: buildx <= 0.20.x (runner distro buildx is 0.15.1) pushes the children of a copied index to the TAG path; Gitea only records manifests PUT by digest, so the index PUT is rejected (MANIFEST_BLOB_UNKNOWN -> 404). buildx >= 0.24 pushes children by digest and works; Gitea then preserves the index digest, so the digest-equality assertion stays. - workflow: install a sha256-verified pinned buildx (v0.28.0) instead of setup-buildx-action (imagetools needs no builder) and pass it as $BUILDX - mirror-index-copy.sh: BUILDX/MIN_BUILDX guard refuses buildx < 0.24 before any registry traffic; digest computed as sha256 of the raw index bytes (buildx 0.28 ignores `inspect --format` here) - test: attestation-children index and old-buildx guard cases Merge with [skip ci] in the merge title: any push to main triggers the HMB image build. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
This commit is contained in:
1 parent
74fc9ef183
commit
cf8f09612f
3 files changed
+74
-7
No files matched your search
@@ -18,18 +18,45 @@
|
||||
## at 4 cannot be rolled back by this script (the tag already moved) but it is
|
||||
## loud and the same digest was already proven at step 2.
|
||||
##
|
||||
## Gitea compatibility (measured against repo.anhonesthost.net, 2026-11-11):
|
||||
## * Gitea's registry accepts whole indexes (OCI index, Docker manifest list,
|
||||
## attestation children, 14-16 children) and preserves the index digest
|
||||
## byte-for-byte, so the digest-equality assertion below holds there too.
|
||||
## * It rejects an index PUT whose child manifests are not already stored as
|
||||
## by-DIGEST versions (404 MANIFEST_BLOB_UNKNOWN). buildx <= 0.20.x pushes the
|
||||
## children of a copied index to the TAG path (works on registry:2, not on
|
||||
## Gitea): the copy dies with `PUT .../manifests/<tag>: 404` and leaves a
|
||||
## stray single-manifest version named <tag>. buildx >= 0.24 pushes children by
|
||||
## digest and works. The distro buildx on the runner is 0.15.1, so the
|
||||
## workflow installs a pinned newer one and this script refuses to run on an
|
||||
## older one (MIN_BUILDX) instead of leaving a stray version behind.
|
||||
##
|
||||
## Env: REQUIRED_PLATFORMS (default "linux/amd64 linux/arm64").
|
||||
## IMAGETOOLS (default "docker buildx imagetools"), for tests/skopeo-free CI.
|
||||
## BUILDX (default "docker buildx"): the buildx command; may be the path of
|
||||
## a pinned docker-buildx binary.
|
||||
## IMAGETOOLS (default "$BUILDX imagetools").
|
||||
## MIN_BUILDX (default 0.24.0; empty disables the version guard).
|
||||
## (buildx talks plain HTTP to a localhost registry, so the test needs no flags.)
|
||||
set -euo pipefail
|
||||
|
||||
SRC="${1:?usage: mirror-index-copy.sh SRC@sha256:DIGEST DST_REPO:TAG}"
|
||||
DST="${2:?usage: mirror-index-copy.sh SRC@sha256:DIGEST DST_REPO:TAG}"
|
||||
REQUIRED_PLATFORMS="${REQUIRED_PLATFORMS:-linux/amd64 linux/arm64}"
|
||||
read -r -a IMGTOOLS <<<"${IMAGETOOLS:-docker buildx imagetools}"
|
||||
BUILDX="${BUILDX:-docker buildx}"
|
||||
MIN_BUILDX="${MIN_BUILDX-0.24.0}"
|
||||
read -r -a BX <<<"$BUILDX"
|
||||
read -r -a IMGTOOLS <<<"${IMAGETOOLS:-${BUILDX} imagetools}"
|
||||
|
||||
fail() { echo "::error::mirror-index-copy: $*" >&2; exit 1; }
|
||||
|
||||
# Version guard: old buildx corrupts the copy on Gitea (see header).
|
||||
if [ -n "$MIN_BUILDX" ]; then
|
||||
have_bx=$("${BX[@]}" version 2>/dev/null | awk 'NR==1{print $2}'); have_bx="${have_bx#v}"
|
||||
[[ "$have_bx" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]] || fail "cannot determine buildx version from '${BUILDX} version'"
|
||||
[ "$(printf '%s\n%s\n' "$MIN_BUILDX" "${have_bx%%[-+]*}" | sort -V | head -1)" = "$MIN_BUILDX" ] \
|
||||
|| fail "buildx ${have_bx} is too old (need >= ${MIN_BUILDX}): older buildx pushes index children by tag, which Gitea rejects"
|
||||
fi
|
||||
|
||||
# Validate shapes (registry host may carry :port, e.g. localhost:5055).
|
||||
[[ "$SRC" =~ ^[a-z0-9][a-z0-9._:/-]*@sha256:[0-9a-f]{64}$ ]] || fail "bad source reference '${SRC}'"
|
||||
[[ "$DST" =~ ^[a-z0-9][a-z0-9._:/-]*:[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || fail "bad destination '${DST}'"
|
||||
@@ -47,7 +74,10 @@ assert_ref() {
|
||||
for want in $REQUIRED_PLATFORMS; do
|
||||
case " $have " in *" $want "*) ;; *) fail "${ref} is missing ${want} (has: ${have})" ;; esac
|
||||
done
|
||||
got=$("${IMGTOOLS[@]}" inspect "$ref" --format '{{.Manifest.Digest}}') || fail "cannot read digest of ${ref}"
|
||||
# Digest = sha256 of the exact index bytes the registry serves (the same
|
||||
# thing a client pins by). Not `--format {{.Manifest.Digest}}`: buildx 0.28
|
||||
# ignores --format here and prints the human summary instead.
|
||||
got="sha256:$(printf '%s' "$raw" | sha256sum | cut -d' ' -f1)"
|
||||
[ "$got" = "$src_digest" ] || fail "${ref} digest ${got} != upstream ${src_digest}"
|
||||
echo "ok: ${ref} platforms=[${have}] digest=${got}"
|
||||
}
|
||||
|
||||
@@ -48,6 +48,26 @@ if "$here/mirror-index-copy.sh" "${REG}/${run}/amd64@${SINGLE}" "${REG}/${run}/d
|
||||
"$here/mirror-index-copy.sh" "${REG}/x:tag" "${REG}/${run}/d:1" >/dev/null 2>&1 && bad "tag-only source accepted" || ok "non-digest source refused"
|
||||
"$here/mirror-index-copy.sh" "${REG}/x@sha256:$(printf 'a%.0s' {1..64})" 'bad dst' >/dev/null 2>&1 && bad "bad dst accepted" || ok "bad destination refused"
|
||||
|
||||
# 6. index WITH buildx attestation children (unknown/unknown), like Docker Hub
|
||||
# official images: copy passes, digest preserved, attestations not counted as platforms
|
||||
printf 'FROM scratch\nLABEL t=att-%s\n' "$run" >"$work/Dockerfile"
|
||||
if docker buildx build -q --platform linux/amd64,linux/arm64 --provenance=mode=min --sbom=false \
|
||||
--output "type=image,name=${REG}/${run}/att:v,push=true" "$work" >/dev/null 2>&1; then
|
||||
ATT=$(digest "${REG}/${run}/att:v")
|
||||
if "$here/mirror-index-copy.sh" "${REG}/${run}/att@${ATT}" "${REG}/${run}/attdst:1" >"$work/o6" 2>&1 \
|
||||
&& [ "$(digest "${REG}/${run}/attdst:1")" = "$ATT" ]; then ok "index with attestation children copied, digest preserved ($ATT)"; else bad "attestation-index copy"; cat "$work/o6"; fi
|
||||
else bad "could not build attestation index"; fi
|
||||
|
||||
# 7. buildx version guard: a too-old buildx is refused BEFORE any registry traffic
|
||||
# (old buildx leaves a stray single-manifest tag on Gitea)
|
||||
printf '#!/bin/sh\necho "github.com/docker/buildx v0.15.1 deadbeef"\n' >"$work/old-buildx"; chmod +x "$work/old-buildx"
|
||||
if BUILDX="$work/old-buildx" "$here/mirror-index-copy.sh" "${REG}/${run}/multi@${MULTI}" "${REG}/${run}/olddst:1" >"$work/o7" 2>&1; then bad "old buildx accepted"; else
|
||||
grep -q 'too old' "$work/o7" && ok "old buildx refused: $(grep -m1 '::error::' "$work/o7")" || { bad "old-buildx failed for the wrong reason"; cat "$work/o7"; }
|
||||
docker buildx imagetools inspect "${REG}/${run}/olddst:1-staging" >/dev/null 2>&1 && bad "old buildx touched the registry" || ok "no staging tag created by refused run"
|
||||
fi
|
||||
MIN_BUILDX= BUILDX="$work/old-buildx" IMAGETOOLS="docker buildx imagetools" "$here/mirror-index-copy.sh" "${REG}/${run}/multi@${MULTI}" "${REG}/${run}/olddst2:1" >/dev/null 2>&1 \
|
||||
&& ok "MIN_BUILDX= disables the guard" || bad "guard not disabled by empty MIN_BUILDX"
|
||||
|
||||
# 5. optional: real upstream digest
|
||||
if [ -n "${MIRROR_TEST_UPSTREAM:-}" ]; then
|
||||
u="${MIRROR_TEST_UPSTREAM}"
|
||||
|
||||
Reference in new issue
Block a user