Merge pull request #37 from AET-DevOps25/development #89
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: π 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 "" |