Chapter 11Lesson 02~125 minutes

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.

Pass/fail labCoveragereport-task.txtGate waitEvidence

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.

Dated lab baseline
  • SonarQube Community Build 26.9.0.129388 at http://localhost:9000.
  • SonarScanner CLI 8.1.0.6389.
  • Python 3.11+ with pytest and pytest-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.
Preflight: use a local lab administrator only to create the disposable gate/project and then use a project-scoped analysis token for scans. Never use a production administrator token as 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:

  1. Create the disposable local project academy-sq-ch11 if it does not already exist.
  2. Create a project analysis token scoped to that project and place its value only in your local secret/environment store as SONAR_TOKEN.
  3. 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.
  4. For this lab only, remove the copied conditions so the experiment has one causal variable.
  5. Add one overall code condition: gate fails when Coverage is less than 80.0%.
  6. Assign this gate explicitly to academy-sq-ch11; do not set it as the instance default.
  7. 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

  1. Preserve the gate definition, pass/fail revision SHAs, coverage XML files, scanner logs, both report-task.txt files, CE JSON, gate JSON, and wait-mode exit evidence.
  2. Do not delete the gate or project until the packet is complete.
  3. Revoke the project-analysis token and record only token name/scope/revocation time—not the token value.
  4. 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.
  5. Keep the evidence directory outside any resource you are about to delete.
Challenge: a teammate wants to lower the coverage threshold from 80 to 50 because Run B failed. Identify the layer that is failing, the evidence that proves the threshold was pre-declared, and the safer production response. The answer should distinguish a legitimate policy review from post-result threshold gaming.

Knowledge check

Why does the lab use a single coverage condition?

What does a successful scanner return without gate wait prove?

Why preserve ceTaskId for each run?

What policy state must stay unchanged between the pass and fail runs?

When is sonar.qualitygate.wait=true useful?

Next lesson

Choose policy patterns instead of copying the lab gate

Lesson 3 separates Sonar way from custom gates, new-code from overall conditions, advisory from enforced workflows, small-change behavior, pull-request boundaries, and centralized standards from explicit exceptions.

Official references and version notes

Version and compatibility note

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.

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