feat(analytics): add Google Analytics gtag #53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build & Deploy to Cloud | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'apps/**' | |
| - 'infrastructure/helm/**' | |
| - '.github/workflows/deploy.yml' | |
| workflow_dispatch: # Manual trigger (builds all) | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_PREFIX: ${{ github.repository_owner }}/taskflow | |
| jobs: | |
| # ============================================================================= | |
| # Detect which services changed | |
| # ============================================================================= | |
| changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| api: ${{ steps.filter.outputs.api }} | |
| sso: ${{ steps.filter.outputs.sso }} | |
| mcp: ${{ steps.filter.outputs.mcp }} | |
| notification: ${{ steps.filter.outputs.notification }} | |
| web: ${{ steps.filter.outputs.web }} | |
| helm: ${{ steps.filter.outputs.helm }} | |
| any_service: ${{ steps.filter.outputs.api == 'true' || steps.filter.outputs.sso == 'true' || steps.filter.outputs.mcp == 'true' || steps.filter.outputs.notification == 'true' || steps.filter.outputs.web == 'true' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Detect changes | |
| uses: dorny/paths-filter@v3 | |
| id: filter | |
| with: | |
| filters: | | |
| api: | |
| - 'apps/api/**' | |
| sso: | |
| - 'apps/sso/**' | |
| mcp: | |
| - 'apps/mcp-server/**' | |
| notification: | |
| - 'apps/notification-service/**' | |
| web: | |
| - 'apps/web/**' | |
| helm: | |
| - 'infrastructure/helm/**' | |
| # ============================================================================= | |
| # Build Docker images (only changed services) | |
| # ============================================================================= | |
| build-api: | |
| needs: changes | |
| if: needs.changes.outputs.api == 'true' || github.event_name == 'workflow_dispatch' | |
| 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: Log in to GHCR | |
| 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 }}/${{ env.IMAGE_PREFIX }}/api | |
| tags: | | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/api | |
| file: apps/api/Dockerfile | |
| push: true | |
| platforms: linux/arm64 | |
| no-cache: true | |
| provenance: false | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-sso: | |
| needs: changes | |
| if: needs.changes.outputs.sso == 'true' || github.event_name == 'workflow_dispatch' | |
| 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: Log in to GHCR | |
| 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 }}/${{ env.IMAGE_PREFIX }}/sso | |
| tags: | | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/sso | |
| file: apps/sso/Dockerfile | |
| push: true | |
| platforms: linux/arm64 | |
| no-cache: true | |
| provenance: false | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| NEXT_PUBLIC_BETTER_AUTH_URL=https://sso.${{ vars.DOMAIN }} | |
| NEXT_PUBLIC_CONTINUE_URL=https://${{ vars.DOMAIN }} | |
| NEXT_PUBLIC_APP_NAME=Taskflow SSO | |
| build-mcp: | |
| needs: changes | |
| if: needs.changes.outputs.mcp == 'true' || github.event_name == 'workflow_dispatch' | |
| 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: Log in to GHCR | |
| 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 }}/${{ env.IMAGE_PREFIX }}/mcp | |
| tags: | | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/mcp-server | |
| file: apps/mcp-server/Dockerfile | |
| push: true | |
| platforms: linux/arm64 | |
| no-cache: true | |
| provenance: false | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-notification: | |
| needs: changes | |
| if: needs.changes.outputs.notification == 'true' || github.event_name == 'workflow_dispatch' | |
| 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: Log in to GHCR | |
| 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 }}/${{ env.IMAGE_PREFIX }}/notification | |
| tags: | | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/notification-service | |
| file: apps/notification-service/Dockerfile | |
| push: true | |
| platforms: linux/arm64 | |
| no-cache: true | |
| provenance: false | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-web: | |
| needs: changes | |
| if: needs.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' | |
| 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: Log in to GHCR | |
| 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 }}/${{ env.IMAGE_PREFIX }}/web | |
| tags: | | |
| type=raw,value=${{ github.sha }} | |
| type=raw,value=latest,enable={{is_default_branch}} | |
| - name: Build and push | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: apps/web | |
| file: apps/web/Dockerfile | |
| push: true | |
| platforms: linux/arm64 | |
| no-cache: true | |
| provenance: false | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| build-args: | | |
| NEXT_PUBLIC_SSO_URL=https://sso.${{ vars.DOMAIN }} | |
| NEXT_PUBLIC_API_URL=https://api.${{ vars.DOMAIN }} | |
| NEXT_PUBLIC_APP_URL=https://${{ vars.DOMAIN }} | |
| NEXT_PUBLIC_OAUTH_REDIRECT_URI=https://${{ vars.DOMAIN }}/api/auth/callback | |
| NEXT_PUBLIC_CHATKIT_DOMAIN_KEY=${{ secrets.CHATKIT_DOMAIN_KEY }} | |
| # ============================================================================= | |
| # Deploy to Kubernetes cluster | |
| # ============================================================================= | |
| deploy: | |
| needs: [changes, build-api, build-sso, build-mcp, build-notification, build-web] | |
| # Run if any build ran OR if only helm changed | |
| if: | | |
| always() && | |
| (needs.changes.outputs.any_service == 'true' || needs.changes.outputs.helm == 'true' || github.event_name == 'workflow_dispatch') && | |
| !contains(needs.*.result, 'failure') | |
| runs-on: ubuntu-latest | |
| environment: production | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up kubectl | |
| uses: azure/setup-kubectl@v3 | |
| with: | |
| version: 'v1.28.0' | |
| - name: Set up Helm | |
| uses: azure/setup-helm@v3 | |
| with: | |
| version: 'v3.13.0' | |
| # For Azure AKS | |
| - name: Azure Login | |
| if: ${{ vars.CLOUD_PROVIDER == 'azure' }} | |
| uses: azure/login@v1 | |
| with: | |
| creds: ${{ secrets.AZURE_CREDENTIALS }} | |
| - name: Get AKS credentials | |
| if: ${{ vars.CLOUD_PROVIDER == 'azure' }} | |
| run: | | |
| az aks get-credentials \ | |
| --resource-group ${{ vars.AZURE_RESOURCE_GROUP }} \ | |
| --name ${{ vars.AZURE_CLUSTER_NAME }} | |
| # For GKE | |
| - name: Authenticate to GKE | |
| if: ${{ vars.CLOUD_PROVIDER == 'gke' }} | |
| uses: google-github-actions/auth@v1 | |
| with: | |
| credentials_json: ${{ secrets.GCP_CREDENTIALS }} | |
| - name: Get GKE credentials | |
| if: ${{ vars.CLOUD_PROVIDER == 'gke' }} | |
| uses: google-github-actions/get-gke-credentials@v1 | |
| with: | |
| cluster_name: ${{ vars.GKE_CLUSTER_NAME }} | |
| location: ${{ vars.GKE_CLUSTER_ZONE }} | |
| # For any provider with kubeconfig | |
| - name: Set kubeconfig | |
| if: ${{ vars.CLOUD_PROVIDER == 'kubeconfig' }} | |
| run: | | |
| mkdir -p ~/.kube | |
| echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config | |
| chmod 600 ~/.kube/config | |
| - name: Add Dapr Helm repo | |
| run: | | |
| helm repo add dapr https://dapr.github.io/helm-charts/ | |
| helm repo update | |
| - name: Install/Upgrade Dapr | |
| run: | | |
| helm upgrade --install dapr dapr/dapr \ | |
| --version=1.15 \ | |
| --namespace dapr-system \ | |
| --create-namespace \ | |
| --set dapr_scheduler.cluster.storageSize=4Gi \ | |
| --wait | |
| - name: Create namespace | |
| run: kubectl create namespace taskflow --dry-run=client -o yaml | kubectl apply -f - | |
| - name: Create GHCR pull secret | |
| run: | | |
| kubectl create secret docker-registry ghcr-secret \ | |
| --namespace taskflow \ | |
| --docker-server=ghcr.io \ | |
| --docker-username=${{ github.actor }} \ | |
| --docker-password=${{ secrets.GITHUB_TOKEN }} \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| - name: Deploy with Helm | |
| run: | | |
| # Determine ingress class based on what's installed | |
| INGRESS_CLASS="${{ vars.INGRESS_CLASS }}" | |
| if [ -z "$INGRESS_CLASS" ]; then | |
| INGRESS_CLASS="traefik" # Default to traefik | |
| fi | |
| # Create temporary values file for comma-containing values (avoids Helm --set parsing issues) | |
| cat > /tmp/helm-overrides.yaml << 'ENDOFVALUES' | |
| sso: | |
| env: | |
| ALLOWED_ORIGINS: "https://${{ vars.DOMAIN }},https://sso.${{ vars.DOMAIN }},https://api.${{ vars.DOMAIN }},https://mcp.${{ vars.DOMAIN }}" | |
| api: | |
| env: | |
| CORS_ORIGINS: "https://${{ vars.DOMAIN }},https://sso.${{ vars.DOMAIN }},https://api.${{ vars.DOMAIN }},https://mcp.${{ vars.DOMAIN }}" | |
| ENDOFVALUES | |
| # Build image override args - only set tags for services that were actually built | |
| IMAGE_ARGS="" | |
| # Always set repositories | |
| IMAGE_ARGS="$IMAGE_ARGS --set sso.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/sso" | |
| IMAGE_ARGS="$IMAGE_ARGS --set api.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/api" | |
| IMAGE_ARGS="$IMAGE_ARGS --set web.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/web" | |
| IMAGE_ARGS="$IMAGE_ARGS --set mcpServer.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/mcp" | |
| IMAGE_ARGS="$IMAGE_ARGS --set notificationService.image.repository=${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/notification" | |
| # Only set tags for services that were built (or all on manual trigger) | |
| if [ "${{ needs.changes.outputs.sso }}" == "true" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| IMAGE_ARGS="$IMAGE_ARGS --set sso.image.tag=${{ github.sha }}" | |
| fi | |
| if [ "${{ needs.changes.outputs.api }}" == "true" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| IMAGE_ARGS="$IMAGE_ARGS --set api.image.tag=${{ github.sha }}" | |
| fi | |
| if [ "${{ needs.changes.outputs.web }}" == "true" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| IMAGE_ARGS="$IMAGE_ARGS --set web.image.tag=${{ github.sha }}" | |
| fi | |
| if [ "${{ needs.changes.outputs.mcp }}" == "true" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| IMAGE_ARGS="$IMAGE_ARGS --set mcpServer.image.tag=${{ github.sha }}" | |
| fi | |
| if [ "${{ needs.changes.outputs.notification }}" == "true" ] || [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| IMAGE_ARGS="$IMAGE_ARGS --set notificationService.image.tag=${{ github.sha }}" | |
| fi | |
| helm upgrade --install taskflow ./infrastructure/helm/taskflow \ | |
| --namespace taskflow \ | |
| --values infrastructure/helm/taskflow/values-cloud.yaml \ | |
| --values /tmp/helm-overrides.yaml \ | |
| --set global.imagePullSecrets[0].name=ghcr-secret \ | |
| $IMAGE_ARGS \ | |
| --set managedServices.neon.enabled=true \ | |
| --set "managedServices.neon.ssoDatabase=${{ secrets.NEON_SSO_DATABASE_URL }}" \ | |
| --set "managedServices.neon.apiDatabase=${{ secrets.NEON_API_DATABASE_URL }}" \ | |
| --set "managedServices.neon.chatkitDatabase=${{ secrets.NEON_CHATKIT_DATABASE_URL }}" \ | |
| --set "managedServices.neon.notificationDatabase=${{ secrets.NEON_NOTIFICATION_DATABASE_URL }}" \ | |
| --set managedServices.upstash.enabled=true \ | |
| --set "managedServices.upstash.host=${{ secrets.UPSTASH_REDIS_HOST }}" \ | |
| --set "managedServices.upstash.password=${{ secrets.UPSTASH_REDIS_PASSWORD }}" \ | |
| --set "managedServices.upstash.restUrl=${{ secrets.REDIS_URL }}" \ | |
| --set "managedServices.upstash.restToken=${{ secrets.REDIS_TOKEN }}" \ | |
| --set "sso.env.BETTER_AUTH_SECRET=${{ secrets.BETTER_AUTH_SECRET }}" \ | |
| --set "sso.env.BETTER_AUTH_URL=https://sso.${{ vars.DOMAIN }}" \ | |
| --set "sso.smtp.user=${{ secrets.SMTP_USER }}" \ | |
| --set "sso.smtp.password=${{ secrets.SMTP_PASSWORD }}" \ | |
| --set "api.openai.apiKey=${{ secrets.OPENAI_API_KEY }}" \ | |
| --set notificationService.enabled=true \ | |
| --set dapr.enabled=true \ | |
| --set "dapr.pubsub.redisHost=${{ secrets.UPSTASH_REDIS_HOST }}" \ | |
| --set "dapr.pubsub.redisPassword=${{ secrets.UPSTASH_REDIS_PASSWORD }}" \ | |
| --set "global.domain=${{ vars.DOMAIN }}" \ | |
| --set "sso.ingress.className=$INGRESS_CLASS" \ | |
| --set "sso.ingress.host=sso.${{ vars.DOMAIN }}" \ | |
| --set "api.ingress.className=$INGRESS_CLASS" \ | |
| --set "api.ingress.host=api.${{ vars.DOMAIN }}" \ | |
| --set "mcpServer.ingress.enabled=true" \ | |
| --set "mcpServer.ingress.className=$INGRESS_CLASS" \ | |
| --set "mcpServer.ingress.host=mcp.${{ vars.DOMAIN }}" \ | |
| --set "mcpServer.env.TASKFLOW_SSO_PUBLIC_URL=https://sso.${{ vars.DOMAIN }}" \ | |
| --set "mcpServer.env.TASKFLOW_MCP_PUBLIC_URL=https://mcp.${{ vars.DOMAIN }}" \ | |
| --set "web.ingress.className=$INGRESS_CLASS" \ | |
| --set "web.ingress.host=${{ vars.DOMAIN }}" \ | |
| --set "ingress-nginx.enabled=false" \ | |
| --wait \ | |
| --timeout 10m | |
| - name: Verify deployment | |
| run: | | |
| echo "Checking pod status..." | |
| kubectl get pods -n taskflow | |
| echo "" | |
| echo "Checking services..." | |
| kubectl get svc -n taskflow | |
| echo "" | |
| echo "Checking ingress..." | |
| kubectl get ingress -n taskflow 2>/dev/null || echo "No ingress configured" | |
| - name: Post deployment URLs | |
| run: | | |
| echo "## Deployment Complete! :rocket:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Services Built" >> $GITHUB_STEP_SUMMARY | |
| echo "| Service | Built |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| API | ${{ needs.changes.outputs.api == 'true' && '✅' || '⏭️ skipped' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| SSO | ${{ needs.changes.outputs.sso == 'true' && '✅' || '⏭️ skipped' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| MCP | ${{ needs.changes.outputs.mcp == 'true' && '✅' || '⏭️ skipped' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Notification | ${{ needs.changes.outputs.notification == 'true' && '✅' || '⏭️ skipped' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Web | ${{ needs.changes.outputs.web == 'true' && '✅' || '⏭️ skipped' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### URLs" >> $GITHUB_STEP_SUMMARY | |
| echo "| Service | URL |" >> $GITHUB_STEP_SUMMARY | |
| echo "|---------|-----|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Web Dashboard | https://${{ vars.DOMAIN }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| SSO Platform | https://sso.${{ vars.DOMAIN }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| API | https://api.${{ vars.DOMAIN }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| MCP Server | https://mcp.${{ vars.DOMAIN }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Commit: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY |