#!/bin/bash # ============================================================================= # Production Deployment Script # ============================================================================= # Usage: ./scripts/deploy-production.sh [OPTIONS] # # Options: # --skip-backup Skip database backup (not recommended) # --skip-migrations Skip database migrations # --skip-build Skip build (only restart services) # --rollback Rollback to previous deployment # --dry-run Show what would be done without executing # -y, --yes Skip confirmation prompt # # This script deploys the main branch to the production server (Hetzner 3). # It should be run ON the production server (cms.c2sgmbh.de). # # IMPORTANT: Always ensure develop is merged to main before deploying! # ============================================================================= set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # No Color # Configuration PROJECT_DIR="${PROJECT_DIR:-/home/payload/payload-cms}" BRANCH="main" BACKUP_DIR="$HOME/backups/pre-deploy" ROLLBACK_FILE="/tmp/previous_deploy_sha" LOG_DIR="$HOME/logs" LOG_FILE="$LOG_DIR/deploy-production-$(date +%Y%m%d).log" HEALTH_CHECK_URL="http://localhost:3001/admin" PRODUCTION_URL="https://cms.c2sgmbh.de" # Parse arguments SKIP_BACKUP=false SKIP_MIGRATIONS=false SKIP_BUILD=false DO_ROLLBACK=false DRY_RUN=false AUTO_YES=false for arg in "$@"; do case $arg in --skip-backup) SKIP_BACKUP=true shift ;; --skip-migrations) SKIP_MIGRATIONS=true shift ;; --skip-build) SKIP_BUILD=true shift ;; --rollback) DO_ROLLBACK=true shift ;; --dry-run) DRY_RUN=true shift ;; -y|--yes) AUTO_YES=true shift ;; -h|--help) head -30 "$0" | tail -25 exit 0 ;; esac done # Functions log() { local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1" echo -e "${BLUE}${msg}${NC}" | tee -a "$LOG_FILE" } success() { local msg="[SUCCESS] $1" echo -e "${GREEN}${msg}${NC}" | tee -a "$LOG_FILE" } warn() { local msg="[WARNING] $1" echo -e "${YELLOW}${msg}${NC}" | tee -a "$LOG_FILE" } error() { local msg="[ERROR] $1" echo -e "${RED}${msg}${NC}" | tee -a "$LOG_FILE" exit 1 } info() { local msg="[INFO] $1" echo -e "${CYAN}${msg}${NC}" | tee -a "$LOG_FILE" } # Ensure directories exist mkdir -p "$BACKUP_DIR" "$LOG_DIR" # Header echo "" echo -e "${CYAN}==============================================" echo -e " PRODUCTION DEPLOYMENT - Hetzner 3" echo -e " URL: $PRODUCTION_URL" echo -e "==============================================${NC}" echo "" # Check if we're on the right server if [ ! -d "$PROJECT_DIR" ]; then error "Project directory not found: $PROJECT_DIR. Are you on the production server?" fi cd "$PROJECT_DIR" # Rollback mode if [ "$DO_ROLLBACK" = true ]; then echo -e "${YELLOW}=== ROLLBACK MODE ===${NC}" if [ ! -f "$ROLLBACK_FILE" ]; then error "No rollback point found. Cannot rollback." fi PREVIOUS_SHA=$(cat "$ROLLBACK_FILE") log "Rolling back to: $PREVIOUS_SHA" if [ "$DRY_RUN" = true ]; then info "[DRY RUN] Would rollback to $PREVIOUS_SHA" exit 0 fi git fetch origin main git checkout main git reset --hard "$PREVIOUS_SHA" pnpm install --frozen-lockfile pm2 stop payload 2>/dev/null || true NODE_OPTIONS="--max-old-space-size=2048" pnpm build pm2 restart payload --update-env pm2 restart queue-worker --update-env success "Rollback complete to $PREVIOUS_SHA" pm2 status exit 0 fi # Show current state log "Checking current state..." CURRENT_SHA=$(git rev-parse HEAD) CURRENT_BRANCH=$(git branch --show-current) info "Current branch: $CURRENT_BRANCH" info "Current commit: $CURRENT_SHA" # Fetch latest log "Fetching latest from origin..." git fetch origin main LATEST_SHA=$(git rev-parse origin/main) info "Latest main: $LATEST_SHA" if [ "$CURRENT_SHA" = "$LATEST_SHA" ]; then warn "Already at latest version. Nothing to deploy." if [ "$AUTO_YES" != true ]; then read -p "Continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 0 fi fi fi # Show changes echo "" log "Changes to be deployed:" git log --oneline "$CURRENT_SHA".."$LATEST_SHA" 2>/dev/null || echo " (fresh deployment)" echo "" # Confirmation if [ "$AUTO_YES" != true ] && [ "$DRY_RUN" != true ]; then echo -e "${YELLOW}PRODUCTION DEPLOYMENT${NC}" echo "" echo "Options:" echo " Skip backup: $SKIP_BACKUP" echo " Skip migrations: $SKIP_MIGRATIONS" echo " Skip build: $SKIP_BUILD" echo "" read -p "Deploy to PRODUCTION? (yes/no) " CONFIRM if [ "$CONFIRM" != "yes" ]; then echo "Deployment cancelled." exit 0 fi fi # Dry run check if [ "$DRY_RUN" = true ]; then info "[DRY RUN] Would deploy $LATEST_SHA" info "[DRY RUN] Would create backup in $BACKUP_DIR" info "[DRY RUN] Would run migrations: $([[ $SKIP_MIGRATIONS = true ]] && echo 'NO' || echo 'YES')" info "[DRY RUN] Would run build: $([[ $SKIP_BUILD = true ]] && echo 'NO' || echo 'YES')" exit 0 fi # Step 1: Create backup if [ "$SKIP_BACKUP" = false ]; then log "Creating database backup..." BACKUP_FILE="$BACKUP_DIR/payload_db_$(date +%Y-%m-%d_%H-%M-%S).sql.gz" if [ -f "$HOME/.pgpass" ]; then # Read password from .pgpass (format: host:port:db:user:password) export PGPASSWORD PGPASSWORD=$(grep 'payload_db' "$HOME/.pgpass" | cut -d: -f5) pg_dump -h localhost -U payload payload_db | gzip > "$BACKUP_FILE" unset PGPASSWORD success "Backup created: $BACKUP_FILE" # Keep only last 5 pre-deploy backups ls -t "$BACKUP_DIR"/payload_db_*.sql.gz 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true else warn "No .pgpass found, skipping backup" fi else warn "Skipping backup (--skip-backup)" fi # Step 2: Save rollback point log "Saving rollback point..." echo "$CURRENT_SHA" > "$ROLLBACK_FILE" info "Rollback SHA saved: $CURRENT_SHA" # Step 3: Pull latest code log "Pulling latest code..." git stash --include-untracked 2>/dev/null || true git checkout main git reset --hard origin/main success "Code updated to: $(git rev-parse --short HEAD)" # Step 4: Install dependencies log "Installing dependencies..." pnpm install --frozen-lockfile success "Dependencies installed" # Step 5: Run migrations if [ "$SKIP_MIGRATIONS" = false ]; then log "Running database migrations..." pnpm payload migrate || warn "No migrations to run or migration failed" else warn "Skipping migrations (--skip-migrations)" fi # Step 6: Build if [ "$SKIP_BUILD" = false ]; then log "Stopping services for build..." pm2 stop payload 2>/dev/null || true pm2 stop queue-worker 2>/dev/null || true log "Building application..." NODE_OPTIONS="--max-old-space-size=2048" pnpm build success "Build completed" else warn "Skipping build (--skip-build)" fi # Step 7: Restart services log "Restarting services..." pm2 restart payload --update-env 2>/dev/null || pm2 start ecosystem.config.cjs --only payload pm2 restart queue-worker --update-env 2>/dev/null || pm2 start ecosystem.config.cjs --only queue-worker success "Services restarted" # Step 8: Health check log "Waiting for service to start..." sleep 10 log "Running health check..." MAX_RETRIES=5 RETRY_COUNT=0 HEALTH_OK=false while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_CHECK_URL" 2>/dev/null || echo "000") if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 400 ]; then success "Health check passed (HTTP $HTTP_STATUS)" HEALTH_OK=true break fi RETRY_COUNT=$((RETRY_COUNT + 1)) warn "Health check attempt $RETRY_COUNT failed (HTTP $HTTP_STATUS), retrying in 5s..." sleep 5 done if [ "$HEALTH_OK" = false ]; then error "Health check failed after $MAX_RETRIES attempts. Consider rolling back with --rollback" fi # Show PM2 status echo "" log "PM2 Status:" pm2 status # Summary echo "" echo -e "${GREEN}==============================================" echo -e " PRODUCTION DEPLOYMENT COMPLETE!" echo -e "==============================================${NC}" echo "" echo " URL: $PRODUCTION_URL" echo " Admin: $PRODUCTION_URL/admin" echo " Commit: $(git rev-parse --short HEAD)" echo " Previous: $(cat $ROLLBACK_FILE | cut -c1-7)" echo " Time: $(date)" echo "" echo " To rollback: ./scripts/deploy-production.sh --rollback" echo "" log "Deployment finished successfully"