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:
1 parent
4b35869477
commit
8a6a6d7b44
8 files changed
+565
-24
No files matched your search
Executable
+103
@@ -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 "")
|
||||
' "$@"
|
||||
Reference in new issue
Block a user