SonarQube/SonarCloud provider - two pure verbs (fetch_findings files new-code issues to the ledger, post_responses transmits triaged dismissals) via the REST client
Resources
2Install
npx skillscat add cuioss/plan-marshall/workflow-integration-sonar Install via the SkillsCat registry.
Sonar Workflow Skill
Sonar provider for the findings-pipeline sonar-issue producer. The provider surface is exactly TWO pure, zero-LLM verbs — no triage judgment lives here:
fetch_findings— fetch gate-blocking new-code issues from SonarQube/SonarCloud, apply the pre-filter (sonar-rules.json), and file onesonar-issuefinding per surviving issue viamanage-findings add. The untrusted Sonarmessageis quarantined underraw_input.{message}(never embedded raw in the top-leveldetail); the batchedmanage-findings ingestpass promotes it aftervalidate_struct.post_responses— apply already-decided triage dispositions back to Sonar (ado_transitiondismissal:wontfixforsuppressed,falsepositiveforrejected), keyed by each finding's ownhash_id.
Both verbs FAIL LOUD when Sonar is not configured (a typed unconfigured status). Consumer dispatch lives in `phase-6-finalize/workflow/sonar-roundtrip.md`.
Architectural context: This SKILL.md owns the producer-side CLI surface. For the producer→store→consumer→gate flow that connects this producer to the unified store, the per-domain
ext-triageconsumer dispatch, and the invariant gate, see `ref-workflow-architecture/standards/findings-pipeline.md`.
Enforcement
Execution mode: Two pure provider verbs — fetch_findings files new-code Sonar issues to the ledger (untrusted message quarantined under raw_input); post_responses transmits already-decided triage dismissals back to Sonar. Triage judgment lives in the consolidated triage pass, NOT in this provider.
Prohibited actions:
- Never make a triage decision inside the provider verbs — they only fetch and transmit already-decided dispositions
- Never read a finding's
raw_input.*from a triage/response surface — read the top-level fields promoted bymanage-findings ingest - Never suppress Sonar issues without documented justification
- Never modify Sonar configuration or quality profiles
- Never skip build verification after implementing fixes
Constraints:
- Suppressions require inline comments explaining the rationale
- Fix-vs-suppress decisions must be logged
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
project |
string | yes | — | SonarQube project key |
pr |
string | no | auto-detect | Pull request ID |
severities |
string | no | all | Filter by severity (comma-separated: BLOCKER,CRITICAL,MAJOR,MINOR,INFO) |
types |
string | no | all | Filter by type (comma-separated: BUG,CODE_SMELL,VULNERABILITY) |
Prerequisites
- Credentials configured via
manage-providersforworkflow-integration-sonar - Script imports
triage_helpersfromref-toon-formatat runtime (seeref-workflow-architecture→ "Shared Infrastructure")
Architecture
workflow-integration-sonar (Sonar issue workflow)
├─> sonar_rest.py (issue fetching, status changes via REST API)
├─> _credentials_core.get_authenticated_client() (credential loading)
└─> triage_helpers (ref-toon-format) — shared triage, error handlingUsage Examples
# FIND: fetch + pre-filter + file one sonar-issue finding per surviving issue (message quarantined under raw_input)
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar fetch_findings \
--plan-id EXAMPLE-PLAN --project com.example:project --pr 123 --severities BLOCKER,CRITICAL
# RESPOND: apply already-decided dismissals (wontfix/falsepositive) back to Sonar, keyed by hash_id
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar post_responses \
--plan-id EXAMPLE-PLAN --project com.example:project
# LLM consumer reads stored findings via manage-findings
python3 .plan/execute-script.py plan-marshall:manage-findings:manage-findings list --plan-id EXAMPLE-PLAN --type sonar-issueWorkflows
Workflow 1: Fetch & Store Issues (Producer-Side)
Purpose: Stage gate-blocking Sonar issues into the per-type finding store, then let the LLM consumer drive fix-vs-suppress decisions from the stored findings.
Producer-side flow: sonar.py fetch_findings is the only callable surface and the single authority on PR-scoped new-code issue enumeration. Before enumerating it performs a synchronous bounded in-Python CE-readiness wait — it polls the Compute-Engine analysis-task state (via /api/ce/component, PR-scoped when --pr is supplied) until the task has settled (SUCCESS/FAILED/CANCELED with an empty queue) or the wait budget expires. The wait reuses the ci_base.poll_until(...) bounded-polling framework (the same framework that replaced blocking shell sleeps for checks wait); it is NOT a shell polling loop. The budget resolves from the plan-local execution-manifest step-params snapshot for default:sonar-roundtrip — the prefix-stripped ce_wait_timeout_seconds param (default 600, the direct sibling of checks_wait_timeout_seconds), read in a single one-stop call alongside the step's touched_file_cleanup and do_transition params via manage-execution-manifest step-params get --phase 6-finalize --step-id default:sonar-roundtrip (the plan-local runtime source) — overridable by an explicit --ce-wait-timeout flag. After CE settles it fetches the PR-scoped new-code issues (pullRequest + inNewCodePeriod=true + unresolved), applies the sonar-rules.json pre-filter (drops issues already documented as suppressable via NOSONAR / test-acceptable rules), and writes one sonar-issue finding per surviving issue via manage-findings add. Severity is derived from the Sonar severity (BLOCKER/CRITICAL/MAJOR → error, MINOR → warning, INFO → info), the rule key is captured in the finding's rule field, and the project key is captured in module.
Verified count + undecidable discriminator: the returned contract carries a verified new_code_issue_count plus a count_status discriminator (confirmed | undecidable). On a confirmed CE-settled run the count is the real PR-scoped new-code total and a reported 0 is a confirmed PR-scoped zero. When the CE wait times out (analysis still processing) OR a REST/auth failure blocks confirmation, the contract carries new_code_issue_count: null, count_status: undecidable, and a count_status_reason — never a false 0.
Scan-summary marker: every fetch also writes one attestation row to artifacts/findings/sonar-scan-summary.jsonl — written unconditionally, including when new_code_issue_count == 0 and on undecidable — so an absent file can never be confused with "not checked." The marker lives in the same archive-surviving findings directory as pr-comment.jsonl (resolved via the shared _findings_core.get_findings_dir) and is read by `phase-6-finalize/workflow/sonar-roundtrip.md` at its success gate. The full row schema is documented in the Scan-Summary Marker section below.
Steps:
Fetch & Store:
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar fetch_findings \ --plan-id {plan_id} --project {project_key} [--pr {pr_number}] [--severities BLOCKER,CRITICAL] [--types BUG,VULNERABILITY] [--ce-wait-timeout {secs}]Output reports the verified
new_code_issue_countandcount_status(confirmed|undecidable, withcount_status_reasononundecidable), the pre-filter counterscount_fetched/count_skipped_suppressable/count_stored, thescan_summary_pathof the written attestation row, andproducer_mismatch_hash_id(set when count_stored ≠ count_fetched − count_skipped_suppressable; the mismatch is also persisted as a Q-Gate finding under phase5-executewith title prefix(producer-mismatch)). When that mismatch finding's own persist is REJECTED,producer_mismatch_hash_idstaysnulland the output carriesqgate_persist_failed: trueplusqgate_persist_failure{title, detail, message}— the mismatch content that never reached the store, with the primitive's rejection message. Both fields are absent when the mismatch finding landed (or when there was no mismatch). Readqgate_persist_failed, not anullhash id, to tell a lost mismatch finding from no mismatch at all;statusstayssuccessbecause the fetch itself succeeded.Query Stored Findings:
python3 .plan/execute-script.py plan-marshall:manage-findings:manage-findings list --plan-id {plan_id} --type sonar-issueIngest, then process — the untrusted Sonar
messagewas quarantined underraw_input.{message}at file time. Run the single batchedmanage-findings ingest --plan-id {plan_id}pass, which validates and promotes it to the top level; the consolidated triage pass then decides fix-vs-suppress from the clean top-level fields (neverraw_input.*). After acting on each finding, callmanage-findings resolve --hash-id {hash} --resolution fixed|suppressed|accepted --detail "{rationale}"; the rationale becomes theresolution_detailthatsonar post_responsestransmits as a Sonar dismissal.
Raw REST search (ad-hoc)
For ad-hoc inspection or non-finding-store integrations, sonar_rest.py search is the raw REST surface (see Canonical invocations → sonar_rest — search). It outputs structured TOON directly. Producer-side flows MUST use sonar.py fetch_findings.
Workflow 2: Triage & Respond (Consumer-Side)
Purpose: Drive the stored sonar-issue findings through the consolidated triage decision core, then transmit the terminal dismissals back to Sonar — the INGEST → TRIAGE → RESPOND tail of the FIND → INGEST → TRIAGE → RESPOND flow whose FIND step is Workflow 1.
Input: sonar-issue findings already populated in the per-type store via Workflow 1.
This provider makes NO triage decision. The fix-vs-suppress-vs-reject decision core is owned by the consolidated triage pass — see `../plan-marshall/workflow/triage.md` (the per-finding FIX / SUPPRESS / ACCEPT / REJECT core, with smart grouping and the escalation guards) and `../plan-marshall/workflow/verification-feedback.md` (the finalize-phase dispatch that drives that core over the finding store). This workflow only stages the findings for that core and transmits the dispositions it already recorded; there is no script-side classification call here.
Steps:
Query Findings:
python3 .plan/execute-script.py plan-marshall:manage-findings:manage-findings list --plan-id {plan_id} --type sonar-issueIngest (untrusted message containment)
Run the single batched ingest pass — the same containment boundary described in Workflow 1 step 3 above: it validates and promotes each quarantined
raw_input.{field}to the clean top-level fields, and triage then reads those top-level fields only, neverraw_input.*.python3 .plan/execute-script.py plan-marshall:manage-findings:manage-findings ingest --plan-id {plan_id}Triage (decision core — not owned here)
The consolidated triage pass reads the clean top-level fields promoted by the ingest pass (never the raw un-ingested
raw_input.*) and records one terminalresolutionper finding viamanage-findings resolve. The decision logic lives in triage.md / verification-feedback.md; this provider contributes no classification. Each finding ends at one of:fixed(cleared in code),suppressed(dismiss on Sonar aswontfix),rejected(dismiss on Sonar asfalsepositive), oraccepted/taken_into_account(no Sonar action). Record the rationale asresolution_detail— it is the text the RESPOND step transmits:python3 .plan/execute-script.py plan-marshall:manage-findings:manage-findings resolve \ --plan-id {plan_id} --hash-id {hash} --resolution fixed|suppressed|rejected|accepted --detail "{rationale}"Apply code changes for
fixedfindingsFor a finding resolved
fixed, read the file at the issue location and apply the fix with the Edit tool. Asuppressed/rejectedfinding is dismissed on Sonar in step 5 (not annotated in code);accepted/taken_into_accountneeds no action.RESPOND — transmit dismissals back to Sonar (keyed by
hash_id):python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar post_responses \ --plan-id {plan_id} --project {project_key}post_responsesmaps each terminal disposition to its Sonardo_transition(suppressed→wontfix,rejected→falsepositive);fixed/accepted/taken_into_accountget no Sonar action. It is idempotent — a finding whose dismissal was already transmitted carries arespondedmarker and is skipped on a re-run, so re-invoking the verb never re-POSTs the same dismissal. This replaces the retired per-findingsonar_rest transitioncall; the raw REST transition surface remains available only for ad-hoc inspection (see Canonical invocations →sonar_rest).
Gate Diagnosis
When diagnosing a Sonar quality-gate failure, MUST read the verdict through the authoritative REST verbs on sonar_rest.py — never the sonarqube MCP convenience tool, which returns stale data during incidents:
gate-status— the authoritative quality-gate verdict fromGET /api/qualitygates/project_status: the overall gate status plus one entry per condition (metricKey,comparator,errorThreshold,actualValue, per-conditionstatus). This is the exact verdict the Maven Sonar plugin gates on.ce-status— the Compute-Engine analysis-task status fromGET /api/ce/activity(+GET /api/ce/component). Use it to distinguish an infra processing failure (errorType/errorMessageon a task) from a real gate failure.hotspots— security hotspots fromGET /api/hotspots/search. Hotspots drivenew_security_hotspots_reviewedand are NOT returned by thesearchissues verb, so a hotspots-only gate failure is invisible without this verb.
Authoritative-verb rule: a CI-red / tool-green disagreement means trusting CI. The CI build runs the same GET /api/qualitygates/project_status verdict that gate-status reports; if a convenience tool reports green while CI reports red, the convenience tool is stale — trust the CI result and re-read through gate-status / ce-status.
All three verbs are read-only (single GET, no transition behavior) and accept --project plus an optional --branch or --pr. See Canonical invocations below for the exact surface.
Scripts
Script: plan-marshall:workflow-integration-sonar:sonar → sonar.py (producer-side fetch + pre-filter + finding store)
Script: plan-marshall:workflow-integration-sonar:sonar_rest → sonar_rest.py (raw REST API client: issue search / transition / metrics + the gate-status / ce-status / hotspots gate-diagnosis verbs)
sonar.py fetch_findings
Purpose: Producer-side flow and single authority on PR-scoped new-code issue enumeration — perform a synchronous bounded CE-readiness wait, fetch the PR-scoped new-code issues via the REST client, apply the pre-filter, persist one sonar-issue finding per surviving issue, and write the verified-scan attestation marker.
Usage:
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar fetch_findings \
--plan-id {plan_id} --project {project_key} [--pr {pr}] [--severities ...] [--types ...] [--ce-wait-timeout {secs}]Output: TOON with the verified new_code_issue_count and count_status discriminator (confirmed | undecidable, plus count_status_reason on undecidable), the pre-filter counters (count_fetched, count_skipped_suppressable, count_stored), the list of stored finding hash_ids, the scan_summary_path of the written attestation row, and producer_mismatch_hash_id when applicable. A confirmed 0 is a confirmed PR-scoped zero; a CE-timeout or auth/REST failure yields count_status: undecidable with new_code_issue_count: null — never an inferred 0.
Scan-Summary Marker (sonar-scan-summary.jsonl)
Every fetch_findings run appends one attestation row to artifacts/findings/sonar-scan-summary.jsonl (resolved via the shared _findings_core.get_findings_dir, so it lives in and survives manage-status archive exactly like pr-comment.jsonl). The row is written unconditionally — including when new_code_issue_count == 0 and when count_status == undecidable — so a verified zero is a positive on-disk fact and an absent file unambiguously means "not checked." This is a distinct artifact kind from sonar-issue.jsonl (a producer-written attestation file, not a finding store managed by the manage-findings add/resolve verbs), and is read by `phase-6-finalize/workflow/sonar-roundtrip.md` at its success gate (which requires count_status == confirmed).
Row fields (written by sonar.py:_write_scan_summary):
| Field | Type | Always present | Description |
|---|---|---|---|
count_status |
string | yes | confirmed (CE settled in budget) or undecidable (CE timeout or REST/auth failure) |
new_code_issue_count |
int | null | yes | Verified PR-scoped new-code total on confirmed; null on undecidable — never a false 0 |
count_status_reason |
string | no | Human-readable reason; emitted only on undecidable (omitted entirely on confirmed) |
pr |
string | null | yes | PR number the fetch was scoped to (null for a non-PR / branch fetch) |
project |
string | yes | Sonar project key the fetch enumerated |
scanned_sha |
string | yes | The worktree HEAD SHA the scan attests to; the empty string "" when the SHA cannot be resolved (not a git tree / git unavailable) — the row is still written |
ts |
string | yes | ISO-8601 UTC timestamp of the fetch |
Write-even-at-count==0 guarantee: the row is appended on every fetch_findings, so a confirmed new_code_issue_count: 0 is a positive on-disk attestation of a verified zero. Survives-archive guarantee: the file resolves through _findings_core.get_findings_dir, so it lives in and survives manage-status archive exactly like pr-comment.jsonl. Distinct artifact kind from sonar-issue.jsonl: this is a producer-written attestation file (append-only, not managed by the manage-findings add/resolve verbs). See `manage-findings/standards/jsonl-format.md` § "Producer-Written Attestation Files" for the artifacts/findings/ inventory entry.
Issue Classification
standards/sonar-rules.json is a pre-filter only for the producer-side fetch_findings flow. Suppressable rules (rules already documented as suppressable, test-acceptable rules) are dropped before findings are written; severity/type boost mappings derive the finding severity field. Final fix-vs-suppress classification of stored findings belongs to the consolidated triage pass reading the validated top-level fields (the message promoted from raw_input.{message} by the manage-findings ingest pass) — never the raw un-ingested raw_input.*.
Key principles:
- Always fix: VULNERABILITY, SECURITY_HOTSPOT, and BLOCKER severity (enforced by script)
- Fix preferred: CRITICAL severity, BUG type, resource leaks
- May suppress: Rules listed in
suppressable_rules(with documented justification) - Test exceptions: Rules in
test_acceptable_rulesare acceptable in test files
Supported languages: Java, JavaScript, TypeScript, Python. Unrecognized rules fall back to the Sonar issue message for triage guidance.
For triage override guidance, see ref-workflow-architecture → "Triage Override Guidance".
Suppression Format
Generated by sonar.py:get_suppression_string() based on file extension and rule prefix:
Java: // NOSONAR java:S1234 - reason for suppression
JavaScript/TypeScript: // NOSONAR javascript:S1234 - reason for suppression
Python: # NOSONAR python:S1234 - reason for suppression
Error Handling
| Failure | Action |
|---|---|
| REST API failure | Report error with HTTP status. Verify credentials configured and project key is correct. |
| REST API returns empty | No issues found — report success with zero counts. |
| triage failure (invalid JSON) | Log warning, skip the issue, continue processing remaining. |
| Fix implementation failure | Report which file/line failed. Do not suppress as fallback — ask the caller. |
| REST status change failure | Log warning, continue — marking resolved is best-effort. |
| Build verification failure after fixes | Report failing tests/compilation. Do not commit broken fixes. |
Standards (Load On-Demand)
| Standard | When to Load |
|---|---|
standards/sonar-rules.json |
Adding/updating classification rules, always-fix types, suppressable rules, fix suggestions, or suppression_syntax templates |
Canonical invocations
The canonical argparse surface for the two entry-point scripts this skill registers: sonar.py (sonar notation) and sonar_rest.py (sonar_rest notation). The plugin-doctor analyzer (_analyze_manage_invocation.py) reads this section as source-of-truth for the manage-invocation-invalid and missing-canonical-block rules. Consuming docs xref this section by name instead of restating the command inline. See `pm-plugin-development:plugin-script-architecture` cross-skill-integration.md § "Script invocation in documentation".
sonar — fetch_findings
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar fetch_findings \
--plan-id PLAN_ID --project PROJECT [--pr PR] [--severities SEVERITIES] [--types TYPES] [--ce-wait-timeout CE_WAIT_TIMEOUT]sonar — post_responses
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar post_responses \
--plan-id PLAN_ID [--project PROJECT]sonar_rest — search
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar_rest search \
--project PROJECT [--pr PR] [--severities SEVERITIES] [--types TYPES]sonar_rest — transition
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar_rest transition \
--issue-key ISSUE_KEY --transition {accept,falsepositive,wontfix}sonar_rest — metrics
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar_rest metrics \
--project PROJECT --component COMPONENT [--metrics METRICS]sonar_rest — gate-status
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar_rest gate-status \
--project PROJECT [--branch BRANCH] [--pr PR]sonar_rest — ce-status
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar_rest ce-status \
--project PROJECT [--branch BRANCH]sonar_rest — hotspots
python3 .plan/execute-script.py plan-marshall:workflow-integration-sonar:sonar_rest hotspots \
--project PROJECT [--branch BRANCH] [--pr PR]Related
See ref-workflow-architecture → "Workflow Skill Orchestration" for the full dependency graph and shared infrastructure documentation. Called by: plan-marshall:workflow-pr-doctor (Sonar issue handling).