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.frontendEach 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.xmlRun analysis per service:
cd services/api && sonar-scanner
cd services/worker && mvn sonar:sonarCI: 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"
doneMonorepo 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.workerCross-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%"
doneOutput:
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.