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
This commit is contained in:
@@ -87,6 +87,17 @@ if [[ ! -f "$PROJECT_DIR/docker-compose.prod.yml" ]]; then
|
||||
error "docker-compose.prod.yml not found. Generate it first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail the release if dev and prod compose files have drifted on critical
|
||||
# healthcheck blocks — catches cases where one file was patched without
|
||||
# the other (bit us on the api start_period fix).
|
||||
if [[ -x "$PROJECT_DIR/scripts/validate-compose-parity.sh" ]]; then
|
||||
if ! bash "$PROJECT_DIR/scripts/validate-compose-parity.sh"; then
|
||||
error "Compose parity check failed. Aborting release build."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
cp "$PROJECT_DIR/docker-compose.prod.yml" "$STAGE_DIR/docker-compose.yml"
|
||||
info "docker-compose.yml (production)"
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[Unit]
|
||||
Description=Changemaker Lite upgrade dispatcher
|
||||
Documentation=https://docs.cmlite.org/docs/admin/services/
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@@ -28,6 +28,14 @@ for unit in "${SCRIPT_DIR}"/changemaker-upgrade.*; do
|
||||
echo " Installed ${filename}"
|
||||
done
|
||||
|
||||
# Ensure logs/ is writable by the install user. The API container creates
|
||||
# subdirs here as root, which locks out the host-side upgrade-watcher service.
|
||||
mkdir -p "${PROJECT_DIR}/logs"
|
||||
chown "${INSTALL_USER}:${INSTALL_USER}" "${PROJECT_DIR}/logs"
|
||||
touch "${PROJECT_DIR}/logs/upgrade-watcher.log"
|
||||
chown "${INSTALL_USER}:${INSTALL_USER}" "${PROJECT_DIR}/logs/upgrade-watcher.log"
|
||||
echo " Prepared ${PROJECT_DIR}/logs (owned by ${INSTALL_USER})"
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now changemaker-upgrade.path
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ write_result() {
|
||||
"success": ${success},
|
||||
"message": "$(echo "$msg" | sed 's/"/\\"/g')",
|
||||
"previousCommit": "${PRE_UPGRADE_SHORT:-unknown}",
|
||||
"newCommit": "$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")",
|
||||
"newCommit": "$(head -1 "$PROJECT_DIR/VERSION" 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || echo "unknown")",
|
||||
"commitCount": ${COMMIT_COUNT:-0},
|
||||
"durationSeconds": ${duration_secs},
|
||||
"warnings": ${warnings_json},
|
||||
@@ -1089,7 +1089,7 @@ fi
|
||||
info "Starting API..."
|
||||
if [[ "$NEEDS_VOLUME_REFRESH" == "true" ]]; then
|
||||
info "Removing old API/admin containers (clearing stale node_modules volumes)..."
|
||||
docker compose rm -sf api admin 2>/dev/null || true
|
||||
docker compose rm -sfv api admin 2>/dev/null || true
|
||||
fi
|
||||
docker compose up -d api
|
||||
|
||||
|
||||
78
scripts/validate-compose-parity.sh
Executable file
78
scripts/validate-compose-parity.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/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)"
|
||||
Reference in New Issue
Block a user