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.
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.
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.
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
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
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
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.
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
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.txtexists 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 capstone must teach production persistence and supported backup/restore boundaries. H2 is a test convenience and does not model the supported production database responsibility.
The wrong coverage path produces a warning but the scanner still uploads. What layer owns the defect?
Scanner/report-input configuration. Preserve the producer report and scanner import warning, fix the path, and rerun the same revision/project key before looking at CE/database/CI layers.
Why archive report-task.txt?
It links the scanner upload to server-side background processing and allows later proof of CE status/analysis identity. Deleting it before correlation weakens the evidence chain.
Does creating a database dump prove disaster recovery?
No. It proves backup creation. Recovery requires an isolated restore, correct reindex/search behavior, project/config verification and a fresh equivalent analysis.
Why is Connected Mode not the release gate?
IDE feedback is local/developer-oriented and may precede the governed server analysis. Release enforcement belongs to server-side analysis/gate evidence consumed by CI/provider automation.
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.
Official references and version notes
Further reading
Verify version-sensitive behavior against primary documentation before using these patterns outside the disposable lab.
- SonarQube downloads — current Community Build, commercial release and LTA identities
- SonarQube Server documentation — server, administration, security and operations
- SonarQube Community Build documentation — free/local product behavior
- Web API — authentication and the ongoing Web API V2 migration
- Backup and restore — database backup/restore and reindex guidance
- Official SonarQube Docker image — current image tags and deployment notes
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.