diff --git a/scripts/summarize-sonar.js b/scripts/summarize-sonar.js index ac3faae..e7bc99f 100644 --- a/scripts/summarize-sonar.js +++ b/scripts/summarize-sonar.js @@ -1,5 +1,6 @@ #!/usr/bin/env node -// Prints the project's SonarQube measures into the job log. +// Prints the project's SonarQube measures, its failing quality gate conditions, +// its open issues and its unreviewed security hotspots into the job log. // // This exists because the numbers are otherwise unreachable. The server is // SonarQube 9.9 Community: it has no Bearer auth, so the official MCP cannot @@ -183,12 +184,96 @@ async function main() { const gate = await get( `${base}/api/qualitygates/project_status?projectKey=${encodeURIComponent(PROJECT)}` ); - const gateStatus = parse(gate.body)?.projectStatus?.status; - lines.push('', `Quality gate ${gateStatus ?? 'unknown'}`, ''); + const projectStatus = parse(gate.body)?.projectStatus; + lines.push('', `Quality gate ${projectStatus?.status ?? 'unknown'}`); + + // Which conditions failed, not just that the gate did. "ERROR" on its own + // sends a reader to the dashboard, which is the thing this script exists to + // avoid needing. + const failed = (projectStatus?.conditions ?? []).filter( + (c) => c.status !== 'OK' && c.status !== 'NO_VALUE' + ); + if (failed.length > 0) { + lines.push('', 'Failing gate conditions:'); + for (const c of failed) { + lines.push( + ` ${c.metricKey}: ${c.actualValue} (${c.comparator} threshold ${c.errorThreshold})` + ); + } + } + + lines.push(...(await describeIssues(base))); + lines.push(...(await describeHotspots(base))); + lines.push(''); console.log(lines.join('\n')); } +/** + * The open maintainability issues, named. + * + * A count is enough to notice debt and useless for clearing it. #181 assumed + * the five smells were the five eslint-plugin-sonarjs warnings, because the + * counts matched — they were fixed, and the count stayed five, so they were a + * different five. Nothing short of the list settles that. + * + * Capped rather than paged: past a couple of dozen the answer is not "read the + * list" anyway, and an unbounded fetch on every CI run is a cost with no reader. + */ +async function describeIssues(base) { + const res = await get( + `${base}/api/issues/search?componentKeys=${encodeURIComponent(PROJECT)}` + + `&resolved=false&types=CODE_SMELL,BUG,VULNERABILITY&ps=25&s=SEVERITY&asc=false` + ); + if (res.status !== 200) return ['', `Open issues: could not fetch (status ${res.status}).`]; + + const json = parse(res.body); + const issues = json?.issues; + if (!Array.isArray(issues)) return ['', 'Open issues: unexpected response shape.']; + if (issues.length === 0) return ['', 'Open issues: none.']; + + const total = json.total ?? issues.length; + const out = ['', `Open issues (${issues.length} of ${total}):`]; + for (const issue of issues) { + // The component is "projectKey:path"; the key is noise in a log line. + const where = String(issue.component ?? '').split(':').slice(1).join(':') || issue.component; + const at = issue.line ? `:${issue.line}` : ''; + out.push(` [${issue.severity}] ${where}${at}`); + out.push(` ${issue.rule} — ${issue.message}`); + out.push(` effort ${issue.effort ?? issue.debt ?? 'n/a'}`); + } + return out; +} + +/** + * Security hotspots awaiting review. + * + * A hotspot is not a defect — it is a place the scanner wants a human to say + * whether the surrounding code is safe. That review happens in the dashboard + * and cannot be done from here, so the point of listing them is to say what is + * waiting and where, rather than to fix anything. + */ +async function describeHotspots(base) { + const res = await get( + `${base}/api/hotspots/search?projectKey=${encodeURIComponent(PROJECT)}` + + `&status=TO_REVIEW&ps=25` + ); + if (res.status !== 200) return ['', `Security hotspots: could not fetch (status ${res.status}).`]; + + const hotspots = parse(res.body)?.hotspots; + if (!Array.isArray(hotspots)) return ['', 'Security hotspots: unexpected response shape.']; + if (hotspots.length === 0) return ['', 'Security hotspots awaiting review: none.']; + + const out = ['', `Security hotspots awaiting review (${hotspots.length}):`]; + for (const h of hotspots) { + const where = String(h.component ?? '').split(':').slice(1).join(':') || h.component; + const at = h.line ? `:${h.line}` : ''; + out.push(` ${where}${at}`); + out.push(` ${h.ruleKey ?? h.securityCategory} — ${h.message}`); + } + return out; +} + main().catch((err) => { // Reported rather than thrown, for the reason in the header: this step must // not be able to fail the job.