-
Notifications
You must be signed in to change notification settings - Fork 0
677 lines (556 loc) · 29.4 KB
/
Copy pathdeploy.yml
File metadata and controls
677 lines (556 loc) · 29.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
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
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 }}