← 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 "═══════════════════════════════════════════════════════════"