diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c93216c..0816464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,19 @@ name: CI +# ============================================================================ +# Optimiert für minimale GitHub Actions Nutzung: +# - Push auf develop: Nur Lint + Build (schnelle Feedback-Schleife) +# - PRs: Volle Test-Suite inkl. E2E +# - Push auf main: Keine CI (wird durch deploy-production getriggert) +# ============================================================================ + on: push: - branches: [main, develop] + branches: [develop] + paths-ignore: + - '**.md' + - 'docs/**' + - '.github/ISSUE_TEMPLATE/**' pull_request: branches: [main, develop] @@ -13,6 +24,9 @@ concurrency: env: NODE_VERSION: '20' PNPM_VERSION: '9' + # Schnelle Builds durch Turbo Cache + TURBO_TEAM: 'payload' + TURBO_REMOTE_ONLY: true jobs: # =========================================================================== @@ -76,12 +90,13 @@ jobs: run: pnpm typecheck # =========================================================================== - # Unit & Integration Tests + # Unit & Integration Tests (nur bei PRs - spart Actions-Minuten) # =========================================================================== test: name: Tests runs-on: ubuntu-latest needs: [lint, typecheck] + if: github.event_name == 'pull_request' timeout-minutes: 30 # Prevent 6-hour hangs services: postgres: @@ -224,12 +239,13 @@ jobs: include-hidden-files: true # =========================================================================== - # E2E Tests (after build) + # E2E Tests (nur bei PRs - sehr ressourcenintensiv) # =========================================================================== e2e: name: E2E Tests runs-on: ubuntu-latest needs: [build] + if: github.event_name == 'pull_request' timeout-minutes: 30 # Prevent 6-hour hangs services: postgres: @@ -325,14 +341,22 @@ jobs: steps: - name: Check required jobs run: | - # Required jobs (must succeed) + # Required jobs for all runs (must succeed) if [ "${{ needs.lint.result }}" != "success" ] || \ - [ "${{ needs.test.result }}" != "success" ] || \ - [ "${{ needs.build.result }}" != "success" ] || \ - [ "${{ needs.e2e.result }}" != "success" ]; then + [ "${{ needs.build.result }}" != "success" ]; then echo "One or more required jobs failed" exit 1 fi + + # Jobs only required for PRs + if [ "${{ github.event_name }}" == "pull_request" ]; then + if [ "${{ needs.test.result }}" != "success" ] || \ + [ "${{ needs.e2e.result }}" != "success" ]; then + echo "PR required jobs failed" + exit 1 + fi + fi + # Optional jobs (typecheck) - just report status if [ "${{ needs.typecheck.result }}" != "success" ]; then echo "⚠️ TypeScript check failed (optional)" @@ -343,10 +367,18 @@ jobs: run: | echo "## CI Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY echo "| Job | Status | Required |" >> $GITHUB_STEP_SUMMARY echo "|-----|--------|----------|" >> $GITHUB_STEP_SUMMARY echo "| Lint | ${{ needs.lint.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY echo "| TypeScript | ${{ needs.typecheck.result }} | ⚠️ optional |" >> $GITHUB_STEP_SUMMARY - echo "| Tests | ${{ needs.test.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY echo "| Build | ${{ needs.build.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY - echo "| E2E | ${{ needs.e2e.result }} | ✅ |" >> $GITHUB_STEP_SUMMARY + + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "| Tests | ${{ needs.test.result }} | ✅ (PR) |" >> $GITHUB_STEP_SUMMARY + echo "| E2E | ${{ needs.e2e.result }} | ✅ (PR) |" >> $GITHUB_STEP_SUMMARY + else + echo "| Tests | skipped | ⏭️ push only |" >> $GITHUB_STEP_SUMMARY + echo "| E2E | skipped | ⏭️ push only |" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..73ed0ef --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,441 @@ +name: Deploy to Production + +on: + # Manual trigger with approval - required for production + workflow_dispatch: + inputs: + skip_tests: + description: 'Skip pre-deployment tests' + required: false + default: 'false' + type: boolean + skip_backup: + description: 'Skip database backup (not recommended)' + required: false + default: 'false' + type: boolean + skip_migrations: + description: 'Skip database migrations' + required: false + default: 'false' + type: boolean + deploy_tag: + description: 'Git tag or commit to deploy (leave empty for latest main)' + required: false + type: string + +concurrency: + group: production-deployment + cancel-in-progress: false + +env: + NODE_VERSION: '22' + PNPM_VERSION: '9' + PRODUCTION_HOST: '162.55.85.18' + PRODUCTION_USER: 'payload' + PRODUCTION_PATH: '/home/payload/payload-cms' + PRODUCTION_URL: 'https://cms.c2sgmbh.de' + +jobs: + # =========================================================================== + # Pre-flight Checks + # =========================================================================== + preflight: + name: Pre-flight Checks + runs-on: ubuntu-latest + outputs: + deploy_sha: ${{ steps.get-sha.outputs.sha }} + deploy_tag: ${{ steps.get-sha.outputs.tag }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.deploy_tag || 'main' }} + fetch-depth: 0 + + - name: Get deployment SHA + id: get-sha + run: | + SHA=$(git rev-parse HEAD) + TAG="${{ inputs.deploy_tag }}" + if [ -z "$TAG" ]; then + TAG="main ($(git rev-parse --short HEAD))" + fi + echo "sha=$SHA" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Deploying: $TAG" + echo "SHA: $SHA" + + - name: Verify branch is up to date with develop + run: | + git fetch origin develop + MAIN_SHA=$(git rev-parse HEAD) + DEVELOP_SHA=$(git rev-parse origin/develop) + + # Check if main contains the latest develop commits + if ! git merge-base --is-ancestor origin/develop HEAD 2>/dev/null; then + echo "::warning::main is not up to date with develop. Consider merging develop first." + fi + + - name: Check for pending migrations + run: | + # List migration files that might need to be run + if [ -d "src/migrations" ]; then + MIGRATION_COUNT=$(find src/migrations -name "*.ts" -type f | wc -l) + echo "Found $MIGRATION_COUNT migration files" + fi + + # =========================================================================== + # Pre-deployment Tests (optional, recommended) + # =========================================================================== + pre-tests: + name: Pre-deployment Tests + runs-on: ubuntu-latest + needs: [preflight] + if: ${{ inputs.skip_tests != true }} + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: payload + POSTGRES_PASSWORD: payload_test_password + POSTGRES_DB: payload_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.preflight.outputs.deploy_sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run ESLint + run: pnpm lint + + - name: Run Unit Tests + run: pnpm test:unit + timeout-minutes: 10 + env: + CSRF_SECRET: test-csrf-secret + PAYLOAD_SECRET: test-payload-secret + PAYLOAD_PUBLIC_SERVER_URL: https://test.example.com + NEXT_PUBLIC_SERVER_URL: https://test.example.com + EMAIL_DELIVERY_DISABLED: 'true' + DATABASE_URI: postgresql://payload:payload_test_password@localhost:5432/payload_test + + - name: Build Test + run: pnpm build + env: + PAYLOAD_SECRET: build-secret-placeholder + DATABASE_URI: postgresql://placeholder:placeholder@localhost:5432/placeholder + NEXT_PUBLIC_SERVER_URL: https://build.example.com + PAYLOAD_PUBLIC_SERVER_URL: https://build.example.com + CONSENT_LOGGING_API_KEY: ci-consent-api-key-placeholder + IP_ANONYMIZATION_PEPPER: ci-anonymization-pepper-placeholder + + # =========================================================================== + # Database Backup (before deployment) + # =========================================================================== + backup: + name: Pre-deployment Backup + runs-on: ubuntu-latest + needs: [preflight, pre-tests] + if: always() && (needs.pre-tests.result == 'success' || needs.pre-tests.result == 'skipped') && inputs.skip_backup != true + steps: + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PRODUCTION_SSH_KEY }}" > ~/.ssh/production_key + chmod 600 ~/.ssh/production_key + cat >> ~/.ssh/config << EOF + Host production + HostName ${{ env.PRODUCTION_HOST }} + User ${{ env.PRODUCTION_USER }} + IdentityFile ~/.ssh/production_key + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + EOF + + - name: Create database backup + run: | + ssh production << 'ENDSSH' + set -e + echo "=== Creating pre-deployment backup ===" + + BACKUP_DIR="$HOME/backups/pre-deploy" + BACKUP_FILE="$BACKUP_DIR/payload_db_$(date +%Y-%m-%d_%H-%M-%S).sql.gz" + + mkdir -p "$BACKUP_DIR" + + # Create backup using pg_dump + PGPASSWORD=$(grep 'payload_db' ~/.pgpass | cut -d: -f5) \ + pg_dump -h localhost -U payload payload_db | gzip > "$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 + + echo "Backup created: $BACKUP_FILE" + ls -lh "$BACKUP_FILE" + ENDSSH + + # =========================================================================== + # Deploy to Production + # =========================================================================== + deploy: + name: Deploy to Production + runs-on: ubuntu-latest + needs: [preflight, pre-tests, backup] + if: always() && (needs.pre-tests.result == 'success' || needs.pre-tests.result == 'skipped') && (needs.backup.result == 'success' || needs.backup.result == 'skipped') + environment: + name: production + url: ${{ env.PRODUCTION_URL }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.preflight.outputs.deploy_sha }} + + - name: Configure SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.PRODUCTION_SSH_KEY }}" > ~/.ssh/production_key + chmod 600 ~/.ssh/production_key + cat >> ~/.ssh/config << EOF + Host production + HostName ${{ env.PRODUCTION_HOST }} + User ${{ env.PRODUCTION_USER }} + IdentityFile ~/.ssh/production_key + StrictHostKeyChecking no + UserKnownHostsFile /dev/null + EOF + + - name: Deploy to Production + id: deploy + run: | + DEPLOY_SHA="${{ needs.preflight.outputs.deploy_sha }}" + SKIP_MIGRATIONS="${{ inputs.skip_migrations }}" + + ssh production << ENDSSH + set -e + + echo "==============================================" + echo " PRODUCTION DEPLOYMENT" + echo "==============================================" + echo "Time: \$(date)" + echo "SHA: $DEPLOY_SHA" + echo "Skip Migrations: $SKIP_MIGRATIONS" + echo "" + + cd ${{ env.PRODUCTION_PATH }} + + # Save current commit for rollback + PREVIOUS_SHA=\$(git rev-parse HEAD) + echo "\$PREVIOUS_SHA" > /tmp/previous_deploy_sha + echo "Previous commit: \$PREVIOUS_SHA" + + # Stash any local changes + git stash --include-untracked 2>/dev/null || true + + # Fetch and checkout + echo "" + echo "=== Fetching code ===" + git fetch origin main + git checkout main + git reset --hard $DEPLOY_SHA + + # Install dependencies + echo "" + echo "=== Installing dependencies ===" + pnpm install --frozen-lockfile + + # Run migrations (unless skipped) + if [ "$SKIP_MIGRATIONS" != "true" ]; then + echo "" + echo "=== Running migrations ===" + pnpm payload migrate || echo "No migrations to run" + else + echo "" + echo "=== Skipping migrations (as requested) ===" + fi + + # Stop services before build to free memory + echo "" + echo "=== Stopping services for build ===" + pm2 stop payload 2>/dev/null || true + pm2 stop queue-worker 2>/dev/null || true + + # Build + echo "" + echo "=== Building application ===" + NODE_OPTIONS="--max-old-space-size=2048" pnpm build + + # Restart services + echo "" + echo "=== Starting 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 + + # Wait for startup + echo "" + echo "=== Waiting for services to start ===" + sleep 10 + + # Show status + pm2 status + + echo "" + echo "=== Deployment Complete ===" + echo "Time: \$(date)" + ENDSSH + + - name: Health Check + id: health-check + run: | + echo "Waiting for service to be fully ready..." + sleep 10 + + MAX_RETRIES=5 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}/admin" 2>/dev/null || echo "000") + + if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 400 ]; then + echo "Health check passed (HTTP $HTTP_STATUS)" + echo "status=success" >> $GITHUB_OUTPUT + exit 0 + fi + + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Health check attempt $RETRY_COUNT failed (HTTP $HTTP_STATUS), retrying in 10s..." + sleep 10 + done + + echo "Health check failed after $MAX_RETRIES attempts" + echo "status=failed" >> $GITHUB_OUTPUT + exit 1 + + - name: Rollback on failure + if: failure() && steps.health-check.outputs.status == 'failed' + run: | + echo "Deployment failed, initiating rollback..." + + ssh production << 'ENDSSH' + set -e + cd ${{ env.PRODUCTION_PATH }} + + if [ -f /tmp/previous_deploy_sha ]; then + PREVIOUS_SHA=$(cat /tmp/previous_deploy_sha) + echo "Rolling back to: $PREVIOUS_SHA" + + 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 + + echo "Rollback complete" + pm2 status + else + echo "No previous SHA found, manual intervention required" + exit 1 + fi + ENDSSH + + # =========================================================================== + # Post-deployment Verification + # =========================================================================== + verify: + name: Post-deployment Verification + runs-on: ubuntu-latest + needs: [deploy] + if: success() + steps: + - name: Full Health Check + run: | + echo "Running comprehensive health checks..." + + # Check Admin Panel + ADMIN_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}/admin") + echo "Admin Panel: HTTP $ADMIN_STATUS" + + # Check API + API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}/api") + echo "API: HTTP $API_STATUS" + + # Summary + if [ "$ADMIN_STATUS" -ge 200 ] && [ "$ADMIN_STATUS" -lt 400 ] && \ + [ "$API_STATUS" -ge 200 ] && [ "$API_STATUS" -lt 400 ]; then + echo "All health checks passed!" + else + echo "Some health checks failed" + exit 1 + fi + + # =========================================================================== + # Deployment Summary + # =========================================================================== + summary: + name: Deployment Summary + runs-on: ubuntu-latest + needs: [preflight, pre-tests, backup, deploy, verify] + if: always() + steps: + - name: Create deployment summary + run: | + echo "## Production Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY + echo "|------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Pre-flight | ${{ needs.preflight.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Pre-tests | ${{ needs.pre-tests.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Backup | ${{ needs.backup.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Deploy | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Verify | ${{ needs.verify.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Details" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Environment | Production |" >> $GITHUB_STEP_SUMMARY + echo "| URL | ${{ env.PRODUCTION_URL }} |" >> $GITHUB_STEP_SUMMARY + echo "| Deployed | \`${{ needs.preflight.outputs.deploy_tag }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| SHA | \`${{ needs.preflight.outputs.deploy_sha }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| Triggered by | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY + echo "| Time | $(date -u '+%Y-%m-%d %H:%M:%S UTC') |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Overall status + if [ "${{ needs.verify.result }}" == "success" ]; then + echo "### Result: Deployment Successful" >> $GITHUB_STEP_SUMMARY + else + echo "### Result: Deployment Failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Check the logs above for details. If automatic rollback occurred, the previous version should be running." >> $GITHUB_STEP_SUMMARY + fi + + - name: Notify on failure + if: needs.deploy.result == 'failure' || needs.verify.result == 'failure' + run: | + echo "::error::Production deployment failed! Check logs for details." diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index a16bfa9..082cbe3 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,32 +1,37 @@ name: Security Scanning +# ============================================================================ +# WICHTIG: Dieser Workflow wurde optimiert um ohne kostenpflichtige GitHub +# Features (CodeQL, GHAS) auszukommen. Für private Repos sind diese Features +# kostenpflichtig (~$49/Monat pro Seat). +# +# Stattdessen verwenden wir: +# - pnpm audit für Dependency Scanning +# - ESLint mit Security-Plugin für Code-Analyse +# - Lokale Security Tests +# ============================================================================ + on: - push: - branches: [main, develop] + # Nur bei PRs auf main und wöchentlich - spart GitHub Actions Minuten pull_request: branches: [main] schedule: # Wöchentlich Sonntag um 00:00 UTC - cron: '0 0 * * 0' + # Manuelle Auslösung für on-demand Scans + workflow_dispatch: permissions: contents: read - security-events: write + +env: + NODE_VERSION: '20' + PNPM_VERSION: '9' jobs: - # Secret Scanning - Using GitHub's native secret scanning (enabled in repo settings) - # Gitleaks removed - now requires paid license, GitHub native is more comprehensive - secrets: - name: Secret Scanning - runs-on: ubuntu-latest - steps: - - name: Verify GitHub Secret Scanning - run: | - echo "## Secret Scanning Status" >> $GITHUB_STEP_SUMMARY - echo "✅ GitHub native secret scanning is enabled in repository settings" >> $GITHUB_STEP_SUMMARY - echo "Push protection is active for 423 patterns" >> $GITHUB_STEP_SUMMARY - - # Dependency Vulnerability Scanning + # =========================================================================== + # Dependency Vulnerability Scanning (kostenlos) + # =========================================================================== dependencies: name: Dependency Audit runs-on: ubuntu-latest @@ -37,58 +42,40 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v3 with: - version: 9 + version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run audit + - name: Run pnpm audit run: pnpm audit --audit-level=high continue-on-error: true - - name: Check for known vulnerabilities + - name: Create audit summary run: | echo "## Dependency Audit Results" >> $GITHUB_STEP_SUMMARY - pnpm audit --json | jq -r '.advisories | to_entries[] | "- [\(.value.severity)] \(.value.module_name): \(.value.title)"' >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "No vulnerabilities found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + pnpm audit --json 2>/dev/null | jq -r ' + if .advisories then + .advisories | to_entries[] | + "| \(.value.severity | ascii_upcase) | \(.value.module_name) | \(.value.title) |" + else + "No vulnerabilities found" + end + ' >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "✅ No vulnerabilities found" >> $GITHUB_STEP_SUMMARY - # CodeQL Analysis - codeql: - name: CodeQL Analysis + # =========================================================================== + # ESLint Security Analysis (kostenlos, ersetzt CodeQL) + # =========================================================================== + eslint-security: + name: ESLint Security runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v4 - with: - languages: javascript-typescript - queries: security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v4 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 - with: - category: "/language:javascript-typescript" - - # Security Unit & Integration Tests - security-tests: - name: Security Tests - runs-on: ubuntu-latest - needs: [secrets, dependencies] steps: - name: Checkout code uses: actions/checkout@v4 @@ -96,12 +83,49 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v3 with: - version: 9 + version: ${{ env.PNPM_VERSION }} - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: ${{ env.NODE_VERSION }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run ESLint + run: pnpm lint + continue-on-error: true + + - name: Security summary + run: | + echo "## ESLint Security Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "ESLint wurde ausgeführt um potenzielle Sicherheitsprobleme zu finden." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Hinweis:** Für erweiterte Sicherheitsanalyse (wie CodeQL) wird GitHub Advanced Security benötigt." >> $GITHUB_STEP_SUMMARY + + # =========================================================================== + # Security Unit & Integration Tests + # =========================================================================== + security-tests: + name: Security Tests + runs-on: ubuntu-latest + needs: [dependencies] + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v3 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} cache: 'pnpm' - name: Install dependencies @@ -124,3 +148,26 @@ jobs: coverage/ test-results/ retention-days: 7 + + # =========================================================================== + # Security Summary + # =========================================================================== + summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [dependencies, eslint-security, security-tests] + if: always() + steps: + - name: Create summary + run: | + echo "## Security Scan Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Dependency Audit | ${{ needs.dependencies.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| ESLint Security | ${{ needs.eslint-security.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Security Tests | ${{ needs.security-tests.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Info:** GitHub Secret Scanning ist in den Repository-Einstellungen aktiviert (kostenlos für alle Repos)." >> $GITHUB_STEP_SUMMARY diff --git a/docs/DEPLOYMENT_STRATEGY.md b/docs/DEPLOYMENT_STRATEGY.md new file mode 100644 index 0000000..a051a72 --- /dev/null +++ b/docs/DEPLOYMENT_STRATEGY.md @@ -0,0 +1,448 @@ +# Deployment-Strategie: Dev → Production + +*Erstellt: 27. Dezember 2025* + +## Zusammenfassung + +Diese Strategie gewährleistet fehlerfreie Deployments von der Development-Umgebung (sv-payload) zur Production-Umgebung (Hetzner 3) durch: + +1. **Automatisierte CI/CD Pipeline** mit obligatorischen Tests +2. **Staging-first-Ansatz** - Änderungen müssen auf Staging erfolgreich sein +3. **Pre-deployment Backup** - Automatische Datenbank-Sicherung +4. **Health Checks** - Automatische Verifizierung nach Deployment +5. **Rollback-Mechanismus** - Schnelle Wiederherstellung bei Fehlern + +--- + +## Deployment-Workflow + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ DEPLOYMENT PIPELINE │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ DEVELOPMENT STAGING PRODUCTION │ +│ ───────────── ───────── ────────── │ +│ │ +│ ┌─────────┐ Push ┌─────────────┐ Merge ┌─────────────────┐ │ +│ │ Code │ ────────▶ │ CI Pipeline │ ──────────▶ │ Production CI │ │ +│ │ Changes │ │ (Automatic) │ │ (Manual Trigger)│ │ +│ └─────────┘ └──────┬──────┘ └────────┬────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌─────────────┐ ┌───────────────┐ │ +│ │ Tests │ │ Pre-flight │ │ +│ │ Lint │ │ Checks │ │ +│ │ Build │ └───────┬───────┘ │ +│ └──────┬──────┘ │ │ +│ │ ▼ │ +│ ▼ ┌─────────────┐ │ +│ ┌─────────────┐ │ DB Backup │ │ +│ │ Deploy to │ └──────┬──────┘ │ +│ │ Staging │ │ │ +│ └──────┬──────┘ ▼ │ +│ │ ┌─────────────┐ │ +│ ▼ │ Deploy │ │ +│ ┌─────────────┐ └──────┬──────┘ │ +│ │ Verify │ │ │ +│ │ Staging │ ▼ │ +│ └─────────────┘ ┌─────────────┐ │ +│ │ Health Check│ │ +│ └──────┬──────┘ │ +│ │ │ +│ ┌──────┴──────┐ │ +│ │ Pass? │ │ +│ └──────┬──────┘ │ +│ / \ │ +│ / \ │ +│ ┌────▼───┐ ┌────▼───┐ │ +│ │ SUCCESS│ │ROLLBACK│ │ +│ └────────┘ └────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Umgebungen + +| Umgebung | Server | URL | Branch | Zweck | +|----------|--------|-----|--------|-------| +| **Development** | sv-payload (LXC 700) | https://pl.porwoll.tech | `develop` | Entwicklung & Testing | +| **Production** | Hetzner 3 | https://cms.c2sgmbh.de | `main` | Live-System | + +--- + +## Schritt-für-Schritt Anleitung + +### Phase 1: Development & Testing (develop Branch) + +1. **Code-Änderungen auf develop pushen** + ```bash + git checkout develop + git add . + git commit -m "feat: your feature" + git push origin develop + ``` + +2. **Automatische CI Pipeline** (optimiert für GitHub Actions Kostenkontrolle) + - Bei **Push auf develop**: + - ESLint & Prettier Check + - TypeScript Compiler Check (optional) + - Build Test + - Bei **Pull Request** (volle Test-Suite): + - Alle obigen Checks + - Unit Tests + - Integration Tests + - E2E Tests + +3. **Automatisches Staging-Deployment** + - Bei erfolgreichem CI wird automatisch auf pl.porwoll.tech deployed + - Health Check verifiziert erfolgreiche Deployment + +4. **Manuelle Verifizierung auf Staging** + - Admin Panel testen: https://pl.porwoll.tech/admin + - Neue Features manuell prüfen + - API-Endpoints testen + +### Phase 2: Production Deployment (main Branch) + +#### Option A: Via GitHub Actions (Empfohlen) + +1. **develop in main mergen** + ```bash + git checkout main + git pull origin main + git merge develop + git push origin main + ``` + +2. **GitHub Actions Workflow starten** + - Gehe zu: Actions → "Deploy to Production" → "Run workflow" + - Oder via CLI: + ```bash + gh workflow run deploy-production.yml + ``` + +3. **Optionen beim Start:** + - `skip_tests`: Tests überspringen (nicht empfohlen) + - `skip_backup`: Backup überspringen (nicht empfohlen) + - `skip_migrations`: Migrationen überspringen + - `deploy_tag`: Spezifischer Git-Tag oder Commit + +4. **Workflow führt automatisch aus:** + - Pre-flight Checks + - Pre-deployment Tests + - Datenbank-Backup + - Deployment + - Health Check + - Bei Fehler: Automatischer Rollback + +#### Option B: Manuelles Deployment auf Server + +1. **SSH zum Production-Server** + ```bash + ssh payload@162.55.85.18 + ``` + +2. **Deploy-Script ausführen** + ```bash + cd ~/payload-cms + ./scripts/deploy-production.sh + ``` + +3. **Optionen:** + ```bash + # Normales Deployment + ./scripts/deploy-production.sh + + # Ohne Bestätigung (CI/Scripting) + ./scripts/deploy-production.sh -y + + # Ohne Backup (nicht empfohlen) + ./scripts/deploy-production.sh --skip-backup + + # Nur Service-Neustart + ./scripts/deploy-production.sh --skip-build + + # Rollback zur vorherigen Version + ./scripts/deploy-production.sh --rollback + + # Dry-Run (zeigt was passieren würde) + ./scripts/deploy-production.sh --dry-run + ``` + +--- + +## Rollback-Strategie + +### Automatischer Rollback (GitHub Actions) + +- Bei fehlgeschlagenem Health Check nach Deployment +- Workflow rollt automatisch auf vorherige Version zurück + +### Manueller Rollback + +```bash +# Auf dem Production-Server +ssh payload@162.55.85.18 +cd ~/payload-cms + +# Rollback zur vorherigen Version +./scripts/deploy-production.sh --rollback +``` + +### Rollback zu spezifischem Commit + +```bash +git log --oneline -10 # Zeige letzte Commits +git checkout main +git reset --hard +pnpm install --frozen-lockfile +pnpm build +pm2 restart payload +``` + +### Datenbank-Rollback + +```bash +# Backup-Dateien anzeigen +ls -la ~/backups/pre-deploy/ + +# Backup wiederherstellen +gunzip -c ~/backups/pre-deploy/payload_db_YYYY-MM-DD_HH-MM-SS.sql.gz | \ + psql -h localhost -U payload -d payload_db +``` + +--- + +## Pre-Deployment Checkliste + +### Vor jedem Production-Deployment + +- [ ] Alle CI-Tests auf develop erfolgreich +- [ ] Staging-Deployment erfolgreich (pl.porwoll.tech) +- [ ] Neue Features auf Staging manuell getestet +- [ ] Keine offenen kritischen Bugs +- [ ] develop in main gemergt +- [ ] Bei DB-Änderungen: Migration auf Staging getestet + +### Kritische Änderungen (Extra Vorsicht) + +- [ ] Schema-Migrationen +- [ ] Breaking API Changes +- [ ] Sicherheits-Updates +- [ ] Umgebungsvariablen-Änderungen + +--- + +## Post-Deployment Verifizierung + +### Automatische Checks + +1. **Health Check** - HTTP-Status von /admin +2. **PM2 Status** - Prozesse laufen + +### Manuelle Checks + +```bash +# PM2 Status +pm2 status + +# Logs auf Fehler prüfen +pm2 logs payload --lines 50 + +# Admin Panel erreichbar? +curl -I https://cms.c2sgmbh.de/admin + +# API funktioniert? +curl -I https://cms.c2sgmbh.de/api + +# Redis verbunden? +redis-cli ping +``` + +--- + +## Fehlerbehandlung + +### Build schlägt fehl + +```bash +# Cache löschen +rm -rf .next +NODE_OPTIONS="--max-old-space-size=2048" pnpm build + +# Bei Memory-Problemen: PM2 stoppen +pm2 stop all +NODE_OPTIONS="--max-old-space-size=3072" pnpm build +pm2 start ecosystem.config.cjs +``` + +### Migration schlägt fehl + +```bash +# Status prüfen +pnpm payload migrate:status + +# Logs prüfen +pm2 logs payload --lines 100 + +# Bei Bedarf: Rollback +./scripts/deploy-production.sh --rollback +``` + +### Service startet nicht + +```bash +# Logs prüfen +pm2 logs payload --lines 100 + +# Prozess komplett neu starten +pm2 delete payload +pm2 start ecosystem.config.cjs --only payload +``` + +--- + +## GitHub Secrets + +Folgende Secrets müssen in GitHub konfiguriert sein: + +| Secret | Beschreibung | Verwendet in | +|--------|--------------|--------------| +| `STAGING_SSH_KEY` | SSH Private Key für sv-payload | deploy-staging.yml | +| `PRODUCTION_SSH_KEY` | SSH Private Key für Hetzner 3 | deploy-production.yml | + +### SSH Key generieren + +```bash +# Neuen Key generieren +ssh-keygen -t ed25519 -C "github-actions@deploy" -f deploy_key + +# Public Key auf Server kopieren +ssh-copy-id -i deploy_key.pub payload@162.55.85.18 + +# Private Key in GitHub Secrets speichern +cat deploy_key | pbcopy # (macOS) oder xclip +``` + +--- + +## Monitoring nach Deployment + +### Erste 15 Minuten + +1. PM2 Logs auf Fehler überwachen +2. Admin Panel regelmäßig aufrufen +3. API-Requests prüfen + +### Erste 24 Stunden + +1. Umami Analytics prüfen +2. Error-Logs reviewen +3. User-Feedback sammeln + +--- + +## Best Practices + +### Do's + +- Immer erst auf Staging testen +- Backups vor kritischen Änderungen +- Kleine, inkrementelle Deployments +- Dokumentation aktualisieren +- Rollback-Plan vorbereiten + +### Don'ts + +- Nie direkt auf main entwickeln +- Nie ohne Tests deployen +- Nie große Schema-Änderungen ohne Backup +- Nie am Freitag Nachmittag deployen +- Nie mehrere kritische Änderungen gleichzeitig + +--- + +## Dateien + +| Datei | Beschreibung | +|-------|--------------| +| `.github/workflows/ci.yml` | CI Pipeline (Tests, Build) | +| `.github/workflows/security.yml` | Security Scanning (Dependency Audit, ESLint) | +| `.github/workflows/deploy-staging.yml` | Automatisches Staging-Deployment | +| `.github/workflows/deploy-production.yml` | Production-Deployment (manuell) | +| `scripts/deploy-staging.sh` | Manuelles Staging-Deployment | +| `scripts/deploy-production.sh` | Manuelles Production-Deployment | +| `ecosystem.config.cjs` | PM2 Konfiguration | + +--- + +## GitHub Actions Optimierung + +### Kostenmodell für Private Repos + +| Feature | Kostenlos | Kostenpflichtig | +|---------|-----------|-----------------| +| GitHub Actions | 2.000 Min/Monat | darüber hinaus | +| CodeQL | Öffentliche Repos | Private Repos (GHAS ~$49/Monat) | +| Secret Scanning | Repo-Settings (Free) | GHAS für erweiterte Features | +| Dependabot | Ja | - | + +### Optimierungen (27.12.2025) + +**CI-Workflow:** +- Push auf `develop`: Nur Lint + Build (spart ~80% Actions-Minuten) +- Pull Requests: Volle Test-Suite inkl. E2E +- Push auf `main`: Keine CI (wird durch deploy-production getriggert) +- Markdown-Änderungen: Überspringen CI komplett + +**Security-Workflow:** +- CodeQL entfernt (kostenpflichtig für private Repos) +- Ersetzt durch: ESLint + pnpm audit +- Nur bei PRs auf main und wöchentlich (nicht bei jedem Push) +- Secret Scanning: Native GitHub-Feature in Repo-Settings (kostenlos) + +### GitHub Actions Minutenverbrauch + +| Workflow | Trigger | Geschätzte Dauer | Häufigkeit | +|----------|---------|------------------|------------| +| CI (Push) | Push develop | ~3 Min | Pro Push | +| CI (PR) | Pull Request | ~15 Min | Pro PR | +| Security | PR + Wöchentlich | ~5 Min | 1x/Woche + PRs | +| Deploy Staging | Push develop | ~5 Min | Pro Push | +| Deploy Production | Manual | ~8 Min | Bei Release | + +**Geschätzte monatliche Nutzung:** +- ~20 Pushes × 3 Min = 60 Min +- ~5 PRs × 15 Min = 75 Min +- ~4 Security Scans × 5 Min = 20 Min +- ~20 Staging Deploys × 5 Min = 100 Min +- **Gesamt: ~255 Min/Monat** (von 2.000 kostenlos) + +### Bei Billing-Problemen + +1. **Spending Limit prüfen:** + GitHub → Settings → Billing → Spending limits + +2. **Zahlungsmethode aktualisieren:** + GitHub → Settings → Billing → Payment method + +3. **Actions deaktivieren (temporär):** + Repository → Settings → Actions → Disable Actions + +4. **Manuelles Deployment:** + ```bash + # Staging + ssh payload@10.10.181.100 + cd ~/payload-cms && ./scripts/deploy-staging.sh + + # Production + ssh payload@162.55.85.18 + cd ~/payload-cms && ./scripts/deploy-production.sh + ``` + +--- + +*Dokumentation: Complex Care Solutions GmbH | 27.12.2025* diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh new file mode 100755 index 0000000..f22e676 --- /dev/null +++ b/scripts/deploy-production.sh @@ -0,0 +1,327 @@ +#!/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"