SonarQube Automation with Native Tools: Quality Gates, Webhooks, and the Web API
One setting in SonarQube decides everything else: the new-code period. Get it right and the quality gate judges this sprint, the code your team wrote in the last two weeks. Get it wrong and the gate judges the last five years, drowns every pull request in inherited debt, and trains your team to merge past red on reflex. A large share of gate-red noise traces back to that one setting, which is why every SonarQube automation effort should start there.
Part one walked the failure patterns: gates blocking releases for issues nobody triages, misconfigured new-code definitions, debt reported quarterly but never trended, gamed thresholds, hotspots rotting unreviewed. This part is the hands-on fix, using only what ships with SonarQube: quality gates, the new-code period, webhooks into CI, the Web API, portfolios and PR decoration. No third-party tooling, no plugins. Budget an afternoon for the first pass.
We'll go feature by feature, in the order you'd configure them:
01Scope
New-code period
Decides what the gate judges. Get this wrong and nothing else matters.
02Rule
Quality gate
Conditions on new code only: coverage, duplication, ratings, hotspots reviewed.
03Enforce
Webhook or wait
The pipeline stays red until the gate is green.
04Trend
Web API
Debt history, filtered issues and hotspot backlogs, per project or portfolio.
05Surface
PR decoration
The gate result lands on the pull request where the reviewer already is.
Feature 1: The quality gate, scoped to new code
SonarQube's built-in gate, Sonar way, is built around Clean as You Code: it only judges code changed within the new-code period, leaving legacy debt out of the pass/fail decision (Sonar docs). That's the right model. The common mistake is bolting conditions on overall code onto it.
Open Quality Gates → Sonar way → Copy and build your own from the copy. Keep every condition scoped to "On New Code":
| Condition (on new code) | Sensible value |
|---|---|
| Coverage | is less than 80% |
| Duplicated Lines (%) | is greater than 3% |
| Maintainability Rating | is worse than A |
| Reliability Rating | is worse than A |
| Security Rating | is worse than A |
| Security Hotspots Reviewed | is less than 100% |
The condition that matters most is the one that isn't there: no condition on overall coverage or overall issues. Add "Overall Coverage is less than 80%" to a ten-year-old codebase and every PR fails for debt its author didn't write. Judge the diff, not the history.
Two smaller settings are worth a look while you're here. Small changesets (under 20 new lines) are exempt from the coverage and duplication conditions by default, under Administration → General → Quality Gate, which stops one-line fixes failing on a single uncovered line. And assign the gate explicitly to projects (Project Settings → Quality Gate) rather than relying on the default, so a later change to the default doesn't silently move every project.
Set the new-code period correctly
This is the setting from the opening. Per project: Project Settings → New Code. Your options:
- Previous version. New code is everything since the last change of
sonar.projectVersion. Best for projects with real release tagging. - Number of days. A rolling window, such as 30. Good for trunk-based teams that don't cut versions.
- Reference branch. New code is the diff against a branch, usually
main. The right default for feature-branch workflows, because the gate then judges exactly what the branch adds.
Set a sane global default under Administration → New Code, then override per project where the release cadence differs. Check the result from the API:
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/new_code_periods/show?project=my-project" | jq '{type, value, inherited}'
If the gate is judging code from 2021, this is where it's misconfigured.
Feature 2: Fail the pipeline properly
Analysis is asynchronous. When CI runs the scanner, it uploads results and returns before the server has computed the gate. A naive pipeline reports green while the gate is still being decided, and then red, and nobody looks back.
The simplest fix is a scanner parameter. With sonar.qualitygate.wait=true, the scanner polls the server until the gate is computed and exits non-zero if it failed (analysis parameters):
sonar-scanner \
-Dsonar.projectKey=my-project \
-Dsonar.qualitygate.wait=true \
-Dsonar.qualitygate.timeout=300
If you want the gate result to drive something other than the CI job (a deploy approval, a chat message, a release dashboard), register a webhook at Administration → Configuration → Webhooks (global) or per project. SonarQube POSTs a JSON payload when the analysis finishes, including qualityGate.status of OK or ERROR (webhook docs). Set a secret on the webhook and verify the X-Sonar-Webhook-HMAC-SHA256 header on the receiving end, so nobody can post a fake green.
On scanners or CI systems where you want the polling in your own hands, use the report-task file the scanner writes:
# 1. The scanner leaves the Compute Engine task id behind
CE_TASK_ID=$(grep '^ceTaskId=' .scannerwork/report-task.txt | cut -d= -f2)
# 2. Poll until the background task leaves the queue
while :; do
TASK=$(curl -s -u "$SONAR_TOKEN:" "$SONAR_HOST/api/ce/task?id=$CE_TASK_ID")
STATE=$(echo "$TASK" | jq -r '.task.status')
case "$STATE" in PENDING|IN_PROGRESS) sleep 5 ;; *) break ;; esac
done
[ "$STATE" = "SUCCESS" ] || { echo "Analysis $STATE"; exit 1; }
# 3. Read the gate for exactly this analysis, fail the build on ERROR
ANALYSIS_ID=$(echo "$TASK" | jq -r '.task.analysisId')
STATUS=$(curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/qualitygates/project_status?analysisId=$ANALYSIS_ID" \
| jq -r '.projectStatus.status')
echo "Quality gate: $STATUS"
[ "$STATUS" = "OK" ] || { echo "Gate failed"; exit 1; }
Reading the gate by analysisId rather than projectKey matters on busy projects: two pipelines finishing close together can otherwise read each other's result. A failed or cancelled background task also exits the loop instead of polling forever.
GitHub Actions users can skip the hand-rolled loop: Sonar's quality gate action waits on the report-task file and fails the job on a red gate. Whichever route you take, the rule is the same: the pipeline must not go green until the gate is green.
Feature 3: The Web API, for trends instead of snapshots
The console shows you today. The Web API shows you the trend, and the trend is the whole point of technical debt tracking. Every call below authenticates with a user token (-u "$SONAR_TOKEN:", trailing colon so curl sends an empty password). The full endpoint list for your version lives at $SONAR_HOST/web_api.
Debt over time
api/measures/search_history returns a metric's value at every analysis. Pull remediation effort (sqale_index, in minutes), the debt ratio, and issue counts across the last hundred analyses:
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/measures/search_history?component=my-project&metrics=sqale_index,sqale_debt_ratio,bugs,vulnerabilities,code_smells&ps=100" \
| jq '.measures[] | {metric, series: [.history[] | {date, value}]}'
A rising sqale_debt_ratio week over week is debt drifting. A flat ratio while you ship features is Clean as You Code working. That ratio is the number to put on a dashboard, rather than a one-off quarterly screenshot.
Issue search with filters that separate signal from noise
Three hundred new issues isn't a to-do list. It's a pile you have to sort, and api/issues/search takes the filters that do the first cut (issues docs):
# Open bugs, blocker or critical, created in the last 14 days
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/issues/search?componentKeys=my-project&types=BUG&severities=BLOCKER,CRITICAL&statuses=OPEN,CONFIRMED,REOPENED&createdInLast=14d&ps=100" \
| jq -r '.issues[] | "\(.severity)\t\(.rule)\t\(.component)\t\(.message)"'
Filter by types=BUG first: a real reliability bug is worth a human's attention in a way a naming-convention code smell rarely is. Then narrow with severities and createdInLast to get from 300 issues to the dozen that could break production this sprint. On SonarQube 10.x running in Multi-Quality Rule mode, the same cut is expressed with impactSoftwareQualities=RELIABILITY&impactSeverities=HIGH, and issueStatuses replaces statuses. Check /web_api on your server for which parameters it accepts.
Group by rule before you read anything. When 40 of the 300 come from one rule on generated code, that's one exclusion (sonar.exclusions or an issue-exclusion pattern under Project Settings → Analysis Scope), not 40 tickets.
Hotspot review status
Security hotspots aren't issues. They live in their own endpoint and their own review workflow, and they're the most commonly ignored surface in SonarQube. List everything still TO_REVIEW:
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/hotspots/search?projectKey=my-project&status=TO_REVIEW&ps=100" \
| jq -r '.hotspots[] | "\(.vulnerabilityProbability)\t\(.securityCategory)\t\(.component)"' \
| sort
Review HIGH probability first. A project whose gate demands 100% of new hotspots reviewed, but which carries hundreds of old ones in TO_REVIEW, is passing the gate on a technicality.
Feature 4: Portfolios and applications, for debt across projects
One project's debt ratio is a data point. The debt story lives at the portfolio level, where you can see which of forty services is drifting. Portfolios are an Enterprise Edition feature, configured under Administration → Configuration → Portfolios (docs). An Application (Developer Edition and above) groups projects that ship together so a gate can judge them as a unit.
Rank a portfolio's children by debt:
curl -s -u "$SONAR_TOKEN:" \
"$SONAR_HOST/api/measures/component_tree?component=my-portfolio&metricKeys=sqale_index,reliability_rating,security_rating&s=metric&metricSort=sqale_index&asc=false&ps=100" \
| jq -r '.components[] | [.name, (.measures[] | "\(.metric)=\(.value)")] | @tsv'
On Community Build (the free edition) and Developer Edition there are no portfolios. Loop the single-project history call over your project list and aggregate the output yourself. It's more work, but the trend data is identical.
Feature 5: PR decoration, where the review already happens
Engineers respect a gate they see at review time, not one in a SonarQube tab they never open. PR decoration posts the gate result and new issues on the pull request itself. Configure it under Project Settings → General Settings → DevOps Platform Integration, bind the project to GitHub, GitLab, Azure DevOps or Bitbucket, and pass the PR parameters to the scanner (GitHub integration docs). PR analysis needs Developer Edition or above.
sonar-scanner \
-Dsonar.pullrequest.key=42 \
-Dsonar.pullrequest.branch=feature/checkout-retry \
-Dsonar.pullrequest.base=main
Most CI integrations fill these in automatically. Now the gate summary and each new issue land as a check and inline comments on the PR. Combined with a reference-branch new-code period, the reviewer sees one thing: what this change added.
Where native SonarQube stops
Everything above is worth doing. It gets you a gate teams respect, debt you can trend, and hotspots you can find. Here's what it doesn't get you.
01Gate
Pass or fail
Counts new issues against a threshold. Doesn’t say which ones matter.
02History
A line, not a plan
Shows which project is drifting. Doesn’t say whether the drift is deliberate.
03Events
No memory
Fires on a push. Doesn’t remember that last week’s 300 issues were never triaged.
The gate decides pass or fail. It doesn't decide what matters. When a PR lights up with 300 new issues, SonarQube ranks them by severity, but severity isn't priority. Deciding that these twelve are real bugs, those forty are style noise your team doesn't care about, and this cluster is a false-positive pattern from a rule that fights your framework takes human judgment, on every PR. The API hands you the list. It doesn't triage it.
Trend data isn't a plan. search_history shows which of forty services is drifting. It won't tell you whether the drift is a monolith you're deliberately strangling or a service quietly rotting. It won't size the debt sprint or argue for it against feature pressure in planning.
Nobody's watching between analyses. The gate fires on a push. Between pushes, the hotspot backlog grows, the noisy rules keep firing, and the person who was going to triage the issue list got pulled onto an incident. The native tooling is event-driven and stateless. It has no memory that last week's 300 issues were never looked at, and that standing backlog is exactly where quality theater grows back.
None of this makes the native setup worthless. It's the floor, and you need it before anything else works. The open question is who watches gate results and issue inflow continuously, and does the triage.
Where this series goes next
Part three covers that layer: CloudThinker connects to SonarQube with a read-only token, watches gate results and new-issue inflow across every project, separates real bugs from style noise and false-positive candidates, trends each project's debt to flag the ones drifting, and proposes triage lists and debt-sprint candidates for your approval.
Related reading
- Jenkins build triage with native tools
- Connect a repo, get a first PR review
- CloudThinker Connections
Want to see your own projects' findings first? Try CloudThinker free with 100 premium credits and no card required.
