diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml index a68c8ea..df51c1b 100644 --- a/.github/workflows/cicd.yaml +++ b/.github/workflows/cicd.yaml @@ -1,21 +1,28 @@ -name: Backend CI/CD Pipeline +name: Backend CI/CD on: push: branches: - cicd paths: - - 'member/**' - - 'mysub/**' - - 'recommend/**' - - 'common/**' + - '**/*.java' + - '**/*.gradle' + - '**/build.gradle' + - '**/settings.gradle' + - '**/gradle.properties' - 'deployment/**' - - '.github/workflows/**' + - '.github/workflows/cicd.yaml' + +env: + REGISTRY: ghcr.io + RESOURCE_GROUP: ictcoe-edu jobs: build: name: Build and Test runs-on: ubuntu-latest + outputs: + image_tag: ${{ steps.set_outputs.outputs.image_tag }} steps: - name: Checkout code @@ -28,76 +35,85 @@ jobs: distribution: 'temurin' cache: gradle + - name: Load environment variables + id: env_vars + run: | + # Load environment variables from file + while IFS= read -r line; do + # Skip comments and empty lines + [[ "$line" =~ ^#.*$ ]] && continue + [[ -z "$line" ]] && continue + + # Extract key and value + key=$(echo "$line" | cut -d= -f1) + value=$(echo "$line" | cut -d= -f2-) + + echo "$key=$value" >> $GITHUB_ENV + done < deployment/deploy_env_vars + + echo "teamid=$teamid" >> $GITHUB_OUTPUT + - name: Grant execute permission for gradlew run: chmod +x ./gradlew - name: Build with Gradle - run: ./gradlew :member:build :mysub-infra:build :recommend:build -x test + run: | + ./gradlew :member:build :mysub-infra:build :recommend:build -x test - name: Run tests run: | - ./gradlew :member:test :member:jacocoTestReport - ./gradlew :mysub-infra:test :mysub-infra:jacocoTestReport - ./gradlew :recommend:test :recommend:jacocoTestReport + ./gradlew :member:test :mysub-infra:test :recommend:test - - name: SonarQube Analysis - Member - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + - name: Run SonarQube Analysis run: | - ./gradlew :member:sonar \ + ./gradlew :member:jacocoTestReport :member:sonar \ -Dsonar.projectKey=lifesub-member \ - -Dsonar.projectName=lifesub-member \ - -Dsonar.host.url=$SONAR_HOST_URL \ - -Dsonar.token=$SONAR_TOKEN \ + -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \ + -Dsonar.login=${{ secrets.SONAR_TOKEN }} \ -Dsonar.java.binaries=build/classes/java/main \ -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** - - - name: SonarQube Analysis - Recommend - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: | - ./gradlew :recommend:sonar \ + + ./gradlew :recommend:jacocoTestReport :recommend:sonar \ -Dsonar.projectKey=lifesub-recommend \ - -Dsonar.projectName=lifesub-recommend \ - -Dsonar.host.url=$SONAR_HOST_URL \ - -Dsonar.token=$SONAR_TOKEN \ + -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \ + -Dsonar.login=${{ secrets.SONAR_TOKEN }} \ -Dsonar.java.binaries=build/classes/java/main \ -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** - - - name: SonarQube Analysis - Mysub - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: | - ./gradlew :mysub-infra:sonar \ + + ./gradlew :mysub-infra:jacocoTestReport :mysub-infra:sonar \ -Dsonar.projectKey=lifesub-mysub \ - -Dsonar.projectName=lifesub-mysub \ - -Dsonar.host.url=$SONAR_HOST_URL \ - -Dsonar.token=$SONAR_TOKEN \ + -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} \ + -Dsonar.login=${{ secrets.SONAR_TOKEN }} \ -Dsonar.java.binaries=build/classes/java/main \ -Dsonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml \ -Dsonar.exclusions=**/config/**,**/entity/**,**/dto/**,**/*Application.class,**/exception/** - - name: Upload artifacts + - name: Generate timestamp for image tag + id: set_outputs + run: | + echo "image_tag=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT + + - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: build-artifacts + name: backend-builds path: | - member/build/libs - mysub-infra/build/libs - recommend/build/libs + member/build/libs/ + mysub-infra/build/libs/ + recommend/build/libs/ deployment/ + retention-days: 1 release: - name: Build and Push Container Images - runs-on: ubuntu-latest + name: Build and Push Docker Images needs: build + runs-on: ubuntu-latest outputs: - image_tag: ${{ steps.set-image-tag.outputs.image_tag }} + member_image: ${{ steps.push_images.outputs.member_image }} + mysub_image: ${{ steps.push_images.outputs.mysub_image }} + recommend_image: ${{ steps.push_images.outputs.recommend_image }} steps: - name: Checkout code @@ -106,70 +122,74 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - name: build-artifacts - path: . + name: backend-builds - - name: Read deployment variables + - name: Load environment variables run: | - source deployment/deploy_env_vars - echo "REGISTRY=$registry" >> $GITHUB_ENV - echo "IMAGE_ORG=$image_org" >> $GITHUB_ENV - echo "NAMESPACE=$namespace" >> $GITHUB_ENV + # Load environment variables from file + while IFS= read -r line; do + # Skip comments and empty lines + [[ "$line" =~ ^#.*$ ]] && continue + [[ -z "$line" ]] && continue + + # Extract key and value + key=$(echo "$line" | cut -d= -f1) + value=$(echo "$line" | cut -d= -f2-) + + echo "$key=$value" >> $GITHUB_ENV + done < deployment/deploy_env_vars + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Log in to Azure Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ secrets.ACR_USERNAME }} - password: ${{ secrets.ACR_PASSWORD }} - - - name: Generate image tag - id: set-image-tag + - name: Build and push Docker images + id: push_images run: | - IMAGE_TAG=$(date +'%Y%m%d%H%M%S') - echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT - echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV - - - name: Build and push Member image - uses: docker/build-push-action@v5 - with: - context: . - push: true - file: deployment/Dockerfile - build-args: | - BUILD_LIB_DIR=member/build/libs - ARTIFACTORY_FILE=member.jar - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/member:${{ env.IMAGE_TAG }} - - - name: Build and push Mysub image - uses: docker/build-push-action@v5 - with: - context: . - push: true - file: deployment/Dockerfile - build-args: | - BUILD_LIB_DIR=mysub-infra/build/libs - ARTIFACTORY_FILE=mysub.jar - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/mysub:${{ env.IMAGE_TAG }} - - - name: Build and push Recommend image - uses: docker/build-push-action@v5 - with: - context: . - push: true - file: deployment/Dockerfile - build-args: | - BUILD_LIB_DIR=recommend/build/libs - ARTIFACTORY_FILE=recommend.jar - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/recommend:${{ env.IMAGE_TAG }} + IMAGE_TAG=${{ needs.build.outputs.image_tag }} + + # Member service + docker build \ + --build-arg BUILD_LIB_DIR="member/build/libs" \ + --build-arg ARTIFACTORY_FILE="member.jar" \ + -f deployment/Dockerfile \ + -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/member:${IMAGE_TAG} . + + docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/member:${IMAGE_TAG} + + # Mysub service + docker build \ + --build-arg BUILD_LIB_DIR="mysub-infra/build/libs" \ + --build-arg ARTIFACTORY_FILE="mysub.jar" \ + -f deployment/Dockerfile \ + -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/mysub:${IMAGE_TAG} . + + docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/mysub:${IMAGE_TAG} + + # Recommend service + docker build \ + --build-arg BUILD_LIB_DIR="recommend/build/libs" \ + --build-arg ARTIFACTORY_FILE="recommend.jar" \ + -f deployment/Dockerfile \ + -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/recommend:${IMAGE_TAG} . + + docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/recommend:${IMAGE_TAG} + + # Set outputs for next job + echo "member_image=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/member:${IMAGE_TAG}" >> $GITHUB_OUTPUT + echo "mysub_image=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/mysub:${IMAGE_TAG}" >> $GITHUB_OUTPUT + echo "recommend_image=${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.image_org }}/recommend:${IMAGE_TAG}" >> $GITHUB_OUTPUT deploy: - name: Deploy to Kubernetes + name: Deploy to AKS + needs: [build, release] runs-on: ubuntu-latest - needs: release steps: - name: Checkout code @@ -178,86 +198,81 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - name: build-artifacts - path: . + name: backend-builds - - name: Setup envsubst - run: sudo apt-get install gettext-base - - - name: Read deployment variables + - name: Load environment variables run: | - source deployment/deploy_env_vars - echo "TEAMID=$teamid" >> $TEAMID - echo "NAMESPACE=$namespace" >> $GITHUB_ENV - echo "REGISTRY=$registry" >> $GITHUB_ENV - echo "IMAGE_ORG=$image_org" >> $GITHUB_ENV - echo "ALLOWED_ORIGINS=$allowed_origins" >> $GITHUB_ENV - echo "JWT_SECRET_KEY=$jwt_secret_key" >> $GITHUB_ENV - echo "POSTGRES_USER=$postgres_user" >> $GITHUB_ENV - echo "POSTGRES_PASSWORD=$postgres_password" >> $GITHUB_ENV - echo "REPLICAS=$replicas" >> $GITHUB_ENV - echo "RESOURCES_REQUESTS_CPU=$resources_requests_cpu" >> $GITHUB_ENV - echo "RESOURCES_REQUESTS_MEMORY=$resources_requests_memory" >> $GITHUB_ENV - echo "RESOURCES_LIMITS_CPU=$resources_limits_cpu" >> $GITHUB_ENV - echo "RESOURCES_LIMITS_MEMORY=$resources_limits_memory" >> $GITHUB_ENV + # Load environment variables from file + while IFS= read -r line; do + # Skip comments and empty lines + [[ "$line" =~ ^#.*$ ]] && continue + [[ -z "$line" ]] && continue + + # Extract key and value + key=$(echo "$line" | cut -d= -f1) + value=$(echo "$line" | cut -d= -f2-) + + echo "$key=$value" >> $GITHUB_ENV + done < deployment/deploy_env_vars - - name: Generate deployment manifest - env: - IMAGE_TAG: ${{ needs.release.outputs.image_tag }} + - name: Install envsubst run: | - export namespace=${{ env.NAMESPACE }} - export allowed_origins=${{ env.ALLOWED_ORIGINS }} - export jwt_secret_key=${{ env.JWT_SECRET_KEY }} - export postgres_user=${{ env.POSTGRES_USER }} - export postgres_password=${{ env.POSTGRES_PASSWORD }} - export replicas=${{ env.REPLICAS }} - export resources_requests_cpu=${{ env.RESOURCES_REQUESTS_CPU }} - export resources_requests_memory=${{ env.RESOURCES_REQUESTS_MEMORY }} - export resources_limits_cpu=${{ env.RESOURCES_LIMITS_CPU }} - export resources_limits_memory=${{ env.RESOURCES_LIMITS_MEMORY }} + sudo apt-get update + sudo apt-get install -y gettext-base - # Set image paths with the released tag - export member_image_path=${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/member:${{ env.IMAGE_TAG }} - export mysub_image_path=${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/mysub:${{ env.IMAGE_TAG }} - export recommend_image_path=${{ env.REGISTRY }}/${{ env.IMAGE_ORG }}/recommend:${{ env.IMAGE_TAG }} - - # Generate manifest - envsubst < deployment/deploy.yaml.template > deployment/deploy.yaml - - # Print manifest for debugging - echo "Generated Kubernetes manifest:" - cat deployment/deploy.yaml + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Set up kubectl uses: azure/setup-kubectl@v3 - - name: Set up AKS context - uses: azure/aks-set-context@v3 - with: - resource-group: ictcoe-edu - cluster-name: ${{ env.TEAMID }}-aks - admin: false - use-kubelogin: true - env: - AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + - name: Get AKS Credentials + run: | + az aks get-credentials --resource-group ${{ env.RESOURCE_GROUP }} --name ${{ env.teamid }}-aks --overwrite-existing - name: Create namespace if not exists run: | - kubectl create namespace ${{ env.NAMESPACE }} --dry-run=client -o yaml | kubectl apply -f - + kubectl create namespace ${{ env.namespace }} --dry-run=client -o yaml | kubectl apply -f - - - name: Deploy to Kubernetes + - name: Generate Kubernetes manifests + run: | + # Set environment variables for the template + export namespace=${{ env.namespace }} + export allowed_origins=${{ env.allowed_origins }} + export jwt_secret_key=${{ env.jwt_secret_key }} + export postgres_user=${{ env.postgres_user }} + export postgres_password=${{ env.postgres_password }} + export replicas=${{ env.replicas }} + export resources_requests_cpu=${{ env.resources_requests_cpu }} + export resources_requests_memory=${{ env.resources_requests_memory }} + export resources_limits_cpu=${{ env.resources_limits_cpu }} + export resources_limits_memory=${{ env.resources_limits_memory }} + + # Set image paths from previous job outputs + export member_image_path="${{ needs.release.outputs.member_image }}" + export mysub_image_path="${{ needs.release.outputs.mysub_image }}" + export recommend_image_path="${{ needs.release.outputs.recommend_image }}" + + # Generate manifest from template + envsubst < deployment/deploy.yaml.template > deployment/deploy.yaml + + # Debug: Print the generated manifest + echo "===== Generated Kubernetes Manifest =====" + cat deployment/deploy.yaml + echo "=========================================" + + - name: Apply Kubernetes manifests run: | kubectl apply -f deployment/deploy.yaml echo "Waiting for deployments to be ready..." - kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/member --timeout=300s - kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/mysub --timeout=300s - kubectl -n ${{ env.NAMESPACE }} wait --for=condition=available deployment/recommend --timeout=300s - - - name: Verify deployment - run: | - echo "Getting services:" - kubectl get svc -n ${{ env.NAMESPACE }} + kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/member --timeout=300s + kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/mysub --timeout=300s + kubectl -n ${{ env.namespace }} wait --for=condition=available deployment/recommend --timeout=300s - echo "Getting ingress:" - kubectl get ingress -n ${{ env.NAMESPACE }} \ No newline at end of file + echo "Deployment completed successfully." + + # Get service IP for ingress + echo "Ingress IP: $(kubectl -n ${{ env.namespace }} get ingress lifesub -o jsonpath='{.status.loadBalancer.ingress[0].ip}')" \ No newline at end of file