sonar-sweep 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.d.mts +4 -0
- package/dist/cli/index.mjs +3 -0
- package/dist/cli-BMrnoS5Q.mjs +650 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +14 -0
- package/dist/index.d.mts +263 -0
- package/dist/index.mjs +3 -0
- package/dist/issue-transition-B5r9hQVC.mjs +338 -0
- package/package.json +36 -11
- package/dist/cli/commands/issue-accept.d.ts +0 -13
- package/dist/cli/commands/issue-accept.js +0 -49
- package/dist/cli/commands/issue-accept.js.map +0 -1
- package/dist/cli/commands/pr-coverage.d.ts +0 -16
- package/dist/cli/commands/pr-coverage.js +0 -79
- package/dist/cli/commands/pr-coverage.js.map +0 -1
- package/dist/cli/commands/pr-issues.d.ts +0 -15
- package/dist/cli/commands/pr-issues.js +0 -78
- package/dist/cli/commands/pr-issues.js.map +0 -1
- package/dist/cli/commands/pr-report.d.ts +0 -13
- package/dist/cli/commands/pr-report.js +0 -94
- package/dist/cli/commands/pr-report.js.map +0 -1
- package/dist/cli/commands/pr-review.d.ts +0 -16
- package/dist/cli/commands/pr-review.js +0 -93
- package/dist/cli/commands/pr-review.js.map +0 -1
- package/dist/cli/index.d.ts +0 -1
- package/dist/cli/index.js +0 -28
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/project-key.d.ts +0 -1
- package/dist/cli/project-key.js +0 -32
- package/dist/cli/project-key.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -8
- package/dist/cli.js.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -7
- package/dist/index.js.map +0 -1
- package/dist/service/issue-transition.d.ts +0 -12
- package/dist/service/issue-transition.js +0 -20
- package/dist/service/issue-transition.js.map +0 -1
- package/dist/service/pr-coverage.d.ts +0 -22
- package/dist/service/pr-coverage.js +0 -48
- package/dist/service/pr-coverage.js.map +0 -1
- package/dist/service/pr-issues.d.ts +0 -29
- package/dist/service/pr-issues.js +0 -45
- package/dist/service/pr-issues.js.map +0 -1
- package/dist/service/pr-report.d.ts +0 -27
- package/dist/service/pr-report.js +0 -59
- package/dist/service/pr-report.js.map +0 -1
- package/dist/service/pr-review.d.ts +0 -39
- package/dist/service/pr-review.js +0 -81
- package/dist/service/pr-review.js.map +0 -1
- package/dist/service/sonarcloud-client.d.ts +0 -100
- package/dist/service/sonarcloud-client.js +0 -112
- package/dist/service/sonarcloud-client.js.map +0 -1
package/README.md
CHANGED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { a as getPullRequestReport, i as getPullRequestIssues, n as getPullRequestReview, o as SonarCloudClient, r as getPullRequestCoverage, t as transitionIssue } from "./issue-transition-B5r9hQVC.mjs";
|
|
2
|
+
import { hideBin } from "yargs/helpers";
|
|
3
|
+
import yargs from "yargs/yargs";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
//#region \0rolldown/runtime.js
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __exportAll = (all, no_symbols) => {
|
|
11
|
+
let target = {};
|
|
12
|
+
for (var name in all) {
|
|
13
|
+
__defProp(target, name, {
|
|
14
|
+
get: all[name],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (!no_symbols) {
|
|
19
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/service/hotspot-transition.ts
|
|
26
|
+
async function reviewHotspot(clientOptions, input) {
|
|
27
|
+
const hotspotKey = input.hotspotKey.trim();
|
|
28
|
+
const resolution = input.resolution;
|
|
29
|
+
const comment = input.comment?.trim();
|
|
30
|
+
if (!hotspotKey) throw new Error("Missing hotspotKey");
|
|
31
|
+
if (!resolution) throw new Error("Missing resolution (SAFE, ACKNOWLEDGED, or FIXED)");
|
|
32
|
+
const validResolutions = [
|
|
33
|
+
"SAFE",
|
|
34
|
+
"ACKNOWLEDGED",
|
|
35
|
+
"FIXED"
|
|
36
|
+
];
|
|
37
|
+
if (!validResolutions.includes(resolution)) throw new Error(`Invalid resolution: ${resolution}. Must be one of: ${validResolutions.join(", ")}`);
|
|
38
|
+
await new SonarCloudClient(clientOptions).changeHotspotStatus(hotspotKey, "REVIEWED", resolution, comment);
|
|
39
|
+
return {
|
|
40
|
+
applied: true,
|
|
41
|
+
hotspotKey,
|
|
42
|
+
resolution,
|
|
43
|
+
status: "REVIEWED"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/cli/commands/hotspot-review.ts
|
|
49
|
+
var hotspot_review_exports = /* @__PURE__ */ __exportAll({
|
|
50
|
+
builder: () => builder$6,
|
|
51
|
+
command: () => command$6,
|
|
52
|
+
describe: () => describe$6,
|
|
53
|
+
handler: () => handler$6
|
|
54
|
+
});
|
|
55
|
+
const command$6 = "hotspot-review <hotspotKey>";
|
|
56
|
+
const describe$6 = "Mark a security hotspot as reviewed (SAFE, ACKNOWLEDGED, or FIXED)";
|
|
57
|
+
function builder$6(yargs) {
|
|
58
|
+
return yargs.positional("hotspotKey", {
|
|
59
|
+
coerce: (value) => String(value).trim(),
|
|
60
|
+
demandOption: "Provide a hotspot key",
|
|
61
|
+
type: "string"
|
|
62
|
+
}).option("token", {
|
|
63
|
+
default: process.env.SONAR_TOKEN,
|
|
64
|
+
defaultDescription: "SONAR_TOKEN",
|
|
65
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
66
|
+
type: "string"
|
|
67
|
+
}).option("baseUrl", {
|
|
68
|
+
alias: ["base-url", "url"],
|
|
69
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
70
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
71
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
72
|
+
type: "string"
|
|
73
|
+
}).option("resolution", {
|
|
74
|
+
alias: "r",
|
|
75
|
+
choices: [
|
|
76
|
+
"SAFE",
|
|
77
|
+
"ACKNOWLEDGED",
|
|
78
|
+
"FIXED"
|
|
79
|
+
],
|
|
80
|
+
default: "SAFE",
|
|
81
|
+
describe: "Resolution type: SAFE (not a risk), ACKNOWLEDGED (risk accepted), FIXED (code changed)",
|
|
82
|
+
type: "string"
|
|
83
|
+
}).option("comment", {
|
|
84
|
+
alias: "c",
|
|
85
|
+
describe: "Optional review comment",
|
|
86
|
+
type: "string"
|
|
87
|
+
}).option("json", {
|
|
88
|
+
default: false,
|
|
89
|
+
describe: "Print raw JSON for automation/agents",
|
|
90
|
+
type: "boolean"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async function handler$6(args) {
|
|
94
|
+
const result = await reviewHotspot({
|
|
95
|
+
baseUrl: args.baseUrl,
|
|
96
|
+
token: args.token
|
|
97
|
+
}, {
|
|
98
|
+
comment: args.comment,
|
|
99
|
+
hotspotKey: args.hotspotKey,
|
|
100
|
+
resolution: args.resolution
|
|
101
|
+
});
|
|
102
|
+
if (args.json) {
|
|
103
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write(`Reviewed hotspot ${result.hotspotKey} as ${result.resolution}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/cli/commands/issue-accept.ts
|
|
111
|
+
var issue_accept_exports = /* @__PURE__ */ __exportAll({
|
|
112
|
+
builder: () => builder$5,
|
|
113
|
+
command: () => command$5,
|
|
114
|
+
describe: () => describe$5,
|
|
115
|
+
handler: () => handler$5
|
|
116
|
+
});
|
|
117
|
+
const command$5 = "issue-accept <issueKey>";
|
|
118
|
+
const describe$5 = "Mark a Sonar issue as accepted (transition=accept)";
|
|
119
|
+
function builder$5(yargs) {
|
|
120
|
+
return yargs.positional("issueKey", {
|
|
121
|
+
coerce: (value) => String(value).trim(),
|
|
122
|
+
demandOption: "Provide an issue key",
|
|
123
|
+
type: "string"
|
|
124
|
+
}).option("token", {
|
|
125
|
+
default: process.env.SONAR_TOKEN,
|
|
126
|
+
defaultDescription: "SONAR_TOKEN",
|
|
127
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
128
|
+
type: "string"
|
|
129
|
+
}).option("baseUrl", {
|
|
130
|
+
alias: ["base-url", "url"],
|
|
131
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
132
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
133
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
134
|
+
type: "string"
|
|
135
|
+
}).option("comment", {
|
|
136
|
+
describe: "Optional acceptance comment",
|
|
137
|
+
type: "string"
|
|
138
|
+
}).option("json", {
|
|
139
|
+
default: false,
|
|
140
|
+
describe: "Print raw JSON for automation/agents",
|
|
141
|
+
type: "boolean"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function handler$5(args) {
|
|
145
|
+
const result = await transitionIssue({
|
|
146
|
+
baseUrl: args.baseUrl,
|
|
147
|
+
token: args.token
|
|
148
|
+
}, {
|
|
149
|
+
comment: args.comment,
|
|
150
|
+
issueKey: args.issueKey,
|
|
151
|
+
transition: "accept"
|
|
152
|
+
});
|
|
153
|
+
if (args.json) {
|
|
154
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
process.stdout.write(`Accepted issue ${result.issueKey} (${result.transition})\n`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/cli/project-key.ts
|
|
162
|
+
function resolveProjectKey(projectKey) {
|
|
163
|
+
if (projectKey?.trim()) return projectKey.trim();
|
|
164
|
+
const sonarPropsPath = join(getGitRoot(), "sonar-project.properties");
|
|
165
|
+
if (!existsSync(sonarPropsPath)) throw new Error(`Missing projectKey and no sonar-project.properties found at ${sonarPropsPath}. Provide --projectKey explicitly.`);
|
|
166
|
+
const key = readFileSync(sonarPropsPath, "utf8").match(/^\s*sonar\.projectKey\s*=\s*(.+)\s*$/m)?.[1]?.trim();
|
|
167
|
+
if (!key) throw new Error(`Could not read sonar.projectKey from ${sonarPropsPath}. Provide --projectKey explicitly.`);
|
|
168
|
+
return key;
|
|
169
|
+
}
|
|
170
|
+
function getGitRoot() {
|
|
171
|
+
try {
|
|
172
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
173
|
+
encoding: "utf8",
|
|
174
|
+
stdio: [
|
|
175
|
+
"ignore",
|
|
176
|
+
"pipe",
|
|
177
|
+
"ignore"
|
|
178
|
+
]
|
|
179
|
+
}).trim();
|
|
180
|
+
} catch {
|
|
181
|
+
return process.cwd();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/cli/commands/pr-coverage.ts
|
|
187
|
+
var pr_coverage_exports = /* @__PURE__ */ __exportAll({
|
|
188
|
+
builder: () => builder$4,
|
|
189
|
+
command: () => command$4,
|
|
190
|
+
describe: () => describe$4,
|
|
191
|
+
handler: () => handler$4
|
|
192
|
+
});
|
|
193
|
+
const command$4 = "pr-coverage <pullRequest> [projectKey]";
|
|
194
|
+
const describe$4 = "List files with low new-code coverage for a pull request";
|
|
195
|
+
function builder$4(yargs) {
|
|
196
|
+
return yargs.positional("projectKey", {
|
|
197
|
+
describe: "Sonar project key (optional; auto-detected from sonar-project.properties)",
|
|
198
|
+
type: "string"
|
|
199
|
+
}).positional("pullRequest", {
|
|
200
|
+
coerce: (value) => String(value).trim(),
|
|
201
|
+
demandOption: "Provide a pull request key/id",
|
|
202
|
+
type: "string"
|
|
203
|
+
}).option("token", {
|
|
204
|
+
default: process.env.SONAR_TOKEN,
|
|
205
|
+
defaultDescription: "SONAR_TOKEN",
|
|
206
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
207
|
+
type: "string"
|
|
208
|
+
}).option("baseUrl", {
|
|
209
|
+
alias: ["base-url", "url"],
|
|
210
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
211
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
212
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
213
|
+
type: "string"
|
|
214
|
+
}).option("projectKey", {
|
|
215
|
+
alias: ["project-key", "k"],
|
|
216
|
+
describe: "Sonar project key (overrides auto-detection)",
|
|
217
|
+
type: "string"
|
|
218
|
+
}).option("threshold", {
|
|
219
|
+
coerce: (value) => Math.max(0, Math.min(100, Number(value))),
|
|
220
|
+
default: 80,
|
|
221
|
+
describe: "Coverage threshold percentage",
|
|
222
|
+
type: "number"
|
|
223
|
+
}).option("includePassing", {
|
|
224
|
+
default: false,
|
|
225
|
+
describe: "Include files that meet threshold too",
|
|
226
|
+
type: "boolean"
|
|
227
|
+
}).option("maxFiles", {
|
|
228
|
+
alias: ["max", "limit"],
|
|
229
|
+
coerce: (value) => Math.max(1, Number(value)),
|
|
230
|
+
default: 20,
|
|
231
|
+
type: "number"
|
|
232
|
+
}).option("json", {
|
|
233
|
+
default: false,
|
|
234
|
+
describe: "Print raw JSON for automation/agents",
|
|
235
|
+
type: "boolean"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function handler$4(args) {
|
|
239
|
+
const projectKey = resolveProjectKey(args.projectKey);
|
|
240
|
+
const report = await getPullRequestCoverage({
|
|
241
|
+
baseUrl: args.baseUrl,
|
|
242
|
+
token: args.token
|
|
243
|
+
}, {
|
|
244
|
+
includePassing: args.includePassing,
|
|
245
|
+
maxFiles: args.maxFiles,
|
|
246
|
+
projectKey,
|
|
247
|
+
pullRequest: args.pullRequest,
|
|
248
|
+
threshold: args.threshold
|
|
249
|
+
});
|
|
250
|
+
if (args.json) {
|
|
251
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
process.stdout.write(`Found ${report.files.length} file(s) below ${report.threshold.toFixed(1)}% new-code coverage\n`);
|
|
255
|
+
for (const file of report.files) process.stdout.write(`- ${file.file}: ${file.coverageOnNewCode.toFixed(1)}% (${file.uncoveredLines}/${file.linesToCover} uncovered lines)\n`);
|
|
256
|
+
process.stdout.write(`See analysis details: ${report.analysisUrl}\n`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/service/pr-hotspots.ts
|
|
261
|
+
async function getPullRequestHotspots(clientOptions, input) {
|
|
262
|
+
const projectKey = input.projectKey.trim();
|
|
263
|
+
const pullRequest = input.pullRequest.trim();
|
|
264
|
+
const status = input.status ?? "TO_REVIEW";
|
|
265
|
+
const page = input.page ?? 1;
|
|
266
|
+
const pageSize = input.pageSize ?? 100;
|
|
267
|
+
if (!projectKey) throw new Error("Missing projectKey");
|
|
268
|
+
if (!pullRequest) throw new Error("Missing pullRequest");
|
|
269
|
+
const baseUrl = (clientOptions.baseUrl ?? "https://sonarcloud.io").replace(/\/$/, "");
|
|
270
|
+
const response = await new SonarCloudClient(clientOptions).getHotspots(projectKey, pullRequest, status, page, pageSize);
|
|
271
|
+
const componentMap = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const component of response.components ?? []) componentMap.set(component.key, component.path ?? component.longName ?? component.key);
|
|
273
|
+
const hotspots = response.hotspots.map((hotspot) => ({
|
|
274
|
+
file: extractFilePath(hotspot.component, projectKey, componentMap),
|
|
275
|
+
key: hotspot.key,
|
|
276
|
+
line: hotspot.line,
|
|
277
|
+
message: hotspot.message,
|
|
278
|
+
resolution: hotspot.resolution,
|
|
279
|
+
securityCategory: hotspot.securityCategory,
|
|
280
|
+
status: hotspot.status,
|
|
281
|
+
vulnerabilityProbability: hotspot.vulnerabilityProbability
|
|
282
|
+
}));
|
|
283
|
+
return {
|
|
284
|
+
analysisUrl: `${baseUrl}/project/security_hotspots?id=${encodeURIComponent(projectKey)}&pullRequest=${encodeURIComponent(pullRequest)}`,
|
|
285
|
+
hotspots,
|
|
286
|
+
page: response.paging.pageIndex,
|
|
287
|
+
pageSize: response.paging.pageSize,
|
|
288
|
+
projectKey,
|
|
289
|
+
pullRequest,
|
|
290
|
+
total: response.paging.total
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function extractFilePath(componentKey, projectKey, componentMap) {
|
|
294
|
+
const mappedPath = componentMap.get(componentKey);
|
|
295
|
+
if (mappedPath) return mappedPath;
|
|
296
|
+
const prefix = `${projectKey}:`;
|
|
297
|
+
if (componentKey.startsWith(prefix)) return componentKey.slice(prefix.length);
|
|
298
|
+
return componentKey;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
//#endregion
|
|
302
|
+
//#region src/cli/commands/pr-hotspots.ts
|
|
303
|
+
var pr_hotspots_exports = /* @__PURE__ */ __exportAll({
|
|
304
|
+
builder: () => builder$3,
|
|
305
|
+
command: () => command$3,
|
|
306
|
+
describe: () => describe$3,
|
|
307
|
+
handler: () => handler$3
|
|
308
|
+
});
|
|
309
|
+
const command$3 = "pr-hotspots <pullRequest> [projectKey]";
|
|
310
|
+
const describe$3 = "Fetch security hotspots for a pull request";
|
|
311
|
+
function builder$3(yargs) {
|
|
312
|
+
return yargs.positional("projectKey", {
|
|
313
|
+
describe: "Sonar project key (optional; auto-detected from sonar-project.properties)",
|
|
314
|
+
type: "string"
|
|
315
|
+
}).positional("pullRequest", {
|
|
316
|
+
coerce: (value) => String(value).trim(),
|
|
317
|
+
demandOption: "Provide a pull request key/id",
|
|
318
|
+
type: "string"
|
|
319
|
+
}).option("token", {
|
|
320
|
+
default: process.env.SONAR_TOKEN,
|
|
321
|
+
defaultDescription: "SONAR_TOKEN",
|
|
322
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
323
|
+
type: "string"
|
|
324
|
+
}).option("baseUrl", {
|
|
325
|
+
alias: ["base-url", "url"],
|
|
326
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
327
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
328
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
329
|
+
type: "string"
|
|
330
|
+
}).option("projectKey", {
|
|
331
|
+
alias: ["project-key", "k"],
|
|
332
|
+
describe: "Sonar project key (overrides auto-detection)",
|
|
333
|
+
type: "string"
|
|
334
|
+
}).option("status", {
|
|
335
|
+
alias: "s",
|
|
336
|
+
choices: ["TO_REVIEW", "REVIEWED"],
|
|
337
|
+
default: "TO_REVIEW",
|
|
338
|
+
describe: "Filter hotspots by status",
|
|
339
|
+
type: "string"
|
|
340
|
+
}).option("page", {
|
|
341
|
+
alias: "p",
|
|
342
|
+
coerce: (value) => Math.max(1, Number(value)),
|
|
343
|
+
default: 1,
|
|
344
|
+
type: "number"
|
|
345
|
+
}).option("pageSize", {
|
|
346
|
+
alias: ["ps", "page-size"],
|
|
347
|
+
coerce: (value) => Math.min(500, Math.max(1, Number(value))),
|
|
348
|
+
default: 100,
|
|
349
|
+
type: "number"
|
|
350
|
+
}).option("json", {
|
|
351
|
+
default: false,
|
|
352
|
+
describe: "Print raw JSON for automation/agents",
|
|
353
|
+
type: "boolean"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async function handler$3(args) {
|
|
357
|
+
const projectKey = resolveProjectKey(args.projectKey);
|
|
358
|
+
const result = await getPullRequestHotspots({
|
|
359
|
+
baseUrl: args.baseUrl,
|
|
360
|
+
token: args.token
|
|
361
|
+
}, {
|
|
362
|
+
page: args.page,
|
|
363
|
+
pageSize: args.pageSize,
|
|
364
|
+
projectKey,
|
|
365
|
+
pullRequest: args.pullRequest,
|
|
366
|
+
status: args.status
|
|
367
|
+
});
|
|
368
|
+
if (args.json) {
|
|
369
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const statusLabel = args.status === "TO_REVIEW" ? "unreviewed" : "reviewed";
|
|
373
|
+
process.stdout.write(`Found ${result.total} ${statusLabel} security hotspot(s) for PR ${result.pullRequest} (${result.projectKey})\n`);
|
|
374
|
+
if (result.hotspots.length === 0) {
|
|
375
|
+
process.stdout.write(`\nSee analysis details: ${result.analysisUrl}\n`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
process.stdout.write("\n");
|
|
379
|
+
for (const hotspot of result.hotspots) {
|
|
380
|
+
const location = hotspot.line ? `${hotspot.file}:${hotspot.line}` : hotspot.file;
|
|
381
|
+
const resolutionInfo = hotspot.resolution ? ` (${hotspot.resolution})` : "";
|
|
382
|
+
process.stdout.write(`[${hotspot.vulnerabilityProbability}] ${hotspot.key}\n ${location}\n ${hotspot.message}${resolutionInfo}\n\n`);
|
|
383
|
+
}
|
|
384
|
+
process.stdout.write(`See analysis details: ${result.analysisUrl}\n`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
//#endregion
|
|
388
|
+
//#region src/cli/commands/pr-issues.ts
|
|
389
|
+
var pr_issues_exports = /* @__PURE__ */ __exportAll({
|
|
390
|
+
builder: () => builder$2,
|
|
391
|
+
command: () => command$2,
|
|
392
|
+
describe: () => describe$2,
|
|
393
|
+
handler: () => handler$2
|
|
394
|
+
});
|
|
395
|
+
const command$2 = "pr-issues <pullRequest> [projectKey]";
|
|
396
|
+
const describe$2 = "Fetch SonarQube Cloud issue details for a pull request";
|
|
397
|
+
function builder$2(yargs) {
|
|
398
|
+
return yargs.positional("projectKey", {
|
|
399
|
+
describe: "Sonar project key (optional; auto-detected from sonar-project.properties)",
|
|
400
|
+
type: "string"
|
|
401
|
+
}).positional("pullRequest", {
|
|
402
|
+
coerce: (value) => String(value).trim(),
|
|
403
|
+
demandOption: "Provide a pull request key/id",
|
|
404
|
+
type: "string"
|
|
405
|
+
}).option("token", {
|
|
406
|
+
default: process.env.SONAR_TOKEN,
|
|
407
|
+
defaultDescription: "SONAR_TOKEN",
|
|
408
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
409
|
+
type: "string"
|
|
410
|
+
}).option("baseUrl", {
|
|
411
|
+
alias: ["base-url", "url"],
|
|
412
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
413
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
414
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
415
|
+
type: "string"
|
|
416
|
+
}).option("projectKey", {
|
|
417
|
+
alias: ["project-key", "k"],
|
|
418
|
+
describe: "Sonar project key (overrides auto-detection)",
|
|
419
|
+
type: "string"
|
|
420
|
+
}).option("page", {
|
|
421
|
+
alias: "p",
|
|
422
|
+
coerce: (value) => Math.max(1, Number(value)),
|
|
423
|
+
default: 1,
|
|
424
|
+
type: "number"
|
|
425
|
+
}).option("pageSize", {
|
|
426
|
+
alias: ["ps", "page-size"],
|
|
427
|
+
coerce: (value) => Math.min(500, Math.max(1, Number(value))),
|
|
428
|
+
default: 100,
|
|
429
|
+
type: "number"
|
|
430
|
+
}).option("json", {
|
|
431
|
+
default: false,
|
|
432
|
+
describe: "Print raw JSON for automation/agents",
|
|
433
|
+
type: "boolean"
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
async function handler$2(args) {
|
|
437
|
+
const projectKey = resolveProjectKey(args.projectKey);
|
|
438
|
+
const report = await getPullRequestIssues({
|
|
439
|
+
baseUrl: args.baseUrl,
|
|
440
|
+
token: args.token
|
|
441
|
+
}, {
|
|
442
|
+
page: args.page,
|
|
443
|
+
pageSize: args.pageSize,
|
|
444
|
+
projectKey,
|
|
445
|
+
pullRequest: args.pullRequest
|
|
446
|
+
});
|
|
447
|
+
if (args.json) {
|
|
448
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
process.stdout.write(`Found ${report.total} open issue(s) in new code for PR ${report.pullRequest} (${report.projectKey})\n`);
|
|
452
|
+
if (report.issues.length === 0) {
|
|
453
|
+
process.stdout.write(`See analysis details: ${report.analysisUrl}\n`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
for (const issue of report.issues) {
|
|
457
|
+
const location = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
458
|
+
process.stdout.write(`- [${issue.issueStatus}] ${issue.severity} ${issue.type} ${issue.rule} ${location} - ${issue.message}\n`);
|
|
459
|
+
}
|
|
460
|
+
process.stdout.write(`See analysis details: ${report.analysisUrl}\n`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
//#endregion
|
|
464
|
+
//#region src/cli/commands/pr-report.ts
|
|
465
|
+
var pr_report_exports = /* @__PURE__ */ __exportAll({
|
|
466
|
+
builder: () => builder$1,
|
|
467
|
+
command: () => command$1,
|
|
468
|
+
describe: () => describe$1,
|
|
469
|
+
handler: () => handler$1
|
|
470
|
+
});
|
|
471
|
+
const command$1 = "pr-report <pullRequest> [projectKey]";
|
|
472
|
+
const describe$1 = "Fetch SonarQube Cloud details for a pull request";
|
|
473
|
+
function builder$1(yargs) {
|
|
474
|
+
return yargs.positional("projectKey", {
|
|
475
|
+
describe: "Sonar project key (optional; auto-detected from sonar-project.properties)",
|
|
476
|
+
type: "string"
|
|
477
|
+
}).positional("pullRequest", {
|
|
478
|
+
coerce: (value) => String(value).trim(),
|
|
479
|
+
demandOption: "Provide a pull request key/id",
|
|
480
|
+
type: "string"
|
|
481
|
+
}).option("token", {
|
|
482
|
+
default: process.env.SONAR_TOKEN,
|
|
483
|
+
defaultDescription: "SONAR_TOKEN",
|
|
484
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
485
|
+
type: "string"
|
|
486
|
+
}).option("baseUrl", {
|
|
487
|
+
alias: ["base-url", "url"],
|
|
488
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
489
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
490
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
491
|
+
type: "string"
|
|
492
|
+
}).option("projectKey", {
|
|
493
|
+
alias: ["project-key", "k"],
|
|
494
|
+
describe: "Sonar project key (overrides auto-detection)",
|
|
495
|
+
type: "string"
|
|
496
|
+
}).option("json", {
|
|
497
|
+
default: false,
|
|
498
|
+
describe: "Print raw JSON for automation/agents",
|
|
499
|
+
type: "boolean"
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
async function handler$1(args) {
|
|
503
|
+
const projectKey = resolveProjectKey(args.projectKey);
|
|
504
|
+
const report = await getPullRequestReport({
|
|
505
|
+
baseUrl: args.baseUrl,
|
|
506
|
+
token: args.token
|
|
507
|
+
}, {
|
|
508
|
+
projectKey,
|
|
509
|
+
pullRequest: args.pullRequest
|
|
510
|
+
});
|
|
511
|
+
if (args.json) {
|
|
512
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const qualityGate = report.qualityGateStatus === "OK" ? "Quality Gate passed" : `Quality Gate failed (${report.qualityGateStatus})`;
|
|
516
|
+
process.stdout.write(`${qualityGate}\n\n`);
|
|
517
|
+
if (report.failingQualityGateConditions.length > 0) {
|
|
518
|
+
process.stdout.write("Quality Gate Conditions\n");
|
|
519
|
+
for (const condition of report.failingQualityGateConditions) {
|
|
520
|
+
const metric = formatMetricLabel(condition.metricKey);
|
|
521
|
+
const threshold = condition.errorThreshold ?? "n/a";
|
|
522
|
+
const actual = condition.actualValue ?? "n/a";
|
|
523
|
+
process.stdout.write(`- ${metric}: actual=${actual}, threshold=${condition.comparator} ${threshold}\n`);
|
|
524
|
+
}
|
|
525
|
+
process.stdout.write("\n");
|
|
526
|
+
}
|
|
527
|
+
process.stdout.write("Issues\n");
|
|
528
|
+
process.stdout.write(`- ${report.issueCounts.newIssues} New issues\n`);
|
|
529
|
+
process.stdout.write(`- ${report.issueCounts.acceptedIssues} Accepted issues\n\n`);
|
|
530
|
+
process.stdout.write("Measures\n");
|
|
531
|
+
process.stdout.write(`- ${report.measures.securityHotspots} Security Hotspots\n`);
|
|
532
|
+
process.stdout.write(`- ${report.measures.coverageOnNewCode.toFixed(1)}% Coverage on New Code\n`);
|
|
533
|
+
process.stdout.write(`- ${report.measures.duplicationOnNewCode.toFixed(1)}% Duplication on New Code\n\n`);
|
|
534
|
+
process.stdout.write(`See analysis details: ${report.analysisUrl}\n`);
|
|
535
|
+
}
|
|
536
|
+
function formatMetricLabel(metricKey) {
|
|
537
|
+
switch (metricKey) {
|
|
538
|
+
case "new_coverage": return "Coverage on New Code";
|
|
539
|
+
case "new_duplicated_lines_density": return "Duplication on New Code";
|
|
540
|
+
case "new_security_hotspots_reviewed": return "Security Hotspots Reviewed on New Code";
|
|
541
|
+
case "new_reliability_rating": return "Reliability Rating on New Code";
|
|
542
|
+
case "new_security_rating": return "Security Rating on New Code";
|
|
543
|
+
case "new_maintainability_rating": return "Maintainability Rating on New Code";
|
|
544
|
+
default: return metricKey;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
//#endregion
|
|
549
|
+
//#region src/cli/commands/pr-review.ts
|
|
550
|
+
var pr_review_exports = /* @__PURE__ */ __exportAll({
|
|
551
|
+
builder: () => builder,
|
|
552
|
+
command: () => command,
|
|
553
|
+
describe: () => describe,
|
|
554
|
+
handler: () => handler
|
|
555
|
+
});
|
|
556
|
+
const command = "pr-review <pullRequest> [projectKey]";
|
|
557
|
+
const describe = "Review Sonar pull request issues with code context snippets";
|
|
558
|
+
function builder(yargs) {
|
|
559
|
+
return yargs.positional("projectKey", {
|
|
560
|
+
describe: "Sonar project key (optional; auto-detected from sonar-project.properties)",
|
|
561
|
+
type: "string"
|
|
562
|
+
}).positional("pullRequest", {
|
|
563
|
+
coerce: (value) => String(value).trim(),
|
|
564
|
+
demandOption: "Provide a pull request key/id",
|
|
565
|
+
type: "string"
|
|
566
|
+
}).option("token", {
|
|
567
|
+
default: process.env.SONAR_TOKEN,
|
|
568
|
+
defaultDescription: "SONAR_TOKEN",
|
|
569
|
+
demandOption: "Provide --token or set SONAR_TOKEN",
|
|
570
|
+
type: "string"
|
|
571
|
+
}).option("baseUrl", {
|
|
572
|
+
alias: ["base-url", "url"],
|
|
573
|
+
coerce: (value) => String(value).replace(/\/$/, ""),
|
|
574
|
+
default: process.env.SONAR_BASE_URL ?? "https://sonarcloud.io",
|
|
575
|
+
defaultDescription: "SONAR_BASE_URL or https://sonarcloud.io",
|
|
576
|
+
type: "string"
|
|
577
|
+
}).option("projectKey", {
|
|
578
|
+
alias: ["project-key", "k"],
|
|
579
|
+
describe: "Sonar project key (overrides auto-detection)",
|
|
580
|
+
type: "string"
|
|
581
|
+
}).option("contextLines", {
|
|
582
|
+
alias: ["context", "C"],
|
|
583
|
+
coerce: (value) => Math.max(0, Number(value)),
|
|
584
|
+
default: 3,
|
|
585
|
+
type: "number"
|
|
586
|
+
}).option("page", {
|
|
587
|
+
alias: "p",
|
|
588
|
+
coerce: (value) => Math.max(1, Number(value)),
|
|
589
|
+
default: 1,
|
|
590
|
+
type: "number"
|
|
591
|
+
}).option("pageSize", {
|
|
592
|
+
alias: ["ps", "page-size"],
|
|
593
|
+
coerce: (value) => Math.min(500, Math.max(1, Number(value))),
|
|
594
|
+
default: 20,
|
|
595
|
+
type: "number"
|
|
596
|
+
}).option("json", {
|
|
597
|
+
default: false,
|
|
598
|
+
describe: "Print raw JSON for automation/agents",
|
|
599
|
+
type: "boolean"
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
async function handler(args) {
|
|
603
|
+
const projectKey = resolveProjectKey(args.projectKey);
|
|
604
|
+
const report = await getPullRequestReview({
|
|
605
|
+
baseUrl: args.baseUrl,
|
|
606
|
+
token: args.token
|
|
607
|
+
}, {
|
|
608
|
+
contextLines: args.contextLines,
|
|
609
|
+
page: args.page,
|
|
610
|
+
pageSize: args.pageSize,
|
|
611
|
+
projectKey,
|
|
612
|
+
pullRequest: args.pullRequest
|
|
613
|
+
});
|
|
614
|
+
if (args.json) {
|
|
615
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
process.stdout.write(`Found ${report.total} open issue(s) in new code for PR ${report.pullRequest} (${report.projectKey})\n\n`);
|
|
619
|
+
for (const issue of report.issues) {
|
|
620
|
+
const location = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
621
|
+
process.stdout.write(`[${issue.issueStatus}] ${issue.severity} ${issue.type} ${issue.rule} ${location}\n${issue.message}\n`);
|
|
622
|
+
process.stdout.write(`Issue URL: ${issue.issueUrl}\n`);
|
|
623
|
+
if (issue.snippet) {
|
|
624
|
+
process.stdout.write("Context:\n");
|
|
625
|
+
for (const line of issue.snippet.lines) {
|
|
626
|
+
const marker = line.highlight ? ">" : " ";
|
|
627
|
+
process.stdout.write(`${marker} ${line.line.toString().padStart(4, " ")} | ${line.text}\n`);
|
|
628
|
+
}
|
|
629
|
+
} else if (issue.sourceError) process.stdout.write(`Context unavailable: ${issue.sourceError}\n`);
|
|
630
|
+
process.stdout.write("\n");
|
|
631
|
+
}
|
|
632
|
+
process.stdout.write(`See analysis details: ${report.analysisUrl}\n`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/cli/index.ts
|
|
637
|
+
async function run(argv = process.argv) {
|
|
638
|
+
await yargs(hideBin(argv)).scriptName("sonar-sweep").env("SONAR").command(hotspot_review_exports).command(issue_accept_exports).command(pr_coverage_exports).command(pr_hotspots_exports).command(pr_issues_exports).command(pr_report_exports).command(pr_review_exports).demandCommand(1, "Provide a command").strict().help().fail((message, error, yargsInstance) => {
|
|
639
|
+
if (error) {
|
|
640
|
+
process.stderr.write(`${error.message}\n`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
process.stderr.write(`${message}\n`);
|
|
644
|
+
yargsInstance.showHelp();
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}).parseAsync();
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
//#endregion
|
|
650
|
+
export { run as t };
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as run } from "./cli-BMrnoS5Q.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/cli.ts
|
|
5
|
+
try {
|
|
6
|
+
await run(process.argv);
|
|
7
|
+
} catch (error) {
|
|
8
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
9
|
+
process.stderr.write(`${message}\n`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { };
|