Quality Gates, Conditions, Policies, and Pipeline Enforcement: Guided Hands-On Workflow
Build a disposable pass/fail quality-gate experiment, trace each Compute Engine task to the resulting gate status, and enforce that result in a CI-style scanner workflow.
Learning objectives
- Create a disposable custom gate scoped only to a lab project.
- Generate one controlled passing analysis and one controlled failing analysis.
- Capture revision, coverage input, scanner report, ceTaskId, Compute Engine status, and gate result for each run.
- Compare default asynchronous scanner behavior with sonar.qualitygate.wait=true enforcement.
- Preserve credentials and policy evidence while avoiding threshold gaming.
- Restore the lab to a documented clean state and retain an auditable evidence packet.
1. Lab contract and assumptions
This lab deliberately uses overall Python coverage as a deterministic teaching signal so the pass/fail result does not depend on whichever analyzer rules happen to change between releases. That does not make overall coverage the recommended universal production gate. The design lesson comes later: production policy normally emphasizes new code.
-
SonarQube Community Build 26.9.0.129388 at
http://localhost:9000. - SonarScanner CLI 8.1.0.6389.
-
Python 3.11+ with
pytestandpytest-cov. - Disposable project key:
academy-sq-ch11. -
Disposable custom gate:
Academy Ch11 Coverage Gate. - No branch-analysis, commercial, cloud, provider, plugin, or enterprise identity feature is required.
SONAR_TOKEN.
2. Inspect before creating anything
git --version
python --version
sonar-scanner --version
curl -fsS http://localhost:9000/api/server/version
# Confirm the project key and gate name are lab-only before proceeding.
export PROJECT_KEY="academy-sq-ch11"
export SONAR_HOST_URL="http://localhost:9000"
Also inspect Quality Gates in the UI and record the existing default gate. The lab must not change the instance default. If the names already exist, either reuse only if they are your prior disposable lab resources or choose a new lab suffix.
3. Build a tiny source tree with deterministic coverage
mkdir -p academy-sq-ch11/src academy-sq-ch11/tests academy-sq-ch11/evidence
cd academy-sq-ch11
git init
git config user.name "Academy Learner"
git config user.email "academy@example.invalid"
cat > .gitignore <<'EOF'
.venv/
.scannerwork/
.coverage
coverage.xml
evidence/
__pycache__/
.pytest_cache/
EOF
python -m venv .venv
# Linux/macOS:
. .venv/bin/activate
# Windows PowerShell equivalent: .\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install pytest pytest-cov
cat > src/calc.py <<'PY'
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("division by zero")
return a / b
PY
cat > tests/test_calc.py <<'PY'
import pytest
from src.calc import add, divide
def test_add():
assert add(2, 3) == 5
def test_divide():
assert divide(8, 2) == 4
def test_zero():
with pytest.raises(ValueError):
divide(1, 0)
PY
cat > sonar-project.properties <<'EOF'
sonar.projectKey=academy-sq-ch11
sonar.projectName=Academy SonarQube Chapter 11
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
EOF
git add .gitignore src tests sonar-project.properties
git commit -m "ch11 pass fixture"
git rev-parse HEAD > evidence/pass-revision.txt
pytest --cov=src --cov-report=term-missing --cov-report=xml
cp coverage.xml evidence/pass-coverage.xml
Expected result: the tiny fixture should report essentially complete coverage. Preserve the exact report instead of writing “100%” from memory.
4. Create one disposable project, token, and custom gate
Use a local lab administrator only for the setup steps that require administration:
-
Create the disposable local project
academy-sq-ch11if it does not already exist. -
Create a project analysis token scoped to that
project and place its value only in your local secret/environment
store as
SONAR_TOKEN. -
With an account that has
Administer Quality Gates, create
Academy Ch11 Coverage Gate. Current Community Build initializes a new custom gate with Sonar way conditions. - For this lab only, remove the copied conditions so the experiment has one causal variable.
- Add one overall code condition: gate fails when Coverage is less than 80.0%.
-
Assign this gate explicitly to
academy-sq-ch11; do not set it as the instance default. - Save a screenshot or read-only API response showing gate name, condition, threshold, and project assignment.
Why one condition? A multi-condition gate can fail for several reasons at once. The lab is a controlled experiment: one metric, one operator, one threshold.
5. Run A — prove a passing gate
export SONAR_TOKEN="<project-analysis-token-from-your-secret-store>"
sonar-scanner \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.token="$SONAR_TOKEN" 2>&1 | tee evidence/pass-scanner.log
cp .scannerwork/report-task.txt evidence/pass-report-task.txt
cat evidence/pass-report-task.txt
Extract the ceTaskId from report-task.txt.
Do not query the gate immediately and assume the first response is
current; wait until the corresponding Compute Engine task is
complete.
CE_TASK_ID=$(awk -F= '$1=="ceTaskId"{print $2}' evidence/pass-report-task.txt)
curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
"$SONAR_HOST_URL/api/ce/task?id=$CE_TASK_ID" \
| tee evidence/pass-ce.json
curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" --get \
--data-urlencode "projectKey=$PROJECT_KEY" \
"$SONAR_HOST_URL/api/qualitygates/project_status" \
| tee evidence/pass-gate.json
Verify, rather than assume: CE task SUCCESS; coverage
at or above 80%; gate OK. If the task is still
pending/in progress, poll conservatively or use the UI
background-task view. Preserve the task ID in every artifact name or
manifest.
6. Run B — create a real failing revision
Add enough untested executable code to reduce overall coverage below the threshold. This is a source change, not an omitted-report trick.
cat >> src/calc.py <<'PY'
def classify(value):
if value < -100:
return "extreme-negative"
if value < 0:
return "negative"
if value == 0:
return "zero"
if value < 10:
return "small"
if value < 100:
return "medium"
if value < 1000:
return "large"
return "extreme-positive"
PY
git add src/calc.py
git commit -m "ch11 add deliberately untested branchy code"
git rev-parse HEAD > evidence/fail-revision.txt
pytest --cov=src --cov-report=term-missing --cov-report=xml
cp coverage.xml evidence/fail-coverage.xml
Before scanning, predict: the Git SHA changes, source line count
changes, coverage falls, the next ceTaskId changes, the
assigned gate definition does not change, and the
expected gate result becomes ERROR.
7. First run the failing revision without gate wait
set +e
sonar-scanner \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.token="$SONAR_TOKEN" 2>&1 | tee evidence/fail-async-scanner.log
SCANNER_RC=$?
set -e
printf 'scanner_rc=%s\n' "$SCANNER_RC" | tee evidence/fail-async-exit.txt
cp .scannerwork/report-task.txt evidence/fail-async-report-task.txt
With the default sonar.qualitygate.wait=false, the
scanner is allowed to finish after upload. A successful scanner
return here means report submission succeeded, not that the
gate passed. Follow the new CE task and query the gate after it
completes. Expected evidence: scanner/upload success, CE
SUCCESS, gate ERROR because coverage is
below 80%.
8. Enforce the same failing result in a CI-style scanner step
set +e
sonar-scanner \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.token="$SONAR_TOKEN" \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300 \
2>&1 | tee evidence/fail-wait-scanner.log
WAIT_RC=$?
set -e
printf 'quality_gate_wait_rc=%s\n' "$WAIT_RC" | tee evidence/fail-wait-exit.txt
Current documentation defines
sonar.qualitygate.wait=true as an opt-in that polls for
the gate and fails the pipeline when the gate fails. Do not
hard-code one numeric exit code into governance; the important
contract is that the analysis step no longer reports success to the
pipeline when the computed gate is failed.
9. Verify, restore, and clean up safely
-
Preserve the gate definition, pass/fail revision SHAs, coverage
XML files, scanner logs, both
report-task.txtfiles, CE JSON, gate JSON, and wait-mode exit evidence. - Do not delete the gate or project until the packet is complete.
- Revoke the project-analysis token and record only token name/scope/revocation time—not the token value.
- If you created the lab project and gate solely for this exercise, delete only those exact named resources after verification. Never run bulk project deletion or broad volume cleanup.
- Keep the evidence directory outside any resource you are about to delete.
Knowledge check
Why does the lab use a single coverage condition?
To isolate causality: one metric and threshold determine the result, so a pass/fail delta can be explained without several simultaneous gate variables.
What does a successful scanner return without gate wait prove?
That scanner-side analysis/report upload completed successfully; it does not prove the server gate passed.
Why preserve ceTaskId for each run?
It correlates a specific upload/revision to its asynchronous Compute Engine result instead of relying on whichever project status happens to be visible later.
What policy state must stay unchanged between the pass and fail runs?
The custom gate name, condition/operator/threshold, and project assignment. Only the intended source/coverage state should change.
When is
sonar.qualitygate.wait=true useful?
When a CI system needs the scanner step itself to wait for and enforce the server-side gate rather than relying on a provider-native asynchronous quality check.
Official references and version notes
- Understanding quality gates — conditions, Sonar way, new/overall code, and the small-change fudge factor.
- Managing custom quality gates — creation, condition changes, permissions, and review/update workflow.
- Changing a project quality gate and fudge factor — project assignment and small-change configuration.
- Quality standards and new code — new-code definitions and Clean-as-You-Code context.
-
Scanner-only analysis parameters
—
sonar.qualitygate.wait, timeout, andreport-task.txt. - CI integration overview — supported quality-gate enforcement patterns.
- Analysis overview — asynchronous server-side processing and quality-gate computation.
- Feature comparison — Community Build versus Server/Cloud branch and pull-request boundaries.
- Web API — authenticated API usage and Web API V2 migration guidance.
- SonarQube releases — current Community Build and Server/LTA release identities.
- SonarScanner CLI 8.1.0.6389 — scanner baseline used by the local lab.
Rechecked 2026-09-07. Mandatory executable examples target SonarQube Community Build 26.9.0.129388 and SonarScanner CLI 8.1.0.6389. Current commercial reference points are SonarQube Server 2026 Release 4.1 and 2026 Release 1.5 LTA; no commercial feature is required. Sonar way, supported gate metrics, pull-request behavior, CI integrations, Web API surfaces, and defaults can evolve, so re-check the linked primary documentation before applying these examples to another release.
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.