opencode-sonarqube 2.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +59 -7
  2. package/dist/index.js +113 -81
  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, {
@@ -19394,6 +19379,20 @@ function groupIssuesBySeverity(issues) {
19394
19379
  }
19395
19380
  function formatIssuesForAgent(issues, qualityGateStatus) {
19396
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
+ }
19397
19396
  return `## SonarQube Analysis Results
19398
19397
 
19399
19398
  **Quality Gate: ${qualityGateStatus}**
@@ -20333,8 +20332,20 @@ async function handleStatus(ctx) {
20333
20332
  output += `
20334
20333
  ### Failed Conditions
20335
20334
  `;
20335
+ const hasHotspotCondition = failedConditions.some((c) => c.metricKey.includes("security_hotspots") || c.metricKey.includes("hotspot"));
20336
20336
  for (const condition of failedConditions) {
20337
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" })\`
20338
20349
  `;
20339
20350
  }
20340
20351
  }
@@ -21263,6 +21274,90 @@ Git operation completed with changes. Consider running:
21263
21274
  }
21264
21275
  };
21265
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
+ };
21266
21361
  safeLog(`=== PLUGIN INIT COMPLETE, returning hooks ===`);
21267
21362
  safeLog(` This instance's pluginImportUrl: "${pluginImportUrl}"`);
21268
21363
  safeLog(` This instance's effectiveDirectory: "${effectiveDirectory}"`);
@@ -21297,73 +21392,10 @@ Git operation completed with changes. Consider running:
21297
21392
  }
21298
21393
  }, "experimental.session.compacting"),
21299
21394
  "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;
21395
+ const systemContext = await buildSystemTransformContext(_input);
21396
+ if (systemContext) {
21397
+ output.system.push(systemContext);
21319
21398
  }
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
21399
  }, "experimental.chat.system.transform"),
21368
21400
  tool: {
21369
21401
  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.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": "^1.2.45",
41
+ "opencode-sonarqube": "latest",
42
42
  "zod": "^3.24.0"
43
43
  },
44
44
  "devDependencies": {