name: CI/CD Pipeline on: push: branches: [ main, develop ] pull_request: branches: [ main ] workflow_dispatch: env: JAVA_VERSION: '21' JAVA_DISTRIBUTION: 'temurin' GCP_REGION: 'us-central1' jobs: # Job 1: Build and Test build-and-test: name: Build and Test runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK ${{ env.JAVA_VERSION }} uses: actions/setup-java@v4 with: java-version: ${{ env.JAVA_VERSION }} distribution: ${{ env.JAVA_DISTRIBUTION }} cache: 'maven' - name: Run tests run: ./mvnw clean verify -B - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results path: target/surefire-reports/ retention-days: 30 # Job 2: Security Scan (Filesystem) security-scan: name: Security Scan runs-on: ubuntu-latest needs: build-and-test permissions: contents: read security-events: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: scan-type: 'fs' scan-ref: '.' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: 'trivy-results.sarif' # Job 3: Build and Push Docker Image build-and-push-image: name: Build and Push Docker Image runs-on: ubuntu-latest needs: [build-and-test, security-scan] if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' permissions: contents: read id-token: write outputs: image-tag: ${{ steps.meta.outputs.tags }} steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Authenticate to Google Cloud id: auth uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 - name: Configure Docker for GCR run: | gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev - name: Extract metadata for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/petclinic/spring-petclinic tags: | type=ref,event=branch type=sha,prefix={{branch}}- type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image uses: docker/build-push-action@v5 with: context: . file: ./Dockerfile push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # Job 4: Container Security Scan scan-image: name: Scan Docker Image runs-on: ubuntu-latest needs: build-and-push-image permissions: contents: read security-events: write steps: - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 - name: Configure Docker run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev - name: Run Trivy vulnerability scanner on image uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/petclinic/spring-petclinic:${{ github.sha }} format: 'sarif' output: 'trivy-image-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v3 if: always() with: sarif_file: 'trivy-image-results.sarif' # Job 5: Deploy to GCP deploy: name: Deploy to GCP runs-on: ubuntu-latest needs: [build-and-push-image, scan-image] if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' environment: name: ${{ github.ref == 'refs/heads/main' && 'production' || 'development' }} permissions: contents: read id-token: write steps: - name: Checkout code uses: actions/checkout@v4 - name: Authenticate to Google Cloud uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} - name: Set up Cloud SDK uses: google-github-actions/setup-gcloud@v2 - name: Set up Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.6.0 - name: Determine environment id: env run: | if [ "${{ github.ref }}" == "refs/heads/main" ]; then echo "environment=prod" >> $GITHUB_OUTPUT echo "min_instances=1" >> $GITHUB_OUTPUT else echo "environment=dev" >> $GITHUB_OUTPUT echo "min_instances=0" >> $GITHUB_OUTPUT fi - name: Terraform Init working-directory: ./terraform run: terraform init - name: Terraform Plan working-directory: ./terraform run: | terraform plan \ -var="project_id=${{ secrets.GCP_PROJECT_ID }}" \ -var="region=${{ env.GCP_REGION }}" \ -var="environment=${{ steps.env.outputs.environment }}" \ -var="image_tag=${{ github.sha }}" \ -var="min_instances=${{ steps.env.outputs.min_instances }}" \ -out=tfplan - name: Terraform Apply working-directory: ./terraform run: terraform apply -auto-approve tfplan - name: Get Cloud Run URL id: deploy run: | URL=$(gcloud run services describe petclinic-${{ steps.env.outputs.environment }} \ --region=${{ env.GCP_REGION }} \ --format='value(status.url)') echo "url=$URL" >> $GITHUB_OUTPUT - name: Run smoke test run: | sleep 30 curl -f ${{ steps.deploy.outputs.url }}/actuator/health || exit 1 # Job 6: Notify notify: name: Notify runs-on: ubuntu-latest needs: [deploy] if: always() steps: - name: Send notification run: | echo "Deployment ${{ needs.deploy.result }}" echo "Status: ${{ job.status }}"