name: Deploy Backend to Cloud Run on: push: branches: [main] paths: - 'backend/**' - '.github/workflows/deploy-backend.yml' workflow_dispatch: inputs: environment: description: 'Environment to deploy to' type: choice required: true default: 'production' options: - production - staging env: PROJECT_ID: mana-core-453821 REGION: europe-west3 ARTIFACT_REGISTRY: europe-west3-docker.pkg.dev SERVICE_NAME: manadeck-backend REPOSITORY_NAME: manadeck-backend WORKING_DIR: backend jobs: test: name: Test & Build Verification runs-on: ubuntu-latest permissions: contents: read packages: read defaults: run: working-directory: ${{ env.WORKING_DIR }} steps: - name: Checkout code uses: actions/checkout@v4 with: persist-credentials: false - name: Configure git for private packages env: GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} run: | git config --global url."https://${GH_TOKEN}@github.com/".insteadOf ssh://git@github.com/ git config --global url."https://${GH_TOKEN}@github.com/".insteadOf git@github.com: - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' cache-dependency-path: ${{ env.WORKING_DIR }}/package-lock.json - name: Patch package-lock.json with authenticated URLs env: GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} working-directory: ${{ env.WORKING_DIR }} run: | # Handle both SSH and HTTPS URLs if grep -q "git+ssh://git@github.com" package-lock.json; then echo "⚠️ SSH URLs found - patching to HTTPS with token..." sed -i "s|git+ssh://git@github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json echo "✓ Lockfile patched successfully" else echo "⚠️ HTTPS URLs found - injecting token..." sed -i "s|git+https://github.com/Memo-2023/|git+https://${GH_TOKEN}@github.com/Memo-2023/|g" package-lock.json echo "✓ Token injected successfully" fi - name: Install dependencies working-directory: ${{ env.WORKING_DIR }} run: npm ci --legacy-peer-deps - name: Type check & build working-directory: ${{ env.WORKING_DIR }} run: npm run build - name: Run tests working-directory: ${{ env.WORKING_DIR }} run: npm test continue-on-error: true - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: dist path: ${{ env.WORKING_DIR }}/dist retention-days: 1 build-and-deploy: name: Build & Deploy to Cloud Run needs: test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Google Cloud Auth uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY_PROD }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 with: version: 'latest' - name: Configure Docker for Artifact Registry run: | gcloud auth configure-docker ${{ env.ARTIFACT_REGISTRY }} - name: Generate version tag id: version run: | SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7) TIMESTAMP=$(date +%Y%m%d-%H%M%S) VERSION="v${TIMESTAMP}-${SHORT_SHA}" echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - name: Build and Push Docker Image env: DOCKER_BUILDKIT: 1 GH_TOKEN: ${{ secrets.GH_PERSONAL_TOKEN }} working-directory: ${{ env.WORKING_DIR }} run: | IMAGE_NAME="${{ env.ARTIFACT_REGISTRY }}/${{ env.PROJECT_ID }}/${{ env.REPOSITORY_NAME }}/${{ env.SERVICE_NAME }}" IMAGE_TAG="${IMAGE_NAME}:${{ steps.version.outputs.version }}" LATEST_TAG="${IMAGE_NAME}:latest" SHA_TAG="${IMAGE_NAME}:${{ steps.version.outputs.short_sha }}" echo "Building image: ${IMAGE_TAG}" docker build \ -t ${IMAGE_TAG} \ -t ${LATEST_TAG} \ -t ${SHA_TAG} \ --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ --build-arg VCS_REF=${{ github.sha }} \ --build-arg VERSION=${{ steps.version.outputs.version }} \ --secret id=github_token,env=GH_TOKEN \ . echo "Pushing images..." docker push ${IMAGE_TAG} docker push ${LATEST_TAG} docker push ${SHA_TAG} echo "image_tag=${IMAGE_TAG}" >> $GITHUB_ENV echo "version=${{ steps.version.outputs.version }}" >> $GITHUB_ENV - name: Deploy to Cloud Run id: deploy run: | echo "Deploying to Cloud Run..." echo "Service account: ${{ secrets.CLOUD_RUN_SERVICE_ACCOUNT }}" gcloud run deploy ${{ env.SERVICE_NAME }} \ --image="${{ env.image_tag }}" \ --project=${{ env.PROJECT_ID }} \ --region=${{ env.REGION }} \ --platform=managed \ --allow-unauthenticated \ --min-instances=0 \ --max-instances=10 \ --memory=512Mi \ --cpu=1 \ --timeout=300 \ --concurrency=80 \ --port=8080 \ --service-account=${{ secrets.CLOUD_RUN_SERVICE_ACCOUNT }} \ --set-env-vars="NODE_ENV=production" \ --set-secrets="MANA_SERVICE_URL=MANA_SERVICE_URL:latest,APP_ID=MANADECK_APP_ID:latest,MANA_SUPABASE_SECRET_KEY=MANA_SUPABASE_SECRET_KEY:latest,SUPABASE_URL=MANADECK_SUPABASE_URL:latest,SUPABASE_ANON_KEY=MANADECK_SUPABASE_ANON_KEY:latest,SUPABASE_SERVICE_KEY=MANADECK_SUPABASE_SERVICE_KEY:latest" \ --labels="environment=production,commit=${{ steps.version.outputs.short_sha }},version=${{ env.version }}" # Ensure 100% traffic goes to the new revision echo "Routing traffic to latest revision..." gcloud run services update-traffic ${{ env.SERVICE_NAME }} \ --to-latest \ --project=${{ env.PROJECT_ID }} \ --region=${{ env.REGION }} \ --platform=managed echo "✅ Traffic routed to latest revision" - name: Get Service URL id: service-url run: | SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} \ --project=${{ env.PROJECT_ID }} \ --region=${{ env.REGION }} \ --format='value(status.url)') echo "service_url=${SERVICE_URL}" >> $GITHUB_OUTPUT echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- **Service**: \`${{ env.SERVICE_NAME }}\`" >> $GITHUB_STEP_SUMMARY echo "- **URL**: ${SERVICE_URL}" >> $GITHUB_STEP_SUMMARY echo "- **Version**: \`${{ env.version }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Image**: \`${{ env.image_tag }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Region**: \`${{ env.REGION }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Commit**: \`${{ steps.version.outputs.short_sha }}\`" >> $GITHUB_STEP_SUMMARY - name: Wait for deployment run: sleep 15 - name: Health Check id: health-check run: | SERVICE_URL="${{ steps.service-url.outputs.service_url }}" echo "Testing health endpoint: ${SERVICE_URL}/health/live" MAX_RETRIES=5 RETRY_COUNT=0 SUCCESS=false while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${SERVICE_URL}/health/live || echo "000") if [ "$HTTP_CODE" = "200" ]; then echo "✅ Health check passed (HTTP $HTTP_CODE)" SUCCESS=true break else echo "⚠️ Health check attempt $((RETRY_COUNT + 1))/$MAX_RETRIES failed (HTTP $HTTP_CODE)" RETRY_COUNT=$((RETRY_COUNT + 1)) if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then echo "Retrying in 10 seconds..." sleep 10 fi fi done if [ "$SUCCESS" = false ]; then echo "❌ Health check failed after $MAX_RETRIES attempts" exit 1 fi - name: Liveness Check run: | SERVICE_URL="${{ steps.service-url.outputs.service_url }}" echo "Testing liveness endpoint: ${SERVICE_URL}/health/live" RESPONSE=$(curl -s ${SERVICE_URL}/health/live) echo "Response: $RESPONSE" STATUS=$(echo $RESPONSE | jq -r '.status') if [ "$STATUS" = "ok" ]; then echo "✅ Liveness check passed" else echo "❌ Liveness check failed" exit 1 fi - name: Deployment Notification if: success() run: | echo "## ✅ Deployment Successful" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Service is healthy and ready to receive traffic." >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Quick Links" >> $GITHUB_STEP_SUMMARY echo "- [Service URL](${{ steps.service-url.outputs.service_url }})" >> $GITHUB_STEP_SUMMARY echo "- [Health Check](${{ steps.service-url.outputs.service_url }}/health)" >> $GITHUB_STEP_SUMMARY echo "- [Cloud Run Console](https://console.cloud.google.com/run/detail/${{ env.REGION }}/${{ env.SERVICE_NAME }}/metrics?project=${{ env.PROJECT_ID }})" >> $GITHUB_STEP_SUMMARY rollback: name: Rollback on Failure runs-on: ubuntu-latest if: failure() && needs.build-and-deploy.result == 'failure' needs: build-and-deploy steps: - name: Google Cloud Auth uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY_PROD }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 with: version: 'latest' - name: Get Previous Revision id: get-revision run: | REVISIONS=$(gcloud run revisions list \ --service=${{ env.SERVICE_NAME }} \ --project=${{ env.PROJECT_ID }} \ --region=${{ env.REGION }} \ --format="value(name)" \ --limit=2) PREV_REVISION=$(echo "$REVISIONS" | tail -n 1) if [ -z "$PREV_REVISION" ]; then echo "❌ No previous revision found for rollback" exit 1 fi echo "prev_revision=${PREV_REVISION}" >> $GITHUB_OUTPUT echo "Found previous revision: ${PREV_REVISION}" - name: Rollback to Previous Revision run: | echo "Rolling back to revision: ${{ steps.get-revision.outputs.prev_revision }}" gcloud run services update-traffic ${{ env.SERVICE_NAME }} \ --to-revisions=${{ steps.get-revision.outputs.prev_revision }}=100 \ --project=${{ env.PROJECT_ID }} \ --region=${{ env.REGION }} echo "## ⚠️ Deployment Failed - Rollback Executed" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Service has been rolled back to revision: \`${{ steps.get-revision.outputs.prev_revision }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Please check the logs and fix the issues before redeploying." >> $GITHUB_STEP_SUMMARY - name: Verify Rollback run: | sleep 10 SERVICE_URL=$(gcloud run services describe ${{ env.SERVICE_NAME }} \ --project=${{ env.PROJECT_ID }} \ --region=${{ env.REGION }} \ --format='value(status.url)') HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" ${SERVICE_URL}/health/live) if [ $HTTP_CODE -eq 200 ]; then echo "✅ Rollback successful - service is healthy" else echo "❌ Rollback verification failed - service may be unhealthy" exit 1 fi