ci: multi-arch-safe php.net guard and emergency workflow (no platform change)

- scripts/image-label-amd64.sh: index-safe read of the amd64 image's labels
  (OCI index with attestations, Docker manifest list, single manifest).
- scripts/php-emergency-guard.sh: used by both build-push.yaml guard sites;
  same decisions as the old inline step on single-platform images, but FAILS
  CLOSED when the labels cannot be read (only "tag not found" passes).
- lsphp-source-emergency.yaml: push by digest + assemble (copy of HMB
  assemble-multiarch-index.sh); optional arm64; refuses to replace a
  multi-arch production tag with amd64-only unless allow_amd64_only.
- Tests wired into Shell-Checks; main build matrix platforms unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXjXyosSkunDsdWz3mb
This commit is contained in:
shadowdaoandClaude Sonnet 5.5 committed 2026-11-11 17:22:24 -08:00
1 parent 4b35869477
commit 8a6a6d7b44
8 files changed
+565 -24

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
## image-label-amd64.sh REF LABEL [LABEL...]
##
## Prints the value of each LABEL (one per line, empty line if the label is
## absent) from the *linux/amd64* image behind REF, whether REF is a plain
## single-platform manifest, an OCI image index (attestation children are
## ignored) or a Docker manifest list.
##
## Why: `imagetools inspect REF --format '{{ index .Image.Config.Labels ... }}'`
## is only meaningful for a single-platform image; on a multi-arch index `.Image`
## is platform-keyed / unavailable depending on the buildx version, and the old
## callers swallowed the error (fail-open). This resolves the amd64 child
## digest from the raw index, then inspects that child directly.
##
## Exit: 0 labels printed; 3 REF does not exist in the registry (not found);
## 1 anything else (network/auth error, malformed manifest, no amd64
## image, config is not amd64, ...) with a clear message on stderr.
## Env: IMAGETOOLS overrides "docker buildx imagetools" (tests use a stub).
set -euo pipefail
IMAGETOOLS=${IMAGETOOLS:-docker buildx imagetools}
die() { echo "image-label-amd64: $*" >&2; exit 1; }
[ $# -ge 2 ] || die "usage: image-label-amd64.sh REF LABEL [LABEL...]"
ref=$1; shift
# Run an imagetools command; map "tag/manifest does not exist" to exit 3.
it() {
local out err rc=0
err=$(mktemp); trap 'rm -f "$err"' RETURN
# shellcheck disable=SC2086
out=$($IMAGETOOLS "$@" 2>"$err") || rc=$?
if [ "$rc" -ne 0 ]; then
if grep -qiE 'not found|manifest unknown|name unknown|: 404' "$err"; then
echo "image-label-amd64: $ref: not found ($(head -c 300 "$err" | tr '\n' ' '))" >&2
return 3
fi
echo "image-label-amd64: imagetools $1 failed (rc=$rc): $(head -c 300 "$err" | tr '\n' ' ')" >&2
return 1
fi
printf '%s' "$out"
}
rc=0; raw=$(it inspect --raw "$ref") || rc=$?
[ "$rc" -eq 0 ] || exit "$rc"
# Classify and, for an index, pick the single linux/amd64 child digest.
rc=0
verdict=$(printf '%s' "$raw" | python3 -I -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception as e:
print("ERR not JSON: %s" % e); sys.exit(0)
if not isinstance(d, dict):
print("ERR manifest is not a JSON object"); sys.exit(0)
mt = d.get("mediaType", "")
if isinstance(d.get("manifests"), list):
kids = [m for m in d["manifests"] if isinstance(m, dict)
and isinstance(m.get("platform"), dict)
and m["platform"].get("os") == "linux"
and m["platform"].get("architecture") == "amd64"
and "vnd.docker.reference.type" not in (m.get("annotations") or {})]
digs = [m.get("digest", "") for m in kids]
if len(digs) != 1:
print("ERR index has %d linux/amd64 images (need exactly 1)" % len(digs)); sys.exit(0)
if not digs[0].startswith("sha256:") or len(digs[0]) != 71:
print("ERR malformed amd64 child digest %r" % digs[0]); sys.exit(0)
print("INDEX " + digs[0])
elif isinstance(d.get("config"), dict) and isinstance(d.get("layers"), list):
print("SINGLE")
else:
print("ERR neither an image index nor an image manifest (mediaType=%r)" % mt)
') || rc=$?
[ "$rc" -eq 0 ] || die "could not parse manifest of $ref"
case "$verdict" in
ERR*) die "$ref: ${verdict#ERR }" ;;
SINGLE) target=$ref ;;
INDEX\ *)
base=${ref%%@*}
last=${base##*/}
[[ $last == *:* ]] && base=${base%:"${last#*:}"}
target="$base@${verdict#INDEX }" ;;
*) die "$ref: unexpected parser output" ;;
esac
rc=0; cfg=$(it inspect "$target" --format '{{json .Image}}') || rc=$?
[ "$rc" -eq 0 ] || { [ "$rc" -eq 3 ] && die "$target: not found although listed by the index"; exit "$rc"; }
printf '%s' "$cfg" | python3 -I -c '
import json, sys
try:
c = json.load(sys.stdin)
except Exception as e:
sys.stderr.write("image-label-amd64: image config is not JSON: %s\n" % e); sys.exit(1)
if not isinstance(c, dict) or c.get("architecture") != "amd64":
sys.stderr.write("image-label-amd64: image config is not linux/amd64 (architecture=%r)\n" % (c.get("architecture") if isinstance(c, dict) else None)); sys.exit(1)
cfg = c.get("config") or {}
labels = cfg.get("Labels") or {}
if not isinstance(labels, dict):
sys.stderr.write("image-label-amd64: Labels is not an object\n"); sys.exit(1)
for name in sys.argv[1:]:
v = labels.get(name, "")
print(v if isinstance(v, str) else "")
' "$@"