name: Deploy on: push: branches: [master] workflow_dispatch: inputs: environment: description: Target environment required: true default: staging type: choice options: - staging - production concurrency: group: deploy-${{ inputs.environment || 'staging' }} cancel-in-progress: false env: REGISTRY: ghcr.io REGISTRY_URL: ghcr.io/${{ github.repository_owner }} jobs: build-api: name: Build API Image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_URL }}/goodgo-api tags: | type=sha,prefix= type=ref,event=branch type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - name: Build and push API image uses: docker/build-push-action@v6 with: context: . file: apps/api/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=api cache-to: type=gha,mode=max,scope=api build-web: name: Build Web Image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_URL }}/goodgo-web tags: | type=sha,prefix= type=ref,event=branch type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - name: Build and push Web image uses: docker/build-push-action@v6 with: context: . file: apps/web/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=web cache-to: type=gha,mode=max,scope=web build-ai: name: Build AI Services Image runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY_URL }}/goodgo-ai-services tags: | type=sha,prefix= type=ref,event=branch type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - name: Build and push AI Services image uses: docker/build-push-action@v6 with: context: ./libs/ai-services file: libs/ai-services/Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha,scope=ai cache-to: type=gha,mode=max,scope=ai deploy-staging: name: Deploy to Staging needs: [build-api, build-web, build-ai] if: github.event_name == 'push' || inputs.environment == 'staging' runs-on: ubuntu-latest environment: staging steps: - name: Checkout uses: actions/checkout@v4 - name: Deploy to staging env: DEPLOY_HOST: ${{ secrets.STAGING_HOST }} DEPLOY_USER: ${{ secrets.STAGING_USER }} DEPLOY_KEY: ${{ secrets.STAGING_SSH_KEY }} IMAGE_TAG: ${{ github.sha }} run: | mkdir -p ~/.ssh echo "$DEPLOY_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null # Copy production compose and deploy scp -i ~/.ssh/deploy_key docker-compose.prod.yml "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/" scp -i ~/.ssh/deploy_key -r monitoring/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/monitoring/" ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << DEPLOY_SCRIPT cd ~/goodgo export IMAGE_TAG="${IMAGE_TAG}" export REGISTRY_URL="${REGISTRY_URL}" # Login to GHCR echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin # Pull new images docker compose -f docker-compose.prod.yml pull api web ai-services # Rolling update — zero downtime docker compose -f docker-compose.prod.yml up -d --no-deps --wait api docker compose -f docker-compose.prod.yml up -d --no-deps --wait web docker compose -f docker-compose.prod.yml up -d --no-deps --wait ai-services # Run database migrations docker compose -f docker-compose.prod.yml exec api npx prisma migrate deploy # Cleanup old images docker image prune -f DEPLOY_SCRIPT - name: Verify staging deployment env: STAGING_URL: ${{ secrets.STAGING_URL }} run: | for i in $(seq 1 10); do if curl -sf "$STAGING_URL/health" > /dev/null 2>&1; then echo "Staging deployment verified successfully" exit 0 fi echo "Waiting for staging to be ready... ($i/10)" sleep 10 done echo "Staging health check failed" exit 1 smoke-test-staging: name: Smoke Test Staging needs: [deploy-staging] runs-on: ubuntu-latest environment: staging steps: - name: Checkout uses: actions/checkout@v4 - name: Run smoke tests env: STAGING_URL: ${{ secrets.STAGING_URL }} run: | chmod +x scripts/smoke-test.sh ./scripts/smoke-test.sh "$STAGING_URL" - name: Notify on failure if: failure() env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -s -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"text\": \":rotating_light: *Staging smoke tests FAILED* for \`${{ github.sha }}\`\", \"blocks\": [{ \"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \":rotating_light: *Staging Smoke Test Failure*\n*Commit:* \`${{ github.sha }}\`\n*Branch:* \`${{ github.ref_name }}\`\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>\" } }] }" deploy-production: name: Deploy to Production needs: [build-api, build-web, build-ai] if: inputs.environment == 'production' runs-on: ubuntu-latest environment: production steps: - name: Checkout uses: actions/checkout@v4 - name: Deploy to production env: DEPLOY_HOST: ${{ secrets.PRODUCTION_HOST }} DEPLOY_USER: ${{ secrets.PRODUCTION_USER }} DEPLOY_KEY: ${{ secrets.PRODUCTION_SSH_KEY }} IMAGE_TAG: ${{ github.sha }} run: | mkdir -p ~/.ssh echo "$DEPLOY_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null scp -i ~/.ssh/deploy_key docker-compose.prod.yml "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/" scp -i ~/.ssh/deploy_key -r monitoring/ "$DEPLOY_USER@$DEPLOY_HOST:~/goodgo/monitoring/" ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << DEPLOY_SCRIPT cd ~/goodgo export IMAGE_TAG="${IMAGE_TAG}" export REGISTRY_URL="${REGISTRY_URL}" echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin docker compose -f docker-compose.prod.yml pull api web ai-services # Rolling update with health checks docker compose -f docker-compose.prod.yml up -d --no-deps --wait api docker compose -f docker-compose.prod.yml up -d --no-deps --wait web docker compose -f docker-compose.prod.yml up -d --no-deps --wait ai-services docker compose -f docker-compose.prod.yml exec api npx prisma migrate deploy docker image prune -f DEPLOY_SCRIPT - name: Verify production deployment env: PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }} run: | for i in $(seq 1 10); do if curl -sf "$PRODUCTION_URL/health" > /dev/null 2>&1; then echo "Production deployment verified successfully" exit 0 fi echo "Waiting for production to be ready... ($i/10)" sleep 10 done echo "Production health check failed" exit 1 smoke-test-production: name: Smoke Test Production needs: [deploy-production] runs-on: ubuntu-latest environment: production steps: - name: Checkout uses: actions/checkout@v4 - name: Run smoke tests env: PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }} run: | chmod +x scripts/smoke-test.sh ./scripts/smoke-test.sh "$PRODUCTION_URL" - name: Notify on success if: success() env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -s -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"text\": \":white_check_mark: *Production deploy successful* for \`${{ github.sha }}\`\", \"blocks\": [{ \"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \":white_check_mark: *Production Deploy Successful*\n*Commit:* \`${{ github.sha }}\`\n*Branch:* \`${{ github.ref_name }}\`\n*All smoke tests passed.*\" } }] }" rollback-production: name: Rollback Production needs: [smoke-test-production] if: failure() runs-on: ubuntu-latest environment: production steps: - name: Rollback to previous images env: DEPLOY_HOST: ${{ secrets.PRODUCTION_HOST }} DEPLOY_USER: ${{ secrets.PRODUCTION_USER }} DEPLOY_KEY: ${{ secrets.PRODUCTION_SSH_KEY }} run: | mkdir -p ~/.ssh echo "$DEPLOY_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" << 'ROLLBACK_SCRIPT' cd ~/goodgo echo "Rolling back to previous container images..." # Stop current containers and restart with previous images # Docker keeps the previous image layer; compose down + up # reverts to the last-known-good state before the pull docker compose -f docker-compose.prod.yml down api web ai-services docker compose -f docker-compose.prod.yml up -d --wait api web ai-services echo "Rollback complete. Verifying health..." sleep 5 curl -sf http://localhost:3001/health || echo "WARNING: health check failed after rollback" ROLLBACK_SCRIPT - name: Notify rollback env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} run: | curl -s -X POST "$SLACK_WEBHOOK" \ -H "Content-Type: application/json" \ -d "{ \"text\": \":warning: *Production ROLLBACK triggered* for \`${{ github.sha }}\`\", \"blocks\": [{ \"type\": \"section\", \"text\": { \"type\": \"mrkdwn\", \"text\": \":warning: *Production Rollback Triggered*\n*Commit:* \`${{ github.sha }}\`\n*Reason:* Smoke tests failed after deploy\n*Action:* Reverted to previous container images\n*Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View logs>\" } }] }"