← back to NationalPaperHangers

scripts/deploy-kamatera.sh

214 lines

#!/usr/bin/env bash
# deploy-kamatera.sh
# Syncs the National Paper Hangers app from Mac2 to Kamatera.
# Covers steps B–E of DEPLOY_KAMATERA.md (PG dump → scp → rsync → npm ci → pm2 start).
#
# SAFETY RULES:
#   1. DRY-RUN by default. Pass --commit to actually execute.
#   2. Refuses to touch nginx if protected domains are present.
#   3. Never runs `pm2 save` on Mac2.
#   4. Never modifies any file outside /root/Projects/NationalPaperHangers on Kamatera.
#
# Usage:
#   bash scripts/deploy-kamatera.sh            # dry-run (prints commands, no-ops)
#   bash scripts/deploy-kamatera.sh --commit   # executes for real

set -euo pipefail

# ── Config ────────────────────────────────────────────────────────────────────
KAMATERA_HOST="root@45.61.58.125"
REMOTE_DIR="/root/Projects/NationalPaperHangers"
LOCAL_DIR="/Users/macstudio3/Projects/NationalPaperHangers"
LOCAL_DB="national_paper_hangers"
REMOTE_DB="national_paper_hangers"
DUMP_FILE="/tmp/nph_deploy_$(date +%Y%m%d_%H%M%S).dump"
TIMESTAMP="$(date '+%Y-%m-%d %H:%M:%S')"
LOG_FILE="/tmp/deploy-kamatera-nph-$(date +%Y%m%d_%H%M%S).log"

# Protected DNS domains — if found in any nginx config this script would touch,
# abort immediately. Steve's standing rule: never touch these.
PROTECTED_DOMAINS=("designerwallcoverings.com" "studentdebtcrisis.org" "studentdebtcrisiscenter.org")

# ── Parse flags ───────────────────────────────────────────────────────────────
COMMIT=false
for arg in "$@"; do
  case "$arg" in
    --commit) COMMIT=true ;;
    --help|-h)
      echo "Usage: $0 [--commit]"
      echo "  Default: dry-run. Add --commit to execute for real."
      exit 0
      ;;
    *)
      echo "Unknown flag: $arg" >&2
      exit 1
      ;;
  esac
done

# ── Logging ───────────────────────────────────────────────────────────────────
exec > >(tee -a "$LOG_FILE") 2>&1
echo "═══════════════════════════════════════════════════════════"
echo " National Paper Hangers → Kamatera deploy script"
echo " Started: $TIMESTAMP"
echo " Mode:    $([ "$COMMIT" = true ] && echo 'COMMIT (live)' || echo 'DRY-RUN')"
echo " Log:     $LOG_FILE"
echo "═══════════════════════════════════════════════════════════"
echo ""

# ── Helper: run or echo ───────────────────────────────────────────────────────
run() {
  if [ "$COMMIT" = true ]; then
    echo "[EXEC] $*"
    eval "$@"
  else
    echo "[DRY ] $*"
  fi
}

# ── GUARD: protected-domain check ─────────────────────────────────────────────
# Inspect the nginx sites-available config we intend to create. We never read
# or write existing Kamatera nginx configs; this guard is a belt-and-suspenders
# check that the *new* nginx block file doesn't accidentally reference a
# protected domain (e.g. if this script is ever adapted for another project).
NEW_NGINX_BLOCK_CONTENT="nationalpaperhangers.com"   # only domain this script targets
for domain in "${PROTECTED_DOMAINS[@]}"; do
  if echo "$NEW_NGINX_BLOCK_CONTENT" | grep -qi "$domain"; then
    echo "[ABORT] Protected domain '$domain' detected in target nginx config. Stopping." >&2
    exit 1
  fi
done
echo "[OK] Protected-domain guard passed."
echo ""

# ── STEP B: pg_dump on Mac2 ───────────────────────────────────────────────────
echo "── Step B: pg_dump (Mac2 → $DUMP_FILE)"
# Full custom-format dump. Use --schema-only for a test deploy that skips data.
# To schema-only: add --schema-only flag to the pg_dump command below.
run "pg_dump -Fc -d '$LOCAL_DB' -f '$DUMP_FILE'"
if [ "$COMMIT" = true ]; then
  if [ ! -f "$DUMP_FILE" ]; then
    echo "[FAIL] Dump file not created: $DUMP_FILE" >&2
    exit 1
  fi
  DUMP_SIZE=$(du -sh "$DUMP_FILE" | cut -f1)
  echo "[OK] Dump created: $DUMP_FILE ($DUMP_SIZE)"
fi
echo ""

# ── STEP B (continued): scp dump to Kamatera ─────────────────────────────────
echo "── Step B (cont): scp dump to Kamatera /tmp/"
REMOTE_DUMP="/tmp/$(basename "$DUMP_FILE")"
run "scp '$DUMP_FILE' '$KAMATERA_HOST:$REMOTE_DUMP'"
echo ""

# ── STEP B (continued): pg_restore on Kamatera ───────────────────────────────
echo "── Step B (cont): pg_restore on Kamatera"
# --no-owner: owner on Kamatera is root (or whatever PG superuser). The app
# connects as a dedicated pg user; ownership is set by the CREATE ROLE step
# in the runbook.
# -c drops objects that exist first (idempotent for re-deploys).
# --if-exists prevents error if a table doesn't exist on first run.
run "ssh '$KAMATERA_HOST' \"pg_restore --no-owner -c --if-exists -d $REMOTE_DB '$REMOTE_DUMP' 2>&1 | tail -20\""
echo ""

# ── STEP C: rsync source code ─────────────────────────────────────────────────
echo "── Step C: rsync code to Kamatera"
# Excludes:
#   node_modules/  — reinstalled on target with npm ci
#   .env           — never transfer; build the .env on Kamatera manually
#   .git/          — not needed on server
#   /tmp dump files — don't loop back
run "rsync -avz --delete \
  --exclude='node_modules/' \
  --exclude='.env' \
  --exclude='.git/' \
  --exclude='*.dump' \
  --exclude='*.log' \
  '$LOCAL_DIR/' \
  '$KAMATERA_HOST:$REMOTE_DIR/'"
echo ""

# ── STEP C (continued): npm ci on Kamatera ────────────────────────────────────
echo "── Step C (cont): npm ci --omit=dev on Kamatera"
# --omit=dev skips nodemon/supertest. Production only.
run "ssh '$KAMATERA_HOST' \"cd $REMOTE_DIR && npm ci --omit=dev\""
echo ""

# ── STEP D: .env reminder ─────────────────────────────────────────────────────
echo "── Step D: .env check"
echo "[INFO] Verifying .env exists on Kamatera (required vars listed in DEPLOY_KAMATERA.md §D)."
if [ "$COMMIT" = true ]; then
  ENV_EXISTS=$(ssh "$KAMATERA_HOST" "test -f '$REMOTE_DIR/.env' && echo yes || echo no")
  if [ "$ENV_EXISTS" != "yes" ]; then
    echo "[WARN] .env not found at $REMOTE_DIR/.env on Kamatera."
    echo "       Create it before starting pm2. See DEPLOY_KAMATERA.md §D for required vars."
    echo "       Continuing — pm2 start will fail fast if SESSION_SECRET is absent in prod mode."
  else
    # Spot-check that SESSION_SECRET is present (value hidden).
    SESSION_SET=$(ssh "$KAMATERA_HOST" "grep -c 'SESSION_SECRET=' '$REMOTE_DIR/.env' || true")
    if [ "$SESSION_SET" -lt 1 ]; then
      echo "[WARN] SESSION_SECRET not found in .env — server will refuse to boot in production."
    else
      echo "[OK] .env present; SESSION_SECRET key found."
    fi
  fi
else
  echo "[DRY ] Would check for $REMOTE_DIR/.env and SESSION_SECRET on Kamatera."
fi
echo ""

# ── STEP E: pm2 start ─────────────────────────────────────────────────────────
echo "── Step E: pm2 start / restart on Kamatera"
# If the process already exists, `pm2 reload` does a zero-downtime restart.
# If it doesn't exist, `pm2 start` creates it.
# pm2 save runs on KAMATERA only — never on Mac2. Saves the Kamatera pm2 dump.
run "ssh '$KAMATERA_HOST' \"
  cd $REMOTE_DIR
  if pm2 describe national-paper-hangers > /dev/null 2>&1; then
    echo '[pm2] Process exists — reloading...'
    pm2 reload national-paper-hangers --update-env
  else
    echo '[pm2] First start...'
    pm2 start $REMOTE_DIR/ecosystem.kamatera.config.js --env production
  fi
  pm2 save
  sleep 2
  pm2 show national-paper-hangers | grep -E 'status|restart|memory|uptime'
\""
echo ""

# ── STEP E (continued): nginx stale-config check ─────────────────────────────
echo "── Step E (cont): nginx sites-enabled stale-file check"
echo "[INFO] Checking for *.bak and *.old files in /etc/nginx/sites-enabled/ on Kamatera."
echo "       These cause silent cross-routing bugs (caught 2026-04-30 on Site Factory)."
if [ "$COMMIT" = true ]; then
  STALE=$(ssh "$KAMATERA_HOST" "find /etc/nginx/sites-enabled/ -name '*.bak' -o -name '*.old' 2>/dev/null || true")
  if [ -n "$STALE" ]; then
    echo "[WARN] Stale nginx configs found:"
    echo "$STALE"
    echo "       Remove them manually: ssh $KAMATERA_HOST 'rm <file>' then nginx -t && systemctl reload nginx"
  else
    echo "[OK] No stale .bak/.old nginx configs found."
  fi
else
  echo "[DRY ] Would run: find /etc/nginx/sites-enabled/ -name '*.bak' -o -name '*.old'"
fi
echo ""

# ── Summary ───────────────────────────────────────────────────────────────────
echo "═══════════════════════════════════════════════════════════"
if [ "$COMMIT" = true ]; then
  echo " Deploy COMPLETE."
  echo " Next steps (manual — see DEPLOY_KAMATERA.md):"
  echo "   F. Install nginx server block + nginx -t && reload"
  echo "   G. certbot --nginx for SSL"
  echo "   H. DNS cutover (GATED on new CF zone-create token)"
  echo "   I. Smoke: curl + npm test against live URL"
else
  echo " DRY-RUN COMPLETE — no changes made."
  echo " Rerun with --commit to execute."
fi
echo " Log saved to: $LOG_FILE"
echo "═══════════════════════════════════════════════════════════"