opencode-sonarqube 2.1.0 → 2.1.2

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.
Files changed (3) hide show
  1. package/README.md +59 -7
  2. package/dist/index.js +123 -83
  3. 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
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://sonarqube.example.com)
6
6
  [![Quality Gate](https://img.shields.io/badge/quality%20gate-passed-brightgreen)](https://sonarqube.example.com)
7
- [![Tests](https://img.shields.io/badge/tests-612%20passing-brightgreen)](https://sonarqube.example.com)
7
+ [![Tests](https://img.shields.io/badge/tests-722%20passing-brightgreen)](https://sonarqube.example.com)
8
8
  [![License](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
9
+ [![npm version](https://img.shields.io/npm/v/opencode-sonarqube)](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
- - **15 Tool Actions**: Comprehensive SonarQube integration for AI agents
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 track security hotspots requiring manual review
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" | "duplications" | "rule" | "history" |
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" // For multi-branch analysis
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 | 612 |
532
+ | Tests | 722 |
481
533
  | Bugs | 0 |
482
534
  | Vulnerabilities | 0 |
483
535
  | Code Smells | 0 |
package/dist/index.js CHANGED
@@ -1,20 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- var __create = Object.create;
3
- var __getProtoOf = Object.getPrototypeOf;
4
2
  var __defProp = Object.defineProperty;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __toESM = (mod, isNodeMode, target) => {
8
- target = mod != null ? __create(__getProtoOf(mod)) : {};
9
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
- for (let key of __getOwnPropNames(mod))
11
- if (!__hasOwnProp.call(to, key))
12
- __defProp(to, key, {
13
- get: () => mod[key],
14
- enumerable: true
15
- });
16
- return to;
17
- };
18
3
  var __export = (target, all) => {
19
4
  for (var name in all)
20
5
  __defProp(target, name, {
@@ -19328,6 +19313,10 @@ class SonarQubeAPI {
19328
19313
  };
19329
19314
  }
19330
19315
  }
19316
+ function createSonarQubeAPIWithCredentials(url2, user, password, logger4) {
19317
+ const client = createClientWithCredentials(url2, user, password, logger4?.child("client"));
19318
+ return new SonarQubeAPI(client, logger4);
19319
+ }
19331
19320
  function createSonarQubeAPIWithToken(url2, token, logger4) {
19332
19321
  const client = createClientWithToken(url2, token, logger4?.child("client"));
19333
19322
  return new SonarQubeAPI(client, logger4);
@@ -19394,6 +19383,20 @@ function groupIssuesBySeverity(issues) {
19394
19383
  }
19395
19384
  function formatIssuesForAgent(issues, qualityGateStatus) {
19396
19385
  if (issues.length === 0) {
19386
+ if (qualityGateStatus !== "OK") {
19387
+ return `## SonarQube Analysis Results
19388
+
19389
+ **Quality Gate: ${qualityGateStatus}**
19390
+
19391
+ No code issues found, but the Quality Gate is not passing.
19392
+ This is typically caused by **unreviewed security hotspots** or **code duplication thresholds**.
19393
+
19394
+ **To fix:**
19395
+ - \`sonarqube({ action: "hotspots" })\` — List security hotspots
19396
+ - \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\` — Bulk-review all hotspots as safe
19397
+ - \`sonarqube({ action: "status" })\` — See which conditions are failing
19398
+ - \`sonarqube({ action: "duplications" })\` — Check code duplication`;
19399
+ }
19397
19400
  return `## SonarQube Analysis Results
19398
19401
 
19399
19402
  **Quality Gate: ${qualityGateStatus}**
@@ -20333,8 +20336,20 @@ async function handleStatus(ctx) {
20333
20336
  output += `
20334
20337
  ### Failed Conditions
20335
20338
  `;
20339
+ const hasHotspotCondition = failedConditions.some((c) => c.metricKey.includes("security_hotspots") || c.metricKey.includes("hotspot"));
20336
20340
  for (const condition of failedConditions) {
20337
20341
  output += `- **${condition.metricKey}**: ${condition.actualValue} (threshold: ${condition.errorThreshold})
20342
+ `;
20343
+ }
20344
+ if (hasHotspotCondition) {
20345
+ output += `
20346
+ ### How to Fix Security Hotspot Condition
20347
+ The Quality Gate is failing because unreviewed security hotspots exist.
20348
+ You can review them directly:
20349
+
20350
+ 1. **List hotspots:** \`sonarqube({ action: "hotspots" })\`
20351
+ 2. **Bulk-review all as safe:** \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\`
20352
+ 3. **Review individually:** \`sonarqube({ action: "reviewhotspot", hotspotKey: "<key>", resolution: "SAFE", comment: "reason" })\`
20338
20353
  `;
20339
20354
  }
20340
20355
  }
@@ -20363,6 +20378,9 @@ Please fix the failed checks before proceeding.`;
20363
20378
  return output;
20364
20379
  }
20365
20380
  // src/tools/handlers/security.ts
20381
+ function createAdminAPI(ctx) {
20382
+ return createSonarQubeAPIWithCredentials(ctx.config.url, ctx.config.user, ctx.config.password);
20383
+ }
20366
20384
  async function handleHotspots(ctx) {
20367
20385
  const { api: api2, projectKey } = ctx;
20368
20386
  const hotspots = await api2.issues.getSecurityHotspots(projectKey);
@@ -20437,12 +20455,13 @@ async function handleReviewHotspot(ctx, hotspotKey, resolution, comment) {
20437
20455
  if (!validResolutions.includes(res)) {
20438
20456
  return `**Error:** Invalid resolution "${resolution}". Must be one of: SAFE, FIXED, ACKNOWLEDGED`;
20439
20457
  }
20458
+ const adminApi = createAdminAPI(ctx);
20440
20459
  if (!hotspotKey) {
20441
20460
  const toReview = await api2.issues.getSecurityHotspotsToReview(projectKey);
20442
20461
  if (toReview.length === 0) {
20443
20462
  return formatSuccess("Review Hotspots", "No pending hotspots to review. All hotspots have already been reviewed.");
20444
20463
  }
20445
- const result = await api2.issues.bulkReviewHotspots(toReview.map((h) => h.key), res, comment ?? `Bulk reviewed as ${res} via opencode-sonarqube plugin`);
20464
+ const result = await adminApi.issues.bulkReviewHotspots(toReview.map((h) => h.key), res, comment ?? `Bulk reviewed as ${res} via opencode-sonarqube plugin`);
20446
20465
  let output = `## Hotspot Bulk Review Complete
20447
20466
 
20448
20467
  **Project:** \`${projectKey}\`
@@ -20464,7 +20483,7 @@ async function handleReviewHotspot(ctx, hotspotKey, resolution, comment) {
20464
20483
  return output;
20465
20484
  }
20466
20485
  try {
20467
- await api2.issues.reviewHotspot(hotspotKey, "REVIEWED", res, comment ?? `Reviewed as ${res} via opencode-sonarqube plugin`);
20486
+ await adminApi.issues.reviewHotspot(hotspotKey, "REVIEWED", res, comment ?? `Reviewed as ${res} via opencode-sonarqube plugin`);
20468
20487
  return `## Hotspot Reviewed
20469
20488
 
20470
20489
  **Hotspot:** \`${hotspotKey}\`
@@ -21263,6 +21282,90 @@ Git operation completed with changes. Consider running:
21263
21282
  }
21264
21283
  };
21265
21284
  };
21285
+ const buildSystemTransformContext = async (_input) => {
21286
+ safeLog(`=== system.transform ENTERED ===`);
21287
+ const inputAny = _input;
21288
+ const sessionID = inputAny?.sessionID;
21289
+ const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
21290
+ const dir = sessionDir || effectiveDirectory;
21291
+ safeLog(` dir used: "${dir}"`);
21292
+ const now = Date.now();
21293
+ const cachedEntry = transformCacheMap.get(dir);
21294
+ if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
21295
+ return cachedEntry.data;
21296
+ }
21297
+ await loadPluginConfig();
21298
+ const sonarConfig = pluginConfig?.["sonarqube"];
21299
+ const config3 = loadConfig(sonarConfig);
21300
+ if (!config3 || config3.level === "off") {
21301
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21302
+ return;
21303
+ }
21304
+ const state = await getProjectState(dir);
21305
+ if (!state?.projectKey) {
21306
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21307
+ return;
21308
+ }
21309
+ const api2 = createSonarQubeAPI(config3, state);
21310
+ const [qgStatus, counts, newCodeResponse] = await Promise.all([
21311
+ api2.qualityGate.getStatus(state.projectKey),
21312
+ api2.issues.getCounts(state.projectKey),
21313
+ api2.issues.search({
21314
+ projectKey: state.projectKey,
21315
+ inNewCode: true,
21316
+ resolved: false,
21317
+ pageSize: 1
21318
+ }).catch(() => ({ paging: { total: 0 } }))
21319
+ ]);
21320
+ const qgFailed = qgStatus.projectStatus.status !== "OK";
21321
+ const newCodeIssues = newCodeResponse.paging.total;
21322
+ const hasIssues = counts.blocker > 0 || counts.critical > 0;
21323
+ if (!hasIssues && !qgFailed && newCodeIssues === 0) {
21324
+ transformCacheMap.set(dir, { data: undefined, timestamp: now });
21325
+ return;
21326
+ }
21327
+ const hotspotInfo = await buildHotspotInfo(api2, state.projectKey);
21328
+ const systemContext = formatSystemContext(qgStatus.projectStatus.status, qgFailed, counts, newCodeIssues, hotspotInfo, config3.level);
21329
+ transformCacheMap.set(dir, { data: systemContext, timestamp: now });
21330
+ return systemContext;
21331
+ };
21332
+ const buildHotspotInfo = async (api2, projectKey) => {
21333
+ try {
21334
+ const hotspotsToReview = await api2.issues.getSecurityHotspotsToReview(projectKey);
21335
+ if (hotspotsToReview.length > 0) {
21336
+ return `**Security Hotspots:** ${hotspotsToReview.length} unreviewed hotspots (may cause Quality Gate to fail)
21337
+ **To fix:** \`sonarqube({ action: "hotspots" })\` to list, then \`sonarqube({ action: "reviewhotspot", resolution: "SAFE" })\` to bulk-review
21338
+ `;
21339
+ }
21340
+ } catch {}
21341
+ return "";
21342
+ };
21343
+ const formatSystemContext = (qgStatusText, qgFailed, counts, newCodeIssues, hotspotInfo, level) => {
21344
+ const lines = [
21345
+ "## SonarQube Code Quality Status",
21346
+ "",
21347
+ `**Quality Gate:** ${qgStatusText}${qgFailed ? " (FAILED)" : ""}`,
21348
+ `**Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major`
21349
+ ];
21350
+ if (newCodeIssues > 0) {
21351
+ lines.push(`**New Code Issues:** ${newCodeIssues} issues in recent changes`);
21352
+ }
21353
+ if (hotspotInfo) {
21354
+ lines.push(hotspotInfo);
21355
+ }
21356
+ if (counts.blocker > 0) {
21357
+ lines.push("**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.");
21358
+ }
21359
+ if (level === "enterprise") {
21360
+ lines.push("This project follows enterprise-level quality standards (zero tolerance for issues).");
21361
+ }
21362
+ 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');
21363
+ if (hotspotInfo) {
21364
+ lines.push('- `sonarqube({ action: "reviewhotspot", resolution: "SAFE" })` - Bulk-review all security hotspots as safe');
21365
+ }
21366
+ return lines.join(`
21367
+ `);
21368
+ };
21266
21369
  safeLog(`=== PLUGIN INIT COMPLETE, returning hooks ===`);
21267
21370
  safeLog(` This instance's pluginImportUrl: "${pluginImportUrl}"`);
21268
21371
  safeLog(` This instance's effectiveDirectory: "${effectiveDirectory}"`);
@@ -21297,73 +21400,10 @@ Git operation completed with changes. Consider running:
21297
21400
  }
21298
21401
  }, "experimental.session.compacting"),
21299
21402
  "experimental.chat.system.transform": safeAsync(async (_input, output) => {
21300
- safeLog(`=== system.transform ENTERED ===`);
21301
- const inputAny = _input;
21302
- const sessionID = inputAny?.sessionID;
21303
- safeLog(` sessionID: "${sessionID}"`);
21304
- const sharedState = readSharedState();
21305
- safeLog(` sharedState sessions: ${JSON.stringify(sharedState.sessionToDirectory)}`);
21306
- safeLog(` sharedState directories: ${JSON.stringify(sharedState.registeredDirectories)}`);
21307
- const sessionDir = sessionID ? getDirectoryForSession(sessionID) : undefined;
21308
- safeLog(` sessionDir from shared state: "${sessionDir}"`);
21309
- safeLog(` effectiveDirectory (fallback): "${effectiveDirectory}"`);
21310
- const dir = sessionDir || effectiveDirectory;
21311
- safeLog(` FINAL dir used: "${dir}"`);
21312
- const now = Date.now();
21313
- const cachedEntry = transformCacheMap.get(dir);
21314
- if (cachedEntry && now - cachedEntry.timestamp < TRANSFORM_CACHE_TTL_MS) {
21315
- if (cachedEntry.data) {
21316
- output.system.push(cachedEntry.data);
21317
- }
21318
- return;
21403
+ const systemContext = await buildSystemTransformContext(_input);
21404
+ if (systemContext) {
21405
+ output.system.push(systemContext);
21319
21406
  }
21320
- await loadPluginConfig();
21321
- const sonarConfig = pluginConfig?.["sonarqube"];
21322
- const config3 = loadConfig(sonarConfig);
21323
- if (!config3 || config3.level === "off") {
21324
- safeLog(` config level is off or null, returning early`);
21325
- transformCacheMap.set(dir, { data: undefined, timestamp: now });
21326
- return;
21327
- }
21328
- const state = await getProjectState(dir);
21329
- if (!state?.projectKey) {
21330
- transformCacheMap.set(dir, { data: undefined, timestamp: now });
21331
- return;
21332
- }
21333
- const api2 = createSonarQubeAPI(config3, state);
21334
- const [qgStatus, counts, newCodeResponse] = await Promise.all([
21335
- api2.qualityGate.getStatus(state.projectKey),
21336
- api2.issues.getCounts(state.projectKey),
21337
- api2.issues.search({
21338
- projectKey: state.projectKey,
21339
- inNewCode: true,
21340
- resolved: false,
21341
- pageSize: 1
21342
- }).catch(() => ({ paging: { total: 0 } }))
21343
- ]);
21344
- const hasIssues = counts.blocker > 0 || counts.critical > 0;
21345
- const qgFailed = qgStatus.projectStatus.status !== "OK";
21346
- const newCodeIssues = newCodeResponse.paging.total;
21347
- if (!hasIssues && !qgFailed && newCodeIssues === 0) {
21348
- transformCacheMap.set(dir, { data: undefined, timestamp: now });
21349
- return;
21350
- }
21351
- const systemContext = `## SonarQube Code Quality Status
21352
-
21353
- **Quality Gate:** ${qgStatus.projectStatus.status}${qgFailed ? " (FAILED)" : ""}
21354
- **Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
21355
- ${newCodeIssues > 0 ? `**New Code Issues:** ${newCodeIssues} issues in recent changes
21356
- ` : ""}
21357
- ${counts.blocker > 0 ? `**IMPORTANT:** There are BLOCKER issues that must be fixed before shipping code.
21358
- ` : ""}
21359
- ${config3.level === "enterprise" ? `This project follows enterprise-level quality standards (zero tolerance for issues).
21360
- ` : ""}
21361
- **Recommended Actions:**
21362
- - \`sonarqube({ action: "newissues" })\` - See issues in your recent changes (Clean as You Code)
21363
- - \`sonarqube({ action: "worstfiles" })\` - Find files needing most attention
21364
- - \`sonarqube({ action: "issues" })\` - See all issues`;
21365
- transformCacheMap.set(dir, { data: systemContext, timestamp: now });
21366
- output.system.push(systemContext);
21367
21407
  }, "experimental.chat.system.transform"),
21368
21408
  tool: {
21369
21409
  sonarqube: tool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-sonarqube",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
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": "^1.2.45",
41
+ "opencode-sonarqube": "latest",
42
42
  "zod": "^3.24.0"
43
43
  },
44
44
  "devDependencies": {