Files
ccs/tests/docker/image-size.sh
T
Kai (Tam Nhu) TranandGitHub adf5a836fc fix: address reviewer findings round 2 (#1261 loop 2) (#1272)
* fix(docker): parameterize image with CCS_IMAGE env so smoke-test exercises new digest

docker/compose.yaml hardcoded image: ghcr.io/kaitranntt/ccs:latest, causing
network-contract.sh to always use the old published image regardless of what
IMAGE_OVERRIDE was passed. Switching to \${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}
lets CI pass CCS_IMAGE=<digest> so the smoke-test actually exercises the
just-built image. Default end-user behaviour is unchanged.

Resolves REV4 (reviewer loop 2, PR #1261).

* fix(test): image-size.sh fails loudly on manifest inspection failure

The --platform branch previously exited 0 when imagetools inspect returned
empty or zero bytes, silently passing the budget check. A broken image
could reach production undetected if buildx had any inspection issue.

Changed to exit 1 with a clear diagnostic message explaining possible causes
(old buildx, manifest format mismatch, image not yet pushed). No --allow-inspect-failure
escape hatch is provided; CI must fix the root cause.

Resolves REV5 (reviewer loop 2, PR #1261).

* test(docker): cover --platform branch in image-size-logic tests

Prior tests only exercised the local docker image inspect path. The
--platform branch (imagetools inspect) had zero coverage, meaning the
REV5 silent-pass regression would not have been caught by CI.

Added 5 mock-based test cases for the --platform branch:
- pass when platform-scoped compressed size < budget
- fail when platform-scoped compressed size > budget
- fail (exit 1) when imagetools inspect errors (REV5 guard)
- fail (exit 1) when reported size is "0" (REV5 guard)
- fail (exit 1) when size output is empty (REV5 guard)

Mocks follow the existing pattern (override docker in PATH with a temp
wrapper), extended to handle buildx imagetools inspect subcommand.

Resolves REV6 (reviewer loop 2, PR #1261).

* fix(test): compose-parity image_name() handles \${VAR:-default} syntax

After parameterizing compose.yaml's image field with \${CCS_IMAGE:-...},
the image_name() sed pipeline cut at the first colon in the shell variable
syntax (:-) instead of the tag separator, returning '\${CCS_IMAGE' and
failing the kaitranntt/ccs grep.

Added a sed step to unwrap \${VAR:-default} by extracting just the default
value before stripping the tag. Existing plain image: name:tag references
are unaffected.
2026-05-17 05:01:15 -04:00

118 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Asserts that a Docker image does not exceed a given byte budget.
#
# Usage: image-size.sh <image:tag> <max-bytes> [--platform <platform>]
# Exit: 0 on pass, 1 on fail
#
# When --platform is given the manifest for that specific platform is inspected
# via `docker buildx imagetools inspect`, summing compressed layer sizes from
# the registry manifest. This avoids pulling the image locally for each arch
# and works on a multi-arch manifest list.
#
# Without --platform the locally cached image is inspected via
# `docker image inspect`, which only reports the host-native architecture.
#
# Examples:
# image-size.sh ghcr.io/kaitranntt/ccs:latest 367001600
# image-size.sh ghcr.io/kaitranntt/ccs:latest 367001600 --platform linux/amd64
# image-size.sh ghcr.io/kaitranntt/ccs:latest 367001600 --platform linux/arm64
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "[X] Usage: $0 <image:tag> <max-bytes> [--platform <platform>]" >&2
exit 1
fi
IMAGE="$1"
MAX_BYTES="$2"
PLATFORM=""
# Parse optional --platform flag
shift 2
while [[ $# -gt 0 ]]; do
case "$1" in
--platform)
PLATFORM="$2"
shift 2
;;
*)
echo "[X] Unknown argument: $1" >&2
exit 1
;;
esac
done
# Validate that max-bytes is a positive integer
if ! [[ "$MAX_BYTES" =~ ^[0-9]+$ ]]; then
echo "[X] max-bytes must be a positive integer, got: ${MAX_BYTES}" >&2
exit 1
fi
MAX_MB=$(( MAX_BYTES / 1048576 ))
if [[ -n "$PLATFORM" ]]; then
# Multi-arch path: sum compressed layer sizes from the registry manifest.
# `docker buildx imagetools inspect --format` returns the manifest JSON for
# the given platform. We sum the `size` field of each layer entry.
echo "[i] Inspecting ${IMAGE} for platform ${PLATFORM} via registry manifest..." >&2
ACTUAL_BYTES=$(
docker buildx imagetools inspect "${IMAGE}" \
--format "{{ range .Manifest.Layers }}{{ .Size }} {{ end }}" \
--raw 2>/dev/null \
| tr ' ' '\n' \
| awk 'NF && /^[0-9]+$/ { sum += $1 } END { print sum+0 }' \
2>/dev/null || echo ""
)
if [[ -z "$ACTUAL_BYTES" || "$ACTUAL_BYTES" == "0" ]]; then
# Fallback: try the platform-specific sub-manifest
echo "[i] Falling back to platform-scoped imagetools inspect..." >&2
ACTUAL_BYTES=$(
docker buildx imagetools inspect "${IMAGE}@$(
docker buildx imagetools inspect "${IMAGE}" \
--format "{{ range .Manifest.Manifests }}{{ if eq .Platform.OS \"$(echo "${PLATFORM}" | cut -d/ -f1)\" }}{{ if eq .Platform.Architecture \"$(echo "${PLATFORM}" | cut -d/ -f2)\" }}{{ .Digest }}{{ end }}{{ end }}{{ end }}" \
2>/dev/null | head -1
)" --format "{{ range .Manifest.Layers }}{{ .Size }} {{ end }}" 2>/dev/null \
| tr ' ' '\n' \
| awk 'NF && /^[0-9]+$/ { sum += $1 } END { print sum+0 }' \
2>/dev/null || echo ""
)
fi
if [[ -z "$ACTUAL_BYTES" || "$ACTUAL_BYTES" == "0" ]]; then
echo "[X] Could not determine size for ${IMAGE} platform=${PLATFORM}" >&2
echo " Possible causes: buildx version too old, manifest format unsupported, image not pushed yet" >&2
echo " Refusing to silently pass — fix the inspection or push the image first" >&2
exit 1
fi
else
# Local-image path: use docker image inspect (host architecture only)
if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then
echo "[i] Pulling ${IMAGE}..." >&2
docker pull "$IMAGE" >&2
fi
ACTUAL_BYTES=$(docker image inspect "$IMAGE" --format='{{.Size}}' 2>/dev/null)
if [[ -z "$ACTUAL_BYTES" ]]; then
echo "[X] Could not inspect image: ${IMAGE}" >&2
exit 1
fi
fi
ACTUAL_MB=$(( ACTUAL_BYTES / 1048576 ))
LABEL="${IMAGE}${PLATFORM:+ (${PLATFORM})}"
if (( ACTUAL_BYTES > MAX_BYTES )); then
echo "[X] Image size check FAILED: ${LABEL}" >&2
echo " Actual: ${ACTUAL_BYTES} bytes (${ACTUAL_MB} MB)" >&2
echo " Budget: ${MAX_BYTES} bytes (${MAX_MB} MB)" >&2
echo " Excess: $(( ACTUAL_BYTES - MAX_BYTES )) bytes ($(( ACTUAL_MB - MAX_MB )) MB over budget)" >&2
exit 1
fi
echo "[OK] Image size check PASSED: ${LABEL}"
echo " Actual: ${ACTUAL_BYTES} bytes (${ACTUAL_MB} MB)"
echo " Budget: ${MAX_BYTES} bytes (${MAX_MB} MB)"
echo " Margin: $(( MAX_BYTES - ACTUAL_BYTES )) bytes ($(( MAX_MB - ACTUAL_MB )) MB remaining)"