SonarQube Monorepo Configuration: Analyze Multiple Projects from One Repo

SonarQube Monorepo Configuration: Analyze Multiple Projects from One Repo

For a monorepo, you have two choices: one SonarQube project for the whole repo, or separate projects per service. Separate projects give you independent quality gates and metrics per service. One project is simpler but mixes metrics for unrelated services.

Key Takeaways

One project per service is usually the right choice. Each service gets its own quality gate, dashboard, and independent new code tracking.

Use sonar.projectKey to identify each sub-project. Run a separate sonar-scanner invocation per service with a unique project key.

sonar.sources and sonar.projectBaseDir scope the analysis. Point them at the service directory to avoid analyzing the entire monorepo for each run.

Only analyze changed services in CI. Use git diff to find changed services; only run SonarQube for those. Reduces CI time significantly in large monorepos.

Share a quality gate profile across all projects. Maintain one "company standard" quality gate in SonarQube and assign all monorepo projects to it.

Strategy: One Project vs. Multiple Projects

One Project for the Entire Monorepo

sonar-project.properties (at root)
sonar.projectKey=my-monorepo
sonar.projectName=My Monorepo
sonar.sources=.
sonar.exclusions=**/node_modules/**,**/vendor/**

Pros: Simple setup, one dashboard Cons: Coverage for service A drags down service B's metrics, can't set different quality gates per service, noisy issue list

Separate Projects Per Service

monorepo/
  services/
    api/          → sonar project: myco.api
    worker/       → sonar project: myco.worker
    frontend/     → sonar project: myco.frontend

Each gets its own sonar-project.properties and separate CI analysis. Pros: independent quality gates, clean metrics, meaningful dashboards. This is the recommended approach for non-trivial monorepos.

Configuring Per-Service Analysis

Each service has its own sonar-project.properties:

# services/api/sonar-project.properties
sonar.projectKey=myco.api
sonar.projectName=API Service
sonar.projectVersion=1.0
sonar.sources=src
sonar.tests=src
sonar.test.inclusions=**/*.test.ts,**/*.spec.ts
sonar.coverage.lcov.reportPaths=coverage/lcov.info
sonar.javascript.file.suffixes=.ts,.tsx
# services/worker/sonar-project.properties
sonar.projectKey=myco.worker
sonar.projectName=Worker Service
sonar.sources=src
sonar.java.binaries=target/classes
sonar.java.test.binaries=target/test-classes
sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml

Run analysis per service:

cd services/api && sonar-scanner
cd services/worker && mvn sonar:sonar

CI: Only Analyze Changed Services

In a large monorepo, running SonarQube on every push for every service wastes CI time. Only analyze what changed:

#!/bin/bash
# find-changed-services.sh

CHANGED_FILES=$(git diff --name-only $BASE_SHA $HEAD_SHA)

CHANGED_SERVICES=()
for file in $CHANGED_FILES; do
  SERVICE=$(echo "$file" | cut -d'/' -f1-2)  # e.g., "services/api"
  CHANGED_SERVICES+=("$SERVICE")
done

# Deduplicate
UNIQUE_SERVICES=($(printf '%s\n' "${CHANGED_SERVICES[@]}" | sort -u))

echo "${UNIQUE_SERVICES[@]}"

GitHub Actions:

jobs:
  changed-services:
    runs-on: ubuntu-latest
    outputs:
      services: ${{ steps.changes.outputs.services }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Find changed services
        id: changes
        run: |
          SERVICES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} \
            | grep '^services/' \
            | cut -d'/' -f1-2 \
            | sort -u \
            | jq -R -s -c 'split("\n") | map(select(length > 0))')
          echo "services=$SERVICES" >> $GITHUB_OUTPUT

  sonarqube:
    needs: changed-services
    runs-on: ubuntu-latest
    strategy:
      matrix:
        service: ${{ fromJson(needs.changed-services.outputs.services) }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Analyze ${{ matrix.service }}
        working-directory: ${{ matrix.service }}
        run: sonar-scanner
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

Shared Quality Gate Configuration

All monorepo services use the same quality gate. Create it once:

# Create quality gate
curl -X POST "http://sonarqube/api/qualityprofiles/create" \
  -u admin:admin \
  -d "language=java&name=MonorepoStandard"

# Add conditions
curl -X POST "http://sonarqube/api/qualitygates/create_condition" \
  -u admin:admin \
  -d "gateName=MonorepoStandard&metric=coverage&op=LT&error=80"

curl -X POST "http://sonarqube/api/qualitygates/create_condition" \
  -u admin:admin \
  -d "gateName=MonorepoStandard&metric=new_violations&op=GT&error=0"

Assign to all projects:

for PROJECT in myco.api myco.worker myco.frontend; do
  curl -X POST "http://sonarqube/api/qualitygates/select" \
    -u admin:admin \
    -d "projectKey=$PROJECT&gateName=MonorepoStandard"
done

Monorepo with Maven Multi-Module

For a Java monorepo using Maven multi-module:

<!-- root pom.xml -->
<modules>
  <module>service-api</module>
  <module>service-worker</module>
  <module>shared-lib</module>
</modules>

Single analysis from the root:

mvn verify sonar:sonar \
  -Dsonar.projectKey=myco.monorepo \
  -Dsonar.projectName="My Monorepo"

This creates one project with all modules. SonarQube shows a module breakdown but it's all one project. For separate projects, run from each module directory with its own project key:

cd service-api && mvn sonar:sonar -Dsonar.projectKey=myco.api
cd service-worker && mvn sonar:sonar -Dsonar.projectKey=myco.worker

Cross-Service Coverage Aggregation

If you want a combined coverage report across all services:

#!/bin/bash
# Fetch coverage for all monorepo services
for SERVICE in api worker frontend; do
  COVERAGE=$(curl -s "http://sonarqube/api/measures/component?component=myco.$SERVICE&metricKeys=coverage" \
    | jq -r '.component.measures[0].value // "N/A"')
  echo "myco.$SERVICE: $COVERAGE%"
done

Output:

myco.api: 82.4%
myco.worker: 75.1%
myco.frontend: 68.3%

Post this as a CI step output or PR comment for a quick overview.

Summary

Decision Recommendation
Project structure One SonarQube project per service
Project keys orgname.servicename format
CI strategy Only analyze changed services
Quality gate Shared gate, assigned to all projects
Coverage reporting Per-service LCOV/JaCoCo reports
Branch analysis Per-project, auto-detected from CI env vars

The key insight: SonarQube projects are logical units, not tied to repositories. A monorepo can contain many SonarQube projects, each with independent quality tracking.

Read more

Start now free