66 lines
4.4 KiB
Bash
Executable File
66 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Update a Linux Docker Compose clone from its configured Git upstream.
|
|
set -Eeuo pipefail
|
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$DIR"
|
|
exec 9>"$DIR/.deploy.lock"
|
|
flock -w 1800 9
|
|
LOG_FILE="${LOG_FILE:-$DIR/logs/docker-update.log}"
|
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"; }
|
|
die() { log "ERROR: $*"; exit 1; }
|
|
migration_image=""
|
|
trap 'if [[ -n "$migration_image" ]]; then docker image rm "$migration_image" >>"$LOG_FILE" 2>&1 || true; fi' EXIT
|
|
trap 'log "Update failed; inspect $LOG_FILE. No volumes or local files were deleted."' ERR
|
|
|
|
# An existing CI deployment is a different owner of the same host port.
|
|
if [ "$(docker inspect --format '{{.State.Running}}' epicnext-cms-app 2>/dev/null || true)" = true ]; then
|
|
die "This host is managed by CI (epicnext-cms-app). Update through CI, not a second Compose deployment."
|
|
fi
|
|
[[ -z "$(git status --porcelain --untracked-files=normal)" ]] || die "Working tree is not clean. Commit or stash local work first."
|
|
git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null || die "Configure this branch's Git upstream before updating."
|
|
script_before="$(git hash-object scripts/docker-update.sh)"
|
|
git pull --ff-only >>"$LOG_FILE" 2>&1
|
|
if [ "$script_before" != "$(git hash-object scripts/docker-update.sh)" ]; then
|
|
log "Updater changed; restarting the newly pulled script."
|
|
exec 9>&-
|
|
exec bash "$DIR/scripts/docker-update.sh"
|
|
fi
|
|
export CMS_RELEASE="$(git rev-parse HEAD)"
|
|
[[ "$CMS_RELEASE" =~ ^[0-9a-f]{40}$ ]] || die "Invalid Git commit."
|
|
[[ -f .env ]] || die "Create .env before installing or updating."
|
|
docker info >/dev/null
|
|
docker compose config --quiet
|
|
log "Building release $CMS_RELEASE from $DIR"
|
|
# The builder contains the matching migration source and locked dependencies.
|
|
# No Node/package manager installation on the host is required.
|
|
migration_image="epicnext-cms-migrations:$CMS_RELEASE"
|
|
docker build --network=host --target builder --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" -t "$migration_image" . >>"$LOG_FILE" 2>&1
|
|
docker compose build --build-arg NEXT_DEPLOYMENT_ID="$CMS_RELEASE" cms >>"$LOG_FILE" 2>&1
|
|
expected_image="$(docker image inspect --format '{{.Id}}' "epicnext-cms:$CMS_RELEASE")"
|
|
revision="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$expected_image")"
|
|
[[ "$revision" = "$CMS_RELEASE" ]] || die "Built image has revision $revision, expected $CMS_RELEASE."
|
|
docker run --rm --network host --entrypoint pnpm "$migration_image" db:migrate >>"$LOG_FILE" 2>&1
|
|
log "Build and migrations completed; recreating only the CMS service."
|
|
docker compose up -d --no-deps --no-build --force-recreate cms >>"$LOG_FILE" 2>&1
|
|
container="$(docker compose ps -q cms)"
|
|
[[ -n "$container" ]] || die "Compose did not start the CMS container."
|
|
actual_image="$(docker inspect --format '{{.Image}}' "$container")"
|
|
[[ "$actual_image" = "$expected_image" ]] || die "Running image $actual_image differs from built image $expected_image."
|
|
# Verify the actual HTTP response, not an environment variable supplied at run time.
|
|
probe='const r=await fetch(process.argv[1],{cache:"no-store",signal:AbortSignal.timeout(5000)});const d=await r.json();if(!r.ok||d.database!==true||d.release!==process.argv[2]){console.error(JSON.stringify({http:r.status(),database:d.database,release:d.release,expected:process.argv[2]}));process.exit(1)}'
|
|
healthy=0
|
|
for attempt in $(seq 1 30); do
|
|
if docker exec "$container" node --input-type=module -e "$probe" "http://127.0.0.1:3002/api/health" "$CMS_RELEASE" >>"$LOG_FILE" 2>&1; then healthy=1; break; fi
|
|
sleep 3
|
|
done
|
|
[[ "$healthy" = 1 ]] || die "HTTP health/release verification failed. The candidate remains available for diagnosis; no success was recorded."
|
|
if [[ -n "${CMS_PUBLIC_URL:-}" ]]; then
|
|
[[ "$CMS_PUBLIC_URL" = https://* || "$CMS_PUBLIC_URL" = http://* ]] || die "CMS_PUBLIC_URL must be an HTTP(S) URL."
|
|
docker exec "$container" node --input-type=module -e "$probe" "${CMS_PUBLIC_URL%/}/api/health?release=$CMS_RELEASE" "$CMS_RELEASE" >>"$LOG_FILE" 2>&1 || die "Public domain serves another release or is unhealthy. Check reverse proxy/CDN destination."
|
|
log "Public URL verified: $CMS_PUBLIC_URL"
|
|
else
|
|
log "Public domain was not checked. Set CMS_PUBLIC_URL to verify reverse proxy/CDN routing as well."
|
|
fi
|
|
log "Verified release $CMS_RELEASE, image $actual_image, container $container"
|