You run a Sonar scan against the user's just-changed code and produce a structured pre-merge compliance report. The artifact is a markdown review the user acts on — not raw scanner output, not a quiet "scan complete".
The API endpoints are identical between the two — only the host differs. If the user is jumping in cold and the repo isn't wired up, run sonar-onboard first.
-
Detect the mode. First thing — read sonar-project.properties and check for a sonar.organization= line:
- Present ⇒ SonarCloud. Host is
https://sonarcloud.io (don't read $SONAR_HOST_URL).
- Absent ⇒ SonarQube server. Host comes from
$SONAR_HOST_URL.
Use the detected mode for every subsequent step. Don't ask the user — the properties file is the source of truth.
-
Verify the environment before scanning. Checks differ by mode:
| Check | Applies to | If missing |
|---|
sonar-project.properties at repo root | both | Stop. Tell the user to run sonar-onboard first — there's no project config to scan against. |
sonar-scanner on PATH | both | Stop. Print the install hint (see step 3). Do not attempt to install it. |
SONAR_TOKEN env var set | both | Stop. Tell the user to export it from their Sonar account → Security → Tokens. |
SONAR_HOST_URL env var set | server only | Stop. Tell the user to export it (e.g. export SONAR_HOST_URL=https://sonar.company.com). |
These are non-negotiable. Don't fabricate findings, don't run a partial scan, don't fall back to a local linter — the artifact this skill produces is a Sonar report or it's nothing.
-
Install hint if sonar-scanner is missing — print verbatim, don't drive the install:
sonar-scanner CLI not found on PATH. Install it:
macOS: brew install sonar-scanner
Linux: https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner/
Docker: docker run --rm -v "$(pwd):/usr/src" sonarsource/sonar-scanner-cli
Then re-run this skill.
-
Identify the change under review. Same precedence as devils-advocate:
- User has shown you a diff or named files — that's the scope.
- Git working tree has unstaged + uncommitted changes —
git diff is the scope.
- User says "the feature I just built" — run
git diff origin/main...HEAD (or HEAD~1 if no upstream).
The scope is the set of changed files, not the whole repo. The scan itself runs against the whole project (Sonar needs the full graph for accurate cross-file analysis), but findings you report are filtered to changed files only — anything else is out-of-scope noise.
-
Set the host variable for the run. Same logic, both modes:
if grep -q '^sonar.organization=' sonar-project.properties; then
SONAR_HOST="https://sonarcloud.io"
else
SONAR_HOST="$SONAR_HOST_URL"
fi
Use $SONAR_HOST for both the scanner CLI and the API calls below.
-
Run the scan. From the repo root:
sonar-scanner \
-Dsonar.host.url="$SONAR_HOST" \
-Dsonar.token="$SONAR_TOKEN" \
-Dsonar.branch.name="$(git rev-parse --abbrev-ref HEAD)"
Note on sonar.branch.name: SonarCloud accepts it on all tiers; SonarQube Community Edition does not. If you see License not valid for branch analysis, drop the flag and re-run — the scan will overwrite the default branch's history (acceptable for a local pre-merge check, not for CI).
If the project uses Maven, Gradle, or dotnet-sonarscanner, prefer those wrappers (they hook into the compile step) — check sonar-project.properties and the build files to decide. See reference.md.
The scan typically takes 30s–3min. Wait for it. Do not summarize before results land — a "should pass" guess is worse than waiting.
-
Fetch the gate status and issues from the Sonar API. After the scan finishes, the scanner writes .scannerwork/report-task.txt with a ceTaskUrl — poll it until status=SUCCESS, then fetch (same endpoints for server and cloud — only $SONAR_HOST differs):
PROJECT_KEY=$(grep '^sonar.projectKey=' sonar-project.properties | cut -d= -f2)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/qualitygates/project_status?projectKey=$PROJECT_KEY&branch=$BRANCH"
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/issues/search?componentKeys=$PROJECT_KEY&branch=$BRANCH&inNewCodePeriod=true&ps=500"
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/hotspots/search?projectKey=$PROJECT_KEY&branch=$BRANCH&inNewCodePeriod=true"
Filter the issues list to changed files (compare each issue's component field against the diff). See reference.md for the exact JSON shapes.
-
Map Sonar severities to the report's severity markers. Keep the mapping consistent across reports:
| Sonar severity | Marker | Meaning in the report |
|---|
BLOCKER | 🟥 Blocker | Will ship a bug or security issue. Must fix before merge. |
CRITICAL | 🟧 Critical | High-impact bug, hotspot, or vulnerability that needs review before merge. |
MAJOR | 🟨 Major | Code smell or quality issue that will hurt within months. Fix before merge if reasonable. |
MINOR | ⚪ Minor | Quality nit. Fine to defer. |
INFO | ⚪ Info | Optional. List only if there are < 5 of them; otherwise summarize the count. |
Do not invent severity. Sonar's catalog assigns it — copy it. The mapping is mechanical, not judgmental.
-
Sweep the five lenses. Sonar categorizes every finding into one of these — group the report by lens, same as devils-advocate. Each lens file describes what to look for and how to phrase the fix:
-
For every finding, write four parts (same shape as devils-advocate):
If the gate passed, lead with that and still report findings (a passing gate doesn't mean zero issues — it means the new code met the conditions; minor findings may still be worth fixing).