Chapter 25Lesson 02~175 minutes

Portfolios, Applications, Enterprise Reporting, and Governance Boundaries: Guided Hands-On Workflow

Build several disposable Community Build projects, preserve their analysis evidence, and generate a free local governance report that faithfully simulates selected application and portfolio ideas without claiming commercial capability.

GovernanceApplicationsPortfoliosReportingAggregation

Learning objectives

  • Create three disposable Community Build projects with independent revisions, analysis tokens, task evidence, and project measures.
  • Build an explicit lifecycle/portfolio membership manifest without pretending Community Build has native Applications or Portfolios.
  • Generate a timestamped Markdown governance report using a bounded read-only user token and documented project APIs.
  • Compute a transparent portfolio-style releasability grade and rating simulation while keeping every project row visible.
  • Aggregate coverage only from underlying counts; never average project percentages blindly.
  • Preserve drill-down links, analysis dates, restricted-object errors, and cleanup evidence.

1. Disposable workflow contract

Local/sandbox only. The mandatory path uses Community Build and creates only synthetic projects. It does not use or imitate a commercial license key, and it never labels the generated report as a native SonarQube Application, Portfolio, or Enterprise PDF.
State Lab assumption
Server Community Build 26.9.0.129388 at http://localhost:9000
Scanner SonarScanner CLI 8.1.0.6389
Projects sq-ch25-payments, sq-ch25-orders, sq-ch25-web
Producer Python 3.11+; pytest + pytest-cov for deterministic coverage evidence
Scanner credentials One disposable project-analysis token per project
Reporting credential One disposable user token owned by a lab user with Browse on only the three projects
Commercial objects Application/Portfolio/PDF paths are optional or faithfully simulated only

2. Build three tiny project fixtures

Each project lives in a separate directory and receives its own SonarQube project key. This ensures that later aggregation cannot hide scanner ownership.

mkdir -p sq-ch25-governance-lab/{payments,orders,web}
cd sq-ch25-governance-lab

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

for p in payments orders web; do
  mkdir -p "$p/src" "$p/tests" "$p/evidence"
done

cat > payments/src/app.py <<'PY'
def fee(total: float) -> float:
    return 0.02 * total if total > 100 else 0.0
PY
cat > payments/tests/test_app.py <<'PY'
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from app import fee

def test_large_payment():
    assert fee(200.0) == 4.0
PY

cat > orders/src/app.py <<'PY'
def priority(quantity: int) -> str:
    if quantity >= 100:
        return "bulk"
    if quantity >= 10:
        return "normal"
    return "small"
PY
cat > orders/tests/test_app.py <<'PY'
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from app import priority

def test_bulk():
    assert priority(120) == "bulk"
PY

cat > web/src/app.py <<'PY'
def banner(user: str | None) -> str:
    return f"Welcome {user}" if user else "Welcome"
PY
cat > web/tests/test_app.py <<'PY'
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
from app import banner

def test_anonymous():
    assert banner(None) == "Welcome"
PY

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

git add .
git commit -m "chapter25 governance baseline"
git rev-parse HEAD | tee baseline-revision.txt

3. Give each project an explicit analysis contract

for p in payments orders web; do
  key="sq-ch25-$p"
  cat > "$p/sonar-project.properties" <<EOF
sonar.projectKey=$key
sonar.projectName=SQ Chapter 25 ${p^}
sonar.sources=src
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
sonar.sourceEncoding=UTF-8
EOF
done

Create all three projects first in the disposable SonarQube UI. Then create one project-analysis token for each project and export them as SONAR_TOKEN_PAYMENTS, SONAR_TOKEN_ORDERS, and SONAR_TOKEN_WEB. Do not reuse an administrator token.

4. Generate test evidence before each scan, then preserve asynchronous results

python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip pytest pytest-cov

scan_project () {
  p="$1"; token_var="$2"
  ( cd "$p"
    python -m pytest --cov=src --cov-report=xml:coverage.xml
    git -C .. rev-parse HEAD > evidence/revision.txt
    SONAR_TOKEN="${!token_var}" sonar-scanner -X 2>&1 | tee evidence/scanner.log
    cp .scannerwork/report-task.txt evidence/report-task.txt
    sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt > evidence/ceTaskId.txt
  )
}

scan_project payments SONAR_TOKEN_PAYMENTS
scan_project orders SONAR_TOKEN_ORDERS
scan_project web SONAR_TOKEN_WEB

For each ceTaskId, query /api/ce/task?id=... until the task is terminal. Preserve task JSON. Scanner success, report upload, CE success, analysis completion, Quality Gate, and report generation remain separate states.

5. Declare governance membership explicitly

The following file is not a SonarQube commercial object. It is a free lab manifest that makes the intended governance meaning reviewable.

{
  "application_simulation": {
    "name": "Checkout",
    "lifecycle": "payments and orders ship together",
    "members": ["sq-ch25-payments", "sq-ch25-orders"]
  },
  "portfolio_simulation": {
    "name": "Digital Commerce",
    "purpose": "executive oversight",
    "members": ["sq-ch25-payments", "sq-ch25-orders", "sq-ch25-web"]
  }
}

A licensed Developer Edition path may create the Application from the first member list. A licensed Enterprise path may create the Portfolio. In both cases, preserve the exact selected branches and definition before changing anything.

6. Build a bounded governance-report client

Create a disposable local user called governance-reader, grant Browse only on the three sample projects, generate a User token, and export it as SONAR_REPORT_TOKEN. This token reads evidence; it does not execute scans.

# governance_report.py
import datetime as dt
import json
import math
import os
import urllib.error
import urllib.parse
import urllib.request

HOST = os.environ.get("SONAR_HOST_URL", "http://localhost:9000").rstrip("/")
TOKEN = os.environ["SONAR_REPORT_TOKEN"]
PROJECTS = ["sq-ch25-payments", "sq-ch25-orders", "sq-ch25-web"]

CANDIDATE_RATINGS = {
    "reliability": ["software_quality_reliability_rating", "reliability_rating"],
    "security": ["software_quality_security_rating", "security_rating"],
    "maintainability": ["software_quality_maintainability_rating", "sqale_rating"],
}
BASE_METRICS = [
    "alert_status", "ncloc", "coverage", "line_coverage",
    "lines_to_cover", "uncovered_lines", "conditions_to_cover", "uncovered_conditions",
]

def api(path, params):
    url = HOST + path + "?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, headers={"Authorization": f"Bearer {TOKEN}"})
    with urllib.request.urlopen(req, timeout=15) as r:
        return json.load(r)

def available_metrics():
    data = api("/api/metrics/search", {"ps": 500})
    return {m["key"] for m in data["metrics"]}

def pick_ratings(available):
    picked = {}
    for label, candidates in CANDIDATE_RATINGS.items():
        picked[label] = next((k for k in candidates if k in available), None)
    return picked

def project_row(key, rating_keys):
    metric_keys = BASE_METRICS + [k for k in rating_keys.values() if k]
    data = api("/api/measures/component", {"component": key, "metricKeys": ",".join(metric_keys)})
    values = {m["metric"]: m.get("value") for m in data["component"].get("measures", [])}
    analyses = api("/api/project_analyses/search", {"project": key, "ps": 1})
    latest = analyses.get("analyses", [{}])[0]
    return {
        "key": key,
        "url": f"{HOST}/dashboard?id={urllib.parse.quote(key)}",
        "analysis_date": latest.get("date", "UNKNOWN"),
        "measures": values,
    }

def grade_releasability(ok_count, total):
    ratio = 0 if total == 0 else 100 * ok_count / total
    if ratio > 80: return "A", ratio
    if ratio > 60: return "B", ratio
    if ratio > 40: return "C", ratio
    if ratio > 20: return "D", ratio
    return "E", ratio

def portfolio_rating(rows, metric_key):
    vals = []
    for row in rows:
        raw = row["measures"].get(metric_key)
        if raw is not None:
            vals.append(float(raw))
    if not vals: return None
    avg = sum(vals) / len(vals)
    rounded = max(1, min(5, math.floor(avg + 0.5)))
    return {"numeric_average": avg, "letter": "ABCDE"[rounded - 1]}

def exact_coverage(rows):
    # Rebuild Sonar's coverage numerator/denominator from raw counts.
    covered = 0.0
    total = 0.0
    for row in rows:
        m = row["measures"]
        needed = ["lines_to_cover", "uncovered_lines", "conditions_to_cover", "uncovered_conditions"]
        if any(m.get(k) is None for k in needed):
            return None
        lines = float(m["lines_to_cover"])
        ulines = float(m["uncovered_lines"])
        conditions = float(m["conditions_to_cover"])
        uconditions = float(m["uncovered_conditions"])
        covered += (lines - ulines) + (conditions - uconditions)
        total += lines + conditions
    return None if total == 0 else 100 * covered / total

def main():
    available = available_metrics()
    rating_keys = pick_ratings(available)
    rows = []
    errors = []
    for key in PROJECTS:
        try:
            rows.append(project_row(key, rating_keys))
        except urllib.error.HTTPError as exc:
            errors.append({"project": key, "status": exc.code, "reason": str(exc.reason)})

    ok_count = sum(r["measures"].get("alert_status") == "OK" for r in rows)
    grade, pass_ratio = grade_releasability(ok_count, len(rows))
    aggregate = {
        "generated_at_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
        "simulation_notice": "Community Build external report; not a native SonarQube Portfolio/Application/PDF",
        "releasability": {"letter": grade, "pass_ratio": pass_ratio, "passing": ok_count, "visible_projects": len(rows)},
        "exact_coverage_if_complete": exact_coverage(rows),
        "ratings": {label: portfolio_rating(rows, key) if key else None for label, key in rating_keys.items()},
        "projects": rows,
        "access_errors": errors,
        "rating_metric_keys": rating_keys,
    }
    with open("governance-report.json", "w", encoding="utf-8") as f:
        json.dump(aggregate, f, indent=2)

    with open("governance-report.md", "w", encoding="utf-8") as f:
        f.write("# Digital Commerce governance simulation\\n\\n")
        f.write(f"Generated UTC: {aggregate['generated_at_utc']}\\n\\n")
        f.write(f"**Boundary:** {aggregate['simulation_notice']}\\n\\n")
        f.write(f"Portfolio-style releasability: **{grade}** ({pass_ratio:.1f}% passing)\\n\\n")
        if aggregate["exact_coverage_if_complete"] is not None:
            f.write(f"Exact reconstructed coverage: **{aggregate['exact_coverage_if_complete']:.2f}%**\\n\\n")
        f.write("## Project drill-down\\n\\n")
        for row in rows:
            m = row["measures"]
            f.write(f"- [{row['key']}]({row['url']}) — gate={m.get('alert_status','UNKNOWN')}, coverage={m.get('coverage','N/A')}%, analysis={row['analysis_date']}\\n")
        if errors:
            f.write("\\n## Restricted/unavailable objects\\n")
            for err in errors:
                f.write(f"- {err['project']}: HTTP {err['status']} — details intentionally not inferred\\n")

if __name__ == "__main__":
    main()

7. Run, inspect, and compare

python governance_report.py
cat governance-report.md
python -m json.tool governance-report.json > /dev/null

Open each drill-down link and compare project-level Quality Gate, coverage, rating, and latest-analysis date against the generated report. Any mismatch is a diagnostic event; do not hand-edit the report until the data path is understood.

8. Optional licensed comparison

On an authorized Developer+ test server, create an Application containing sq-ch25-payments and sq-ch25-orders, record its definition, then compare its synthetic measures/gate with the free lifecycle summary. On Enterprise+, create a Portfolio containing all three projects and compare Sonar’s native releasability/rating outputs with the script’s explicitly implemented portfolio-style formulas.

Do not expect every displayed metric to match the external report: native objects have product-specific availability, branch selection, recalculation, permissions, trend history, and metric-support rules.

9. Challenge: choose the right evidence layer

An executive asks why the local report says Portfolio-style releasability A while one project page is red. The correct response is not “the report is wrong.” First show the defined pass-ratio formula, the exact included project list, each project’s gate, and the report timestamp. Then explain that an A aggregate does not mean every member passes.

Knowledge check

Why does the free report use a User token instead of a project-analysis token?

Why does the report list projects even after computing an aggregate grade?

When may the script calculate an aggregate coverage percentage?

Does matching the native portfolio releasability grade prove the script is a native portfolio replacement?

A project query returns 403. Should the report use an administrator token to fill the row?

Next lesson

Choose aggregation and reporting patterns deliberately

Lesson 3 turns the mechanics into durable choices for lifecycle, audience, grouping, permissions, and trend comparability.

Official references and version notes

  • SonarQube downloads and editions — current Community Build, commercial release stream, Developer/Enterprise/Data Center feature boundaries, and current LTA.
  • SonarQube Server — Applications — lifecycle-oriented synthetic aggregation, consolidated application view/gate, and recalculation model.
  • Managing applications — creation/admin permissions, project/branch membership, and background recalculation.
  • SonarQube Server — Portfolios — Enterprise boundary, releasability, rating conversion/averaging, breakdown, trend, and last-analysis context.
  • Managing portfolios — permissions, project/branch selection, applications/nested portfolios, and recalculation.
  • PDF reports — Enterprise Edition+ reports for projects/applications/portfolios, subscriptions, and permanent-branch constraints.
  • Measures and metrics — coverage numerator/denominator, ratings, Quality Gate metrics, and portfolio-visible metric boundaries.
  • Community Build Web API — bearer authentication and documented project evidence retrieval used by the free lab.
Version and compatibility note

Rechecked 2026-09-08. Mandatory examples target Community Build 26.9.0.129388 and SonarScanner CLI 8.1.0.6389. The current commercial intermediate line is SonarQube Server 2026 Release 4.1 / 2026.4.1; the current LTA is 2026.1.5 LTA. Applications start in Developer Edition. Portfolios and native PDF reporting start in Enterprise Edition. Current portfolio releasability is a Quality-Gate pass ratio with A/B/C/D/E thresholds; portfolio quality ratings use documented A=1 through E=5 conversion and averaging. Do not extrapolate those formulas to coverage, duplication, SCA, or other percentages/counts without metric-specific documentation. Aggregate objects and reports can lag component analysis because recalculation/reporting are separate state transitions; always record member branches, analysis dates, object definition, permissions, report timestamp, and mode (MQR or Standard Experience).

Governance boundary. This chapter never asks learners to bypass licensing, copy commercial binaries, expose restricted project data, lower Quality Gates, delete failing members, or edit SonarQube database/search state. Community Build exercises use project-level evidence and a clearly labeled external simulation. Any licensed Application/Portfolio/PDF exercise belongs only on an authorized disposable Server instance.

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.