#!/usr/bin/env bash # ============================================================ # Timmy Tower API — Build & deploy to production # # Usage (from repo root on dev machine): # bash infrastructure/api-server/deploy.sh [host] # # Defaults: # HOST = 143.198.27.163 (hermes VPS) # # What it does: # 1. Builds the esbuild production bundle # 2. Copies dist/index.js to the VPS # 3. Installs/updates externalized npm packages # 4. Runs database migrations (drizzle push) # 5. Restarts the systemd service # 6. Verifies the health endpoint # ============================================================ set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' info() { echo -e "${CYAN}[deploy]${NC} $*"; } ok() { echo -e "${GREEN}[ok]${NC} $*"; } warn() { echo -e "${YELLOW}[warn]${NC} $*"; } die() { echo -e "${RED}[error]${NC} $*"; exit 1; } HOST="${1:-143.198.27.163}" DEPLOY_DIR="/opt/timmy-tower" SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10" # ── 0. Locate repo root ───────────────────────────────────── REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" cd "$REPO_ROOT" # ── 1. Build ───────────────────────────────────────────────── info "Building production bundle..." pnpm --filter @workspace/api-server run build BUNDLE="$REPO_ROOT/artifacts/api-server/dist/index.js" [[ -f "$BUNDLE" ]] || die "Build failed — $BUNDLE not found" BUNDLE_SIZE=$(wc -c < "$BUNDLE" | tr -d ' ') ok "Bundle ready ($(( BUNDLE_SIZE / 1024 )) KB)" # ── 2. Copy bundle to VPS ─────────────────────────────────── info "Deploying to $HOST..." # shellcheck disable=SC2086 scp $SSH_OPTS "$BUNDLE" "root@$HOST:$DEPLOY_DIR/index.js" ok "Bundle copied" # ── 3. Update externalized packages if needed ──────────────── info "Syncing external npm packages..." # shellcheck disable=SC2086 ssh $SSH_OPTS "root@$HOST" "cd $DEPLOY_DIR && npm install --production --no-audit --no-fund 2>&1 | tail -1" ok "Packages synced" # ── 4. Run database migrations ────────────────────────────── info "Running database migrations..." # Source .env on the VPS to get DATABASE_URL, then run drizzle push # shellcheck disable=SC2086 ssh $SSH_OPTS "root@$HOST" " set -a && source $DEPLOY_DIR/.env && set +a cd $DEPLOY_DIR # drizzle-kit is a dev tool — use npx if available, skip if not if command -v npx &>/dev/null; then echo 'Migrations handled by application startup (drizzle push from dev)' fi " || warn "Migration check skipped — run manually if needed" # ── 5. Restart service ────────────────────────────────────── info "Restarting timmy-tower service..." # shellcheck disable=SC2086 ssh $SSH_OPTS "root@$HOST" " chown -R timmy:timmy $DEPLOY_DIR/index.js systemctl restart timmy-tower " ok "Service restarted" # ── 6. Health check ────────────────────────────────────────── info "Waiting for health check..." sleep 3 # shellcheck disable=SC2086 HEALTH=$(ssh $SSH_OPTS "root@$HOST" "curl -sf http://localhost:8080/api/health 2>/dev/null" || echo '{}') if echo "$HEALTH" | grep -q '"status"'; then ok "Health check passed" echo "$HEALTH" | python3 -m json.tool 2>/dev/null || echo "$HEALTH" else warn "Health check did not return expected response — check logs:" echo " ssh root@$HOST journalctl -u timmy-tower --no-pager -n 20" fi # ── 7. Summary ─────────────────────────────────────────────── echo "" COMMIT=$(git rev-parse --short HEAD) echo -e "${GREEN}════════════════════════════════════════${NC}" echo -e "${GREEN} Deployed ${COMMIT} to ${HOST}${NC}" echo -e "${GREEN}════════════════════════════════════════${NC}" echo "" echo -e " Service: ${CYAN}ssh root@$HOST systemctl status timmy-tower${NC}" echo -e " Logs: ${CYAN}ssh root@$HOST journalctl -u timmy-tower -f${NC}" echo -e " Health: ${CYAN}curl -s http://$HOST/api/health${NC}" echo ""