mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14:12 +00:00
Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 3 to 4. - [Release notes](https://github.com/pnpm/action-setup/releases) - [Commits](https://github.com/pnpm/action-setup/compare/v3...v4) --- updated-dependencies: - dependency-name: pnpm/action-setup dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
439 lines
16 KiB
YAML
439 lines
16 KiB
YAML
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@v4
|
|
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
|
|
CRON_SECRET: ci-cron-secret-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')
|
|
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 ==="
|
|
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
|
|
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 (Payload serves /api/users as a valid endpoint)
|
|
API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ env.PRODUCTION_URL }}/api/users")
|
|
echo "API: HTTP $API_STATUS"
|
|
|
|
# Summary (API returns 401 without auth — that's expected and proves it's running)
|
|
if [ "$ADMIN_STATUS" -ge 200 ] && [ "$ADMIN_STATUS" -lt 400 ] && \
|
|
[ "$API_STATUS" -ge 200 ] && [ "$API_STATUS" -lt 500 ]; 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."
|