changemaker.lite/scripts/validate-compose-parity.sh
bunker-admin 23df6a8b52 Fresh-install + upgrade-path hardening bundle
Six independent fixes surfaced during the v2.9.1 → v2.9.2 admin-UI
upgrade validation today. Together they make a clean install on a new
box work end-to-end without in-session patching.

- Fix 1: scripts/validate-compose-parity.sh + build-release.sh hook —
  fail release builds when api/admin/media-api/nginx healthcheck
  blocks drift between docker-compose.yml and docker-compose.prod.yml.
  Previous boot-race fix had to be applied to both files manually.

- Fix 2: scripts/systemd/install.sh chowns logs/ to the install user
  (the API container creates subdirs there as root, locking the
  host-side watcher out), pre-creates logs/upgrade-watcher.log, and
  changemaker-upgrade.service adds StartLimitIntervalSec=0 so a
  single transient failure can't wedge the .path unit permanently.

- Fix 3: /api/upgrade/status now returns a `watcher` sub-object that
  flags the host systemd watcher as stalled when trigger.json has
  been pending >30s. Admin SettingsPage SystemUpgradeTab renders a
  warning Alert with the systemctl recovery command when unhealthy.

- Fix 4: scripts/upgrade.sh write_result() — prefer head -1 VERSION
  over `git rev-parse HEAD` so release-mode upgrades report the new
  tag in result.json instead of "unknown".

- Fix 5: admin container healthcheck start_period 20s → 60s in both
  compose files, same class as the earlier api fix. Matches Gancio
  convention.

- Fix 7: /api/pangolin/sync now detects resources bound to a stale
  siteId (common after --pangolin-site new rotations), deletes and
  recreates them against the current site, and reports them under
  a new `reassigned` response field.

Bunker Admin
2026-04-15 11:57:50 -06:00

79 lines
2.9 KiB
Bash
Executable File

#!/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 `^ <service>:`, 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)"