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.
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
| 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?
The reporting client needs bounded Browse/API access to several projects. Scanner project-analysis tokens are for analysis submission, not a substitute for a governance reader identity.
Why does the report list projects even after computing an aggregate grade?
Aggregate status can hide individual failures. Drill-down rows preserve actionable project evidence.
When may the script calculate an aggregate coverage percentage?
Only when the underlying lines/conditions and uncovered counts are available for all included projects so the documented formula can be reconstructed.
Does matching the native portfolio releasability grade prove the script is a native portfolio replacement?
No. It only validates one formula for one snapshot; native portfolios also own definitions, permissions, recalculation, trends, and product-supported measures.
A project query returns 403. Should the report use an administrator token to fill the row?
No. Preserve the access error and review the intended authorization model. Escalating to admin would defeat least privilege and could leak restricted data.
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.
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).
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.