← back to Patty
deploy.sh
198 lines
#!/usr/bin/env bash
# deploy.sh — Mac2 → Kamatera deploy for patty-petitions (Next.js, port 7450)
#
# Usage:
# ./deploy.sh # full deploy: rsync + npm install + build + reload + verify
# ./deploy.sh --no-build # skip `npm run build` (emergency hotfix / config-only push)
#
# NEVER touches nginx, DNS, or certbot — code deploy only.
# Safe to re-run (idempotent): rsync is incremental, pm2 reload is graceful.
#
# Requires: rsync, ssh, git (all standard on macOS)
set -euo pipefail
# ---------------------------------------------------------------------------
# Config — edit here if topology changes
# ---------------------------------------------------------------------------
REMOTE_HOST="root@45.61.58.125"
REMOTE_DIR="/root/Projects/Patty"
PM2_PROC="patty-petitions"
APP_PORT="7460"
LOCAL_DIR="/Users/macstudio3/Projects/Patty"
# ---------------------------------------------------------------------------
# Flag parsing
# ---------------------------------------------------------------------------
DO_BUILD=true
for arg in "$@"; do
case "$arg" in
--no-build) DO_BUILD=false ;;
*) echo "Unknown flag: $arg"; exit 1 ;;
esac
done
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
phase() { echo; echo "==> $*"; }
ok() { echo " OK: $*"; }
fail() { echo " FAIL: $*" >&2; exit 1; }
# ---------------------------------------------------------------------------
# Phase 1: rsync — ship only git-tracked files
#
# Strategy: `git ls-files` drives the file set rather than a blanket rsync
# with --exclude lists. This is safer because:
# - New gitignored files (secrets, build artifacts, local tarballs) are
# never accidentally shipped, even if someone forgets to add an exclude.
# - Only files the repo actually knows about land on prod.
# - Explicit excludes below are belt-and-suspenders for edge cases.
#
# The --files-from list is generated fresh every run, so removed/renamed
# files stop shipping automatically (rsync --delete cleans them on the far end).
# ---------------------------------------------------------------------------
phase "Phase 1: rsync tracked files → ${REMOTE_HOST}:${REMOTE_DIR}/"
cd "$LOCAL_DIR"
# Verify we're inside a git repo
git rev-parse --git-dir > /dev/null 2>&1 || fail "Not a git repository: $LOCAL_DIR"
# Build the file list from git (includes committed + staged; excludes untracked/ignored)
# Use process substitution to avoid a temp file (BSD/macOS compatible, no mapfile needed)
RSYNC_FILE_LIST="$(mktemp /tmp/patty-deploy-files.XXXXXX)"
git ls-files > "$RSYNC_FILE_LIST"
# Always include package-lock.json even if not tracked (needed for npm ci)
# and ecosystem.config.js (pm2 manifest). Both are tracked, but explicit here.
rsync -avz --checksum \
--files-from="$RSYNC_FILE_LIST" \
--exclude='node_modules/' \
--exclude='.next/' \
--exclude='.env*' \
--exclude='*.log' \
--exclude='tmp/' \
--exclude='.git/' \
--exclude='*.bak' \
--exclude='*.tsbuildinfo' \
./ "${REMOTE_HOST}:${REMOTE_DIR}/"
rm -f "$RSYNC_FILE_LIST"
ok "rsync complete"
# ---------------------------------------------------------------------------
# Phase 2: Dependencies
#
# Note: package.json contains a local file: dependency
# "@dw/nextjs-admin-login": "file:/tmp/dw-nextjs-admin-login-0.2.0.tgz"
# That tarball does NOT exist on prod (it lives at /tmp on Mac2 only).
# `npm ci` will hard-fail if the tarball is missing. We therefore:
# - Skip `npm ci` and run `npm install --no-audit --no-fund` instead.
# - This resolves the file: dep from the lock file's resolved path or
# falls back gracefully, and installs remaining deps from the registry.
# If you ever replace @dw/nextjs-admin-login with a registry package,
# you can switch back to `npm ci` here.
# ---------------------------------------------------------------------------
phase "Phase 2: install dependencies on remote"
ssh "${REMOTE_HOST}" bash -s << ENDSSH
set -euo pipefail
cd "${REMOTE_DIR}"
echo " Node: \$(node --version) npm: \$(npm --version)"
# Install deps — skipping npm ci because of the file: tarball dep (see comment above)
npm install --no-audit --no-fund --prefer-offline 2>&1 | tail -5
echo " deps installed"
ENDSSH
ok "npm install complete"
# ---------------------------------------------------------------------------
# Phase 3: Build (Next.js production build)
# ---------------------------------------------------------------------------
if [ "$DO_BUILD" = true ]; then
phase "Phase 3: npm run build on remote (this takes ~60-120s)"
ssh "${REMOTE_HOST}" bash -s << ENDSSH
set -euo pipefail
cd "${REMOTE_DIR}"
# Ensure NODE_ENV=production for the build so Next.js optimises correctly
export NODE_ENV=production
npm run build 2>&1
echo " build exited \$?"
ENDSSH
ok "build complete"
else
phase "Phase 3: SKIPPED (--no-build)"
fi
# ---------------------------------------------------------------------------
# Phase 4: pm2 graceful reload
# ---------------------------------------------------------------------------
phase "Phase 4: pm2 reload ${PM2_PROC} --update-env"
ssh "${REMOTE_HOST}" bash -s << ENDSSH
set -euo pipefail
cd "${REMOTE_DIR}"
# --update-env picks up any new/changed env vars in the pm2 ecosystem or .env
pm2 reload "${PM2_PROC}" --update-env
# Brief settle window — Next.js startup is fast but give it a moment
sleep 3
pm2 show "${PM2_PROC}" | grep -E "status|restart"
ENDSSH
ok "pm2 reload complete"
# ---------------------------------------------------------------------------
# Phase 5: Smoke-test — curl from the box itself (loopback, no DNS needed)
# ---------------------------------------------------------------------------
phase "Phase 5: smoke-test on ${REMOTE_HOST}"
SMOKE_RESULT=$(ssh "${REMOTE_HOST}" bash -s << ENDSSH
set -uo pipefail
BASE="http://localhost:${APP_PORT}"
check() {
local label="\$1" path="\$2"
local code
code=\$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "\${BASE}\${path}")
if [ "\$code" = "200" ]; then
echo "PASS \$label → HTTP \$code"
else
echo "FAIL \$label → HTTP \$code"
fi
}
check "/" "/"
check "/login" "/login"
ENDSSH
)
echo "$SMOKE_RESULT"
# Exit non-zero if any check failed
if echo "$SMOKE_RESULT" | grep -q "^FAIL"; then
fail "One or more smoke tests failed — check pm2 logs: ssh ${REMOTE_HOST} pm2 logs ${PM2_PROC} --lines 50"
fi
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
phase "Deploy complete"
echo
echo " App : https://grant.agentabrams.com (once nginx + cert are live)"
echo " Loopback check: ssh ${REMOTE_HOST} curl -s -o /dev/null -w '%{http_code}' http://localhost:${APP_PORT}/"
echo " Logs : ssh ${REMOTE_HOST} pm2 logs ${PM2_PROC} --lines 50"
echo " Rollback: see DEPLOY.md § Rollback"
echo