119 lines
4.8 KiB
Bash
Executable File
119 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# ==============================================================================
|
|
# docker-update.sh — Automatic daily update for the Dockerized EpicNext-CMS.
|
|
#
|
|
# Steps:
|
|
# 1. Verify the working tree is clean (uncommitted changes abort).
|
|
# 2. git pull (fast-forward only).
|
|
# 3. Run CMS migrations on the HOST (the slim runtime container has no source).
|
|
# 4. docker compose build (auto-detects pnpm/yarn/npm via lockfile).
|
|
# 5. docker compose up -d && wait for a healthy container.
|
|
# 6. Record everything in update.log.
|
|
#
|
|
# Exit codes: 0 ok, 1 update skipped, 2 build/deploy failed, 3 health failed.
|
|
# ==============================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
cd "$DIR" || exit 2
|
|
|
|
# Share the CI lock before pulling or touching the live application.
|
|
exec 9>"$DIR/.deploy.lock"
|
|
flock -w 1800 9 || exit 2
|
|
|
|
LOG_FILE="${LOG_FILE:-$DIR/logs/docker-update.log}"
|
|
PM2_APP="${PM2_APP:-next}" # host-side CMS that must stay stopped (port 3002)
|
|
|
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
|
|
die() { log "ERROR: $*"; exit "${2:-2}"; }
|
|
|
|
touch "$LOG_FILE"
|
|
|
|
log "=== Start docker-update ==="
|
|
|
|
# --- 0. Preflight: verify this VPS is ready (permissions, ports, deps) ---
|
|
if ! "$DIR/scripts/docker-preflight.sh"; then
|
|
die "preflight failed — fix issues first (see '--fix' flag)" 1
|
|
fi
|
|
log "preflight OK"
|
|
|
|
# --- 0b. Guard: uncommitted changes would break git pull / taint deploys ---
|
|
if ! { git diff --quiet --exit-code && git diff --cached --quiet --exit-code; }; then
|
|
die "working tree has uncommitted changes; commit or stash first" 1
|
|
fi
|
|
|
|
# --- 1. Pull latest ---
|
|
git pull --ff-only --quiet 2>>"$LOG_FILE"
|
|
pull_status=$?
|
|
if [ $pull_status -ne 0 ]; then
|
|
die "git pull failed (status $pull_status)" 1
|
|
fi
|
|
log "git pull OK: $(git rev-parse --short HEAD)"
|
|
|
|
# --- 2. Host-side migrations (idempotent; only applies CMS-owned tables) ---
|
|
if [ -f pnpm-lock.yaml ] && command -v pnpm >/dev/null 2>&1; then
|
|
pnpm db:migrate >>"$LOG_FILE" 2>&1 || die "db:migrate (pnpm) failed"
|
|
elif command -v npm >/dev/null 2>&1; then
|
|
npm run db:migrate >>"$LOG_FILE" 2>&1 || die "db:migrate (npm) failed"
|
|
else
|
|
die "no package manager found for migrations" 2
|
|
fi
|
|
log "db:migrate OK"
|
|
|
|
# --- 3. Node major gate (patches auto, major upgrades need review) ---
|
|
# `node:alpine` floats within, then across, Node majors. Patches/minors are
|
|
# safe to apply silently; a NEW major (e.g. 26 -> 27) is a breaking risk for
|
|
# native addons / Next compatibility, so require an explicit review before it
|
|
# goes live. Compare the major of the deployed runtime image vs the floating
|
|
# tag; abort (not deploy) when they differ.
|
|
deployed_major="$(docker inspect --format '{{.Config.Image}}' epicnext-cms 2>/dev/null || true)"
|
|
# Resolve the currently-deployed Node major from its image.
|
|
if [ -n "$deployed_major" ] && docker image inspect "$deployed_major" >/dev/null 2>&1; then
|
|
deployed_major="$(docker run --rm --entrypoint sh "$deployed_major" -c 'node -p "process.versions.node.split(\".\")[0]"' 2>/dev/null || true)"
|
|
fi
|
|
float_major="$(docker run --rm --entrypoint sh node:alpine -c 'node -p "process.versions.node.split(\".\")[0]"' 2>/dev/null || true)"
|
|
if [ -n "$deployed_major" ] && [ -n "$float_major" ] && [ "$deployed_major" != "$float_major" ]; then
|
|
die "Node major change detected (deployed v$deployed_major, floating tag v$float_major). Major upgrades require review; update engines/Dockerfile deliberately first." 1
|
|
fi
|
|
log "Node major gate OK (major=${float_major:-?})"
|
|
|
|
# --- 4. Rebuild the image ---
|
|
# Reset the BuildKit cache first so the build doesn't accumulate unbounded
|
|
# layers on disk across daily rebuilds.
|
|
docker builder prune -af --filter "until=1h" --keep-storage=0 2>>"$LOG_FILE" || true
|
|
docker compose build >>"$LOG_FILE" 2>&1 || die "docker compose build failed" 2
|
|
log "docker compose build OK"
|
|
|
|
# --- 5. Recreate the container ---
|
|
docker compose up -d >>"$LOG_FILE" 2>&1 || die "docker compose up failed" 2
|
|
log "docker compose up OK"
|
|
|
|
# --- 6. Wait for health (up to ~4 min) ---
|
|
healthy=0
|
|
for i in $(seq 1 16); do
|
|
status="$(docker inspect --format='{{.State.Health.Status}}' epicnext-cms 2>/dev/null || true)"
|
|
case "$status" in
|
|
healthy) healthy=1; break ;;
|
|
unhealthy) break ;;
|
|
esac
|
|
sleep 15
|
|
done
|
|
|
|
if [ "$healthy" -eq 1 ]; then
|
|
log "CMS healthy after update (commit $(git rev-parse --short HEAD))"
|
|
else
|
|
log "WARNING: container not healthy (status='${status:-unknown}')"
|
|
# Leave the container running so it can be debugged; report failure exit.
|
|
exit 3
|
|
fi
|
|
|
|
# --- 7. Make sure the stale host-side PM2 CMS stays stopped ---
|
|
if command -v pm2 >/dev/null 2>&1 && pm2 jlist >/dev/null 2>&1; then
|
|
if pm2 list 2>/dev/null | grep -q "${PM2_APP}"; then
|
|
pm2 stop "$PM2_APP" >/dev/null 2>&1 && log "pm2 '${PM2_APP}' kept stopped (avoids port 3002 clash)"
|
|
fi
|
|
fi
|
|
|
|
log "=== docker-update finished OK ==="
|
|
exit 0 |