Files
ccs/tests/docker/compose-parity.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

150 lines
5.6 KiB
Bash

#!/usr/bin/env bash
# tests/docker/compose-parity.sh
#
# Asserts that docker/compose.yaml and docker/docker-compose.integrated.yml
# agree on the fields that form the stable network contract:
# - image name (without tag)
# - exposed host ports
# - named volume mounts
#
# This prevents drift between the public quickstart compose and the
# `ccs docker` CLI compose that is bundled with the package.
#
# Usage: bash tests/docker/compose-parity.sh
# (called from repo root)
#
set -euo pipefail
CANONICAL="docker/compose.yaml"
INTEGRATED="docker/docker-compose.integrated.yml"
fail=0
log() { printf '[i] %s\n' "$*"; }
ok() { printf '[OK] %s\n' "$*"; }
fail() { printf '[X] %s\n' "$*" >&2; fail=1; }
# ---------------------------------------------------------------------------
# Helper: extract image name (repo path without tag) for a service.
# Handles both plain image references and ${VAR:-default} shell variable
# syntax (e.g. image: ${CCS_IMAGE:-ghcr.io/kaitranntt/ccs:latest}).
# ---------------------------------------------------------------------------
image_name() {
local file="$1" service="$2"
# Match lines like:
# image: ghcr.io/owner/repo:tag
# image: ${CCS_IMAGE:-ghcr.io/owner/repo:tag}
grep -A 50 "^ ${service}:" "$file" \
| grep -m1 '^\s*image:' \
| sed 's/.*image:\s*//' \
| sed 's/\${[^:-]*:-\([^}]*\)}/\1/' \
| sed 's/:.*//' \
| tr -d ' '
}
# ---------------------------------------------------------------------------
# Helper: extract sorted list of internal container ports exposed
# ---------------------------------------------------------------------------
exposed_ports() {
local file="$1"
# Match port mappings: "HOST:CONTAINER" — extract CONTAINER port number
grep -E '^\s+- "[0-9]+:[0-9]+"' "$file" \
| sed 's/.*:\([0-9]*\)".*/\1/' \
| sort -n
}
# ---------------------------------------------------------------------------
# Helper: extract sorted list of named volume mount targets (container paths)
# ---------------------------------------------------------------------------
volume_targets() {
local file="$1"
# Match volume entries: - name:/container/path
grep -E '^\s+- [a-z_]+:/' "$file" \
| sed 's/.*:\(\/[^[:space:]]*\).*/\1/' \
| sort
}
# ---------------------------------------------------------------------------
# 1. Image name (repo without tag) — both should reference kaitranntt/ccs
# ---------------------------------------------------------------------------
log "Checking image name parity..."
CANONICAL_IMAGE=$(image_name "$CANONICAL" "ccs")
INTEGRATED_IMAGE=$(image_name "$INTEGRATED" "ccs-cliproxy")
# Both must contain kaitranntt/ccs (integrated builds locally but from the same Dockerfile)
if echo "$CANONICAL_IMAGE" | grep -q "kaitranntt/ccs" && \
echo "$INTEGRATED_IMAGE" | grep -q "ccs"; then
ok "Image names reference expected repo (canonical: ${CANONICAL_IMAGE}, integrated: ${INTEGRATED_IMAGE})"
else
fail "Image name mismatch — canonical='${CANONICAL_IMAGE}' integrated='${INTEGRATED_IMAGE}'"
fi
# ---------------------------------------------------------------------------
# 2. Exposed ports — both must expose 3000 and 8317
# Matches both quoted ("HOST:CONTAINER") and unquoted (HOST:CONTAINER) forms,
# as well as variable-interpolated host ports like "${VAR:-3000}:3000".
# ---------------------------------------------------------------------------
log "Checking exposed port parity..."
port_exposed() {
local file="$1" port="$2"
# Match container port in: "anything:PORT" or anything:PORT (quoted or bare)
grep -E "(\"[^\"]*:${port}\"|[[:space:]]-[[:space:]]+[^\"]*:${port}[^0-9])" "$file" \
> /dev/null 2>&1
}
REQUIRED_PORTS=("3000" "8317")
for port in "${REQUIRED_PORTS[@]}"; do
IN_CANONICAL=0
IN_INTEGRATED=0
port_exposed "$CANONICAL" "$port" && IN_CANONICAL=1 || true
port_exposed "$INTEGRATED" "$port" && IN_INTEGRATED=1 || true
if [[ "$IN_CANONICAL" -eq 1 && "$IN_INTEGRATED" -eq 1 ]]; then
ok "Port ${port} exposed in both compose files"
elif [[ "$IN_CANONICAL" -eq 0 ]]; then
fail "Port ${port} missing from ${CANONICAL}"
else
fail "Port ${port} missing from ${INTEGRATED}"
fi
done
# ---------------------------------------------------------------------------
# 3. Named volume mount targets — both must mount /root/.ccs and /var/log/ccs
# ---------------------------------------------------------------------------
log "Checking volume mount parity..."
REQUIRED_MOUNTS=("/root/.ccs" "/var/log/ccs")
for mount in "${REQUIRED_MOUNTS[@]}"; do
if grep -q ":${mount}" "$CANONICAL" && grep -q ":${mount}" "$INTEGRATED"; then
ok "Volume mount ${mount} present in both compose files"
elif ! grep -q ":${mount}" "$CANONICAL"; then
fail "Volume mount ${mount} missing from ${CANONICAL}"
else
fail "Volume mount ${mount} missing from ${INTEGRATED}"
fi
done
# ---------------------------------------------------------------------------
# 4. Network name — canonical must define ccs-net; integrated inherits default
# ---------------------------------------------------------------------------
log "Checking ccs-net network definition..."
if grep -q "name: ccs-net" "$CANONICAL"; then
ok "ccs-net network defined in ${CANONICAL}"
else
fail "ccs-net network definition missing from ${CANONICAL}"
fi
# ---------------------------------------------------------------------------
# Result
# ---------------------------------------------------------------------------
if [[ "$fail" -ne 0 ]]; then
echo "" >&2
echo "[X] Compose parity check FAILED — update ${INTEGRATED} to match ${CANONICAL}" >&2
exit 1
fi
echo ""
ok "Compose parity check passed"