mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +00:00
BREAKING: drizzle-kit push with --force can delete columns that exist in the database but not in the schema, causing data loss. Changes: - Remove automatic drizzle-kit push from deploy-production.sh - Add warnings to sync-schema.sh about potential data loss - Only use Payload migrations for safe schema changes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
331 lines
8.6 KiB
Bash
Executable file
331 lines
8.6 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 and schema sync
|
|
if [ "$SKIP_MIGRATIONS" = false ]; then
|
|
log "Running database migrations..."
|
|
pnpm payload migrate || warn "No migrations to run or migration failed"
|
|
|
|
# WICHTIG: drizzle-kit push wurde aus dem automatischen Deployment entfernt
|
|
# um Datenverlust zu verhindern. Nur Payload-Migrationen sind sicher.
|
|
# Für manuelle Schema-Erweiterungen: ./scripts/sync-schema.sh --dry-run
|
|
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"
|