managarten/.github/workflows/cd-production.yml
Wuesteon dcdc15f154 🚀 ci(picture): add picture app to CI/CD deployment pipeline
- Add picture-backend and picture-web to CI Docker build matrix
- Add picture services to staging deployment workflow
- Add picture-backend to production deployment workflow
- Create Dockerfile and docker-entrypoint.sh for picture-web
- Fix picture-backend Dockerfile port (3003→3006) and health endpoint
- Add picture routes to Caddyfile.staging
- Add REPLICATE_API_TOKEN and MANA_CORE_SERVICE_KEY env vars
2025-12-17 18:47:42 +01:00

390 lines
13 KiB
YAML

# Production Deployment
#
# Triggered by:
# - Manual only (workflow_dispatch with confirmation)
#
# Flow: dev (staging) → main (production)
# Requires typing "deploy" to confirm
name: CD - Production Deployment
on:
workflow_dispatch:
inputs:
service:
description: 'Service to deploy'
required: true
type: choice
options:
- all
- mana-core-auth
- maerchenzauber-backend
- chat-backend
- manadeck-backend
- nutriphi-backend
- news-api
- picture-backend
environment:
description: 'Deployment environment'
required: true
type: choice
options:
- production
confirm:
description: 'Type "deploy" to confirm production deployment'
required: true
type: string
env:
NODE_VERSION: '20'
PNPM_VERSION: '9.15.0'
jobs:
validate-deployment:
name: Validate Deployment Request
runs-on: ubuntu-latest
steps:
- name: Validate confirmation
run: |
if [ "${{ github.event.inputs.confirm }}" != "deploy" ]; then
echo "❌ Deployment not confirmed. Please type 'deploy' to confirm."
exit 1
fi
echo "✅ Deployment confirmed"
- name: Validate branch
run: |
if [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "❌ Production deployments must be from main branch"
exit 1
fi
echo "✅ Deploying from main branch"
- name: Check recent commits
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Verify recent CI passes
run: |
echo "Checking recent CI status..."
# This would check recent CI runs, simplified for now
echo "✅ Recent CI checks verified"
# Request manual approval for production
request-approval:
name: Request Production Approval
runs-on: ubuntu-latest
needs: validate-deployment
environment:
name: production-approval
steps:
- name: Approval granted
run: |
echo "## Production Deployment Approved" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Approved by**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
echo "- **Service**: ${{ github.event.inputs.service }}" >> $GITHUB_STEP_SUMMARY
echo "- **Timestamp**: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
# Create deployment backup
create-backup:
name: Create Production Backup
runs-on: ubuntu-latest
needs: request-approval
environment:
name: production
steps:
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
- name: Add production server to known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.PRODUCTION_HOST }} >> ~/.ssh/known_hosts
- name: Create database backup
run: |
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
cd ~/manacore-production
# Backup timestamp
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="backups/$TIMESTAMP"
mkdir -p $BACKUP_DIR
# Backup PostgreSQL
docker compose exec -T postgres pg_dumpall -U $POSTGRES_USER > $BACKUP_DIR/postgres_backup.sql
# Backup Redis (if applicable)
docker compose exec -T redis redis-cli SAVE || echo "Redis backup skipped"
# Backup docker-compose and env files
cp docker-compose.yml $BACKUP_DIR/
cp .env $BACKUP_DIR/.env.backup
echo "Backup created at: $BACKUP_DIR"
ls -lh $BACKUP_DIR/
EOF
- name: Tag current deployment
run: |
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
cd ~/manacore-production
docker compose images > deployment_images.txt
echo "Current deployment tagged: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
EOF
# Deploy to production
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: create-backup
environment:
name: production
url: https://api.manacore.app
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
- name: Add production server to known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.PRODUCTION_HOST }} >> ~/.ssh/known_hosts
- name: Copy deployment files
run: |
scp docker-compose.production.yml ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }}:~/manacore-production/docker-compose.yml
- name: Update environment variables
run: |
# Create production env file from secrets
cat > .env.production << EOF
# Database
POSTGRES_HOST=${{ secrets.PRODUCTION_POSTGRES_HOST }}
POSTGRES_PORT=${{ secrets.PRODUCTION_POSTGRES_PORT }}
POSTGRES_DB=${{ secrets.PRODUCTION_POSTGRES_DB }}
POSTGRES_USER=${{ secrets.PRODUCTION_POSTGRES_USER }}
POSTGRES_PASSWORD=${{ secrets.PRODUCTION_POSTGRES_PASSWORD }}
# Redis
REDIS_HOST=${{ secrets.PRODUCTION_REDIS_HOST }}
REDIS_PORT=${{ secrets.PRODUCTION_REDIS_PORT }}
REDIS_PASSWORD=${{ secrets.PRODUCTION_REDIS_PASSWORD }}
# Mana Core Auth
MANA_SERVICE_URL=${{ secrets.PRODUCTION_MANA_SERVICE_URL }}
JWT_SECRET=${{ secrets.PRODUCTION_JWT_SECRET }}
JWT_PUBLIC_KEY=${{ secrets.PRODUCTION_JWT_PUBLIC_KEY }}
JWT_PRIVATE_KEY=${{ secrets.PRODUCTION_JWT_PRIVATE_KEY }}
# Supabase
SUPABASE_URL=${{ secrets.PRODUCTION_SUPABASE_URL }}
SUPABASE_ANON_KEY=${{ secrets.PRODUCTION_SUPABASE_ANON_KEY }}
SUPABASE_SERVICE_ROLE_KEY=${{ secrets.PRODUCTION_SUPABASE_SERVICE_ROLE_KEY }}
# Azure OpenAI
AZURE_OPENAI_ENDPOINT=${{ secrets.PRODUCTION_AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_API_KEY=${{ secrets.PRODUCTION_AZURE_OPENAI_API_KEY }}
AZURE_OPENAI_API_VERSION=2024-12-01-preview
# Environment
NODE_ENV=production
EOF
scp .env.production ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }}:~/manacore-production/.env
rm .env.production
- name: Pull latest images
run: |
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
cd ~/manacore-production
docker compose pull
EOF
- name: Run database migrations
run: |
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
cd ~/manacore-production
echo "=== Running Database Migrations ==="
echo ""
# Migration function with retry logic
run_migration() {
local service=$1
local max_attempts=3
local timeout=300 # 5 minutes
local attempt=1
while [ $attempt -le $max_attempts ]; do
echo "[$service] Migration attempt $attempt/$max_attempts..."
# Run migration with timeout using a temporary container
if timeout $timeout docker compose run --rm $service pnpm run db:migrate 2>&1; then
echo "✅ [$service] Migration succeeded"
return 0
else
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo "⚠️ [$service] Migration timeout after ${timeout}s"
else
echo "⚠️ [$service] Migration failed with exit code $exit_code"
fi
attempt=$((attempt + 1))
if [ $attempt -le $max_attempts ]; then
wait_time=$((10 * attempt)) # Backoff: 10s, 20s, 30s
echo " Waiting ${wait_time}s before retry..."
sleep $wait_time
fi
fi
done
echo "❌ [$service] Migration failed after $max_attempts attempts"
return 1
}
# Run migrations for mana-core-auth (central auth service)
run_migration mana-core-auth || {
echo "❌ mana-core-auth migration failed"
echo "⚠️ Continuing with deployment - manual migration may be required"
}
echo ""
echo "✅ Migration step completed"
EOF
- name: Deploy with zero-downtime
run: |
SERVICE="${{ github.event.inputs.service }}"
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << EOF
cd ~/manacore-production
if [ "$SERVICE" == "all" ]; then
# Rolling update for all services
for service in mana-core-auth maerchenzauber-backend chat-backend manadeck-backend nutriphi-backend news-api picture-backend; do
echo "Deploying \$service..."
docker compose up -d --no-deps --scale \$service=2 \$service
sleep 10
docker compose up -d --no-deps --scale \$service=1 \$service
done
else
# Single service deployment
echo "Deploying $SERVICE..."
docker compose up -d --no-deps $SERVICE
fi
# Cleanup old images
docker image prune -f
EOF
- name: Verify deployment
run: |
# Wait for services to stabilize
sleep 30
SERVICES=(
"mana-core-auth:3001:/api/v1/health"
"maerchenzauber-backend:3002:/health"
"chat-backend:3002:/api/health"
)
for SERVICE_CONFIG in "${SERVICES[@]}"; do
IFS=':' read -r SERVICE PORT PATH <<< "$SERVICE_CONFIG"
echo "Verifying $SERVICE..."
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << EOF
HEALTH=\$(docker compose -f ~/manacore-production/docker-compose.yml exec -T $SERVICE wget -q -O - http://localhost:$PORT$PATH || echo "FAILED")
if [[ "\$HEALTH" == *"FAILED"* ]]; then
echo "❌ Health check failed for $SERVICE"
docker compose -f ~/manacore-production/docker-compose.yml logs --tail=100 $SERVICE
exit 1
else
echo "✅ Health check passed for $SERVICE"
fi
EOF
done
- name: Monitor for 5 minutes
run: |
echo "Monitoring services for 5 minutes..."
for i in {1..5}; do
echo "Check $i/5..."
sleep 60
ssh ${{ secrets.PRODUCTION_USER }}@${{ secrets.PRODUCTION_HOST }} << 'EOF'
cd ~/manacore-production
docker compose ps
EOF
done
echo "✅ Monitoring complete - services stable"
# Post-deployment verification
post-deployment-checks:
name: Post-Deployment Checks
runs-on: ubuntu-latest
needs: deploy-production
steps:
- name: Run smoke tests
run: |
# Test key endpoints
ENDPOINTS=(
"${{ secrets.PRODUCTION_API_URL }}/api/v1/health"
"${{ secrets.PRODUCTION_API_URL }}/health"
)
for ENDPOINT in "${ENDPOINTS[@]}"; do
echo "Testing: $ENDPOINT"
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" $ENDPOINT)
if [ "$RESPONSE" -eq 200 ]; then
echo "✅ $ENDPOINT is healthy"
else
echo "❌ $ENDPOINT returned $RESPONSE"
exit 1
fi
done
- name: Deployment summary
run: |
echo "## Production Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Environment**: Production" >> $GITHUB_STEP_SUMMARY
echo "- **Deployed by**: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
echo "- **Service**: ${{ github.event.inputs.service }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "- **Timestamp**: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Deployment Status" >> $GITHUB_STEP_SUMMARY
echo "✅ All services deployed and verified successfully" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Backup Information" >> $GITHUB_STEP_SUMMARY
echo "Pre-deployment backup created and stored" >> $GITHUB_STEP_SUMMARY
# Notify team
notify-deployment:
name: Notify Team
runs-on: ubuntu-latest
needs: post-deployment-checks
if: always()
steps:
- name: Deployment notification
run: |
STATUS="${{ needs.post-deployment-checks.result }}"
if [ "$STATUS" == "success" ]; then
echo "✅ Production deployment completed successfully"
echo "Service: ${{ github.event.inputs.service }}"
else
echo "❌ Production deployment failed"
echo "Please check logs and consider rollback"
exit 1
fi