Chapter 24Lesson 02~190 minutes

Monorepos, Multi-Module Builds, Generated Code, and Complex Repository Layouts: Guided Hands-On Workflow

Build a disposable two-module repository, compare one-project and multi-project analysis topologies, prove indexed/report ownership, preserve a deliberate report-path failure, and repair it at the same revision.

MonoreposModulesAnalysis scopeReport pathsGovernance

Learning objectives

  • Create a disposable two-module Git repository with tests, coverage reports, generated/vendor directories, and exact revision evidence.
  • Analyze the repository once as one SonarQube project and then as two independently owned Community Build projects.
  • Prove indexed-file and report ownership using scanner logs, report contents, task IDs, measures, and fixed Git SHA.
  • Engineer a wrong module-relative coverage path that preserves source indexing but drops coverage evidence.
  • Repair only the report-path coordinate at the same revision and prove the resulting measure change independently.
  • Keep every project token, scanner work directory, report artifact, and cleanup action bounded to the lab.

1. Disposable workflow contract

Local/sandbox only. Use a disposable Community Build instance and synthetic source. Create three lab projects—one whole-repository comparison project plus two module projects—and project-analysis tokens for each. Never reuse a production project key or token.
State Lab value
Server Community Build 26.9.0.129388 at http://localhost:9000
Scanner SonarScanner CLI 8.1.0.6389
Repository sq-ch24-layout-lab, one Git history
Whole project sq-ch24-all
Module projects sq-ch24-api, sq-ch24-worker
Producer Python 3.11+ with pytest and pytest-cov
Tokens Three disposable project-analysis tokens in environment variables

2. Build the two-module fixture

mkdir -p sq-ch24-layout-lab/modules/api/{src,tests}
mkdir -p sq-ch24-layout-lab/modules/worker/{src,tests}
mkdir -p sq-ch24-layout-lab/{generated,vendor,evidence}
cd sq-ch24-layout-lab

git init
git config user.name "SonarQube Learner"
git config user.email "learner@example.invalid"

cat > modules/api/src/pricing.py <<'PY'
def discounted(total: float, vip: bool) -> float:
    return total * 0.9 if vip else total
PY
cat > modules/api/tests/test_pricing.py <<'PY'
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from pricing import discounted

def test_vip_discount():
    assert discounted(100.0, True) == 90.0
PY

cat > modules/worker/src/jobs.py <<'PY'
def next_delay(attempt: int) -> int:
    if attempt <= 0:
        return 1
    return min(60, 2 ** attempt)
PY
cat > modules/worker/tests/test_jobs.py <<'PY'
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from jobs import next_delay

def test_first_retry():
    assert next_delay(1) == 2
PY

cat > generated/client_generated.py <<'PY'
# Synthetic generated output; not first-party ownership in this lab.
SCHEMA_VERSION = "demo-v1"
PY
cat > vendor/third_party_copy.py <<'PY'
# Synthetic stand-in for vendored code; not an actual third-party package.
def external_helper():
    return "demo"
PY

cat > .gitignore <<'EOF'
.venv/
__pycache__/
.pytest_cache/
.scannerwork*/
evidence/
*.coverage
EOF

git add .
git commit -m "baseline two-module layout"
git rev-parse HEAD | tee /tmp/ch24-revision.txt

The generated/vendor directories are deliberately present so the ownership decision is visible rather than hidden. They are not automatically included in the module project scopes.

3. Produce module reports before scanning

python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest pytest-cov
python -m pytest --version | tee evidence/pytest-version.txt
python -m coverage --version | tee evidence/coverage-version.txt

(
  cd modules/api
  ../../.venv/bin/python -m pytest tests --cov=src --cov-report=xml:coverage.xml
)
(
  cd modules/worker
  ../../.venv/bin/python -m pytest tests --cov=src --cov-report=xml:coverage.xml
)

# Inspect report paths before importing them.
grep -n 'filename=' modules/api/coverage.xml | head | tee evidence/api-report-paths.txt
grep -n 'filename=' modules/worker/coverage.xml | head | tee evidence/worker-report-paths.txt

Each report was produced from its module directory, so its internal paths should map naturally to that module when the scanner uses the same module as projectBaseDir.

4. Preflight three project identities without embedding secrets

Create the three disposable projects in the local UI. Generate a project-analysis token for each. Keep the values only in the shell or a local secret manager.

export SONAR_HOST_URL="http://localhost:9000"
read -rsp "sq-ch24-all token: " SONAR_TOKEN_ALL; echo
read -rsp "sq-ch24-api token: " SONAR_TOKEN_API; echo
read -rsp "sq-ch24-worker token: " SONAR_TOKEN_WORKER; echo
export SONAR_TOKEN_ALL SONAR_TOKEN_API SONAR_TOKEN_WORKER

git rev-parse HEAD | tee evidence/revision.txt
sonar-scanner --version | tee evidence/scanner-version.txt
curl -fsS "$SONAR_HOST_URL/api/system/status" | tee evidence/server-status.json

5. Design A: one SonarQube project for the whole repository

For the comparison scan, generate one coverage report from the repository root so report-internal paths use the same coordinate system as the whole-project scan.

.venv/bin/python -m pytest modules/api/tests modules/worker/tests \
  --cov=modules/api/src --cov=modules/worker/src \
  --cov-report=xml:coverage-all.xml

grep -n 'filename=' coverage-all.xml | head | tee evidence/all-report-paths.txt

SONAR_TOKEN="$SONAR_TOKEN_ALL" sonar-scanner -X \
  -Dsonar.projectKey=sq-ch24-all \
  -Dsonar.projectName="SQ Ch24 Whole Repository" \
  -Dsonar.sources=modules/api/src,modules/worker/src \
  -Dsonar.tests=modules/api/tests,modules/worker/tests \
  -Dsonar.python.coverage.reportPaths=coverage-all.xml \
  -Dsonar.working.directory=.scannerwork-all \
  2>&1 | tee evidence/all-scanner.log

cp .scannerwork-all/report-task.txt evidence/all-report-task.txt

This design produces one history, one New Code definition, one permission boundary and one Quality Gate for both modules. That can be correct when the modules share ownership and release lifecycle. It is wrong merely because “the repository is one folder.”

6. Design B: two SonarQube projects from the same Git revision

SONAR_TOKEN="$SONAR_TOKEN_API" sonar-scanner -X \
  -Dsonar.projectKey=sq-ch24-api \
  -Dsonar.projectName="SQ Ch24 API" \
  -Dsonar.projectBaseDir=modules/api \
  -Dsonar.sources=src \
  -Dsonar.tests=tests \
  -Dsonar.python.coverage.reportPaths=coverage.xml \
  -Dsonar.working.directory=.scannerwork-ch24-api \
  2>&1 | tee evidence/api-scanner.log

SONAR_TOKEN="$SONAR_TOKEN_WORKER" sonar-scanner -X \
  -Dsonar.projectKey=sq-ch24-worker \
  -Dsonar.projectName="SQ Ch24 Worker" \
  -Dsonar.projectBaseDir=modules/worker \
  -Dsonar.sources=src \
  -Dsonar.tests=tests \
  -Dsonar.python.coverage.reportPaths=coverage.xml \
  -Dsonar.working.directory=.scannerwork-ch24-worker \
  2>&1 | tee evidence/worker-scanner-good-baseline.log

cp modules/api/.scannerwork-ch24-api/report-task.txt evidence/api-report-task.txt
cp modules/worker/.scannerwork-ch24-worker/report-task.txt evidence/worker-report-task-good-baseline.txt

Both scans use the same Git SHA but distinct project keys, bases, report files, scanner work directories and tokens. Inspect logs to prove the API project did not index worker source and vice versa.

grep -Ei 'files indexed|indexing|coverage|report' evidence/api-scanner.log | tee evidence/api-index-report-summary.txt
grep -Ei 'files indexed|indexing|coverage|report' evidence/worker-scanner-good-baseline.log | tee evidence/worker-index-report-summary.txt

7. Deliberate defect: duplicate the module prefix in the report path

Now keep the worker source, Git SHA, project key, profile and gate unchanged. Change only the coverage report parameter:

# BROKEN ON PURPOSE: projectBaseDir is already modules/worker.
SONAR_TOKEN="$SONAR_TOKEN_WORKER" sonar-scanner -X \
  -Dsonar.projectKey=sq-ch24-worker \
  -Dsonar.projectBaseDir=modules/worker \
  -Dsonar.sources=src \
  -Dsonar.tests=tests \
  -Dsonar.python.coverage.reportPaths=modules/worker/coverage.xml \
  -Dsonar.working.directory=.scannerwork-ch24-worker-broken \
  2>&1 | tee evidence/worker-scanner-broken.log

cp modules/worker/.scannerwork-ch24-worker-broken/report-task.txt \
  evidence/worker-report-task-broken.txt

grep -Ei 'coverage|report.*not|no report|cannot' evidence/worker-scanner-broken.log \
  | tee evidence/worker-broken-report-evidence.txt || true

The expected causal signature is: source files still index, scanner/report upload may still complete, but the coverage importer cannot find the intended report. Preserve the first broken log and task evidence. Do not “fix” the dashboard by excluding worker files from coverage.

8. Repair only the coordinate and prove the same-revision result

git rev-parse HEAD | tee evidence/revision-before-repair.txt

SONAR_TOKEN="$SONAR_TOKEN_WORKER" sonar-scanner -X \
  -Dsonar.projectKey=sq-ch24-worker \
  -Dsonar.projectBaseDir=modules/worker \
  -Dsonar.sources=src \
  -Dsonar.tests=tests \
  -Dsonar.python.coverage.reportPaths=coverage.xml \
  -Dsonar.working.directory=.scannerwork-ch24-worker-fixed \
  2>&1 | tee evidence/worker-scanner-fixed.log

cp modules/worker/.scannerwork-ch24-worker-fixed/report-task.txt \
  evidence/worker-report-task-fixed.txt

git rev-parse HEAD | tee evidence/revision-after-repair.txt
cmp evidence/revision-before-repair.txt evidence/revision-after-repair.txt

grep -Ei 'coverage|report' evidence/worker-scanner-fixed.log \
  | tee evidence/worker-fixed-report-evidence.txt

Then query the project measure after Compute Engine completes. The important proof is not a particular percentage—it is that the corrected scan imports the report under the same source revision while the broken scan did not.

9. Preserve asynchronous task evidence for every scan

TASK_ID="$(sed -n 's/^ceTaskId=//p' evidence/worker-report-task-fixed.txt)"
echo "$TASK_ID" | tee evidence/worker-fixed-ceTaskId.txt

curl -fsS -H "Authorization: Bearer $SONAR_TOKEN_WORKER" \
  "$SONAR_HOST_URL/api/ce/task?id=$TASK_ID" \
  | tee evidence/worker-fixed-ce.json

curl -fsS -H "Authorization: Bearer $SONAR_TOKEN_WORKER" \
  "$SONAR_HOST_URL/api/measures/component?component=sq-ch24-worker&metricKeys=ncloc,coverage,line_coverage" \
  | tee evidence/worker-fixed-measures.json

Poll until the task is terminal before interpreting measures. Scanner process success, report upload, Compute Engine success, project analysis completion and Quality Gate result remain separate states.

10. Challenge: which layer owns the defect?

The worker has ncloc but no imported coverage after a successful scanner upload. The report file exists at modules/worker/coverage.xml. Which layer should you inspect first?

  1. Record sonar.projectBaseDir.
  2. Resolve the configured report path from that base.
  3. Inspect the report’s internal filenames.
  4. Inspect scanner import logs.
  5. Only then inspect server measures/CE state.

Changing the Quality Gate, coverage exclusions, project key, or source files would alter unrelated state and hide the path defect.

Knowledge check

Why generate module coverage reports before the scanner?

What stays constant during the deliberate worker report-path failure and repair?

Why use separate project-analysis tokens for API and worker?

If the broken scan uploads successfully, did coverage import necessarily succeed?

Why compare one-project and two-project designs on the same revision?

Next lesson

Choose repository-analysis topology deliberately

Lesson 3 compares one/multiple projects, build-native modules, generated-code policies, concurrency and commercial aggregation.

Official references and version notes

Version and compatibility note

Rechecked 2026-09-08. Mandatory examples target Community Build 26.9.0.129388 and SonarScanner CLI 8.1.0.6389. For CLI-managed projects, sonar.projectBaseDir changes the analysis directory; sonar.sources/sonar.tests are simple paths relative to that base unless absolute. Most coverage/external report paths are project-root/base-relative unless their parameter docs state otherwise. sonar.working.directory must be unique for each project and is deleted before each analysis, so parallel jobs must isolate scanner/report artifacts. Current native monorepo onboarding/binding is an Enterprise Edition feature and still requires explicit projects; SonarQube does not infer monorepo projects automatically. Applications are a commercial aggregation starting in Developer Edition; Portfolios start in Enterprise Edition. Community Build can still model multiple independent projects from one repository manually with distinct project keys/base directories and an external governance-summary fallback. Recheck the exact scanner/build integration and report parameter semantics before production use.

Complex-layout evidence rule. Preserve repository root, exact revision, project key/owner, projectBaseDir, source/test roots, inclusions/exclusions, generated/vendor policy, report producer/path/internal filenames, unique scanner working directory, token owner/type (never value), scanner log, report-task.txt, ceTaskId, Compute Engine state, measures/gate, CI concurrency identity and aggregation edition/definition separately.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.