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

+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Assemble the multi-arch index tag(s) from per-platform images that were
# already pushed BY DIGEST (no tag). Used by the trusted x86 publish job
# (copied verbatim from haproxy-manager-base PR #17; see its build-push-coraza.yaml; template for the other WHP
# image repos, docs/arm64/IMAGE-PIPELINE-PLAN.md section 4.1/4.4).
#
# assemble-multiarch-index.sh \
# --amd64 sha256:<digest> \
# --arm64 sha256:<digest> --arm64-outcome success|failure|cancelled|skipped \
# --repo <registry/path> [--repo <other registry/path> ...] \
# --tag <tag> [--tag <tag> ...] [--builder <buildx builder>]
#
# Rules (all enforced here so the workflow stays thin and these are testable):
# * amd64 is mandatory. A missing/malformed amd64 digest, or an amd64 digest
# that does not inspect as linux/amd64, aborts with exit 1 and publishes
# NOTHING (the same "build failed, no tag" outcome as before multi-arch).
# * arm64 is attached ONLY when its step outcome is exactly "success" AND the
# digest is well-formed AND it inspects as linux/arm64. Otherwise the tag is
# published amd64-only (warning, exit 0). The arm64 digest must come from
# THIS run's step output - the workflow never reads it from a registry tag,
# so a previous build's arm64 can never be re-attached to a newer amd64.
# * every --tag is created in every --repo from <repo>@<digest> sources in
# that same repo (both digests were pushed to every --repo by the build).
#
# Env: IMAGETOOLS overrides the command (default: "docker buildx imagetools");
# the test suite points it at a stub.
set -euo pipefail
IMAGETOOLS=${IMAGETOOLS:-docker buildx imagetools}
amd64=""; arm64=""; outcome="skipped"; builder=""
repos=(); tags=()
die() { echo "assemble-multiarch-index: $*" >&2; exit 1; }
warn() { echo "::warning::assemble-multiarch-index: $*"; }
while [ $# -gt 0 ]; do
case "$1" in
--amd64) amd64=${2-}; shift 2 ;;
--arm64) arm64=${2-}; shift 2 ;;
--arm64-outcome) outcome=${2-}; shift 2 ;;
--repo) repos+=("${2-}"); shift 2 ;;
--tag) tags+=("${2-}"); shift 2 ;;
--builder) builder=${2-}; shift 2 ;;
*) die "unknown argument: $1" ;;
esac
done
re='^sha256:[0-9a-f]{64}$'
[ ${#repos[@]} -gt 0 ] || die "need at least one --repo"
[ ${#tags[@]} -gt 0 ] || die "need at least one --tag"
[[ $amd64 =~ $re ]] || die "amd64 digest missing or malformed ('$amd64'); refusing to publish"
bflag=(); [ -n "$builder" ] && bflag=(--builder "$builder")
# arch_of <ref>: prints the image config's architecture ("" on any failure).
arch_of() {
# shellcheck disable=SC2086
$IMAGETOOLS inspect "${bflag[@]}" "$1" --format '{{json .Image}}' 2>/dev/null \
| python3 -I -c 'import json,sys; print(json.load(sys.stdin).get("architecture",""))' 2>/dev/null || true
}
for r in "${repos[@]}"; do
[ "$(arch_of "$r@$amd64")" = amd64 ] || die "$r@$amd64 does not inspect as linux/amd64; refusing to publish"
done
use_arm64=0
if [ "$outcome" != success ]; then
warn "arm64 step outcome is '$outcome': publishing amd64-only (no arm64 digest is carried over)"
elif ! [[ $arm64 =~ $re ]]; then
warn "arm64 step reported success but its digest is missing/malformed: publishing amd64-only"
elif [ "$arm64" = "$amd64" ]; then
warn "arm64 digest equals the amd64 digest: publishing amd64-only"
else
use_arm64=1
for r in "${repos[@]}"; do
if [ "$(arch_of "$r@$arm64")" != arm64 ]; then
warn "$r@$arm64 does not inspect as linux/arm64: publishing amd64-only"
use_arm64=0; break
fi
done
fi
for r in "${repos[@]}"; do
targs=(); for t in "${tags[@]}"; do targs+=(-t "$r:$t"); done
srcs=("$r@$amd64"); [ "$use_arm64" = 1 ] && srcs+=("$r@$arm64")
echo "assemble-multiarch-index: $r tags=${tags[*]} sources=${#srcs[@]} ($([ "$use_arm64" = 1 ] && echo 'amd64+arm64' || echo 'amd64-only'))"
# shellcheck disable=SC2086
$IMAGETOOLS create "${bflag[@]}" "${targs[@]}" "${srcs[@]}"
done
echo "platforms=$([ "$use_arm64" = 1 ] && echo linux/amd64,linux/arm64 || echo linux/amd64)"
+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 "")
' "$@"
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
## index-platforms.sh REF
## Prints "index <os/arch> ..." (real platforms only, attestation children
## excluded) when REF is an image index / manifest list, or "single" when it is
## a plain single-platform manifest. Exit 3 = REF not found, 1 = any other
## failure (fail closed). Used by lsphp-source-emergency.yaml to refuse to
## replace a multi-arch tag with an amd64-only one.
## Env: IMAGETOOLS overrides "docker buildx imagetools" (tests use a stub).
set -euo pipefail
IMAGETOOLS=${IMAGETOOLS:-docker buildx imagetools}
ref=${1:?usage: index-platforms.sh REF}
err=$(mktemp); trap 'rm -f "$err"' EXIT
rc=0
# shellcheck disable=SC2086
raw=$($IMAGETOOLS inspect --raw "$ref" 2>"$err") || rc=$?
if [ "$rc" -ne 0 ]; then
if grep -qiE 'not found|manifest unknown|name unknown|: 404' "$err"; then echo "index-platforms: $ref not found" >&2; exit 3; fi
echo "index-platforms: imagetools failed: $(head -c 300 "$err" | tr '\n' ' ')" >&2; exit 1
fi
printf '%s' "$raw" | python3 -I -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception as e:
sys.stderr.write("index-platforms: not JSON: %s\n" % e); sys.exit(1)
if isinstance(d, dict) and isinstance(d.get("manifests"), list):
ps = []
for m in d["manifests"]:
p = m.get("platform") if isinstance(m, dict) else None
if not isinstance(p, dict) or "vnd.docker.reference.type" in (m.get("annotations") or {}):
continue
if p.get("os") in (None, "", "unknown") or p.get("architecture") in (None, "", "unknown"):
continue
ps.append("%s/%s" % (p["os"], p["architecture"]))
print("index " + " ".join(sorted(set(ps))))
elif isinstance(d, dict) and isinstance(d.get("config"), dict):
print("single")
else:
sys.stderr.write("index-platforms: neither index nor image manifest\n"); sys.exit(1)
'
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
## php-emergency-guard.sh REF NEW_PHP
##
## "Do not roll back a newer php.net emergency image." REF is the published
## :phpNN tag a normal build is about to overwrite; NEW_PHP is the PHP version
## the normal build packages. Exit 0 = go ahead, 1 = refuse (or cannot tell).
##
## Decision (same predicate as the old inline step, evaluated on the amd64
## image only, so it is correct for a single-platform image AND a multi-arch
## index):
## REF does not exist yet (registry "not found") -> proceed (nothing to protect)
## labels read, source != php.net -> proceed
## labels read, source = php.net, cur == new -> proceed
## labels read, source = php.net, cur > new (sort -V) -> REFUSE
## labels read, source = php.net, cur < new -> proceed
## labels absent (older image without them) -> proceed (as before)
## ANYTHING ELSE (registry/auth/network error, malformed manifest, index
## without exactly one linux/amd64 image, non-amd64 config) -> REFUSE
## (FAIL CLOSED; the old step failed open and silently disabled the guard).
## Env: LABEL_SCRIPT (tests), IMAGETOOLS passed through.
set -euo pipefail
ref=${1:?usage: php-emergency-guard.sh REF NEW_PHP}
new=${2-}
here=$(cd "$(dirname "$0")" && pwd)
labeler=${LABEL_SCRIPT:-$here/image-label-amd64.sh}
rc=0
out=$("$labeler" "$ref" org.whp.lsphp-source org.whp.php-version) || rc=$?
if [ "$rc" -eq 3 ]; then
echo "published $ref: not found (first build); this build: packaged $new. Nothing to protect."
exit 0
fi
if [ "$rc" -ne 0 ]; then
echo "::error::cannot read the php.net-emergency labels of the amd64 image behind $ref (rc=$rc). Refusing to overwrite it blind (fail closed). Retry the run; if it persists check registry access and the manifest."
exit 1
fi
src=$(printf '%s\n' "$out" | sed -n 1p)
cur=$(printf '%s\n' "$out" | sed -n 2p)
echo "published $ref (amd64): source=${src:-?} php=${cur:-?}; this build: packaged $new"
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then
echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed."
exit 1
fi
exit 0
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
## image-label-amd64.test.sh — fixtures for scripts/image-label-amd64.sh and
## scripts/php-emergency-guard.sh, driven through a stub IMAGETOOLS.
## Proves: index-safe (two-platform OCI index WITH attestation children, Docker
## manifest list, single-platform manifest), fail-closed on malformed/ambiguous
## input, "not found" = proceed, and that for single-platform images the guard
## makes the SAME decision as the old inline step on every (src,cur,new) case.
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
mkdir -p "$T/raw" "$T/cfg" "$T/err"
fail=0
chk() { if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1: got '$2' want '$3'"; fail=1; fi; }
# ---- stub: inspect --raw REF / inspect REF --format ... ; fixtures keyed by ref
key() { printf '%s' "$1" | tr '/:@' '___'; }
cat >"$T/it" <<'STUB'
#!/usr/bin/env bash
# args: inspect --raw REF | inspect REF --format F
d=$FIXDIR
[ "$1" = inspect ] || exit 2
if [ "$2" = --raw ]; then ref=$3; kind=raw; else ref=$2; kind=cfg; fi
k=$(printf '%s' "$ref" | tr '/:@' '___')
if [ -f "$d/err/$k" ]; then cat "$d/err/$k" >&2; exit 1; fi
[ -f "$d/$kind/$k" ] || { echo "ERROR: $ref: not found" >&2; exit 1; }
cat "$d/$kind/$k"
STUB
chmod +x "$T/it"
export FIXDIR=$T IMAGETOOLS="$T/it"
A=sha256:$(printf 'a%.0s' {1..64}); B=sha256:$(printf 'b%.0s' {1..64})
C=sha256:$(printf 'c%.0s' {1..64}); D=sha256:$(printf 'd%.0s' {1..64})
REPO=reg.example/org/img
put() { printf '%s' "$3" >"$T/$1/$(key "$2")"; }
cfg() { # arch src php -> image config json
printf '{"architecture":"%s","os":"linux","config":{"Labels":{"org.whp.lsphp-source":"%s","org.whp.php-version":"%s"}}}' "$1" "$2" "$3"; }
# 1. OCI index: amd64 + arm64 + two attestation children (unknown/unknown)
put raw $REPO:php85 '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$A'","platform":{"architecture":"amd64","os":"linux"}},
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$C'","platform":{"architecture":"unknown","os":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest","vnd.docker.reference.digest":"'$A'"}},
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$B'","platform":{"architecture":"arm64","os":"linux"}},
{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"'$D'","platform":{"architecture":"unknown","os":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest","vnd.docker.reference.digest":"'$B'"}}]}'
put cfg $REPO@$A "$(cfg amd64 php.net 8.5.12)"
put cfg $REPO@$B "$(cfg arm64 php.net 8.5.99)" # must never be read
printf '%s\n%s\n' php.net 8.5.12 >"$T/want"
chk "index+attestations: amd64 labels" "$("$HERE/image-label-amd64.sh" $REPO:php85 org.whp.lsphp-source org.whp.php-version)" "$(cat "$T/want")"
# 2. Docker manifest list (no attestations)
put raw $REPO:ml '{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.list.v2+json","manifests":[
{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"'$B'","platform":{"architecture":"arm64","os":"linux","variant":"v8"}},
{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"'$A'","platform":{"architecture":"amd64","os":"linux"}}]}'
chk "docker manifest list" "$("$HERE/image-label-amd64.sh" $REPO:ml org.whp.php-version)" "8.5.12"
# 3. Single-platform manifest (today's amd64-only images); ref with digest too
put raw $REPO:single '{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"digest":"'$A'"},"layers":[]}'
put cfg $REPO:single "$(cfg amd64 litespeed 8.5.1)"
chk "single-platform manifest" "$("$HERE/image-label-amd64.sh" $REPO:single org.whp.lsphp-source org.whp.php-version | tr '\n' ,)" "litespeed,8.5.1,"
chk "absent label prints empty" "$("$HERE/image-label-amd64.sh" $REPO:single org.whp.nope)" ""
put raw $REPO:nolabels '{"mediaType":"x","config":{},"layers":[]}'
put cfg $REPO:nolabels '{"architecture":"amd64","config":{}}'
chk "config without Labels" "$("$HERE/image-label-amd64.sh" $REPO:nolabels org.whp.php-version)" ""
# 4. failures: each must exit non-zero (1) with nothing on stdout
bad() { # name ref
local out rc=0; out=$("$HERE/image-label-amd64.sh" "$2" org.whp.php-version 2>/dev/null) || rc=$?
chk "$1 -> rc $3" "$rc" "$3"; chk "$1 -> no stdout" "$out" ""
}
bad "tag not found" $REPO:nosuch 3
put raw $REPO:garbage 'this is not json'; bad "malformed JSON" $REPO:garbage 1
put raw $REPO:arr '[1,2]'; bad "JSON not an object" $REPO:arr 1
put raw $REPO:unk '{"foo":"bar"}'; bad "neither index nor manifest" $REPO:unk 1
put raw $REPO:armonly '{"manifests":[{"digest":"'$B'","platform":{"architecture":"arm64","os":"linux"}}]}'; bad "index without amd64" $REPO:armonly 1
put raw $REPO:two '{"manifests":[{"digest":"'$A'","platform":{"architecture":"amd64","os":"linux"}},{"digest":"'$D'","platform":{"architecture":"amd64","os":"linux"}}]}'; bad "ambiguous amd64" $REPO:two 1
put raw $REPO:baddig '{"manifests":[{"digest":"sha256:short","platform":{"architecture":"amd64","os":"linux"}}]}'; bad "malformed child digest" $REPO:baddig 1
put raw $REPO:badcfg '{"manifests":[{"digest":"'$D'","platform":{"architecture":"amd64","os":"linux"}}]}'
put cfg $REPO@$D 'not json'; bad "malformed config" $REPO:badcfg 1
put raw $REPO:wrongarch '{"manifests":[{"digest":"'$C'","platform":{"architecture":"amd64","os":"linux"}}]}'
put cfg $REPO@$C "$(cfg arm64 php.net 8.5.12)"; bad "amd64 slot with arm64 config" $REPO:wrongarch 1
printf 'ERROR: denied: requested access to the resource is denied\n' >"$T/err/$(key $REPO:denied)"; bad "auth/network error is NOT 'not found'" $REPO:denied 1
put raw $REPO:orphan '{"manifests":[{"digest":"'$B'","platform":{"architecture":"amd64","os":"linux"}}]}'; bad "child listed but missing" $REPO:orphan 1
# 5. Guard == old inline logic on single-platform fixtures, all combos
old() { # src cur new -> proceed|refuse (verbatim predicate of the old step)
local src=$1 cur=$2 new=$3
if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then echo refuse; else echo proceed; fi; }
n=0
for src in php.net litespeed ""; do for cur in "" 8.5.11 8.5.12 8.5.13 8.4.99 8.10.0; do for new in 8.5.12 8.5.9 8.6.0; do
put raw $REPO:g '{"mediaType":"x","config":{},"layers":[]}'
put cfg $REPO:g "$(cfg amd64 "$src" "$cur")"
rc=0; "$HERE/php-emergency-guard.sh" $REPO:g "$new" >/dev/null 2>&1 || rc=$?
got=proceed; [ "$rc" -ne 0 ] && got=refuse
w=$(old "$src" "$cur" "$new"); n=$((n+1))
[ "$got" = "$w" ] || { echo "FAIL guard vs old: src='$src' cur='$cur' new='$new' got=$got old=$w"; fail=1; }
done; done; done
echo "ok guard == old inline logic on $n single-platform combos"
# 6. Guard on a multi-arch index: refuses the rollback (old logic could not even read it)
rc=0; "$HERE/php-emergency-guard.sh" $REPO:php85 8.5.9 >/dev/null 2>&1 || rc=$?; chk "index: emergency 8.5.12 vs packaged 8.5.9 refused" "$rc" 1
rc=0; "$HERE/php-emergency-guard.sh" $REPO:php85 8.6.0 >/dev/null 2>&1 || rc=$?; chk "index: packaged newer proceeds" "$rc" 0
# 7. fail closed vs first build
rc=0; "$HERE/php-emergency-guard.sh" $REPO:nosuch 8.5.9 >/dev/null 2>&1 || rc=$?; chk "tag absent: proceed (first build)" "$rc" 0
for t in garbage denied armonly orphan wrongarch; do
rc=0; "$HERE/php-emergency-guard.sh" $REPO:$t 8.5.9 >/dev/null 2>&1 || rc=$?; chk "unreadable ($t): FAIL CLOSED" "$rc" 1
done
[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "FAILURES"; exit 1; }
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
## index-platforms.test.sh — scripts/index-platforms.sh and
## scripts/assemble-multiarch-index.sh (copy of haproxy-manager-base's) against a stub IMAGETOOLS.
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
fail=0
chk() { if [ "$2" = "$3" ]; then echo "ok $1"; else echo "FAIL $1: got '$2' want '$3'"; fail=1; fi; }
A=sha256:$(printf 'a%.0s' {1..64}); B=sha256:$(printf 'b%.0s' {1..64}); C=sha256:$(printf 'c%.0s' {1..64})
cat >"$T/it" <<'STUB'
#!/usr/bin/env bash
# index-platforms mode: serves $FIX/<ref-with-/:@ as _> for "inspect --raw REF".
# assemble mode (STUB_LOG set): "inspect REF --format .." -> arch from $ARCHES (ref=arch lines);
# "create ..." -> appended to $STUB_LOG.
if [ -n "${STUB_LOG:-}" ]; then
if [ "$1" = create ]; then echo "create $*" >>"$STUB_LOG"; exit 0; fi
ref=$2; a=$(grep -F "$ref=" "$ARCHES" | head -1 | cut -d= -f2); [ -n "$a" ] || exit 1
printf '{"architecture":"%s"}' "$a"; exit 0
fi
[ "$2" = --raw ] || exit 2
k=$(printf '%s' "$3" | tr '/:@' '___')
if [ -f "$FIX/err_$k" ]; then cat "$FIX/err_$k" >&2; exit 1; fi
[ -f "$FIX/$k" ] || { echo "ERROR: $3: not found" >&2; exit 1; }
cat "$FIX/$k"
STUB
chmod +x "$T/it"; export IMAGETOOLS="$T/it" FIX=$T
w() { printf '%s' "$2" >"$T/$(printf '%s' "$1" | tr '/:@' '___')"; }
w r/i:idx '{"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"digest":"'$A'","platform":{"os":"linux","architecture":"amd64"}},{"digest":"'$B'","platform":{"os":"linux","architecture":"arm64"}},{"digest":"'$C'","platform":{"os":"unknown","architecture":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest"}}]}'
w r/i:amd '{"manifests":[{"digest":"'$A'","platform":{"os":"linux","architecture":"amd64"}},{"digest":"'$C'","platform":{"os":"unknown","architecture":"unknown"},"annotations":{"vnd.docker.reference.type":"attestation-manifest"}}]}'
w r/i:one '{"config":{"digest":"'$A'"},"layers":[]}'
w r/i:bad 'garbage'
printf 'ERROR: denied\n' >"$T/err_r_i_den"
chk "two-platform index" "$("$HERE/index-platforms.sh" r/i:idx)" "index linux/amd64 linux/arm64"
chk "amd64 index with attestation" "$("$HERE/index-platforms.sh" r/i:amd)" "index linux/amd64"
chk "single manifest" "$("$HERE/index-platforms.sh" r/i:one)" "single"
for pair in "nosuch 3" "bad 1" "den 1"; do set -- $pair
rc=0; "$HERE/index-platforms.sh" r/i:$1 >/dev/null 2>&1 || rc=$?; chk "$1 -> rc $2" "$rc" "$2"; done
# ---- assemble-multiarch-index.sh (arm64 attach rules)
export STUB_LOG=$T/log ARCHES=$T/arches
R=reg/x
printf '%s\n' "$R@$A=amd64" "$R@$B=arm64" >"$ARCHES"
run() { : >"$STUB_LOG"; "$HERE/assemble-multiarch-index.sh" "$@" >/dev/null 2>&1; echo $?; }
chk "both ok -> rc 0" "$(run --amd64 $A --arm64 $B --arm64-outcome success --repo $R --tag t1 --tag t2)" 0
chk "both ok -> 1 create with 2 sources, 2 tags" "$(grep -c "create create -t $R:t1 -t $R:t2 $R@$A $R@$B\$" "$STUB_LOG")" 1
chk "arm64 failed (digest present, stale) -> amd64 only" "$(run --amd64 $A --arm64 $B --arm64-outcome failure --repo $R --tag t1)" 0
chk " ... create has no arm64 digest" "$(grep -c "$B" "$STUB_LOG" || true)" 0
run --amd64 $A --repo $R --tag t1 >/dev/null
chk "arm64 skipped -> amd64 only" "$(grep -c "$B" "$STUB_LOG" || true)" "0"
chk "malformed amd64 digest -> refuse" "$(run --amd64 nope --repo $R --tag t1)" 1
chk " ... nothing created" "$(grep -c create "$STUB_LOG" || true)" 0
printf '%s\n' "$R@$A=arm64" >"$ARCHES"
chk "amd64 digest that is not amd64 -> refuse" "$(run --amd64 $A --repo $R --tag t1)" 1
[ "$fail" -eq 0 ] && echo "ALL PASS" || { echo FAILURES; exit 1; }