mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:01:08 +02:00
338 lines
12 KiB
YAML
338 lines
12 KiB
YAML
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
|
|
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
|
|
|
|
# Run migrations before deploying new code
|
|
docker compose run --rm mana-core-auth pnpm run db:migrate || echo "Migrations completed or skipped"
|
|
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; 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
|