Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
name: Build and Deploy Docker Images (on VM)
name: Build Docker Images

# Triggered on every push to main.
# Builds all service images and pushes them to GitHub Container Registry (GHCR).
# The deploy workflow listens for this workflow's completion before deploying.

on:
push:
branches:
- main

- main

# Read access to repo contents; write access to push images to GHCR.
permissions:
contents: read
packages: write
Expand All @@ -16,26 +21,35 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4

# Authenticate with GHCR so we can push images.
# GITHUB_TOKEN is automatically provided by GitHub Actions.
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# QEMU enables building images for non-native architectures.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

# Buildx is required for advanced build features including GHA layer caching.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

# Each service uses a separate cache scope so they don't overwrite each other.
# cache-from: restores cached layers from the previous build.
# cache-to mode=max: saves all intermediate layers, not just the final image.
- name: Build and Push api-gateway
uses: docker/build-push-action@v5
with:
context: ./server
file: ./server/api-gateway/Dockerfile
push: true
tags: ghcr.io/aet-devops26/team-devvopps/api-gateway:latest
cache-from: type=gha,scope=api-gateway
cache-to: type=gha,scope=api-gateway,mode=max

- name: Build and Push user-service
uses: docker/build-push-action@v5
Expand All @@ -44,6 +58,8 @@ jobs:
file: ./server/user-service/Dockerfile
push: true
tags: ghcr.io/aet-devops26/team-devvopps/user-service:latest
cache-from: type=gha,scope=user-service
cache-to: type=gha,scope=user-service,mode=max

- name: Build and Push course-service
uses: docker/build-push-action@v5
Expand All @@ -52,6 +68,8 @@ jobs:
file: ./server/course-service/Dockerfile
push: true
tags: ghcr.io/aet-devops26/team-devvopps/course-service:latest
cache-from: type=gha,scope=course-service
cache-to: type=gha,scope=course-service,mode=max

- name: Build and Push roadmap-service
uses: docker/build-push-action@v5
Expand All @@ -60,6 +78,8 @@ jobs:
file: ./server/roadmap-service/Dockerfile
push: true
tags: ghcr.io/aet-devops26/team-devvopps/roadmap-service:latest
cache-from: type=gha,scope=roadmap-service
cache-to: type=gha,scope=roadmap-service,mode=max

- name: Build and Push client
uses: docker/build-push-action@v5
Expand All @@ -68,6 +88,18 @@ jobs:
file: ./client/Dockerfile
push: true
tags: ghcr.io/aet-devops26/team-devvopps/client:latest
cache-from: type=gha,scope=client
cache-to: type=gha,scope=client,mode=max

- name: Build and Push llm-service
uses: docker/build-push-action@v5
with:
context: ./server/llm-service
file: ./server/llm-service/Dockerfile
push: true
tags: ghcr.io/aet-devops26/team-devvopps/llm-service:latest
cache-from: type=gha,scope=llm-service
cache-to: type=gha,scope=llm-service,mode=max

- name: Build and Push course-seeder
uses: docker/build-push-action@v5
Expand All @@ -76,44 +108,5 @@ jobs:
file: ./server/course-service/Dockerfile.seeder
push: true
tags: ghcr.io/aet-devops26/team-devvopps/course-seeder:latest

deploy:
needs: build
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Copy Files to VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.AZURE_PUBLIC_IP }}
username: ${{ vars.AZURE_USER }}
key: ${{ secrets.AZURE_PRIVATE_KEY }}
source: "compose.azure.yml,server/init-databases.sql"
target: /home/${{ vars.AZURE_USER }}

- name: SSH to VM and Create .env.prod
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.AZURE_PUBLIC_IP }}
username: ${{ vars.AZURE_USER }}
key: ${{ secrets.AZURE_PRIVATE_KEY }}
script: |
rm -f .env.prod
touch .env.prod
echo "CLIENT_HOST=client.${{ vars.AZURE_PUBLIC_IP }}.nip.io" >> .env.prod
echo "SERVER_HOST=api.${{ vars.AZURE_PUBLIC_IP }}.nip.io" >> .env.prod
echo "PUBLIC_API_URL=https://api.${{ vars.AZURE_PUBLIC_IP }}.nip.io" >> .env.prod

- name: SSH to VM and Execute Docker-Compose Up
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.AZURE_PUBLIC_IP }}
username: ${{ vars.AZURE_USER }}
key: ${{ secrets.AZURE_PRIVATE_KEY }}
command_timeout: 5m
script: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker compose -f compose.azure.yml --env-file=.env.prod up --pull=always -d
cache-from: type=gha,scope=course-seeder
cache-to: type=gha,scope=course-seeder,mode=max
18 changes: 18 additions & 0 deletions .github/workflows/deploy-k8s.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Deploy to AET Kubernetes Cluster

# Can be triggered manually via workflow_dispatch, or automatically on every push to main.
# concurrency ensures only one deploy runs at a time per branch
# if a new push comes in while deploying, the in-progress deploy is cancelled.
on:
push:
branches:
Expand All @@ -26,6 +29,8 @@ jobs:
with:
version: 'latest'

# Write the kubeconfig from secrets so kubectl and Helm can reach the cluster.
# Fails early with a clear message if the secret is missing.
- name: Configure kubectl
env:
KUBECONFIG_CONTENT: ${{ secrets.KUBECONFIG }}
Expand All @@ -42,6 +47,8 @@ jobs:

echo "kubectl configured successfully"

# Validate the Helm chart before attempting a deploy.
# Catches templating errors and missing required values early.
- name: Lint Helm chart
env:
POSTGRES_USER: ${{ secrets.POSTGRES_USER || 'postgres' }}
Expand All @@ -51,26 +58,35 @@ jobs:
--set postgres.credentials.username="$POSTGRES_USER" \
--set postgres.credentials.password="$POSTGRES_PASSWORD"

# Remove pods stuck in Failed/Unknown state that would block the Helm rollout.
- name: Clean up failed deployments
run: |
echo "Cleaning up failed and unknown pods..."
kubectl delete pods -n "$K8S_NAMESPACE" --field-selector=status.phase=Failed 2>/dev/null || true
kubectl delete pods -n "$K8S_NAMESPACE" --field-selector=status.phase=Unknown 2>/dev/null || true
echo "Cleanup complete"

# helm upgrade --install: creates the release if it doesn't exist, upgrades if it does.
# Sensitive values (DB credentials, API keys) are passed at deploy time, not stored in values files.
# --wait blocks until all pods are ready or the timeout is hit.
- name: Deploy with Helm
env:
POSTGRES_USER: ${{ secrets.POSTGRES_USER || 'postgres' }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD || 'postgres' }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
LOGOS_API_KEY: ${{ secrets.LOGOS_API_KEY }}
run: |
helm upgrade --install "$HELM_RELEASE_NAME" helm/team-devvopps/ \
-f helm/team-devvopps/values-aet.yaml \
--set postgres.credentials.username="$POSTGRES_USER" \
--set postgres.credentials.password="$POSTGRES_PASSWORD" \
--set llmService.groqApiKey="$GROQ_API_KEY" \
--set llmService.logosApiKey="$LOGOS_API_KEY" \
-n "$K8S_NAMESPACE" \
--wait \
--timeout 5m

# Quick sanity check — prints pod/service/ingress state immediately after deploy.
- name: Verify deployment
run: |
echo "Checking pod status..."
Expand All @@ -82,6 +98,8 @@ jobs:
echo "Release status:"
helm status "$HELM_RELEASE_NAME" -n "$K8S_NAMESPACE"

# Exclude the course-seeder job from readiness check (it is a one-off job
# that runs to completion and exits, so it will never reach Ready state).
- name: Wait for pods to be ready
run: |
echo "Waiting for deployment pods to be ready..."
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/deploy-vm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: Deploy Docker Images (on VM)

# Runs automatically after the Build workflow completes successfully on main.
# Copies the compose file to the Azure VM and restarts all services via Docker Compose.
on:
workflow_run:
workflows:
- Build Docker Images
types:
- completed
branches:
- main


permissions:
contents: read
packages: write

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v4

# Copy the Docker Compose file and database init script to the VM.
# These are the only files needed on the VM; images are pulled from GHCR.
- name: Copy Files to VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.AZURE_PUBLIC_IP }}
username: ${{ vars.AZURE_USER }}
key: ${{ secrets.AZURE_PRIVATE_KEY }}
source: "compose.azure.yml,server/init-databases.sql"
target: /home/${{ vars.AZURE_USER }}

# Write a .env.prod file on the VM with hostnames derived from the public IP.
# nip.io provides free wildcard DNS so we don't need a custom domain.
- name: SSH to VM and Create .env.prod
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.AZURE_PUBLIC_IP }}
username: ${{ vars.AZURE_USER }}
key: ${{ secrets.AZURE_PRIVATE_KEY }}
script: |
rm -f .env.prod
touch .env.prod
echo "CLIENT_HOST=client.${{ vars.AZURE_PUBLIC_IP }}.nip.io" >> .env.prod
echo "SERVER_HOST=api.${{ vars.AZURE_PUBLIC_IP }}.nip.io" >> .env.prod
echo "PUBLIC_API_URL=https://api.${{ vars.AZURE_PUBLIC_IP }}.nip.io" >> .env.prod

# Pull the latest images from GHCR and restart all services.
- name: SSH to VM and Execute Docker-Compose Up
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.AZURE_PUBLIC_IP }}
username: ${{ vars.AZURE_USER }}
key: ${{ secrets.AZURE_PRIVATE_KEY }}
command_timeout: 5m
script: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker compose -f compose.azure.yml --env-file=.env.prod up --pull=always -d
9 changes: 9 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Lint Code

# Runs on PRs to main and on any branch push except main.
# Acts as a quality gate before code is merged.
on:
pull_request:
branches:
Expand All @@ -9,6 +11,7 @@ on:
- main

jobs:
# Lints the React frontend using ESLint.
lint-client:
name: Lint React Client (ESLint)
runs-on: ubuntu-latest
Expand All @@ -30,6 +33,8 @@ jobs:
run: npm run lint
working-directory: client

# Lints all Java services using SpotBugs via Gradle.
# -x test skips unit tests; --scan uploads a build scan for inspection.
lint-java:
name: Lint Java Services (SpotBugs)
runs-on: ubuntu-latest
Expand All @@ -48,6 +53,8 @@ jobs:
run: ./gradlew check -x test --scan
working-directory: server

# Validates GitHub Actions workflow files themselves for correctness.
# Catches common mistakes like invalid expressions or missing required fields.
lint-actions:
name: Lint GitHub Actions Workflows
runs-on: ubuntu-latest
Expand All @@ -59,6 +66,8 @@ jobs:
- name: Run actionlint
uses: devops-actions/actionlint@v0.1.12

# Validates the Helm chart template rendering and value schema.
# Uses dummy credentials so required secret values don't block CI.
lint-helm:
name: Lint Helm Chart
runs-on: ubuntu-latest
Expand Down
Loading
Loading