#!/bin/bash # ============================================================================ # POUNCE ZERO-DOWNTIME DEPLOY SCRIPT # - Builds locally first (optional) # - Syncs only changed files # - Hot-reloads backend without full restart # - Rebuilds frontend in background # ============================================================================ set -euo pipefail # Colors GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' RED='\033[0;31m' CYAN='\033[0;36m' NC='\033[0m' # No Color # Server config SERVER_USER="user" SERVER_HOST="10.42.0.73" SERVER_PATH="/home/user/pounce" SERVER_PASS="user" SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" if ! command -v sshpass >/dev/null 2>&1; then echo -e "${RED}✗ sshpass is required but not installed.${NC}" echo -e " Install with: ${CYAN}brew install sshpass${NC}" exit 1 fi # Parse flags QUICK_MODE=false BACKEND_ONLY=false FRONTEND_ONLY=false while [[ "$#" -gt 0 ]]; do case $1 in -q|--quick) QUICK_MODE=true ;; -b|--backend) BACKEND_ONLY=true ;; -f|--frontend) FRONTEND_ONLY=true ;; *) COMMIT_MSG="$1" ;; esac shift done echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" echo -e "${BLUE} POUNCE ZERO-DOWNTIME DEPLOY ${NC}" echo -e "${BLUE}═══════════════════════════════════════════════════════════════${NC}" if $QUICK_MODE; then echo -e "${CYAN}⚡ Quick mode: Skipping git, only syncing changes${NC}" fi if $BACKEND_ONLY; then echo -e "${CYAN}🔧 Backend only mode${NC}" fi if $FRONTEND_ONLY; then echo -e "${CYAN}🎨 Frontend only mode${NC}" fi # Step 1: Git (unless quick mode) if ! $QUICK_MODE; then echo -e "\n${YELLOW}[1/4] Git operations...${NC}" # Check for changes (including untracked) if [ -z "$(git status --porcelain)" ]; then echo " No changes to commit" else git add -A if [ -z "$COMMIT_MSG" ]; then COMMIT_MSG="Deploy: $(date '+%Y-%m-%d %H:%M')" fi git commit -m "$COMMIT_MSG" || true echo " ✓ Committed: $COMMIT_MSG" fi git push origin main 2>/dev/null && echo " ✓ Pushed to git.6bit.ch" || echo " ⚠ Push failed or nothing to push" else echo -e "\n${YELLOW}[1/4] Skipping git (quick mode)${NC}" fi # Step 2: Sync files (only changed) echo -e "\n${YELLOW}[2/4] Syncing changed files...${NC}" RSYNC_OPTS="-avz --delete" if ! $BACKEND_ONLY; then echo " Frontend:" sshpass -p "$SERVER_PASS" rsync -e "ssh $SSH_OPTS" $RSYNC_OPTS \ --exclude 'node_modules' \ --exclude '.next' \ --exclude '.git' \ frontend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/frontend/ fi if ! $FRONTEND_ONLY; then echo " Backend:" sshpass -p "$SERVER_PASS" rsync -e "ssh $SSH_OPTS" $RSYNC_OPTS \ --exclude '__pycache__' \ --exclude '.pytest_cache' \ --exclude 'venv' \ --exclude '.git' \ --exclude '*.pyc' \ --exclude '.env' \ --exclude '*.db' \ backend/ $SERVER_USER@$SERVER_HOST:$SERVER_PATH/backend/ fi # Step 3: Reload backend (graceful, no restart) if ! $FRONTEND_ONLY; then echo -e "\n${YELLOW}[3/4] Reloading backend (graceful)...${NC}" sshpass -p "$SERVER_PASS" ssh $SSH_OPTS $SERVER_USER@$SERVER_HOST << 'BACKEND_EOF' set -e cd ~/pounce/backend if [ -f "venv/bin/activate" ]; then source venv/bin/activate elif [ -f "../venv/bin/activate" ]; then source ../venv/bin/activate else echo " ✗ venv not found (expected backend/venv or ../venv)" exit 1 fi echo " Running DB migrations..." python -c "from app.database import init_db; import asyncio; asyncio.run(init_db())" echo " ✓ DB migrations applied" # Restart backend process (production typically runs without --reload) BACKEND_PID=$(pgrep -f 'uvicorn app.main:app' | awk 'NR==1{print; exit}') if [ -n "$BACKEND_PID" ]; then echo " Restarting backend (PID: $BACKEND_PID)..." kill "$BACKEND_PID" 2>/dev/null || true sleep 1 nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 > backend.log 2>&1 & sleep 2 echo " ✓ Backend restarted" else echo " ⚠ Backend not running, starting..." nohup uvicorn app.main:app --host 0.0.0.0 --port 8000 > backend.log 2>&1 & sleep 2 echo " ✓ Backend started" fi BACKEND_EOF else echo -e "\n${YELLOW}[3/4] Skipping backend (frontend only)${NC}" fi # Step 4: Rebuild frontend (in background to minimize downtime) if ! $BACKEND_ONLY; then echo -e "\n${YELLOW}[4/4] Rebuilding frontend...${NC}" sshpass -p "$SERVER_PASS" ssh $SSH_OPTS $SERVER_USER@$SERVER_HOST << 'FRONTEND_EOF' set -e cd ~/pounce/frontend echo " Installing dependencies..." if [ -f "package-lock.json" ]; then npm ci else npm install fi # Build new version echo " Building..." npm run build BUILD_EXIT=$? if [ $BUILD_EXIT -eq 0 ]; then # Next.js standalone output requires public + static inside standalone folder mkdir -p .next/standalone/.next ln -sfn ../../static .next/standalone/.next/static ln -sfn ../../public .next/standalone/public # Gracefully restart Next.js NEXT_PID=$(pgrep -af 'node \\.next/standalone/server\\.js|next start|next-server|next-serv' | awk 'NR==1{print $1; exit}') if [ -n "$NEXT_PID" ]; then echo " Restarting Next.js (PID: $NEXT_PID)..." kill $NEXT_PID 2>/dev/null sleep 1 fi # Ensure port is free (avoid EADDRINUSE) lsof -ti:3000 2>/dev/null | xargs -r kill -9 2>/dev/null || true sleep 1 # Start new instance if [ -f ".next/standalone/server.js" ]; then echo " Starting Next.js (standalone)..." nohup env NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000 node .next/standalone/server.js > frontend.log 2>&1 & else echo " Starting Next.js (npm start)..." nohup env NODE_ENV=production npm run start > frontend.log 2>&1 & fi sleep 2 # Verify NEW_PID=$(pgrep -af 'node \\.next/standalone/server\\.js|next start|next-server|next-serv' | awk 'NR==1{print $1; exit}') if [ -n "$NEW_PID" ]; then echo " ✓ Frontend running (PID: $NEW_PID)" else echo " ⚠ Frontend may not have started correctly" echo " Last 80 lines of frontend.log:" tail -n 80 frontend.log || true fi else echo " ✗ Build failed, keeping old version" echo " Last 120 lines of build output (frontend.log):" tail -n 120 frontend.log || true fi FRONTEND_EOF else echo -e "\n${YELLOW}[4/4] Skipping frontend (backend only)${NC}" fi # Summary echo -e "\n${GREEN}═══════════════════════════════════════════════════════════════${NC}" echo -e "${GREEN} ✅ DEPLOY COMPLETE! ${NC}" echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" echo -e "" echo -e " Frontend: ${BLUE}https://pounce.ch${NC} / http://$SERVER_HOST:3000" echo -e " Backend: ${BLUE}https://pounce.ch/api${NC} / http://$SERVER_HOST:8000" echo -e "" echo -e "${CYAN}Quick commands:${NC}" echo -e " ./deploy.sh -q # Quick sync, no git" echo -e " ./deploy.sh -b # Backend only" echo -e " ./deploy.sh -f # Frontend only" echo -e " ./deploy.sh \"message\" # Full deploy with commit message"