opencode-sonarqube 2.0.3 → 2.1.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 +59 -7
- package/dist/index.js +244 -87
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,16 +4,17 @@ OpenCode Plugin for SonarQube integration - Enterprise-level code quality from t
|
|
|
4
4
|
|
|
5
5
|
[](https://sonarqube.example.com)
|
|
6
6
|
[](https://sonarqube.example.com)
|
|
7
|
-
[](https://sonarqube.example.com)
|
|
8
8
|
[](./LICENSE)
|
|
9
|
+
[](https://www.npmjs.com/package/opencode-sonarqube)
|
|
9
10
|
|
|
10
11
|
## Features
|
|
11
12
|
|
|
12
13
|
- **Automatic Analysis**: Triggers SonarQube analysis when the AI agent becomes idle
|
|
13
|
-
- **
|
|
14
|
+
- **16 Tool Actions**: Comprehensive SonarQube integration for AI agents
|
|
14
15
|
- **Clean as You Code**: Focus on new code issues with `newissues` action
|
|
15
16
|
- **Custom Command**: Use `/sonarqube` command for quick analysis
|
|
16
|
-
- **Security Hotspots**: Review and
|
|
17
|
+
- **Security Hotspots**: Review, resolve, and bulk-dismiss security hotspots directly via API
|
|
17
18
|
- **Quality Gate Integration**: Shows pass/fail status with detailed metrics
|
|
18
19
|
- **Git Integration**: Detects git operations and suggests quality checks
|
|
19
20
|
- **System Prompt Injection**: AI always knows current quality status
|
|
@@ -100,6 +101,44 @@ Add these to your `~/.zshrc` or `~/.bashrc` to make them permanent.
|
|
|
100
101
|
- Tell the AI: "Setup SonarQube for this project"
|
|
101
102
|
- Or use: `sonarqube({ action: "setup" })`
|
|
102
103
|
|
|
104
|
+
## Updating
|
|
105
|
+
|
|
106
|
+
The plugin is installed per project via `package.json`. To update to the latest version:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Update in the current project
|
|
110
|
+
bun add opencode-sonarqube@latest
|
|
111
|
+
|
|
112
|
+
# Or with npm
|
|
113
|
+
npm install opencode-sonarqube@latest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Then restart OpenCode to load the new version.
|
|
117
|
+
|
|
118
|
+
**Tip — Always get the latest version automatically:**
|
|
119
|
+
|
|
120
|
+
By default, `bun add` pins a specific version (e.g., `"^2.1.0"`). To always pull the newest release on every OpenCode restart, change your `package.json` dependency to:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"dependencies": {
|
|
125
|
+
"opencode-sonarqube": "latest"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This ensures you always run the most recent version without manual updates.
|
|
131
|
+
|
|
132
|
+
**Update all projects at once:**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Run this from any directory to update all projects that use the plugin
|
|
136
|
+
for dir in $(grep -rl '"opencode-sonarqube"' ~/Projekte/*/package.json 2>/dev/null | xargs -I{} dirname {}); do
|
|
137
|
+
echo "Updating: $dir"
|
|
138
|
+
(cd "$dir" && bun add opencode-sonarqube@latest)
|
|
139
|
+
done
|
|
140
|
+
```
|
|
141
|
+
|
|
103
142
|
## Configuration
|
|
104
143
|
|
|
105
144
|
### Environment Variables (Required)
|
|
@@ -192,6 +231,7 @@ The plugin adds a `sonarqube` tool with these actions:
|
|
|
192
231
|
| `newissues` | Get only issues in NEW code (Clean as You Code) |
|
|
193
232
|
| `worstfiles` | Show files with most issues (prioritize refactoring) |
|
|
194
233
|
| `hotspots` | Get security hotspots that need manual review |
|
|
234
|
+
| `reviewhotspot` | Review/resolve hotspots as SAFE, FIXED, or ACKNOWLEDGED |
|
|
195
235
|
| `duplications` | Find code duplications across the project |
|
|
196
236
|
|
|
197
237
|
### Status & Validation
|
|
@@ -216,15 +256,18 @@ The plugin adds a `sonarqube` tool with these actions:
|
|
|
216
256
|
```typescript
|
|
217
257
|
sonarqube({
|
|
218
258
|
action: "analyze" | "issues" | "newissues" | "worstfiles" | "status" |
|
|
219
|
-
"validate" | "hotspots" | "
|
|
220
|
-
"profile" | "branches" | "metrics" | "setup",
|
|
259
|
+
"validate" | "hotspots" | "reviewhotspot" | "duplications" | "rule" |
|
|
260
|
+
"history" | "profile" | "branches" | "metrics" | "setup",
|
|
221
261
|
scope: "all" | "new" | "changed", // What to analyze
|
|
222
262
|
severity: "blocker" | "critical" | "major" | "minor" | "info" | "all",
|
|
223
263
|
fix: true | false, // Include fix suggestions
|
|
224
264
|
projectKey: "override-key", // Optional override
|
|
225
265
|
force: true | false, // Force re-initialization
|
|
226
266
|
ruleKey: "typescript:S1234", // For "rule" action
|
|
227
|
-
branch: "feature-branch"
|
|
267
|
+
branch: "feature-branch", // For multi-branch analysis
|
|
268
|
+
hotspotKey: "uuid-of-hotspot", // For "reviewhotspot" action (omit for bulk review)
|
|
269
|
+
resolution: "SAFE" | "FIXED" | "ACKNOWLEDGED", // Hotspot review resolution
|
|
270
|
+
comment: "Reason for the decision" // Optional review comment
|
|
228
271
|
})
|
|
229
272
|
```
|
|
230
273
|
|
|
@@ -316,6 +359,15 @@ sonarqube({ action: "rule", ruleKey: "typescript:S3776" })
|
|
|
316
359
|
|
|
317
360
|
// Enterprise validation before release
|
|
318
361
|
sonarqube({ action: "validate" })
|
|
362
|
+
|
|
363
|
+
// List security hotspots
|
|
364
|
+
sonarqube({ action: "hotspots" })
|
|
365
|
+
|
|
366
|
+
// Review a single hotspot as safe
|
|
367
|
+
sonarqube({ action: "reviewhotspot", hotspotKey: "abc-123", resolution: "SAFE", comment: "Form field name, not a password" })
|
|
368
|
+
|
|
369
|
+
// Bulk-review ALL pending hotspots as safe
|
|
370
|
+
sonarqube({ action: "reviewhotspot", resolution: "SAFE" })
|
|
319
371
|
```
|
|
320
372
|
|
|
321
373
|
## CLI Usage
|
|
@@ -477,7 +529,7 @@ This project maintains enterprise-level quality:
|
|
|
477
529
|
| Metric | Value |
|
|
478
530
|
|--------|-------|
|
|
479
531
|
| Test Coverage | 96% |
|
|
480
|
-
| Tests |
|
|
532
|
+
| Tests | 722 |
|
|
481
533
|
| Bugs | 0 |
|
|
482
534
|
| Vulnerabilities | 0 |
|
|
483
535
|
| Code Smells | 0 |
|
package/dist/index.js
CHANGED
|
@@ -17808,6 +17808,36 @@ class IssuesAPI extends BaseAPI {
|
|
|
17808
17808
|
const hotspots = await this.getSecurityHotspots(projectKey);
|
|
17809
17809
|
return hotspots.filter((h) => h.status === "TO_REVIEW");
|
|
17810
17810
|
}
|
|
17811
|
+
async reviewHotspot(hotspotKey, status, resolution, comment) {
|
|
17812
|
+
this.logger.info(`Reviewing hotspot ${hotspotKey}: status=${status}, resolution=${resolution}`);
|
|
17813
|
+
const body = {
|
|
17814
|
+
hotspot: hotspotKey,
|
|
17815
|
+
status
|
|
17816
|
+
};
|
|
17817
|
+
if (resolution && status === "REVIEWED") {
|
|
17818
|
+
body.resolution = resolution;
|
|
17819
|
+
}
|
|
17820
|
+
if (comment) {
|
|
17821
|
+
body.comment = comment;
|
|
17822
|
+
}
|
|
17823
|
+
await this.client.post("/api/hotspots/change_status", body);
|
|
17824
|
+
this.logger.info(`Hotspot ${hotspotKey} reviewed successfully`);
|
|
17825
|
+
}
|
|
17826
|
+
async bulkReviewHotspots(hotspotKeys, resolution, comment) {
|
|
17827
|
+
let success2 = 0;
|
|
17828
|
+
let failed = 0;
|
|
17829
|
+
const errors4 = [];
|
|
17830
|
+
for (const key of hotspotKeys) {
|
|
17831
|
+
try {
|
|
17832
|
+
await this.reviewHotspot(key, "REVIEWED", resolution, comment);
|
|
17833
|
+
success2++;
|
|
17834
|
+
} catch (error45) {
|
|
17835
|
+
failed++;
|
|
17836
|
+
errors4.push(`${key}: ${error45 instanceof Error ? error45.message : String(error45)}`);
|
|
17837
|
+
}
|
|
17838
|
+
}
|
|
17839
|
+
return { success: success2, failed, errors: errors4 };
|
|
17840
|
+
}
|
|
17811
17841
|
}
|
|
17812
17842
|
// src/api/quality-gate/api.ts
|
|
17813
17843
|
init_types2();
|
|
@@ -19349,6 +19379,20 @@ function groupIssuesBySeverity(issues) {
|
|
|
19349
19379
|
}
|
|
19350
19380
|
function formatIssuesForAgent(issues, qualityGateStatus) {
|
|
19351
19381
|
if (issues.length === 0) {
|
|
19382
|
+
if (qualityGateStatus !== "OK") {
|
|
19383
|
+
return `## SonarQube Analysis Results
|
|
19384
|
+
|
|
19385
|
+
**Quality Gate: ${qualityGateStatus}**
|
|
19386
|
+
|
|
19387
|
+
No code issues found, but the Quality Gate is not passing.
|
|
19388
|
+
This is typically caused by **unreviewed security hotspots** or **code duplication thresholds**.
|
|
19389
|
+
|
|
19390
|
+
**To fix:**
|
|
19391
|
+
- \`sonarqube({ action: "hotspots" })\` — List security hotspots
|
|
19392
|
+
- \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\` — Bulk-review all hotspots as safe
|
|
19393
|
+
- \`sonarqube({ action: "status" })\` — See which conditions are failing
|
|
19394
|
+
- \`sonarqube({ action: "duplications" })\` — Check code duplication`;
|
|
19395
|
+
}
|
|
19352
19396
|
return `## SonarQube Analysis Results
|
|
19353
19397
|
|
|
19354
19398
|
**Quality Gate: ${qualityGateStatus}**
|
|
@@ -20288,8 +20332,20 @@ async function handleStatus(ctx) {
|
|
|
20288
20332
|
output += `
|
|
20289
20333
|
### Failed Conditions
|
|
20290
20334
|
`;
|
|
20335
|
+
const hasHotspotCondition = failedConditions.some((c) => c.metricKey.includes("security_hotspots") || c.metricKey.includes("hotspot"));
|
|
20291
20336
|
for (const condition of failedConditions) {
|
|
20292
20337
|
output += `- **${condition.metricKey}**: ${condition.actualValue} (threshold: ${condition.errorThreshold})
|
|
20338
|
+
`;
|
|
20339
|
+
}
|
|
20340
|
+
if (hasHotspotCondition) {
|
|
20341
|
+
output += `
|
|
20342
|
+
### How to Fix Security Hotspot Condition
|
|
20343
|
+
The Quality Gate is failing because unreviewed security hotspots exist.
|
|
20344
|
+
You can review them directly:
|
|
20345
|
+
|
|
20346
|
+
1. **List hotspots:** \`sonarqube({ action: "hotspots" })\`
|
|
20347
|
+
2. **Bulk-review all as safe:** \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\`
|
|
20348
|
+
3. **Review individually:** \`sonarqube({ action: "reviewhotspot", hotspotKey: "<key>", resolution: "SAFE", comment: "reason" })\`
|
|
20293
20349
|
`;
|
|
20294
20350
|
}
|
|
20295
20351
|
}
|
|
@@ -20338,38 +20394,103 @@ async function handleHotspots(ctx) {
|
|
|
20338
20394
|
output += `### Hotspots Requiring Review
|
|
20339
20395
|
|
|
20340
20396
|
`;
|
|
20341
|
-
output += `| Risk | File | Line | Issue |
|
|
20397
|
+
output += `| # | Risk | Key | File | Line | Issue |
|
|
20342
20398
|
`;
|
|
20343
|
-
output +=
|
|
20399
|
+
output += `|---|------|-----|------|------|-------|
|
|
20344
20400
|
`;
|
|
20345
|
-
for (
|
|
20401
|
+
for (let i = 0;i < Math.min(toReview.length, 50); i++) {
|
|
20402
|
+
const hotspot = toReview[i];
|
|
20346
20403
|
const risk = hotspot.vulnerabilityProbability;
|
|
20347
20404
|
const file2 = hotspot.component.split(":").pop() ?? hotspot.component;
|
|
20348
20405
|
const line = hotspot.line ?? "?";
|
|
20349
|
-
const msg = hotspot.message.length >
|
|
20350
|
-
output += `| ${risk} | ${file2} | ${line} | ${msg} |
|
|
20406
|
+
const msg = hotspot.message.length > 50 ? hotspot.message.substring(0, 47) + "..." : hotspot.message;
|
|
20407
|
+
output += `| ${i + 1} | ${risk} | \`${hotspot.key.substring(0, 8)}\` | ${file2} | ${line} | ${msg} |
|
|
20351
20408
|
`;
|
|
20352
20409
|
}
|
|
20353
|
-
if (toReview.length >
|
|
20410
|
+
if (toReview.length > 50) {
|
|
20354
20411
|
output += `
|
|
20355
|
-
*... and ${toReview.length -
|
|
20412
|
+
*... and ${toReview.length - 50} more hotspots*
|
|
20356
20413
|
`;
|
|
20357
20414
|
}
|
|
20358
20415
|
output += `
|
|
20359
|
-
### Review
|
|
20416
|
+
### How to Review Hotspots
|
|
20417
|
+
|
|
20418
|
+
You can review hotspots directly using this tool:
|
|
20360
20419
|
|
|
20361
|
-
|
|
20362
|
-
|
|
20363
|
-
|
|
20420
|
+
**Review a single hotspot:**
|
|
20421
|
+
\`\`\`
|
|
20422
|
+
sonarqube({ action: "reviewhotspot", hotspotKey: "<key>", resolution: "SAFE", comment: "Reviewed: no security risk because..." })
|
|
20423
|
+
\`\`\`
|
|
20364
20424
|
|
|
20365
|
-
|
|
20366
|
-
|
|
20367
|
-
|
|
20368
|
-
|
|
20425
|
+
**Review ALL pending hotspots as SAFE:**
|
|
20426
|
+
\`\`\`
|
|
20427
|
+
sonarqube({ action: "reviewhotspot", resolution: "SAFE", comment: "Bulk review: all hotspots verified safe" })
|
|
20428
|
+
\`\`\`
|
|
20429
|
+
|
|
20430
|
+
**Resolutions:**
|
|
20431
|
+
- \`SAFE\` — The code is secure despite the warning (most common)
|
|
20432
|
+
- \`FIXED\` — The vulnerability has been remediated
|
|
20433
|
+
- \`ACKNOWLEDGED\` — Known risk, accepted
|
|
20434
|
+
|
|
20435
|
+
### Review Guidelines
|
|
20436
|
+
|
|
20437
|
+
1. **HIGH risk**: Review the code carefully — potential security vulnerability
|
|
20438
|
+
2. **MEDIUM risk**: Check regex patterns, randomness, protocol usage
|
|
20439
|
+
3. **LOW risk**: Usually informational — hardcoded IPs, HTTP URLs in non-production code
|
|
20369
20440
|
`;
|
|
20370
20441
|
}
|
|
20371
20442
|
return output;
|
|
20372
20443
|
}
|
|
20444
|
+
async function handleReviewHotspot(ctx, hotspotKey, resolution, comment) {
|
|
20445
|
+
const { api: api2, projectKey } = ctx;
|
|
20446
|
+
const validResolutions = ["SAFE", "FIXED", "ACKNOWLEDGED"];
|
|
20447
|
+
const res = resolution?.toUpperCase() ?? "SAFE";
|
|
20448
|
+
if (!validResolutions.includes(res)) {
|
|
20449
|
+
return `**Error:** Invalid resolution "${resolution}". Must be one of: SAFE, FIXED, ACKNOWLEDGED`;
|
|
20450
|
+
}
|
|
20451
|
+
if (!hotspotKey) {
|
|
20452
|
+
const toReview = await api2.issues.getSecurityHotspotsToReview(projectKey);
|
|
20453
|
+
if (toReview.length === 0) {
|
|
20454
|
+
return formatSuccess("Review Hotspots", "No pending hotspots to review. All hotspots have already been reviewed.");
|
|
20455
|
+
}
|
|
20456
|
+
const result = await api2.issues.bulkReviewHotspots(toReview.map((h) => h.key), res, comment ?? `Bulk reviewed as ${res} via opencode-sonarqube plugin`);
|
|
20457
|
+
let output = `## Hotspot Bulk Review Complete
|
|
20458
|
+
|
|
20459
|
+
**Project:** \`${projectKey}\`
|
|
20460
|
+
**Resolution:** ${res}
|
|
20461
|
+
**Reviewed:** ${result.success} hotspots
|
|
20462
|
+
**Failed:** ${result.failed} hotspots
|
|
20463
|
+
`;
|
|
20464
|
+
if (result.errors.length > 0) {
|
|
20465
|
+
output += `
|
|
20466
|
+
### Errors
|
|
20467
|
+
`;
|
|
20468
|
+
for (const err of result.errors.slice(0, 10)) {
|
|
20469
|
+
output += `- ${err}
|
|
20470
|
+
`;
|
|
20471
|
+
}
|
|
20472
|
+
}
|
|
20473
|
+
output += `
|
|
20474
|
+
> Run \`sonarqube({ action: "status" })\` to check the updated Quality Gate.`;
|
|
20475
|
+
return output;
|
|
20476
|
+
}
|
|
20477
|
+
try {
|
|
20478
|
+
await api2.issues.reviewHotspot(hotspotKey, "REVIEWED", res, comment ?? `Reviewed as ${res} via opencode-sonarqube plugin`);
|
|
20479
|
+
return `## Hotspot Reviewed
|
|
20480
|
+
|
|
20481
|
+
**Hotspot:** \`${hotspotKey}\`
|
|
20482
|
+
**Status:** REVIEWED
|
|
20483
|
+
**Resolution:** ${res}
|
|
20484
|
+
${comment ? `**Comment:** ${comment}` : ""}
|
|
20485
|
+
|
|
20486
|
+
> Run \`sonarqube({ action: "hotspots" })\` to see remaining hotspots.`;
|
|
20487
|
+
} catch (error45) {
|
|
20488
|
+
const msg = error45 instanceof Error ? error45.message : String(error45);
|
|
20489
|
+
return `**Error reviewing hotspot:** ${msg}
|
|
20490
|
+
|
|
20491
|
+
Make sure the hotspot key is correct. Use \`sonarqube({ action: "hotspots" })\` to list hotspot keys.`;
|
|
20492
|
+
}
|
|
20493
|
+
}
|
|
20373
20494
|
// src/tools/handlers/duplications.ts
|
|
20374
20495
|
async function handleDuplications(ctx) {
|
|
20375
20496
|
const { api: api2, projectKey } = ctx;
|
|
@@ -20496,14 +20617,17 @@ Run \`sonarqube({ action: "analyze" })\` to generate metrics.`));
|
|
|
20496
20617
|
// src/tools/sonarqube.ts
|
|
20497
20618
|
var logger10 = new Logger("sonarqube-tool");
|
|
20498
20619
|
var SonarQubeToolArgsSchema = exports_external2.object({
|
|
20499
|
-
action: exports_external2.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform: analyze (run scanner), issues (all issues), newissues (only new code issues), status (quality gate), init/setup (initialize), validate (enterprise check), hotspots (security review), duplications (code duplicates), rule (explain rule), history (past analyses), profile (quality profile), branches (branch status), metrics (detailed metrics), worstfiles (files with most issues)"),
|
|
20620
|
+
action: exports_external2.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "reviewhotspot", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform: analyze (run scanner), issues (all issues), newissues (only new code issues), status (quality gate), init/setup (initialize), validate (enterprise check), hotspots (security review), duplications (code duplicates), rule (explain rule), history (past analyses), profile (quality profile), branches (branch status), metrics (detailed metrics), worstfiles (files with most issues)"),
|
|
20500
20621
|
scope: exports_external2.enum(["all", "new", "changed"]).optional().default("all").describe("Scope of analysis: all files, only new code, or changed files"),
|
|
20501
20622
|
severity: exports_external2.enum(["blocker", "critical", "major", "minor", "info", "all"]).optional().default("all").describe("Filter issues by minimum severity level"),
|
|
20502
20623
|
fix: exports_external2.boolean().optional().default(false).describe("If true, include fix suggestions in the response"),
|
|
20503
20624
|
projectKey: exports_external2.string().optional().describe("Override the project key (usually auto-detected)"),
|
|
20504
20625
|
force: exports_external2.boolean().optional().default(false).describe("Force re-initialization even if already set up"),
|
|
20505
20626
|
ruleKey: exports_external2.string().optional().describe("Rule key to explain (e.g., 'typescript:S1234') - required for 'rule' action"),
|
|
20506
|
-
branch: exports_external2.string().optional().describe("Branch name for multi-branch analysis (default: main branch)")
|
|
20627
|
+
branch: exports_external2.string().optional().describe("Branch name for multi-branch analysis (default: main branch)"),
|
|
20628
|
+
hotspotKey: exports_external2.string().optional().describe("Hotspot key to review (UUID). If omitted with 'reviewhotspot' action, all TO_REVIEW hotspots are bulk-reviewed."),
|
|
20629
|
+
resolution: exports_external2.enum(["SAFE", "FIXED", "ACKNOWLEDGED"]).optional().describe("Hotspot review resolution: SAFE (no risk), FIXED (remediated), ACKNOWLEDGED (accepted risk)"),
|
|
20630
|
+
comment: exports_external2.string().optional().describe("Review comment explaining the decision")
|
|
20507
20631
|
});
|
|
20508
20632
|
async function executeSonarQubeTool(args, context) {
|
|
20509
20633
|
const directory = context.directory ?? process.cwd();
|
|
@@ -20556,6 +20680,8 @@ ${setupResult.message}`);
|
|
|
20556
20680
|
return await handleValidate(ctx);
|
|
20557
20681
|
case "hotspots":
|
|
20558
20682
|
return await handleHotspots(ctx);
|
|
20683
|
+
case "reviewhotspot":
|
|
20684
|
+
return await handleReviewHotspot(ctx, args.hotspotKey, args.resolution, args.comment);
|
|
20559
20685
|
case "duplications":
|
|
20560
20686
|
return await handleDuplications(ctx);
|
|
20561
20687
|
case "rule":
|
|
@@ -21148,6 +21274,90 @@ Git operation completed with changes. Consider running:
|
|
|
21148
21274
|
}
|
|
21149
21275
|
};
|
|
21150
21276
|
};
|
|
21277
|
+
const buildSystemTransformContext = async (_input) => {
|
|
21278
|
+
safeLog(`=== system.transform ENTERED ===`);
|
|
21279
|
+
const inputAny = _input;
|
|
21280
|
+
const sessionID = inputAny?.sessionID;
|
|
21281
|
+
const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
|
|
21282
|
+
const dir = sessionDir || effectiveDirectory;
|
|
21283
|
+
safeLog(` dir used: "${dir}"`);
|
|
21284
|
+
const now = Date.now();
|
|
21285
|
+
const cachedEntry = transformCacheMap.get(dir);
|
|
21286
|
+
if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
|
|
21287
|
+
return cachedEntry.data;
|
|
21288
|
+
}
|
|
21289
|
+
await loadPluginConfig();
|
|
21290
|
+
const sonarConfig = pluginConfig?.["sonarqube"];
|
|
21291
|
+
const config3 = loadConfig(sonarConfig);
|
|
21292
|
+
if (!config3 || config3.level === "off") {
|
|
21293
|
+
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21294
|
+
return;
|
|
21295
|
+
}
|
|
21296
|
+
const state = await getProjectState(dir);
|
|
21297
|
+
if (!state?.projectKey) {
|
|
21298
|
+
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21299
|
+
return;
|
|
21300
|
+
}
|
|
21301
|
+
const api2 = createSonarQubeAPI(config3, state);
|
|
21302
|
+
const [qgStatus, counts, newCodeResponse] = await Promise.all([
|
|
21303
|
+
api2.qualityGate.getStatus(state.projectKey),
|
|
21304
|
+
api2.issues.getCounts(state.projectKey),
|
|
21305
|
+
api2.issues.search({
|
|
21306
|
+
projectKey: state.projectKey,
|
|
21307
|
+
inNewCode: true,
|
|
21308
|
+
resolved: false,
|
|
21309
|
+
pageSize: 1
|
|
21310
|
+
}).catch(() => ({ paging: { total: 0 } }))
|
|
21311
|
+
]);
|
|
21312
|
+
const qgFailed = qgStatus.projectStatus.status !== "OK";
|
|
21313
|
+
const newCodeIssues = newCodeResponse.paging.total;
|
|
21314
|
+
const hasIssues = counts.blocker > 0 || counts.critical > 0;
|
|
21315
|
+
if (!hasIssues && !qgFailed && newCodeIssues === 0) {
|
|
21316
|
+
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21317
|
+
return;
|
|
21318
|
+
}
|
|
21319
|
+
const hotspotInfo = await buildHotspotInfo(api2, state.projectKey);
|
|
21320
|
+
const systemContext = formatSystemContext(qgStatus.projectStatus.status, qgFailed, counts, newCodeIssues, hotspotInfo, config3.level);
|
|
21321
|
+
transformCacheMap.set(dir, { data: systemContext, timestamp: now });
|
|
21322
|
+
return systemContext;
|
|
21323
|
+
};
|
|
21324
|
+
const buildHotspotInfo = async (api2, projectKey) => {
|
|
21325
|
+
try {
|
|
21326
|
+
const hotspotsToReview = await api2.issues.getSecurityHotspotsToReview(projectKey);
|
|
21327
|
+
if (hotspotsToReview.length > 0) {
|
|
21328
|
+
return `**Security Hotspots:** ${hotspotsToReview.length} unreviewed hotspots (may cause Quality Gate to fail)
|
|
21329
|
+
**To fix:** \`sonarqube({ action: "hotspots" })\` to list, then \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\` to bulk-review
|
|
21330
|
+
`;
|
|
21331
|
+
}
|
|
21332
|
+
} catch {}
|
|
21333
|
+
return "";
|
|
21334
|
+
};
|
|
21335
|
+
const formatSystemContext = (qgStatusText, qgFailed, counts, newCodeIssues, hotspotInfo, level) => {
|
|
21336
|
+
const lines = [
|
|
21337
|
+
"## SonarQube Code Quality Status",
|
|
21338
|
+
"",
|
|
21339
|
+
`**Quality Gate:** ${qgStatusText}${qgFailed ? " (FAILED)" : ""}`,
|
|
21340
|
+
`**Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major`
|
|
21341
|
+
];
|
|
21342
|
+
if (newCodeIssues > 0) {
|
|
21343
|
+
lines.push(`**New Code Issues:** ${newCodeIssues} issues in recent changes`);
|
|
21344
|
+
}
|
|
21345
|
+
if (hotspotInfo) {
|
|
21346
|
+
lines.push(hotspotInfo);
|
|
21347
|
+
}
|
|
21348
|
+
if (counts.blocker > 0) {
|
|
21349
|
+
lines.push("**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.");
|
|
21350
|
+
}
|
|
21351
|
+
if (level === "enterprise") {
|
|
21352
|
+
lines.push("This project follows enterprise-level quality standards (zero tolerance for issues).");
|
|
21353
|
+
}
|
|
21354
|
+
lines.push("", "**Recommended Actions:**", '- `sonarqube({ action: "newissues" })` - See issues in your recent changes (Clean as You Code)', '- `sonarqube({ action: "worstfiles" })` - Find files needing most attention', '- `sonarqube({ action: "issues" })` - See all issues');
|
|
21355
|
+
if (hotspotInfo) {
|
|
21356
|
+
lines.push('- `sonarqube({ action: "reviewhotspot", resolution: "SAFE" })` - Bulk-review all security hotspots as safe');
|
|
21357
|
+
}
|
|
21358
|
+
return lines.join(`
|
|
21359
|
+
`);
|
|
21360
|
+
};
|
|
21151
21361
|
safeLog(`=== PLUGIN INIT COMPLETE, returning hooks ===`);
|
|
21152
21362
|
safeLog(` This instance's pluginImportUrl: "${pluginImportUrl}"`);
|
|
21153
21363
|
safeLog(` This instance's effectiveDirectory: "${effectiveDirectory}"`);
|
|
@@ -21182,73 +21392,10 @@ Git operation completed with changes. Consider running:
|
|
|
21182
21392
|
}
|
|
21183
21393
|
}, "experimental.session.compacting"),
|
|
21184
21394
|
"experimental.chat.system.transform": safeAsync(async (_input, output) => {
|
|
21185
|
-
|
|
21186
|
-
|
|
21187
|
-
|
|
21188
|
-
safeLog(` sessionID: "${sessionID}"`);
|
|
21189
|
-
const sharedState = readSharedState();
|
|
21190
|
-
safeLog(` sharedState sessions: ${JSON.stringify(sharedState.sessionToDirectory)}`);
|
|
21191
|
-
safeLog(` sharedState directories: ${JSON.stringify(sharedState.registeredDirectories)}`);
|
|
21192
|
-
const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
|
|
21193
|
-
safeLog(` sessionDir from shared state: "${sessionDir}"`);
|
|
21194
|
-
safeLog(` effectiveDirectory (fallback): "${effectiveDirectory}"`);
|
|
21195
|
-
const dir = sessionDir || effectiveDirectory;
|
|
21196
|
-
safeLog(` FINAL dir used: "${dir}"`);
|
|
21197
|
-
const now = Date.now();
|
|
21198
|
-
const cachedEntry = transformCacheMap.get(dir);
|
|
21199
|
-
if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
|
|
21200
|
-
if (cachedEntry.data) {
|
|
21201
|
-
output.system.push(cachedEntry.data);
|
|
21202
|
-
}
|
|
21203
|
-
return;
|
|
21204
|
-
}
|
|
21205
|
-
await loadPluginConfig();
|
|
21206
|
-
const sonarConfig = pluginConfig?.["sonarqube"];
|
|
21207
|
-
const config3 = loadConfig(sonarConfig);
|
|
21208
|
-
if (!config3 || config3.level === "off") {
|
|
21209
|
-
safeLog(` config level is off or null, returning early`);
|
|
21210
|
-
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21211
|
-
return;
|
|
21395
|
+
const systemContext = await buildSystemTransformContext(_input);
|
|
21396
|
+
if (systemContext) {
|
|
21397
|
+
output.system.push(systemContext);
|
|
21212
21398
|
}
|
|
21213
|
-
const state = await getProjectState(dir);
|
|
21214
|
-
if (!state?.projectKey) {
|
|
21215
|
-
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21216
|
-
return;
|
|
21217
|
-
}
|
|
21218
|
-
const api2 = createSonarQubeAPI(config3, state);
|
|
21219
|
-
const [qgStatus, counts, newCodeResponse] = await Promise.all([
|
|
21220
|
-
api2.qualityGate.getStatus(state.projectKey),
|
|
21221
|
-
api2.issues.getCounts(state.projectKey),
|
|
21222
|
-
api2.issues.search({
|
|
21223
|
-
projectKey: state.projectKey,
|
|
21224
|
-
inNewCode: true,
|
|
21225
|
-
resolved: false,
|
|
21226
|
-
pageSize: 1
|
|
21227
|
-
}).catch(() => ({ paging: { total: 0 } }))
|
|
21228
|
-
]);
|
|
21229
|
-
const hasIssues = counts.blocker > 0 || counts.critical > 0;
|
|
21230
|
-
const qgFailed = qgStatus.projectStatus.status !== "OK";
|
|
21231
|
-
const newCodeIssues = newCodeResponse.paging.total;
|
|
21232
|
-
if (!hasIssues && !qgFailed && newCodeIssues === 0) {
|
|
21233
|
-
transformCacheMap.set(dir, { data: undefined, timestamp: now });
|
|
21234
|
-
return;
|
|
21235
|
-
}
|
|
21236
|
-
const systemContext = `## SonarQube Code Quality Status
|
|
21237
|
-
|
|
21238
|
-
**Quality Gate:** ${qgStatus.projectStatus.status}${qgFailed ? " (FAILED)" : ""}
|
|
21239
|
-
**Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
|
|
21240
|
-
${newCodeIssues > 0 ? `**New Code Issues:** ${newCodeIssues} issues in recent changes
|
|
21241
|
-
` : ""}
|
|
21242
|
-
${counts.blocker > 0 ? `**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.
|
|
21243
|
-
` : ""}
|
|
21244
|
-
${config3.level === "enterprise" ? `This project follows enterprise-level quality standards (zero tolerance for issues).
|
|
21245
|
-
` : ""}
|
|
21246
|
-
**Recommended Actions:**
|
|
21247
|
-
- \`sonarqube({ action: "newissues" })\` - See issues in your recent changes (Clean as You Code)
|
|
21248
|
-
- \`sonarqube({ action: "worstfiles" })\` - Find files needing most attention
|
|
21249
|
-
- \`sonarqube({ action: "issues" })\` - See all issues`;
|
|
21250
|
-
transformCacheMap.set(dir, { data: systemContext, timestamp: now });
|
|
21251
|
-
output.system.push(systemContext);
|
|
21252
21399
|
}, "experimental.chat.system.transform"),
|
|
21253
21400
|
tool: {
|
|
21254
21401
|
sonarqube: tool({
|
|
@@ -21263,6 +21410,7 @@ Actions:
|
|
|
21263
21410
|
- status: Get quality gate status and metrics
|
|
21264
21411
|
- validate: Check if project meets enterprise quality standards
|
|
21265
21412
|
- hotspots: Get security hotspots that need review
|
|
21413
|
+
- reviewhotspot: Review/resolve security hotspots (mark as SAFE, FIXED, or ACKNOWLEDGED)
|
|
21266
21414
|
- duplications: Find code duplications across the project
|
|
21267
21415
|
- rule: Explain a specific SonarQube rule
|
|
21268
21416
|
- history: Show past analysis history
|
|
@@ -21276,16 +21424,22 @@ Example usage:
|
|
|
21276
21424
|
- sonarqube({ action: "newissues" }) - Issues only in your recent changes
|
|
21277
21425
|
- sonarqube({ action: "worstfiles" }) - Files needing most attention
|
|
21278
21426
|
- sonarqube({ action: "issues", severity: "critical" }) - Get critical+ issues
|
|
21279
|
-
- sonarqube({ action: "status" }) - Check quality gate status
|
|
21427
|
+
- sonarqube({ action: "status" }) - Check quality gate status
|
|
21428
|
+
- sonarqube({ action: "hotspots" }) - List security hotspots
|
|
21429
|
+
- sonarqube({ action: "reviewhotspot", resolution: "SAFE" }) - Bulk-review all hotspots as SAFE
|
|
21430
|
+
- sonarqube({ action: "reviewhotspot", hotspotKey: "abc123", resolution: "SAFE", comment: "No risk" }) - Review single hotspot`,
|
|
21280
21431
|
args: {
|
|
21281
|
-
action: tool.schema.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform"),
|
|
21432
|
+
action: tool.schema.enum(["analyze", "issues", "newissues", "status", "init", "setup", "validate", "hotspots", "reviewhotspot", "duplications", "rule", "history", "profile", "branches", "metrics", "worstfiles"]).describe("Action to perform"),
|
|
21282
21433
|
scope: tool.schema.enum(["all", "new", "changed"]).optional().describe("Scope of analysis"),
|
|
21283
21434
|
severity: tool.schema.enum(["blocker", "critical", "major", "minor", "info", "all"]).optional().describe("Filter issues by minimum severity"),
|
|
21284
21435
|
fix: tool.schema.boolean().optional().describe("Include fix suggestions in response"),
|
|
21285
21436
|
projectKey: tool.schema.string().optional().describe("Override project key"),
|
|
21286
21437
|
force: tool.schema.boolean().optional().describe("Force re-initialization"),
|
|
21287
21438
|
ruleKey: tool.schema.string().optional().describe("Rule key to explain (e.g., 'typescript:S1234')"),
|
|
21288
|
-
branch: tool.schema.string().optional().describe("Branch name for multi-branch projects")
|
|
21439
|
+
branch: tool.schema.string().optional().describe("Branch name for multi-branch projects"),
|
|
21440
|
+
hotspotKey: tool.schema.string().optional().describe("Hotspot key (UUID) to review. If omitted with reviewhotspot action, all pending hotspots are bulk-reviewed."),
|
|
21441
|
+
resolution: tool.schema.string().optional().describe("Hotspot review resolution: SAFE, FIXED, or ACKNOWLEDGED"),
|
|
21442
|
+
comment: tool.schema.string().optional().describe("Review comment explaining the decision")
|
|
21289
21443
|
},
|
|
21290
21444
|
async execute(args, _ctx) {
|
|
21291
21445
|
const result = await executeSonarQubeTool({
|
|
@@ -21296,7 +21450,10 @@ Example usage:
|
|
|
21296
21450
|
projectKey: args.projectKey,
|
|
21297
21451
|
force: args.force ?? false,
|
|
21298
21452
|
ruleKey: args.ruleKey,
|
|
21299
|
-
branch: args.branch
|
|
21453
|
+
branch: args.branch,
|
|
21454
|
+
hotspotKey: args.hotspotKey,
|
|
21455
|
+
resolution: args.resolution,
|
|
21456
|
+
comment: args.comment
|
|
21300
21457
|
}, {
|
|
21301
21458
|
directory: getDirectory(),
|
|
21302
21459
|
config: pluginConfig
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sonarqube",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "OpenCode Plugin for SonarQube integration - Enterprise-level code quality from the start",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"homepage": "https://github.com/mguttmann/opencode-sonarqube#readme",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@opencode-ai/plugin": "^1.1.34",
|
|
41
|
-
"opencode-sonarqube": "
|
|
41
|
+
"opencode-sonarqube": "latest",
|
|
42
42
|
"zod": "^3.24.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|