Skip to content

Merge pull request #37 from AET-DevOps25/development #89

Merge pull request #37 from AET-DevOps25/development

Merge pull request #37 from AET-DevOps25/development #89

Workflow file for this run

name: πŸš€ FlexFit CI/CD Pipeline
# 🎯 CI/CD Strategy:
# β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
# β”‚ Branch Type β”‚ Unit Tests β”‚ Integration β”‚ Build & Push β”‚
# β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
# β”‚ Feature/* β”‚ πŸ”§ Manual β”‚ πŸ”§ Manual β”‚ πŸ”§ Manual β”‚
# β”‚ Pull Requests β”‚ βœ… Always β”‚ βœ… Always β”‚ ❌ Skip β”‚
# β”‚ Main/Dev/Prod β”‚ βœ… Always β”‚ βœ… Always β”‚ βœ… Always β”‚
# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
on:
push:
branches: [ main, development, production ]
pull_request:
branches: [ main, development, production ]
workflow_dispatch:
inputs:
test_level:
description: 'Test level to run'
required: true
default: 'full'
type: choice
options:
- unit-only
- integration-only
- quick
- full
env:
JAVA_VERSION: '21'
PYTHON_VERSION: '3.9'
jobs:
# Job 1: Setup & Validation
setup:
name: πŸ”§ Setup & Validation
runs-on: ubuntu-latest
outputs:
should_run_unit: ${{ steps.decide.outputs.should_run_unit }}
should_run_integration: ${{ steps.decide.outputs.should_run_integration }}
target_branch: ${{ github.base_ref || github.ref_name }}
is_pr: ${{ github.event_name == 'pull_request' }}
is_main: ${{ github.ref == 'refs/heads/main' }}
is_development: ${{ github.ref == 'refs/heads/development' }}
is_production: ${{ github.ref == 'refs/heads/production' }}
is_stable_branch: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/development' || github.ref == 'refs/heads/production' }}
steps:
- name: πŸ“‹ Pipeline Information
run: |
echo "πŸš€ FlexFit CI/CD Pipeline"
echo "Event: ${{ github.event_name }}"
echo "Branch: ${{ github.ref_name }}"
echo "Target: ${{ github.base_ref || 'N/A' }}"
echo "Actor: ${{ github.actor }}"
echo ""
if [[ "${{ github.ref }}" == refs/heads/feature/* ]]; then
echo "🌿 Feature Branch - Unit Tests βœ… | Integration Tests βœ… | Build & Push ❌"
elif [ "${{ github.event_name }}" == "pull_request" ]; then
echo "πŸ”„ Pull Request - Unit Tests βœ… | Integration Tests βœ… | Build & Push ❌"
elif [[ "${{ github.ref }}" == refs/heads/main || "${{ github.ref }}" == refs/heads/development || "${{ github.ref }}" == refs/heads/production ]]; then
echo "🎯 Stable Branch (${{ github.ref_name }}) - Unit Tests βœ… | Integration Tests βœ… | Build & Push βœ…"
else
echo "πŸ” Other Branch - Unit Tests βœ… | Integration Tests βœ… | Build & Push ❌"
fi
- name: 🎯 Decide what to run
id: decide
run: |
# Default: run unit and integration tests
UNIT="true"
INTEGRATION="true"
# Manual trigger - respect user choice
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
case "${{ github.event.inputs.test_level }}" in
"unit-only")
UNIT="true"; INTEGRATION="false"
;;
"integration-only")
UNIT="false"; INTEGRATION="true"
;;
"quick")
UNIT="true"; INTEGRATION="true"
;;
*)
UNIT="true"; INTEGRATION="true"
;;
esac
fi
echo "should_run_unit=$UNIT" >> $GITHUB_OUTPUT
echo "should_run_integration=$INTEGRATION" >> $GITHUB_OUTPUT
echo "🎯 Pipeline Plan:"
echo " Unit Tests: $UNIT"
echo " Integration Tests: $INTEGRATION"
# Job 2: Unit Tests
unit-tests:
name: πŸ§ͺ Unit Tests
runs-on: ubuntu-latest
needs: setup
if: needs.setup.outputs.should_run_unit == 'true'
steps:
- name: πŸ“₯ Checkout code
uses: actions/checkout@v4
- name: β˜• Set up Java
uses: actions/setup-java@v3
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: 🐍 Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: πŸ“¦ Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install pytest requests fastapi uvicorn httpx
- name: πŸ§ͺ Run Java Unit Tests
run: |
echo "πŸ§ͺ Running Java unit tests..."
cd server/user-service && chmod +x ./mvnw && ./mvnw test -Dspring.profiles.active=test
cd ../workout-plan-service && chmod +x ./mvnw && ./mvnw test -Dspring.profiles.active=test
echo "βœ… Java unit tests completed"
- name: πŸ§ͺ Run Python Unit Tests
continue-on-error: true
env:
CHAIR_API_KEY: ${{ secrets.CHAIR_API_KEY }}
run: |
echo "πŸ§ͺ Running Python unit tests..."
cd genai && python -m pytest test_workout_worker.py -v
python -m pytest test_workout_worker_local.py -v
echo "βœ… Python unit tests completed"
- name: 🌐 Run Client Tests
run: |
echo "🌐 Running client tests..."
cd client && node tests/core-workflows.test.js
node tests/ai-preference-integration.test.js
echo "βœ… Client tests completed"
- name: πŸ“Š Unit Test Summary
run: |
echo "βœ… Unit tests completed successfully!"
# Job 3: Integration Tests
integration-tests:
name: πŸ”— Integration Tests
runs-on: ubuntu-latest
needs: [setup, unit-tests]
if: needs.setup.outputs.should_run_integration == 'true'
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: flexfit_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: πŸ“₯ Checkout code
uses: actions/checkout@v4
- name: β˜• Set up Java
uses: actions/setup-java@v3
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
- name: 🐍 Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: πŸ“¦ Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install pytest requests fastapi uvicorn
- name: 🐳 Start services
env:
CHAIR_API_KEY: ${{ secrets.CHAIR_API_KEY }}
run: |
echo "🐳 Starting services for integration tests..."
chmod +x run-integration-tests.sh
./run-integration-tests.sh --start-only
echo "βœ… Services started"
- name: πŸ”— Run Integration Tests
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/flexfit_test
SPRING_DATASOURCE_USERNAME: postgres
SPRING_DATASOURCE_PASSWORD: postgres
SPRING_PROFILES_ACTIVE: test
run: |
echo "πŸ”— Running integration tests..."
echo "Starting Java services directly on Ubuntu (using CI PostgreSQL)..."
# Start Service Registry
echo "Starting Service Registry..."
cd server/service-registry
./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8761" &
SERVICE_REGISTRY_PID=$!
cd ../..
# Wait for Service Registry
sleep 15
echo "Waiting for Service Registry to be ready..."
timeout 60 bash -c 'while ! curl -s http://localhost:8761/actuator/health; do sleep 2; done'
# Start User Service
echo "Starting User Service..."
cd server/user-service
./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8081 --eureka.client.service-url.defaultZone=http://localhost:8761/eureka" &
USER_SERVICE_PID=$!
cd ../..
# Start Workout Plan Service
echo "Starting Workout Plan Service..."
cd server/workout-plan-service
./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8082 --eureka.client.service-url.defaultZone=http://localhost:8761/eureka" &
WORKOUT_SERVICE_PID=$!
cd ../..
# Start API Gateway
echo "Starting API Gateway..."
cd server/api-gateway
./mvnw spring-boot:run -Dspring-boot.run.arguments="--server.port=8080 --eureka.client.service-url.defaultZone=http://localhost:8761/eureka" &
API_GATEWAY_PID=$!
cd ../..
# Wait for all services to be ready
echo "Waiting for services to be ready..."
sleep 30
# Check service health
echo "Checking service health..."
curl -f http://localhost:8761/actuator/health || echo "Service Registry not ready"
curl -f http://localhost:8081/actuator/health || echo "User Service not ready"
curl -f http://localhost:8082/actuator/health || echo "Workout Service not ready"
curl -f http://localhost:8080/actuator/health || echo "API Gateway not ready"
# Run integration tests
echo "Running integration tests..."
./run-integration-tests.sh --test-only
# Cleanup processes
echo "Stopping services..."
kill $SERVICE_REGISTRY_PID $USER_SERVICE_PID $WORKOUT_SERVICE_PID $API_GATEWAY_PID 2>/dev/null || true
echo "βœ… Integration tests completed"
- name: 🧹 Cleanup
if: always()
run: |
echo "🧹 Cleaning up integration test processes..."
# Kill any remaining Java processes
pkill -f "spring-boot:run" || true
# Clean up any Docker containers (if any were started)
docker ps -q | xargs -r docker stop || true
# Job 4: Build & Push to GHCR (parallel with integration tests - temporary)
build-and-push-ghcr:
name: 🐳 Build & Push to GHCR
runs-on: ubuntu-latest
needs: [setup, unit-tests]
if: always() && needs.unit-tests.result == 'success' && (needs.setup.outputs.is_stable_branch == 'true' || github.event_name == 'workflow_dispatch')
permissions:
contents: read
packages: write
strategy:
matrix:
service:
- name: service-registry
context: ./server/service-registry
- name: api-gateway
context: ./server/api-gateway
- name: user-service
context: ./server/user-service
- name: workout-plan-service
context: ./server/workout-plan-service
- name: genai-worker
context: ./genai
- name: genai-worker-local
context: ./genai
dockerfile: ./Dockerfile.local
- name: frontend
context: ./client
steps:
- name: πŸ“₯ Checkout code
uses: actions/checkout@v4
- name: πŸ”‘ Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: πŸ“‹ Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/${{ matrix.service.name }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/development' }}
type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=production,enable=${{ github.ref == 'refs/heads/production' }}
type=ref,event=branch,enable=${{ !contains(fromJSON('["refs/heads/development", "refs/heads/main", "refs/heads/production"]'), github.ref) }}
type=sha,prefix={{branch}}-
- name: 🐳 Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ${{ matrix.service.context }}
file: ${{ matrix.service.context }}/${{ matrix.service.dockerfile || 'Dockerfile' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_API_URL=http://localhost:8080
# Job 5: Deploy to Kubernetes
deploy-to-kubernetes:
name: πŸš€ Deploy to Kubernetes
runs-on: ubuntu-latest
needs: [setup, unit-tests, integration-tests, build-and-push-ghcr]
if: always() && needs.unit-tests.result == 'success' && needs.build-and-push-ghcr.result == 'success' && (needs.setup.outputs.is_stable_branch == 'true' || github.event_name == 'workflow_dispatch')
environment:
name: ${{ needs.setup.outputs.is_development == 'true' && 'development' || 'production' }}
url: ${{ needs.setup.outputs.is_development == 'true' && 'https://flexfit-dev.local' || 'https://flexfit-prod.local' }}
steps:
- name: πŸ“₯ Checkout code
uses: actions/checkout@v4
- name: πŸ”§ Set up kubectl
uses: azure/setup-kubectl@v3
with:
version: 'latest'
- name: πŸ”§ Set up Helm
uses: azure/setup-helm@v3
with:
version: 'latest'
- name: πŸ”‘ Configure Kubernetes context
run: |
echo "πŸ”§ Setting up TUM Kubernetes cluster access..."
# Create .kube directory
mkdir -p ~/.kube
# Write kubeconfig from GitHub secret
echo "${{ secrets.TUM_KUBECONFIG }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
# Verify connection
kubectl config current-context
kubectl version --client
echo "βœ… Connected to TUM Kubernetes cluster"
- name: 🌍 Set deployment environment
id: env
run: |
if [[ "${{ needs.setup.outputs.is_development }}" == "true" ]]; then
echo "namespace=team-code-compass-development" >> $GITHUB_OUTPUT
echo "values_file=values-tum.yaml" >> $GITHUB_OUTPUT
echo "environment=development" >> $GITHUB_OUTPUT
elif [[ "${{ needs.setup.outputs.is_main }}" == "true" ]]; then
echo "namespace=team-code-compass-production" >> $GITHUB_OUTPUT
echo "values_file=values-tum-production.yaml" >> $GITHUB_OUTPUT
echo "environment=production" >> $GITHUB_OUTPUT
else
echo "namespace=team-code-compass-development" >> $GITHUB_OUTPUT
echo "values_file=values-tum.yaml" >> $GITHUB_OUTPUT
echo "environment=development" >> $GITHUB_OUTPUT
fi
- name: πŸ”„ Wait for image propagation
run: |
echo "⏳ Waiting 30 seconds for GHCR images to propagate..."
sleep 30
- name: πŸš€ Deploy to Kubernetes using Helm
run: |
echo "🎯 Deploying to ${{ steps.env.outputs.environment }} environment"
echo "πŸ“¦ Namespace: ${{ steps.env.outputs.namespace }}"
echo "πŸ“ Values file: ${{ steps.env.outputs.values_file }}"
# Set required environment variables for deployment
export TUM_ID="${{ secrets.TUM_ID }}"
export CHAIR_API_KEY="${{ secrets.CHAIR_API_KEY }}"
export POSTGRES_PASSWORD="${{ secrets.POSTGRES_PASSWORD }}"
export GRAFANA_ADMIN_PASSWORD="${{ secrets.GRAFANA_ADMIN_PASSWORD }}"
# Email alerts (optional)
export ALERT_EMAIL_FROM="${{ secrets.ALERT_EMAIL_FROM || 'hakanduranyt@gmail.com' }}"
export ALERT_EMAIL_TO="${{ secrets.ALERT_EMAIL_TO || 'hakanduranyt@gmail.com' }}"
export ALERT_EMAIL_USERNAME="${{ secrets.ALERT_EMAIL_USERNAME || 'hakanduranyt@gmail.com' }}"
export ALERT_EMAIL_PASSWORD="${{ secrets.ALERT_EMAIL_PASSWORD || 'your_gmail_app_password_here' }}"
export SMTP_HOST="${{ secrets.SMTP_HOST || 'smtp.gmail.com:587' }}"
# Set IMAGE_TAG based on branch
if [[ "${{ needs.setup.outputs.is_main }}" == "true" ]]; then
export IMAGE_TAG="main"
echo "🏷️ Using IMAGE_TAG=main for production deployment"
else
export IMAGE_TAG="latest"
echo "🏷️ Using IMAGE_TAG=latest for development deployment"
fi
# Validate required secrets
if [[ -z "$TUM_ID" || -z "$CHAIR_API_KEY" || -z "$POSTGRES_PASSWORD" || -z "$GRAFANA_ADMIN_PASSWORD" ]]; then
echo "❌ Missing required secrets. Please configure:"
echo " - TUM_ID"
echo " - CHAIR_API_KEY"
echo " - POSTGRES_PASSWORD"
echo " - GRAFANA_ADMIN_PASSWORD"
echo ""
echo "πŸ“‹ Optional TUM Kubernetes secrets:"
echo " - TUM_KUBECONFIG (base64 encoded kubeconfig)"
echo " - TUM_NAMESPACE (e.g., ge85zat-devops25)"
echo " - TUM_INGRESS_HOST (e.g., ge85zat-devops25.student.k8s.aet.cit.tum.de)"
exit 1
fi
# Deploy using envsubst and helm
cd helm/flexfit
envsubst < ${{ steps.env.outputs.values_file }} | \
helm upgrade --install flexfit . \
--namespace ${{ steps.env.outputs.namespace }} \
--create-namespace \
--wait \
--timeout=10m \
-f -
- name: βœ… Verify deployment
run: |
echo "πŸ” Verifying deployment in ${{ steps.env.outputs.namespace }}"
kubectl get pods -n ${{ steps.env.outputs.namespace }}
kubectl get services -n ${{ steps.env.outputs.namespace }}
echo "🌐 Checking service health..."
kubectl wait --for=condition=ready pod -l app=frontend -n ${{ steps.env.outputs.namespace }} --timeout=300s || true
kubectl wait --for=condition=ready pod -l app=api-gateway -n ${{ steps.env.outputs.namespace }} --timeout=300s || true
- name: πŸ“Š Deployment summary
run: |
echo "## πŸš€ Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Environment**: ${{ steps.env.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
echo "- **Namespace**: ${{ steps.env.outputs.namespace }}" >> $GITHUB_STEP_SUMMARY
echo "- **Branch**: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- **Commit**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### πŸ“¦ Deployed Services:" >> $GITHUB_STEP_SUMMARY
kubectl get pods -n ${{ steps.env.outputs.namespace }} --no-headers | while read line; do
echo "- $line" >> $GITHUB_STEP_SUMMARY
done
# Job 6: Pipeline Summary
summary:
name: πŸ“Š Pipeline Summary
runs-on: ubuntu-latest
needs: [setup, unit-tests, integration-tests, build-and-push-ghcr, deploy-to-kubernetes]
if: always()
steps:
- name: πŸ“Š Pipeline Results
run: |
echo "πŸ“Š FlexFit CI/CD Pipeline Results"
echo "=================================="
echo "Event: ${{ github.event_name }}"
echo "Branch: ${{ needs.setup.outputs.target_branch }}"
echo "Is PR: ${{ needs.setup.outputs.is_pr }}"
echo ""
echo "Test Results:"
echo " πŸ§ͺ Unit Tests: ${{ needs.unit-tests.result || 'skipped' }}"
echo " πŸ”— Integration Tests: ${{ needs.integration-tests.result || 'skipped' }}"
echo " 🐳 GHCR Push: ${{ needs.build-and-push-ghcr.result || 'skipped' }}"
echo " πŸš€ Kubernetes Deploy: ${{ needs.deploy-to-kubernetes.result || 'skipped' }}"
echo ""