Chapter 34Lesson 02~260 minutes

Capstone: Operate a Governed Production SonarQube Quality Platform: Guided Hands-On Workflow

Build and operate a disposable Community Build platform end to end: source, tests, scanner, gate, API, developer feedback, backup evidence, incident and recovery.

Hands-onCommunity BuildQuality gateAPIBackup

Learning objectives

  • Assemble a disposable Community Build + PostgreSQL platform with recorded image/version assumptions and fake credentials.
  • Create one synthetic repository, produce test/coverage evidence, analyze an exact revision and correlate scanner upload with Compute Engine completion.
  • Exercise a quality-gate decision, least-privilege API read, developer feedback and a bounded incident without changing project identity.
  • Capture database-backup evidence and verify every before/after claim in an evidence directory.
  • Clean up only the resources owned by this lab.

1. Guided workflow mission

Build one small but complete quality platform. The project key is sq34:capstone, the local server is http://localhost:9000, and the source repository lives under sq34-capstone/. Every rerun uses the same project key and revision unless the exercise explicitly creates a new commit. The evidence directory is part of the lab deliverable, not disposable noise.

Pinned capstone baseline

Generation date: 2026-09-08. Mandatory executable work uses SonarQube Community Build 26.9.0.129388 and the official sonarqube:26.9.0.129388-community image. The recorded scanner baseline is SonarScanner CLI 8.1.0.6389. When scanner JRE auto-provisioning is unavailable or disabled, use Java 21 or newer. The local database family is PostgreSQL 17.x; the 2026.1 Server LTA documentation supports PostgreSQL 14–18. Commercial references are SonarQube Server 2026 Release 4.1 and 2026.1.5 LTA. Record the exact image digest, scanner output and database image digest you actually run.

Disposable-only boundary

Run this only on an authorized local machine. The database password and token names below are fake lab values. Do not copy employer source, real CI secrets or production backups into this exercise.

2. Assemble the local Community Build platform

Use a supported external database rather than the embedded test database so the capstone can teach real persistence boundaries. The named volumes are local lab resources; they are not presented as a backup strategy.

services:
  db:
    image: postgres:17
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: sq34-local-db-only
      POSTGRES_DB: sonarqube
    volumes:
      - sq34_db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U sonar -d sonarqube"]
      interval: 10s
      timeout: 5s
      retries: 12

  sonar:
    image: sonarqube:26.9.0.129388-community
    depends_on:
      db:
        condition: service_healthy
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonarqube
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: sq34-local-db-only
    ports:
      - "9000:9000"
    volumes:
      - sq34_data:/opt/sonarqube/data
      - sq34_logs:/opt/sonarqube/logs
      - sq34_extensions:/opt/sonarqube/extensions

volumes:
  sq34_db:
  sq34_data:
  sq34_logs:
  sq34_extensions:
mkdir -p sq34-capstone/{src,tests,evidence/{scanner,server,api,ci,backup,governance}}
cd sq34-capstone
# Save the compose file above as compose.yaml.
docker compose pull
docker compose up -d

docker compose ps | tee evidence/server/compose-ps.start.txt
docker image inspect sonarqube:26.9.0.129388-community \
  --format '{{index .RepoDigests 0}}' | tee evidence/00-sonarqube-image.txt

docker compose logs --no-color --tail=300 sonar \
  > evidence/server/sonar-startup.log
curl -fsS http://localhost:9000/api/system/status \
  | tee evidence/api/system-status.json
Expected observation

The SonarQube container reaches an operational state and /api/system/status returns a healthy/up status for this local instance. Preserve startup logs before later incident exercises.

3. Create a representative synthetic repository and test evidence

The application is intentionally small. What matters is the producer → report → scanner import chain. pytest produces test/coverage evidence before the scan; SonarQube does not run those tests for you.

# src/calc.py
def safe_divide(a: float, b: float) -> float:
    if b == 0:
        raise ValueError("b must be non-zero")
    return a / b


def clamp(value: int, lower: int, upper: int) -> int:
    if lower > upper:
        raise ValueError("lower must not exceed upper")
    return max(lower, min(value, upper))
# tests/test_calc.py
import pytest
from src.calc import safe_divide, clamp

def test_safe_divide():
    assert safe_divide(8, 2) == 4
    with pytest.raises(ValueError):
        safe_divide(1, 0)

def test_clamp():
    assert clamp(5, 0, 10) == 5
    assert clamp(-2, 0, 10) == 0
    with pytest.raises(ValueError):
        clamp(1, 10, 0)
python -m venv .venv
. .venv/bin/activate   # PowerShell: .venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest pytest-cov
python -m pytest --cov=src --cov-report=xml:coverage.xml --junitxml=junit.xml

git init
git add src tests coverage.xml junit.xml
git -c user.name='SQ34 Lab' -c user.email='sq34@example.invalid' commit -m 'sq34 capstone fixture'
git rev-parse HEAD | tee evidence/00-revision.txt
git status --porcelain=v1 | tee evidence/00-worktree-status.txt
python --version | tee evidence/00-python-version.txt
Expected observation

coverage.xml and junit.xml exist before analysis, and the repository has a stable commit SHA. Do not regenerate reports after a failed scan and then claim the same input was rerun unless you record the changed evidence.

4. Configure and run the first governed analysis

Save the following as sonar-project.properties. Repository configuration defines stable, reviewable scope; the token remains an injected secret. Parameter precedence still matters: command-line/CI overrides can change effective behavior, so the scanner log is part of the evidence packet.

sonar.projectKey=sq34:capstone
sonar.projectName=SQ34 Capstone Fixture
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
sonar.sourceEncoding=UTF-8
# Create a project-analysis token in the local UI and export it without echoing it.
export SONAR_HOST_URL=http://localhost:9000
export SONAR_TOKEN='FAKE-LOCAL-TOKEN-REPLACE-IN-YOUR-LAB'

sonar-scanner --version | tee evidence/00-scanner-version.txt
# Do not store the token in sonar-project.properties or this command line.
sonar-scanner -Dsonar.qualitygate.wait=true \
  2>&1 | tee evidence/scanner/analysis-01.log

cp .scannerwork/report-task.txt evidence/scanner/report-task-01.txt
# Preserve the revision again beside analysis evidence.
git rev-parse HEAD > evidence/scanner/revision-01.txt
Gate-wait semantics

sonar.qualitygate.wait=true asks the scanner to wait for the server-side quality-gate result. A nonzero job can therefore represent gate failure after upload rather than scanner transport failure. Preserve the task/result evidence so those states are not collapsed.

5. Correlate upload, Compute Engine, durable result and gate

Open report-task-01.txt. It records the hand-off identifiers/URLs used to correlate scanner and server. Confirm the corresponding background task is successful before interpreting measures. Then capture the gate and a small measure set.

# Extract the CE task URL without printing the token.
cat evidence/scanner/report-task-01.txt

# Example read-only project evidence. Verify endpoint names in your instance API docs.
curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
  "http://localhost:9000/api/qualitygates/project_status?projectKey=sq34%3Acapstone" \
  | tee evidence/api/gate-01.json

curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
  "http://localhost:9000/api/measures/component?component=sq34%3Acapstone&metricKeys=coverage,ncloc,bugs,vulnerabilities,code_smells" \
  | tee evidence/api/measures-01.json
State What to record Do not infer
Scanner process exit status + scanner log that CE succeeded
Upload report/task hand-off file that gate passed
CE task task ID/status/timestamps that CI/provider reported the same result
Analysis analysis identity/time + measures/issues that policy is appropriate
Gate gate name/conditions/status that the CI job succeeded unless it waited/checked

6. Enforce a gate without gaming policy

For the mandatory free path, use the current local UI to inspect the assigned quality gate. If you have permission, create a lab-only gate such as SQ34 Lab Gate, add one understandable New Code coverage condition, assign it only to sq34:capstone, and record the before/after assignment. Do not weaken a shared production gate. If your exact Community Build release or permission model prevents that mutation, keep the default gate and document a faithful decision simulation in evidence/governance/gate-simulation.md.

Why this is still enforcement

Enforcement means the delivery workflow makes a decision from the server-side gate result and preserves which policy made that decision. It does not mean every lab must mutate a global gate or force a failure.

7. Scoped API automation and developer feedback

The API exercise is intentionally read-only. Administrative endpoints require specific permissions; the narrowest token that can perform the required job is preferable to a system administrator token. Current API guidance recommends bearer authentication and exposes token-expiration metadata in responses where applicable.

# Store only metadata about the token, never its value.
cat > evidence/governance/token-record.md <<'EOF'
# SQ34 token record
Owner: local sq34 lab operator
Purpose: analyze/read sq34:capstone only
Storage: process environment (value intentionally not recorded)
Rotation/revocation: delete after lab
EOF

# Read a result with bearer auth; record response headers for expiry metadata if present.
curl -fsS -D evidence/api/gate.headers.txt \
  -H "Authorization: Bearer $SONAR_TOKEN" \
  "http://localhost:9000/api/qualitygates/project_status?projectKey=sq34%3Acapstone" \
  -o evidence/api/gate.latest.json

For developer feedback, optionally connect SonarQube for IDE to this local server in Connected Mode and record: IDE/plugin version, bound project key, rule/profile synchronization time, and one local finding that maps to the server standard. Treat this as fast feedback only; the authoritative release decision remains the server/CI evidence chain. If you do not use an IDE, create evidence/ci/ide-simulation.md that names the expected boundary and why it is not a gate substitute.

8. Exercise one bounded incident and recovery

The fault is deterministic scanner/report configuration: the test producer created coverage.xml, but the scanner is told to import a nonexistent path. Preserve the warning/failure evidence. Do not delete .scannerwork or rotate the token because neither owns the cause.

# Preserve the good configuration first.
cp sonar-project.properties evidence/scanner/sonar-project.good.properties

# Intentionally break ONLY the coverage report path on this disposable project.
cp sonar-project.properties sonar-project.broken.properties
printf '
sonar.python.coverage.reportPaths=missing-coverage.xml
' >> sonar-project.broken.properties

# Run with a one-shot command-line override instead of replacing project identity.
sonar-scanner \
  -Dproject.settings=sonar-project.broken.properties \
  2>&1 | tee evidence/scanner/incident-wrong-report.log || true

# Repair the owning layer: restore the supported report path, same project key and revision.
sonar-scanner -Dsonar.qualitygate.wait=true \
  2>&1 | tee evidence/scanner/analysis-after-repair.log
cp .scannerwork/report-task.txt evidence/scanner/report-task-after-repair.txt
cmp evidence/00-revision.txt evidence/scanner/revision-01.txt
Recovery proof

The repair restores only the report path, retains sq34:capstone, uses the same source revision and produces a new task/result whose coverage import can be compared with the broken run.

9. Capture backup and recovery evidence

A successful pg_dump proves only that a backup artifact was produced. It does not prove recoverability. Lesson 4 and the checkpoint turn this artifact into an isolated restore/reindex verification. Preserve checksum, tool/database version, time, source database identity and restore instructions.

# Database backup: use the database vendor tool against the disposable DB.
docker compose exec -T db pg_dump -U sonar -d sonarqube -Fc \
  > evidence/backup/sonarqube.dump
sha256sum evidence/backup/sonarqube.dump \
  > evidence/backup/sonarqube.dump.sha256
ls -lh evidence/backup/sonarqube.dump | tee evidence/backup/backup-size.txt

cat > evidence/backup/restore-plan.md <<'EOF'
Restore target: isolated disposable stack, never overwrite the active lab first.
Sequence: stop restore-target SonarQube -> restore supported DB backup -> start with fresh/reindexed search state -> verify project/config -> run a fresh equivalent analysis.
Search data is not treated as the durable backup.
EOF

10. Verification and cleanup

  • Revision in baseline and repaired run is identical unless a documented commit was intentionally created.
  • Scanner logs identify imported coverage path and indexed scope.
  • report-task.txt exists for successful uploads and referenced CE tasks are checked.
  • Gate result is captured separately from scanner exit and CI simulation.
  • Token value is absent from evidence; ownership/purpose/expiry or revocation is documented.
  • Backup artifact has checksum and restore plan; no search-volume copy is labeled a complete backup.
  • The evidence archive exists outside Docker volumes before optional volume deletion.
# First archive the evidence packet outside the compose volumes.
tar -czf ../sq34-evidence-guided.tgz evidence sonar-project.properties coverage.xml junit.xml

# Revoke/delete the local token in the UI, then remove only this compose project's resources.
docker compose down
# Optional final destruction of lab volumes ONLY after the evidence archive and only for this lab:
docker compose down -v
unset SONAR_TOKEN SONAR_HOST_URL

11. Small challenge

A CI run is red after the scanner uploaded successfully. The CE task is successful and the gate is green, but the provider status is missing. Which layer do you inspect next? Do not rerun the scan. Preserve the CI job artifact, provider integration/permission response and revision mapping. The missing status is downstream of the verified server result, so changing scanner scope, project key or gate conditions would destroy useful equivalence without addressing the owning layer.

12. Knowledge check

Why use an external PostgreSQL database in the capstone instead of H2?

The wrong coverage path produces a warning but the scanner still uploads. What layer owns the defect?

Why archive report-task.txt?

Does creating a database dump prove disaster recovery?

Why is Connected Mode not the release gate?

13. Summary and bridge

You assembled and exercised the minimum governed platform. Lesson 3 now turns the working lab into architecture decisions: what should stay minimal, what may be automated, when LTA or commercial capabilities are justified, and how availability, disaster recovery and exceptions are governed without obscuring evidence ownership.

Next lesson

Capstone: Operate a Governed Production SonarQube Quality Platform: Configuration, Design Patterns, and Trade-Offs

Continue to the next lesson to build on this lesson’s evidence, workflow, and operational practices.

Official references and version notes

Further reading

Verify version-sensitive behavior against primary documentation before using these patterns outside the disposable lab.

Version and compatibility note

SonarQube product names, editions, release trains, scanner runtimes, APIs, authentication options, and platform prerequisites can change independently. Re-check the linked SonarSource primary documentation for the exact target release before applying version-sensitive commands or operational guidance outside the disposable course environment.

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.