Use direct path to webpack binary instead of npx #452
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 and Deploy Major League Github on GCP 🚀 ☁️ | |
| on: | |
| push: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| env: | |
| PROJECT_NAME: ${{ secrets.PROJECT_NAME }} | |
| VERSION: v1.0.${{ github.run_number }} | |
| ENVIRONMENT: production | |
| # Google Tag Manager | |
| GTM_ID: ${{ secrets.GTM_ID }} | |
| # GCP Configuration | |
| GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} | |
| GKE_CLUSTER: ${{ secrets.GKE_CLUSTER }} | |
| GKE_ZONE: ${{ secrets.GKE_ZONE }} | |
| # GitHub Container Registry | |
| REGISTRY: ghcr.io | |
| REGISTRY_NAME: ${{ github.repository }} | |
| # Kubernetes namespace (convert repo name to valid namespace) | |
| NAMESPACE: ${{ github.event.repository.name }} | |
| DOMAIN_NAME: ${{ secrets.DOMAIN_NAME }} | |
| DOMAIN_SUFFIX: ${{ secrets.DOMAIN_SUFFIX }} | |
| FORWARD_TO_WWW: ${{ secrets.FORWARD_TO_WWW || 'false' }} | |
| STATIC_IP_NAME: "main-ingress-ip" | |
| STATIC_IP_ADDRESS: ${{ secrets.STATIC_IP_ADDRESS }} | |
| GH_API_TOKENS: ${{ secrets.GH_API_TOKENS }} | |
| LINKEDIN_CLIENT_ID: ${{ secrets.LINKEDIN_CLIENT_ID }} | |
| LINKEDIN_CLIENT_SECRET: ${{ secrets.LINKEDIN_CLIENT_SECRET }} | |
| # Resource configuration | |
| BACKEND_REPLICAS: 1 | |
| FRONTEND_REPLICAS: 1 | |
| # Backend resources (minimal for single-node cluster) | |
| BACKEND_REQUEST_MEMORY: 256Mi | |
| BACKEND_REQUEST_CPU: 50m | |
| BACKEND_LIMIT_MEMORY: 1Gi | |
| BACKEND_LIMIT_CPU: 500m | |
| # Frontend resources (minimal for single-node cluster) | |
| FRONTEND_REQUEST_MEMORY: 64Mi | |
| FRONTEND_REQUEST_CPU: 25m | |
| FRONTEND_LIMIT_MEMORY: 256Mi | |
| FRONTEND_LIMIT_CPU: 200m | |
| # Java settings | |
| JAVA_MAX_HEAP: 1200m | |
| JAVA_MIN_HEAP: 512m | |
| # Redis Configuration | |
| REDIS_HOST: ${{ secrets.REDIS_IP }} | |
| jobs: | |
| build-and-deploy: | |
| runs-on: ubuntu-22.04 | |
| permissions: | |
| contents: read | |
| packages: write | |
| deployments: write | |
| steps: | |
| - name: Verify Required Secrets 🔒 | |
| run: | | |
| missing_secrets=() | |
| # Core project secrets | |
| if [ -z "${{ secrets.PROJECT_NAME }}" ]; then | |
| missing_secrets+=("PROJECT_NAME") | |
| fi | |
| # GCP Infrastructure secrets | |
| if [ -z "${{ secrets.GCP_PROJECT_ID }}" ]; then | |
| missing_secrets+=("GCP_PROJECT_ID") | |
| fi | |
| if [ -z "${{ secrets.GCP_SA_KEY }}" ]; then | |
| missing_secrets+=("GCP_SA_KEY") | |
| fi | |
| if [ -z "${{ secrets.GKE_CLUSTER }}" ]; then | |
| missing_secrets+=("GKE_CLUSTER") | |
| fi | |
| if [ -z "${{ secrets.GKE_ZONE }}" ]; then | |
| missing_secrets+=("GKE_ZONE") | |
| fi | |
| # Container Registry auto-detected from repository | |
| # DNS secrets | |
| if [ -z "${{ secrets.DNS_PROJECT_ID }}" ]; then | |
| missing_secrets+=("DNS_PROJECT_ID") | |
| fi | |
| if [ -z "${{ secrets.DNS_ZONE }}" ]; then | |
| missing_secrets+=("DNS_ZONE") | |
| fi | |
| # Redis secret | |
| if [ -z "${{ secrets.REDIS_IP }}" ]; then | |
| missing_secrets+=("REDIS_IP") | |
| fi | |
| # Domain secrets | |
| if [ -z "${{ secrets.DOMAIN_NAME }}" ]; then | |
| missing_secrets+=("DOMAIN_NAME") | |
| fi | |
| if [ -z "${{ secrets.DOMAIN_SUFFIX }}" ]; then | |
| missing_secrets+=("DOMAIN_SUFFIX") | |
| fi | |
| # FORWARD_TO_WWW is optional (defaults to false) | |
| # Application secrets | |
| if [ -z "${{ secrets.GH_API_TOKENS }}" ]; then | |
| missing_secrets+=("GH_API_TOKENS") | |
| fi | |
| if [ -z "${{ secrets.LINKEDIN_CLIENT_ID }}" ]; then | |
| missing_secrets+=("LINKEDIN_CLIENT_ID") | |
| fi | |
| if [ -z "${{ secrets.LINKEDIN_CLIENT_SECRET }}" ]; then | |
| missing_secrets+=("LINKEDIN_CLIENT_SECRET") | |
| fi | |
| if [ -z "${{ secrets.GTM_ID }}" ]; then | |
| missing_secrets+=("GTM_ID") | |
| fi | |
| if [ ${#missing_secrets[@]} -ne 0 ]; then | |
| echo "❌ Error: Missing required secrets: ${missing_secrets[*]}" | |
| exit 1 | |
| fi | |
| echo "✅ All required secrets are set" | |
| - name: Checkout code 📦 | |
| uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Set up Google Cloud CLI 🛠️ | |
| uses: google-github-actions/auth@v2 | |
| with: | |
| credentials_json: ${{ secrets.GCP_SA_KEY }} | |
| - name: Configure gcloud 🔧 | |
| uses: google-github-actions/setup-gcloud@v2 | |
| - name: Get GKE credentials 🔑 | |
| uses: google-github-actions/get-gke-credentials@v2 | |
| with: | |
| cluster_name: ${{ env.GKE_CLUSTER }} | |
| location: ${{ env.GKE_ZONE }} | |
| - name: Login to GitHub Container Registry 🐳 | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Check for changes 🔍 | |
| uses: dorny/paths-filter@v3 | |
| id: changes | |
| with: | |
| filters: | | |
| backend: | |
| - 'backend/**' | |
| - 'kubernetes/**/*backend*' | |
| - '.github/workflows/deploy.yml' | |
| cache_updater: | |
| - 'backend/**' | |
| - 'kubernetes/**/*cache*' | |
| - '.github/workflows/deploy.yml' | |
| frontend: | |
| - 'frontend/**' | |
| - 'kubernetes/**/*frontend*' | |
| - '.github/workflows/deploy.yml' | |
| - name: Set Webapp Environment Variables 🎨 | |
| run: | | |
| # Set OG Meta Tags environment variables | |
| echo "OG_TITLE=Major League GitHub" >> $GITHUB_ENV | |
| echo "OG_DESCRIPTION=GitHub Scouting Report: Major League Edition" >> $GITHUB_ENV | |
| echo "OG_TYPE=website" >> $GITHUB_ENV | |
| echo "OG_IMAGE_URL=/og-image.png" >> $GITHUB_ENV | |
| echo "OG_SITE_NAME=Major League GitHub" >> $GITHUB_ENV | |
| - name: Build and push backend service image 🏗️ | |
| if: steps.changes.outputs.backend == 'true' | |
| run: | | |
| echo "🚀 Building backend service image..." | |
| docker build \ | |
| --build-arg PROFILE=backend-service \ | |
| -t ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/backend-service:${{ env.VERSION }} \ | |
| -t ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/backend-service:latest \ | |
| backend | |
| echo "🚀 Pushing backend service images..." | |
| docker push ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/backend-service:${{ env.VERSION }} | |
| docker push ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/backend-service:latest | |
| - name: Skip backend service build 🏗️ | |
| if: steps.changes.outputs.backend == 'false' | |
| run: echo "⏩ Skipping backend service build - no changes detected" | |
| - name: Build and push cache updater image 🏗️ | |
| if: steps.changes.outputs.cache_updater == 'true' | |
| run: | | |
| echo "🚀 Building cache updater image..." | |
| docker build \ | |
| --build-arg PROFILE=cache-updater \ | |
| -t ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/cache-updater:${{ env.VERSION }} \ | |
| -t ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/cache-updater:latest \ | |
| backend | |
| echo "🚀 Pushing cache updater images..." | |
| docker push ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/cache-updater:${{ env.VERSION }} | |
| docker push ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/cache-updater:latest | |
| - name: Skip cache updater build 🏗️ | |
| if: steps.changes.outputs.cache_updater == 'false' | |
| run: echo "⏩ Skipping cache updater build - no changes detected" | |
| - name: Build and push frontend image 🎨 | |
| if: steps.changes.outputs.frontend == 'true' | |
| env: | |
| OG_TITLE: ${{ env.OG_TITLE }} | |
| OG_DESCRIPTION: ${{ env.OG_DESCRIPTION }} | |
| OG_TYPE: ${{ env.OG_TYPE }} | |
| OG_IMAGE_URL: ${{ env.OG_IMAGE_URL }} | |
| OG_SITE_NAME: ${{ env.OG_SITE_NAME }} | |
| WEBAPP_EXTRA_BUTTON_LINK: ${{ env.WEBAPP_EXTRA_BUTTON_LINK }} | |
| WEBAPP_EXTRA_BUTTON_TEXT: ${{ env.WEBAPP_EXTRA_BUTTON_TEXT }} | |
| run: | | |
| echo "🚀 Building frontend image..." | |
| # Determine redirect configuration | |
| ENABLE_REDIRECT="false" | |
| ROOT_DOMAIN="" | |
| TARGET_DOMAIN="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| if [ "${{ env.FORWARD_TO_WWW }}" = "true" ]; then | |
| # Check if DOMAIN_NAME is a subdomain (doesn't contain dots) | |
| if [[ "${{ env.DOMAIN_NAME }}" != *"."* ]]; then | |
| # This is a simple subdomain like "www", so enable redirect | |
| ENABLE_REDIRECT="true" | |
| ROOT_DOMAIN="${{ env.DOMAIN_SUFFIX }}" | |
| echo "📍 Enabling redirect: $ROOT_DOMAIN → $TARGET_DOMAIN" | |
| else | |
| echo "⚠️ DOMAIN_NAME contains dots, assuming complex subdomain - no redirect" | |
| fi | |
| fi | |
| docker build frontend \ | |
| --build-arg NODE_ENV=production \ | |
| --build-arg BACKEND_API_URL="/" \ | |
| --build-arg GTM_ID="${{ env.GTM_ID }}" \ | |
| --build-arg OG_TITLE="${{ env.OG_TITLE }}" \ | |
| --build-arg OG_DESCRIPTION="${{ env.OG_DESCRIPTION }}" \ | |
| --build-arg OG_TYPE="${{ env.OG_TYPE }}" \ | |
| --build-arg OG_IMAGE_URL="${{ env.OG_IMAGE_URL }}" \ | |
| --build-arg OG_URL="https://$TARGET_DOMAIN" \ | |
| --build-arg OG_SITE_NAME="${{ env.OG_SITE_NAME }}" \ | |
| --build-arg WEBAPP_EXTRA_BUTTON_LINK="${{ env.WEBAPP_EXTRA_BUTTON_LINK }}" \ | |
| --build-arg WEBAPP_EXTRA_BUTTON_TEXT="${{ env.WEBAPP_EXTRA_BUTTON_TEXT }}" \ | |
| --build-arg ENABLE_REDIRECT="$ENABLE_REDIRECT" \ | |
| --build-arg ROOT_DOMAIN="$ROOT_DOMAIN" \ | |
| --build-arg TARGET_DOMAIN="$TARGET_DOMAIN" \ | |
| -t "${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/frontend:${{ env.VERSION }}" \ | |
| -t "${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/frontend:latest" | |
| echo "🚀 Pushing frontend images..." | |
| docker push ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/frontend:${{ env.VERSION }} | |
| docker push ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }}/frontend:latest | |
| - name: Skip frontend build 🎨 | |
| if: steps.changes.outputs.frontend == 'false' | |
| run: echo "⏩ Skipping frontend build - no changes detected" | |
| - name: Create Kubernetes namespace and secrets 🔐 | |
| run: | | |
| # Create namespace | |
| kubectl create namespace ${{ env.NAMESPACE }} --dry-run=client -o yaml | kubectl apply -f - | |
| # Create application secrets | |
| kubectl create secret generic app-secrets \ | |
| --namespace=${{ env.NAMESPACE }} \ | |
| --from-literal=github.tokens="${{ env.GH_API_TOKENS }}" \ | |
| --from-literal=linkedin.client.id="${{ env.LINKEDIN_CLIENT_ID }}" \ | |
| --from-literal=linkedin.client.secret="${{ env.LINKEDIN_CLIENT_SECRET }}" \ | |
| --dry-run=client -o yaml | kubectl apply -f - | |
| echo "✅ Kubernetes secrets created successfully" | |
| - name: Cleanup GKE Resources 🧹 | |
| run: | | |
| echo "🧹 Cleaning up all existing resources in namespace ${{ env.NAMESPACE }}..." | |
| # Clean up all resources in namespace (except secrets and configmaps we'll recreate) | |
| kubectl delete all --all -n ${{ env.NAMESPACE }} --ignore-not-found=true || echo "No standard resources to clean up" | |
| kubectl delete ingress --all -n ${{ env.NAMESPACE }} --ignore-not-found=true || echo "No ingress resources to clean up" | |
| kubectl delete backendconfig --all -n ${{ env.NAMESPACE }} --ignore-not-found=true || echo "No backendconfig resources to clean up" | |
| # Wait for NEG cleanup to complete (they take time to detach from load balancers) | |
| echo "⏳ Waiting for NEG cleanup to complete..." | |
| sleep 30 | |
| echo "✅ Cleanup completed successfully" | |
| - name: Deploy to GKE 🚀 | |
| env: | |
| REGISTRY_URL: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAME }} | |
| DOMAIN_SUFFIX: ${{ env.DOMAIN_SUFFIX }} | |
| DOMAIN_NAME: ${{ env.DOMAIN_NAME }} | |
| ENVIRONMENT: ${{ env.ENVIRONMENT }} | |
| VERSION: ${{ env.VERSION }} | |
| BACKEND_SERVICE_NAME: "backend-service" | |
| CACHE_UPDATER_NAME: "cache-updater" | |
| BACKEND_SERVICE_PORT: "8080" | |
| CACHE_UPDATER_PORT: "8081" | |
| FRONTEND_PORT: "80" | |
| STATIC_IP_ADDRESS: ${{ secrets.STATIC_IP_ADDRESS }} | |
| run: | | |
| echo "🚀 Processing and applying Kubernetes configurations..." | |
| # Always use 'latest' tag for active K8s deployments for simplicity | |
| BACKEND_VERSION="latest" | |
| CACHE_UPDATER_VERSION="latest" | |
| FRONTEND_VERSION="latest" | |
| echo "🏷️ Using 'latest' tags for all K8s deployments" | |
| # Create temporary directory | |
| mkdir -p processed_k8s | |
| # Process config files | |
| echo "🚀 Processing config.yaml..." | |
| envsubst < kubernetes/base/config.yaml > processed_k8s/config.yaml | |
| kubectl apply -n ${{ env.NAMESPACE }} -f processed_k8s/config.yaml | |
| # Process and apply backend service | |
| echo "🚀 Processing backend service..." | |
| SERVICE_NAME=backend-service \ | |
| SERVER_PORT=8080 \ | |
| VERSION=$BACKEND_VERSION \ | |
| envsubst < kubernetes/base/backend-service.yaml > processed_k8s/backend-service.yaml | |
| kubectl apply --server-side=true --force-conflicts -n ${{ env.NAMESPACE }} -f processed_k8s/backend-service.yaml | |
| # Process and apply cache updater | |
| echo "🚀 Processing cache updater..." | |
| SERVICE_NAME=cache-updater \ | |
| SERVER_PORT=8081 \ | |
| VERSION=$CACHE_UPDATER_VERSION \ | |
| envsubst < kubernetes/base/cache-updater-service.yaml > processed_k8s/cache-updater.yaml | |
| kubectl apply --server-side=true --force-conflicts -n ${{ env.NAMESPACE }} -f processed_k8s/cache-updater.yaml | |
| # Process and apply frontend (creates frontend-service as ClusterIP with NEG) | |
| echo "🚀 Processing frontend..." | |
| # Determine redirect configuration for runtime | |
| ENABLE_REDIRECT="false" | |
| ROOT_DOMAIN="" | |
| if [ "${{ env.FORWARD_TO_WWW }}" = "true" ]; then | |
| # Check if DOMAIN_NAME is a subdomain (doesn't contain dots) | |
| if [[ "${{ env.DOMAIN_NAME }}" != *"."* ]]; then | |
| # This is a simple subdomain like "www", so enable redirect | |
| ENABLE_REDIRECT="true" | |
| ROOT_DOMAIN="${{ env.DOMAIN_SUFFIX }}" | |
| echo "📍 Frontend nginx redirect: $ROOT_DOMAIN → ${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| else | |
| echo "⚠️ DOMAIN_NAME contains dots, assuming complex subdomain - no redirect" | |
| fi | |
| fi | |
| VERSION=$FRONTEND_VERSION \ | |
| ENABLE_REDIRECT="$ENABLE_REDIRECT" \ | |
| ROOT_DOMAIN="$ROOT_DOMAIN" \ | |
| envsubst < kubernetes/base/frontend.yaml > processed_k8s/frontend.yaml | |
| kubectl apply --server-side=true --force-conflicts -n ${{ env.NAMESPACE }} -f processed_k8s/frontend.yaml | |
| echo "✅ Frontend applied successfully" | |
| echo "✅ All resources cleanup completed" | |
| # Check deployment status immediately after creation | |
| echo "🔍 Immediate deployment status check..." | |
| kubectl get deployments -n ${{ env.NAMESPACE }} -o wide | |
| kubectl get pods -n ${{ env.NAMESPACE }} -o wide | |
| kubectl get events -n ${{ env.NAMESPACE }} --sort-by='.lastTimestamp' | tail -20 | |
| echo "🔍 Cache-updater specific details:" | |
| kubectl describe deployment/cache-updater -n ${{ env.NAMESPACE }} | |
| echo "🔍 Cache-updater pod details:" | |
| kubectl describe pods -n ${{ env.NAMESPACE }} -l app=cache-updater | |
| echo "🔍 Cache-updater logs (if any):" | |
| kubectl logs -n ${{ env.NAMESPACE }} -l app=cache-updater --tail=50 || echo "No logs yet" | |
| # Wait for deployments to be ready | |
| echo "🚀 Now waiting for deployments to be ready..." | |
| kubectl wait --for=condition=available deployment/backend-service -n ${{ env.NAMESPACE }} --timeout=300s | |
| kubectl wait --for=condition=available deployment/cache-updater -n ${{ env.NAMESPACE }} --timeout=300s | |
| kubectl wait --for=condition=available deployment/frontend -n ${{ env.NAMESPACE }} --timeout=300s | |
| # Create static IP if it doesn't exist | |
| echo "🚀 Ensuring static IP exists..." | |
| if ! gcloud compute addresses describe main-ingress-ip --global >/dev/null 2>&1; then | |
| echo "Creating static IP main-ingress-ip with address ${STATIC_IP_ADDRESS}..." | |
| gcloud compute addresses create main-ingress-ip --global --addresses=${STATIC_IP_ADDRESS} | |
| else | |
| echo "Static IP main-ingress-ip already exists" | |
| fi | |
| # Note: Root domain redirect is now handled by nginx (not ingress) | |
| if [ "${{ env.FORWARD_TO_WWW }}" = "true" ]; then | |
| echo "🔄 FORWARD_TO_WWW enabled - root domain redirect handled by nginx" | |
| fi | |
| # Create ingress AFTER all services exist with static IP | |
| echo "🚀 Processing ingress after services are ready..." | |
| # Process ingress configuration | |
| # Determine redirect configuration for ingress | |
| ROOT_DOMAIN_CERT="" | |
| ROOT_DOMAIN_RULE="" | |
| if [ "${{ env.FORWARD_TO_WWW }}" = "true" ]; then | |
| if [[ "${{ env.DOMAIN_NAME }}" != *"."* ]]; then | |
| # Add root domain to certificate and ingress rules | |
| ROOT_DOMAIN_CERT=$'\n - ${{ env.DOMAIN_SUFFIX }}' | |
| ROOT_DOMAIN_RULE=$'\n - host: ${{ env.DOMAIN_SUFFIX }}\n http:\n paths:\n - path: /\n pathType: Prefix\n backend:\n service:\n name: frontend-service\n port:\n number: 80' | |
| echo "📍 Adding root domain ${{ env.DOMAIN_SUFFIX }} to ingress and SSL certificate" | |
| fi | |
| fi | |
| DOMAIN_NAME="${{ env.DOMAIN_NAME }}" \ | |
| DOMAIN_SUFFIX="${{ env.DOMAIN_SUFFIX }}" \ | |
| ROOT_DOMAIN_CERT="$ROOT_DOMAIN_CERT" \ | |
| ROOT_DOMAIN_RULE="$ROOT_DOMAIN_RULE" \ | |
| envsubst < kubernetes/base/ingress.yaml > processed_k8s/ingress.yaml | |
| # Check if ManagedCertificate already exists and validate domain | |
| if kubectl get managedcertificate main-ingress-ssl-cert -n ${{ env.NAMESPACE }} >/dev/null 2>&1; then | |
| CERT_STATUS=$(kubectl get managedcertificate main-ingress-ssl-cert -n ${{ env.NAMESPACE }} -o jsonpath='{.status.certificateStatus}' 2>/dev/null || echo "Unknown") | |
| CURRENT_DOMAINS=$(kubectl get managedcertificate main-ingress-ssl-cert -n ${{ env.NAMESPACE }} -o jsonpath='{.spec.domains[*]}' 2>/dev/null || echo "") | |
| NEW_DOMAIN="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| echo "🔍 Certificate status: $CERT_STATUS" | |
| echo "🔍 Current certificate domains: $CURRENT_DOMAINS" | |
| echo "🔍 New target domain: $NEW_DOMAIN" | |
| # Check if the new domain is already in the certificate | |
| DOMAIN_EXISTS="false" | |
| for domain in $CURRENT_DOMAINS; do | |
| if [ "$domain" = "$NEW_DOMAIN" ]; then | |
| DOMAIN_EXISTS="true" | |
| break | |
| fi | |
| done | |
| if [ "$DOMAIN_EXISTS" != "true" ]; then | |
| echo "🔄 Domain '$NEW_DOMAIN' not found in certificate domains [$CURRENT_DOMAINS] - recreating certificate" | |
| echo "🗑️ Deleting existing ManagedCertificate..." | |
| kubectl delete managedcertificate main-ingress-ssl-cert -n ${{ env.NAMESPACE }} --ignore-not-found=true | |
| echo "⏳ Waiting for certificate cleanup..." | |
| sleep 10 | |
| echo "🆕 Creating new ManagedCertificate for domain: $NEW_DOMAIN" | |
| kubectl apply -n ${{ env.NAMESPACE }} -f processed_k8s/ingress.yaml | |
| echo "📊 New certificate status:" | |
| kubectl get managedcertificate main-ingress-ssl-cert -n ${{ env.NAMESPACE }} -o yaml | grep -A 10 "spec:" || echo "Certificate created, status pending..." | |
| else | |
| echo "✅ ManagedCertificate already contains target domain '$NEW_DOMAIN' (status: $CERT_STATUS) - skipping recreation" | |
| # Apply only the ingress part, not the ManagedCertificate | |
| kubectl apply -f processed_k8s/ingress.yaml --selector='!networking.gke.io/managed-certificates' -n ${{ env.NAMESPACE }} || \ | |
| kubectl apply -f <(sed '/^apiVersion: networking.gke.io\/v1/,/^---$/d' processed_k8s/ingress.yaml) -n ${{ env.NAMESPACE }} | |
| fi | |
| else | |
| echo "🆕 ManagedCertificate doesn't exist - creating new one" | |
| kubectl apply -n ${{ env.NAMESPACE }} -f processed_k8s/ingress.yaml | |
| fi | |
| # Cleanup | |
| rm -rf processed_k8s | |
| echo "✅ Deployment completed successfully" | |
| - name: Verify Deployment 🔍 | |
| run: | | |
| echo "🔍 Checking deployment status..." | |
| echo "Pod Status:" | |
| kubectl -n ${{ env.NAMESPACE }} get pods -o wide | |
| echo "Service Status:" | |
| kubectl -n ${{ env.NAMESPACE }} get services | |
| echo "LoadBalancer Status:" | |
| kubectl -n ${{ env.NAMESPACE }} get ingress main-ingress -o wide | |
| echo "SSL Certificate Status:" | |
| kubectl -n ${{ env.NAMESPACE }} get managedcertificate main-ingress-ssl-cert -o wide || echo "No managed certificate found" | |
| # Use static IP instead of waiting for dynamic assignment | |
| LB_IP="${{ env.STATIC_IP_ADDRESS }}" | |
| echo "✅ Using static IP: ${LB_IP}" | |
| echo "🌐 Frontend URL: http://${LB_IP}/" | |
| echo "🌐 Backend API URL: http://${LB_IP}/api/actuator/health" | |
| echo "✅ Migration Complete - Proper Kubernetes Architecture!" | |
| echo "🌐 Single External Gateway: http://${LB_IP}" | |
| echo " - Frontend: http://${LB_IP}/" | |
| echo " - API: http://${LB_IP}/api" | |
| echo "" | |
| echo "🔒 Internal Services (ClusterIP only):" | |
| echo " - frontend-service:80 (internal)" | |
| echo " - backend-service-service:8080 (internal)" | |
| echo "" | |
| if [ "${{ env.FORWARD_TO_WWW }}" = "true" ]; then | |
| echo "🌍 Primary Domain: https://${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| echo "🔄 Root Domain Redirect: https://${{ env.DOMAIN_SUFFIX }} → https://${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| else | |
| echo "🌍 Domain: https://${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| fi | |
| - name: Update DNS Record (Cross-Project) 🌍 | |
| run: | | |
| LB_IP="${{ env.STATIC_IP_ADDRESS }}" | |
| echo "🌍 Updating DNS record for ${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }} to ${LB_IP}" | |
| # DNS project details | |
| DNS_PROJECT_ID="${{ secrets.DNS_PROJECT_ID }}" | |
| DNS_ZONE="${{ secrets.DNS_ZONE }}" | |
| # Set project context to DNS project | |
| gcloud config set project $DNS_PROJECT_ID | |
| # Debug: List all DNS zones in the project | |
| echo "🔍 Available DNS zones in project $DNS_PROJECT_ID:" | |
| gcloud dns managed-zones list --project=$DNS_PROJECT_ID --format="table(name,dnsName)" || echo "No zones found or permission denied" | |
| echo "🔍 Looking for DNS zone: $DNS_ZONE" | |
| # Check if DNS zone exists | |
| if ! gcloud dns managed-zones describe $DNS_ZONE --project=$DNS_PROJECT_ID --format="value(name)" > /dev/null 2>&1; then | |
| echo "❌ Cloud DNS zone '$DNS_ZONE' not found in project $DNS_PROJECT_ID" | |
| echo "🌍 FALLBACK - Manual DNS Update Required:" | |
| echo "Load Balancer IP: ${LB_IP}" | |
| echo "Go to: https://console.cloud.google.com/net-services/dns/zones?project=$DNS_PROJECT_ID" | |
| exit 1 | |
| fi | |
| # Get existing A record if it exists | |
| EXISTING_IP=$(gcloud dns record-sets list --zone=$DNS_ZONE --project=$DNS_PROJECT_ID --name="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}." --type=A --format="value(rrdatas[0])" 2>/dev/null | head -1 || echo "") | |
| if [ ! -z "$EXISTING_IP" ] && [ "$EXISTING_IP" != "null" ] && [ "$EXISTING_IP" != "$LB_IP" ]; then | |
| echo "🌍 Updating existing A record from $EXISTING_IP to $LB_IP" | |
| # Get the existing record with full details including TTL | |
| EXISTING_TTL=$(gcloud dns record-sets list --zone=$DNS_ZONE --project=$DNS_PROJECT_ID --name="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}." --type=A --format="value(ttl)" 2>/dev/null | head -1) | |
| if [ ! -z "$EXISTING_TTL" ] && [ "$EXISTING_TTL" != "null" ]; then | |
| echo "Found existing record: TTL=$EXISTING_TTL, IP=$EXISTING_IP" | |
| # Use transaction to update the record with exact existing values | |
| gcloud dns record-sets transaction start --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction remove "$EXISTING_IP" \ | |
| --name="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}." \ | |
| --ttl=$EXISTING_TTL \ | |
| --type=A \ | |
| --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction add "$LB_IP" \ | |
| --name="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}." \ | |
| --ttl=300 \ | |
| --type=A \ | |
| --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction execute --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| else | |
| echo "Could not find existing record details, will create new record instead" | |
| # Fall back to creating new record | |
| gcloud dns record-sets transaction start --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction add "$LB_IP" \ | |
| --name="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}." \ | |
| --ttl=300 \ | |
| --type=A \ | |
| --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction execute --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| fi | |
| elif [ -z "$EXISTING_IP" ] || [ "$EXISTING_IP" = "null" ]; then | |
| echo "🌍 Creating new A record pointing to $LB_IP" | |
| # Create new A record | |
| gcloud dns record-sets transaction start --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction add "$LB_IP" \ | |
| --name="${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}." \ | |
| --ttl=300 \ | |
| --type=A \ | |
| --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| gcloud dns record-sets transaction execute --zone=$DNS_ZONE --project=$DNS_PROJECT_ID | |
| else | |
| echo "🌍 DNS record already points to correct IP: $LB_IP" | |
| fi | |
| echo "✅ DNS record updated successfully" | |
| # Note: Root domain redirect is now handled entirely by nginx | |
| if [ "${{ env.FORWARD_TO_WWW }}" = "true" ]; then | |
| echo "ℹ️ FORWARD_TO_WWW enabled - redirect logic handled by nginx in frontend container" | |
| else | |
| echo "ℹ️ FORWARD_TO_WWW disabled - no redirect configured" | |
| fi | |
| echo "🌍 Domain will be accessible at: https://${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }}" | |
| echo "⏳ DNS propagation may take 5-15 minutes" | |
| # Switch back to original project | |
| gcloud config set project ${{ env.GCP_PROJECT_ID }} | |
| - name: Create GitHub Deployment 📦 | |
| uses: chrnorm/deployment-action@v2 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| environment: production | |
| initial-status: success | |
| environment-url: https://${{ env.DOMAIN_NAME }}.${{ env.DOMAIN_SUFFIX }} | |
| - name: Cleanup old images 🧹 | |
| if: always() | |
| uses: dataaxiom/ghcr-cleanup-action@v1 | |
| continue-on-error: true | |
| with: | |
| owner: ${{ github.repository_owner }} | |
| package: ${{ github.event.repository.name }}/backend-service | |
| delete-ghost-images: true | |
| delete-tags: "*" | |
| exclude-tags: latest,${{ env.VERSION }} | |
| - name: Cleanup old images 🧹 | |
| if: always() | |
| uses: dataaxiom/ghcr-cleanup-action@v1 | |
| continue-on-error: true | |
| with: | |
| owner: ${{ github.repository_owner }} | |
| package: ${{ github.event.repository.name }}/cache-updater | |
| delete-ghost-images: true | |
| delete-tags: "*" | |
| exclude-tags: latest,${{ env.VERSION }} | |
| - name: Cleanup old images 🧹 | |
| if: always() | |
| uses: dataaxiom/ghcr-cleanup-action@v1 | |
| continue-on-error: true | |
| with: | |
| owner: ${{ github.repository_owner }} | |
| package: ${{ github.event.repository.name }}/frontend | |
| delete-ghost-images: true | |
| delete-tags: "*" | |
| exclude-tags: latest,${{ env.VERSION }} | |