chore(sonar): name the findings instead of counting them (#181) #275
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/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
|
// 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
|
// 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(
|
const gate = await get(
|
||||||
`${base}/api/qualitygates/project_status?projectKey=${encodeURIComponent(PROJECT)}`
|
`${base}/api/qualitygates/project_status?projectKey=${encodeURIComponent(PROJECT)}`
|
||||||
);
|
);
|
||||||
const gateStatus = parse(gate.body)?.projectStatus?.status;
|
const projectStatus = parse(gate.body)?.projectStatus;
|
||||||
lines.push('', `Quality gate ${gateStatus ?? 'unknown'}`, '');
|
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'));
|
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) => {
|
main().catch((err) => {
|
||||||
// Reported rather than thrown, for the reason in the header: this step must
|
// Reported rather than thrown, for the reason in the header: this step must
|
||||||
// not be able to fail the job.
|
// not be able to fail the job.
|
||||||
|
|||||||
Reference in New Issue
Block a user