diff --git a/README.md b/README.md index 9ec1f51..d903b92 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,13 @@ can clear it directly: A pin you added yourself (no marker) is never touched, and the script refuses to run if an unmarked pin points somewhere other than the gateway. +If a per-arch push reports *"is a manifest list"*, that is BuildKit's provenance +attestation: since Buildx 0.10 it is on by default, so even a single-platform +build publishes an OCI **index** rather than a plain manifest, and +`docker manifest create` refuses to nest one. The script builds with +`--provenance=false --sbom=false` and composes with `docker buildx imagetools +create`, which handles either shape. + Requirements: a Gitea PAT with **`write:package`** (`read:package` is pull-only), LAN reachability of the gateway, and qemu/binfmt registered for the arm64 build. diff --git a/push-image.sh b/push-image.sh index bad87c7..4c97abe 100755 --- a/push-image.sh +++ b/push-image.sh @@ -91,23 +91,46 @@ esac docker login "$REGISTRY" || die "login failed — the PAT needs write:package, not read:package" +# --provenance=false --sbom=false: since Buildx 0.10 attestations are ON by +# default, so even a SINGLE-platform build publishes an OCI INDEX (image + +# attestation) instead of a plain manifest. `docker manifest create` then +# refuses it with "is a manifest list". Suppressing them keeps each arch a +# plain manifest. Harmless if the flags are unsupported — we probe first. +PROV_FLAGS="" +if docker build --help 2>/dev/null | grep -q -- '--provenance'; then + PROV_FLAGS="--provenance=false --sbom=false" +fi + for arch in amd64 arm64; do echo "building linux/$arch" - docker build --platform "linux/$arch" -t "$REPO:$TAG-$arch" . \ - || die "build failed for $arch (arm64 needs qemu/binfmt registered)" + # shellcheck disable=SC2086 # PROV_FLAGS is deliberately word-split + docker build $PROV_FLAGS --platform "linux/$arch" -t "$REPO:$TAG-$arch" . \ + || die "build failed for $arch (arm64 needs qemu/binfmt: docker run --privileged --rm tonistiigi/binfmt --install arm64)" # Explicit tag, never $_ — in zsh $_ is the last ARGUMENT, which here is '.' docker push "$REPO:$TAG-$arch" || die "push failed for $arch" done -docker manifest rm "$REPO:$TAG" >/dev/null 2>&1 || true -docker manifest create "$REPO:$TAG" --amend "$REPO:$TAG-amd64" --amend "$REPO:$TAG-arm64" -docker manifest push "$REPO:$TAG" +# `docker buildx imagetools create` rather than `docker manifest create`: it +# composes correctly whether the sources are plain manifests OR indexes, so it +# does not fail with "is a manifest list" when attestations sneak back in. It +# is a client-side registry operation, so it honours the /etc/hosts pin too. +if docker buildx imagetools create --help >/dev/null 2>&1; then + docker buildx imagetools create -t "$REPO:$TAG" "$REPO:$TAG-amd64" "$REPO:$TAG-arm64" \ + || die "imagetools create failed" +else + docker manifest rm "$REPO:$TAG" >/dev/null 2>&1 || true + docker manifest create "$REPO:$TAG" --amend "$REPO:$TAG-amd64" --amend "$REPO:$TAG-arm64" \ + || die "manifest create failed — if it says \"is a manifest list\", rebuild with --provenance=false" + docker manifest push "$REPO:$TAG" +fi # Verify by reading it back, rather than trusting the push's exit code. echo "verifying the published manifest lists both architectures" -got=$(docker manifest inspect "$REPO:$TAG" | grep -o '"architecture": *"[a-z0-9]*"' | sort -u | wc -l) -[ "$got" -ge 2 ] || die "published manifest does not list two architectures" -docker manifest inspect "$REPO:$TAG" | grep -o '"architecture": *"[a-z0-9]*"' | sort -u +inspect=$(docker buildx imagetools inspect "$REPO:$TAG" 2>/dev/null || docker manifest inspect "$REPO:$TAG") +for want in amd64 arm64; do + echo "$inspect" | grep -q "$want" || die "published manifest is missing linux/$want" +done +echo "$inspect" | grep -iE 'platform|architecture' | sort -u | head -6 cat <