#!/usr/bin/env bash # ============================================================================= # Changemaker Lite — Compose Parity Validator # # The dev (docker-compose.yml) and prod (docker-compose.prod.yml) files share # ~95% of their service definitions verbatim, but there is no tooling that # ensures they stay in sync. A drift in healthcheck tolerances between them # can cause release-tarball installs to silently fail where dev installs pass # (or vice versa). # # This script compares the `healthcheck:` block for a fixed set of critical # services between the two files and exits non-zero if any of them diverge. # # Run manually: bash scripts/validate-compose-parity.sh # Also invoked by scripts/build-release.sh before packaging the tarball. # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" DEV_FILE="${PROJECT_DIR}/docker-compose.yml" PROD_FILE="${PROJECT_DIR}/docker-compose.prod.yml" # Services whose healthcheck must be identical across dev and prod. CRITICAL_SERVICES=(api media-api admin nginx) if [[ ! -f "$DEV_FILE" ]] || [[ ! -f "$PROD_FILE" ]]; then echo "ERROR: Could not find both compose files (expected $DEV_FILE and $PROD_FILE)" >&2 exit 2 fi # Extract the healthcheck block for a given service from a compose file. # Uses awk to walk indentation: find `^ :`, then within it the # ` healthcheck:` block, and print the healthcheck lines until a sibling # key (same 4-space indent) or end of service. extract_healthcheck() { local file="$1" service="$2" awk -v svc="$service" ' # Entering the target service definition? $0 ~ "^ "svc":[[:space:]]*$" { in_svc=1; next } # Next top-level service — stop scanning in_svc && /^ [a-zA-Z0-9_-]+:[[:space:]]*$/ { in_svc=0 } # Inside target service, watch for healthcheck block in_svc && /^ healthcheck:[[:space:]]*$/ { in_hc=1; print; next } # Inside healthcheck: print until we hit a sibling key at same indent in_hc { if (/^ [a-zA-Z0-9_-]+:/) { in_hc=0 } else { print } } ' "$file" } FAIL=0 for svc in "${CRITICAL_SERVICES[@]}"; do dev_hc="$(extract_healthcheck "$DEV_FILE" "$svc")" prod_hc="$(extract_healthcheck "$PROD_FILE" "$svc")" if [[ -z "$dev_hc" ]] && [[ -z "$prod_hc" ]]; then continue # service not defined in either — fine (e.g. media-api optional) fi if [[ "$dev_hc" != "$prod_hc" ]]; then echo "DRIFT: healthcheck block for service '${svc}' differs between dev and prod compose files" >&2 echo "--- $(basename "$DEV_FILE")" >&2 echo "$dev_hc" >&2 echo "--- $(basename "$PROD_FILE")" >&2 echo "$prod_hc" >&2 echo "" >&2 FAIL=1 fi done if [[ "$FAIL" -ne 0 ]]; then echo "Compose parity check FAILED. Update both files before releasing." >&2 exit 1 fi echo "Compose parity: OK (${#CRITICAL_SERVICES[@]} services checked)"