cms.c2sgmbh/scripts/deploy-production.sh
Martin Porwoll c505f29ebf fix: optimize GitHub Actions workflows to reduce costs
- Remove CodeQL Analysis (requires paid GHAS for private repos)
- Replace with ESLint + pnpm audit for security scanning
- CI: Run full tests only on PRs, not on every push to develop
- CI: Skip CI for markdown-only changes
- Security: Run only on PRs to main and weekly schedule
- Add deploy-production.yml workflow with rollback support
- Add deploy-production.sh script for manual deployments
- Document GitHub Actions cost optimization in DEPLOYMENT_STRATEGY.md

Estimated savings: ~68% of GitHub Actions minutes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 13:52:45 +00:00

327 lines
8.4 KiB
Bash
Executable file

#!/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"