opencode-sonarqube 0.2.0 → 0.2.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.
- package/README.md +77 -67
- package/dist/index.js +86 -152
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,36 +85,46 @@ Add these to your `~/.zshrc` or `~/.bashrc` to make them permanent.
|
|
|
85
85
|
|
|
86
86
|
## Configuration
|
|
87
87
|
|
|
88
|
-
###
|
|
88
|
+
### Environment Variables (Required)
|
|
89
|
+
|
|
90
|
+
Add these to your `~/.zshrc` or `~/.bashrc`:
|
|
89
91
|
|
|
90
92
|
```bash
|
|
91
|
-
|
|
93
|
+
export SONAR_HOST_URL="https://your-sonarqube-server.com"
|
|
94
|
+
export SONAR_USER="admin"
|
|
95
|
+
export SONAR_PASSWORD="your-password"
|
|
92
96
|
```
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
- Update SonarQube URL
|
|
96
|
-
- Change credentials
|
|
97
|
-
- Test connection
|
|
98
|
-
- Reset project state
|
|
99
|
-
|
|
100
|
-
### Environment Variables (Required)
|
|
101
|
-
|
|
102
|
-
| Variable | Description |
|
|
103
|
-
|----------|-------------|
|
|
104
|
-
| `SONAR_HOST_URL` | SonarQube server URL (e.g., `https://sonarqube.example.com`) |
|
|
105
|
-
| `SONAR_USER` | Username for authentication |
|
|
106
|
-
| `SONAR_PASSWORD` | Password for authentication |
|
|
98
|
+
### Plugin Configuration (Optional)
|
|
107
99
|
|
|
108
|
-
|
|
100
|
+
Create `.sonarqube/config.json` in your project root:
|
|
109
101
|
|
|
110
|
-
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"level": "enterprise",
|
|
105
|
+
"autoAnalyze": true,
|
|
106
|
+
"autoFix": false,
|
|
107
|
+
"sources": "src",
|
|
108
|
+
"tests": "tests",
|
|
109
|
+
"exclusions": "**/node_modules/**,**/dist/**",
|
|
110
|
+
"newCodeDefinition": "previous_version"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
|
115
|
-
|
|
116
|
-
|
|
|
117
|
-
|
|
|
114
|
+
### All Configuration Options
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Description |
|
|
117
|
+
|--------|------|---------|-------------|
|
|
118
|
+
| `level` | `"enterprise"` \| `"standard"` \| `"relaxed"` \| `"off"` | `"enterprise"` | Analysis strictness level |
|
|
119
|
+
| `autoAnalyze` | `boolean` | `true` | Auto-analyze when AI becomes idle |
|
|
120
|
+
| `autoFix` | `boolean` | `false` | Automatically attempt to fix issues |
|
|
121
|
+
| `projectKey` | `string` | auto | SonarQube project key (auto-generated from package.json or directory) |
|
|
122
|
+
| `projectName` | `string` | auto | Display name on SonarQube |
|
|
123
|
+
| `qualityGate` | `string` | `"Sonar way"` | Quality gate to use |
|
|
124
|
+
| `newCodeDefinition` | `"previous_version"` \| `"number_of_days"` \| `"reference_branch"` \| `"specific_analysis"` | `"previous_version"` | How to define 'new code' |
|
|
125
|
+
| `sources` | `string` | `"src"` | Source directories (comma-separated) |
|
|
126
|
+
| `tests` | `string` | - | Test directories (comma-separated) |
|
|
127
|
+
| `exclusions` | `string` | - | File exclusion patterns (glob) |
|
|
118
128
|
|
|
119
129
|
### Strictness Levels
|
|
120
130
|
|
|
@@ -125,6 +135,34 @@ The plugin uses these defaults (configurable in future versions):
|
|
|
125
135
|
| `relaxed` | Only blocker/critical, blocks on blocker |
|
|
126
136
|
| `off` | Plugin disabled |
|
|
127
137
|
|
|
138
|
+
### Example Configurations
|
|
139
|
+
|
|
140
|
+
**Enterprise (strictest)**:
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"level": "enterprise",
|
|
144
|
+
"autoAnalyze": true,
|
|
145
|
+
"autoFix": false
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Standard (balanced)**:
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"level": "standard",
|
|
153
|
+
"autoAnalyze": true,
|
|
154
|
+
"sources": "src,lib"
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Relaxed (lenient)**:
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"level": "relaxed",
|
|
162
|
+
"autoAnalyze": false
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
128
166
|
## Tool Actions (15 total)
|
|
129
167
|
|
|
130
168
|
The plugin adds a `sonarqube` tool with these actions:
|
|
@@ -296,51 +334,23 @@ bun run src/index.ts --status --project-key=my-project
|
|
|
296
334
|
bun run src/index.ts --setup --force
|
|
297
335
|
```
|
|
298
336
|
|
|
299
|
-
##
|
|
337
|
+
## Project State
|
|
300
338
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const api = createSonarQubeAPI(config, state);
|
|
314
|
-
|
|
315
|
-
// Health check
|
|
316
|
-
const health = await api.healthCheck();
|
|
317
|
-
console.log("Healthy:", health.healthy);
|
|
318
|
-
|
|
319
|
-
// Get issues
|
|
320
|
-
const issues = await api.issues.getFormattedIssues({
|
|
321
|
-
projectKey: "my-project",
|
|
322
|
-
severities: ["BLOCKER", "CRITICAL"],
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
// Get quality gate status
|
|
326
|
-
const status = await api.qualityGate.getStatus("my-project");
|
|
327
|
-
console.log("Status:", status.projectStatus.status);
|
|
328
|
-
|
|
329
|
-
// Get new code issues only
|
|
330
|
-
const newIssues = await api.issues.search({
|
|
331
|
-
projectKey: "my-project",
|
|
332
|
-
inNewCode: true,
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
// Get worst files for refactoring
|
|
336
|
-
const worstFiles = await api.components.getWorstFiles("my-project", 10);
|
|
337
|
-
|
|
338
|
-
// Run full analysis
|
|
339
|
-
const result = await runAnalysis(config, state, { projectKey: "my-project" }, "./");
|
|
340
|
-
console.log("Quality Gate:", result.qualityGateStatus);
|
|
339
|
+
The plugin stores project state in `.sonarqube/project.json`:
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"projectKey": "my-project",
|
|
344
|
+
"projectToken": "sqp_xxx...",
|
|
345
|
+
"tokenName": "opencode-my-project-...",
|
|
346
|
+
"initializedAt": "2024-01-01T00:00:00.000Z",
|
|
347
|
+
"languages": ["typescript", "javascript"],
|
|
348
|
+
"qualityGate": "Sonar way",
|
|
349
|
+
"setupComplete": true
|
|
350
|
+
}
|
|
341
351
|
```
|
|
342
352
|
|
|
343
|
-
|
|
353
|
+
**Important:** Add `.sonarqube/` to your `.gitignore` - it contains authentication tokens!
|
|
344
354
|
|
|
345
355
|
## Documentation
|
|
346
356
|
|
|
@@ -383,16 +393,16 @@ This project maintains enterprise-level quality:
|
|
|
383
393
|
|
|
384
394
|
| Metric | Value |
|
|
385
395
|
|--------|-------|
|
|
386
|
-
| Test Coverage |
|
|
396
|
+
| Test Coverage | 87.9% |
|
|
387
397
|
| Tests | 626 |
|
|
388
398
|
| Bugs | 0 |
|
|
389
399
|
| Vulnerabilities | 0 |
|
|
390
400
|
| Code Smells | 0 |
|
|
391
401
|
| Duplications | 0% |
|
|
392
|
-
| Quality Gate | Passed |
|
|
393
402
|
| Reliability Rating | A |
|
|
394
403
|
| Security Rating | A |
|
|
395
404
|
| Maintainability Rating | A |
|
|
405
|
+
| Lines of Code | ~6,000 |
|
|
396
406
|
|
|
397
407
|
## License
|
|
398
408
|
|
package/dist/index.js
CHANGED
|
@@ -19178,33 +19178,43 @@ function generateTokenName(projectKey) {
|
|
|
19178
19178
|
const uuid3 = crypto.randomUUID().split("-")[0];
|
|
19179
19179
|
return `opencode-${projectKey}-${timestamp}-${uuid3}`;
|
|
19180
19180
|
}
|
|
19181
|
-
|
|
19182
|
-
|
|
19181
|
+
function isValidDirectory(dir) {
|
|
19182
|
+
return Boolean(dir && dir !== "/" && dir !== "." && dir.length >= 2);
|
|
19183
|
+
}
|
|
19184
|
+
function resolveDirectoryFromImportMeta() {
|
|
19183
19185
|
try {
|
|
19184
|
-
const
|
|
19185
|
-
|
|
19186
|
-
|
|
19187
|
-
|
|
19188
|
-
|
|
19189
|
-
|
|
19190
|
-
|
|
19191
|
-
|
|
19192
|
-
const pathParts = pluginPath.split("/");
|
|
19193
|
-
const nodeModulesIndex = pathParts.findIndex((p) => p === "node_modules");
|
|
19194
|
-
if (nodeModulesIndex > 0) {
|
|
19195
|
-
const projectPath = pathParts.slice(0, nodeModulesIndex).join("/");
|
|
19196
|
-
if (projectPath && projectPath !== "/" && projectPath.length > 1) {
|
|
19197
|
-
directory = projectPath;
|
|
19198
|
-
try {
|
|
19199
|
-
const { appendFileSync: appendFileSync4 } = await import("node:fs");
|
|
19200
|
-
appendFileSync4("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [BOOTSTRAP] Fixed directory from import.meta.url: ${directory}
|
|
19201
|
-
`);
|
|
19202
|
-
} catch {}
|
|
19203
|
-
}
|
|
19186
|
+
const pluginUrl = import.meta.url;
|
|
19187
|
+
const pluginPath = decodeURIComponent(pluginUrl.replace("file://", ""));
|
|
19188
|
+
const pathParts = pluginPath.split("/");
|
|
19189
|
+
const nodeModulesIndex = pathParts.indexOf("node_modules");
|
|
19190
|
+
if (nodeModulesIndex > 0) {
|
|
19191
|
+
const projectPath = pathParts.slice(0, nodeModulesIndex).join("/");
|
|
19192
|
+
if (isValidDirectory(projectPath)) {
|
|
19193
|
+
return projectPath;
|
|
19204
19194
|
}
|
|
19205
|
-
}
|
|
19195
|
+
}
|
|
19196
|
+
} catch {}
|
|
19197
|
+
return null;
|
|
19198
|
+
}
|
|
19199
|
+
async function generateAnalysisToken(client, tokenName, projectKey) {
|
|
19200
|
+
try {
|
|
19201
|
+
return await client.post("/api/user_tokens/generate", { name: tokenName, type: "PROJECT_ANALYSIS_TOKEN", projectKey });
|
|
19202
|
+
} catch {
|
|
19203
|
+
logger5.warn("PROJECT_ANALYSIS_TOKEN not available, using GLOBAL_ANALYSIS_TOKEN");
|
|
19204
|
+
return await client.post("/api/user_tokens/generate", { name: tokenName, type: "GLOBAL_ANALYSIS_TOKEN" });
|
|
19206
19205
|
}
|
|
19207
|
-
|
|
19206
|
+
}
|
|
19207
|
+
async function bootstrap(options) {
|
|
19208
|
+
let { config: config2, directory, force = false } = options;
|
|
19209
|
+
logger5.info("Starting bootstrap", { directory, projectKey: config2.projectKey || "(auto)" });
|
|
19210
|
+
if (!isValidDirectory(directory)) {
|
|
19211
|
+
const resolved = resolveDirectoryFromImportMeta();
|
|
19212
|
+
if (resolved) {
|
|
19213
|
+
directory = resolved;
|
|
19214
|
+
logger5.info("Resolved directory from import.meta.url", { directory });
|
|
19215
|
+
}
|
|
19216
|
+
}
|
|
19217
|
+
if (!isValidDirectory(directory)) {
|
|
19208
19218
|
logger5.error("Invalid directory for bootstrap", { directory });
|
|
19209
19219
|
return {
|
|
19210
19220
|
success: false,
|
|
@@ -19212,21 +19222,20 @@ async function bootstrap(options) {
|
|
|
19212
19222
|
projectToken: "",
|
|
19213
19223
|
qualityGate: "",
|
|
19214
19224
|
languages: [],
|
|
19215
|
-
message: `Invalid directory: ${directory}
|
|
19225
|
+
message: `Invalid directory: ${directory}`,
|
|
19216
19226
|
isNewProject: false
|
|
19217
19227
|
};
|
|
19218
19228
|
}
|
|
19219
|
-
logger5.info("Starting SonarQube bootstrap", { directory });
|
|
19220
19229
|
if (!force) {
|
|
19221
|
-
const
|
|
19222
|
-
if (
|
|
19223
|
-
logger5.info("Project already bootstrapped", { projectKey:
|
|
19230
|
+
const existingState = await loadProjectState(directory);
|
|
19231
|
+
if (existingState?.setupComplete) {
|
|
19232
|
+
logger5.info("Project already bootstrapped", { projectKey: existingState.projectKey });
|
|
19224
19233
|
return {
|
|
19225
19234
|
success: true,
|
|
19226
|
-
projectKey:
|
|
19227
|
-
projectToken:
|
|
19228
|
-
qualityGate:
|
|
19229
|
-
languages:
|
|
19235
|
+
projectKey: existingState.projectKey,
|
|
19236
|
+
projectToken: existingState.projectToken,
|
|
19237
|
+
qualityGate: existingState.qualityGate,
|
|
19238
|
+
languages: existingState.languages,
|
|
19230
19239
|
message: "Project already configured",
|
|
19231
19240
|
isNewProject: false
|
|
19232
19241
|
};
|
|
@@ -19239,49 +19248,24 @@ async function bootstrap(options) {
|
|
|
19239
19248
|
}
|
|
19240
19249
|
logger5.info("Connected to SonarQube", { version: health.version });
|
|
19241
19250
|
const detection = await detectProjectType(directory);
|
|
19242
|
-
logger5.info("Detected project type", { languages: detection.languages });
|
|
19243
19251
|
const projectKey = config2.projectKey || sanitizeProjectKey(await deriveProjectKey(directory));
|
|
19244
19252
|
const projectName = config2.projectName || projectKey;
|
|
19245
|
-
logger5.info("Project identification", { projectKey, projectName });
|
|
19246
19253
|
const projectsApi = new ProjectsAPI(adminClient);
|
|
19247
|
-
let isNewProject = false;
|
|
19248
19254
|
const exists = await projectsApi.exists(projectKey);
|
|
19249
|
-
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
logger5.info("
|
|
19253
|
-
await projectsApi.create({
|
|
19254
|
-
projectKey,
|
|
19255
|
-
name: projectName,
|
|
19256
|
-
visibility: "private"
|
|
19257
|
-
});
|
|
19258
|
-
isNewProject = true;
|
|
19255
|
+
const isNewProject = !exists;
|
|
19256
|
+
if (isNewProject) {
|
|
19257
|
+
await projectsApi.create({ projectKey, name: projectName, visibility: "private" });
|
|
19258
|
+
logger5.info("Created new project", { projectKey });
|
|
19259
19259
|
}
|
|
19260
19260
|
const tokenName = generateTokenName(projectKey);
|
|
19261
|
-
|
|
19262
|
-
let tokenResponse;
|
|
19263
|
-
try {
|
|
19264
|
-
tokenResponse = await adminClient.post("/api/user_tokens/generate", {
|
|
19265
|
-
name: tokenName,
|
|
19266
|
-
type: "PROJECT_ANALYSIS_TOKEN",
|
|
19267
|
-
projectKey
|
|
19268
|
-
});
|
|
19269
|
-
} catch {
|
|
19270
|
-
logger5.warn("PROJECT_ANALYSIS_TOKEN not available, using GLOBAL_ANALYSIS_TOKEN");
|
|
19271
|
-
tokenResponse = await adminClient.post("/api/user_tokens/generate", {
|
|
19272
|
-
name: tokenName,
|
|
19273
|
-
type: "GLOBAL_ANALYSIS_TOKEN"
|
|
19274
|
-
});
|
|
19275
|
-
}
|
|
19261
|
+
const tokenResponse = await generateAnalysisToken(adminClient, tokenName, projectKey);
|
|
19276
19262
|
const qualityGateName = config2.qualityGate ?? QUALITY_GATE_MAPPING[config2.level];
|
|
19277
19263
|
try {
|
|
19278
19264
|
await setProjectQualityGate(adminClient, projectKey, qualityGateName);
|
|
19279
|
-
logger5.info("Quality gate configured", { qualityGate: qualityGateName });
|
|
19280
19265
|
} catch (error45) {
|
|
19281
19266
|
logger5.warn("Failed to set quality gate", { error: String(error45) });
|
|
19282
19267
|
}
|
|
19283
19268
|
await configureProjectSettings(adminClient, projectKey, detection.languages, config2);
|
|
19284
|
-
logger5.info("Project settings configured");
|
|
19285
19269
|
const state = createInitialState({
|
|
19286
19270
|
projectKey,
|
|
19287
19271
|
projectToken: tokenResponse.token,
|
|
@@ -19298,7 +19282,7 @@ async function bootstrap(options) {
|
|
|
19298
19282
|
projectToken: tokenResponse.token,
|
|
19299
19283
|
qualityGate: qualityGateName,
|
|
19300
19284
|
languages: detection.languages,
|
|
19301
|
-
message: isNewProject ? `Created new project '${projectKey}'
|
|
19285
|
+
message: isNewProject ? `Created new project '${projectKey}'` : `Configured existing project '${projectKey}'`,
|
|
19302
19286
|
isNewProject
|
|
19303
19287
|
};
|
|
19304
19288
|
}
|
|
@@ -20112,26 +20096,6 @@ try {
|
|
|
20112
20096
|
appendFileSync4("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [LOAD] Plugin module loaded! CWD=${process.cwd()}
|
|
20113
20097
|
`);
|
|
20114
20098
|
} catch {}
|
|
20115
|
-
var LOG_FILE4 = "/tmp/sonarqube-plugin-debug.log";
|
|
20116
|
-
var debugLog = {
|
|
20117
|
-
_write: (level, msg, extra) => {
|
|
20118
|
-
const timestamp = new Date().toISOString();
|
|
20119
|
-
const logLine = `${timestamp} [${level}] ${msg} ${extra ? JSON.stringify(extra) : ""}
|
|
20120
|
-
`;
|
|
20121
|
-
try {
|
|
20122
|
-
appendFileSync4(LOG_FILE4, logLine);
|
|
20123
|
-
} catch {}
|
|
20124
|
-
},
|
|
20125
|
-
info: (msg, extra) => {
|
|
20126
|
-
debugLog._write("INFO", msg, extra);
|
|
20127
|
-
},
|
|
20128
|
-
warn: (msg, extra) => {
|
|
20129
|
-
debugLog._write("WARN", msg, extra);
|
|
20130
|
-
},
|
|
20131
|
-
error: (msg, extra) => {
|
|
20132
|
-
debugLog._write("ERROR", msg, extra);
|
|
20133
|
-
}
|
|
20134
|
-
};
|
|
20135
20099
|
var IGNORED_FILE_PATTERNS2 = [
|
|
20136
20100
|
/node_modules/,
|
|
20137
20101
|
/\.git/,
|
|
@@ -20173,7 +20137,7 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
|
|
|
20173
20137
|
const pluginUrl = import.meta.url;
|
|
20174
20138
|
const pluginPath = decodeURIComponent(pluginUrl.replace("file://", ""));
|
|
20175
20139
|
const pathParts = pluginPath.split("/");
|
|
20176
|
-
const nodeModulesIndex = pathParts.
|
|
20140
|
+
const nodeModulesIndex = pathParts.indexOf("node_modules");
|
|
20177
20141
|
if (nodeModulesIndex > 0) {
|
|
20178
20142
|
const projectPath = pathParts.slice(0, nodeModulesIndex).join("/");
|
|
20179
20143
|
if (projectPath && projectPath !== "/" && projectPath.length > 1) {
|
|
@@ -20205,98 +20169,69 @@ var SonarQubePlugin = async ({ client, directory, worktree }) => {
|
|
|
20205
20169
|
const getConfig = () => pluginConfig;
|
|
20206
20170
|
const getDirectory = () => effectiveDirectory;
|
|
20207
20171
|
const loadPluginConfig = async () => {
|
|
20208
|
-
|
|
20209
|
-
if (pluginConfig) {
|
|
20210
|
-
debugLog.info("Config already loaded, skipping");
|
|
20172
|
+
if (pluginConfig)
|
|
20211
20173
|
return;
|
|
20212
|
-
|
|
20174
|
+
const dir = getDirectory();
|
|
20175
|
+
const sonarConfigPath = `${dir}/.sonarqube/config.json`;
|
|
20213
20176
|
try {
|
|
20214
|
-
const
|
|
20215
|
-
debugLog.info("Loading config from", { configPath });
|
|
20216
|
-
const configFile = Bun.file(configPath);
|
|
20177
|
+
const configFile = Bun.file(sonarConfigPath);
|
|
20217
20178
|
if (await configFile.exists()) {
|
|
20218
|
-
|
|
20219
|
-
|
|
20220
|
-
|
|
20221
|
-
|
|
20179
|
+
const config2 = await configFile.json();
|
|
20180
|
+
pluginConfig = { sonarqube: config2 };
|
|
20181
|
+
safeLog(`Config loaded from ${sonarConfigPath}`);
|
|
20182
|
+
return;
|
|
20222
20183
|
}
|
|
20223
|
-
} catch
|
|
20224
|
-
|
|
20225
|
-
|
|
20184
|
+
} catch {}
|
|
20185
|
+
pluginConfig = {};
|
|
20186
|
+
safeLog("Using environment variables for config");
|
|
20226
20187
|
};
|
|
20227
20188
|
const hooks = createHooks(getConfig, getDirectory);
|
|
20228
20189
|
let currentSessionId;
|
|
20229
20190
|
let initialCheckDone = false;
|
|
20191
|
+
const buildQualityNotification = (qgStatus, counts, qgFailed) => {
|
|
20192
|
+
const statusEmoji = qgFailed ? "[FAIL]" : "[WARN]";
|
|
20193
|
+
const blockerNote = counts.blocker > 0 ? `**Action Required:** There are BLOCKER issues that should be fixed.
|
|
20194
|
+
` : "";
|
|
20195
|
+
return `## SonarQube Status: ${statusEmoji}
|
|
20196
|
+
|
|
20197
|
+
**Quality Gate:** ${qgStatus}
|
|
20198
|
+
**Outstanding Issues:** ${counts.blocker} blockers, ${counts.critical} critical, ${counts.major} major
|
|
20199
|
+
|
|
20200
|
+
${blockerNote}Use \`sonarqube({ action: "issues" })\` to see details or \`sonarqube({ action: "analyze" })\` to re-analyze.`;
|
|
20201
|
+
};
|
|
20230
20202
|
const performInitialQualityCheck = async (sessionId) => {
|
|
20231
|
-
|
|
20232
|
-
if (initialCheckDone) {
|
|
20233
|
-
debugLog.info("Initial check already done, skipping");
|
|
20203
|
+
if (initialCheckDone)
|
|
20234
20204
|
return;
|
|
20235
|
-
}
|
|
20236
20205
|
initialCheckDone = true;
|
|
20237
20206
|
try {
|
|
20238
20207
|
await loadPluginConfig();
|
|
20239
20208
|
const sonarConfig = pluginConfig?.["sonarqube"];
|
|
20240
|
-
debugLog.info("Loading SonarQube config", { hasSonarConfig: !!sonarConfig });
|
|
20241
20209
|
const config2 = loadConfig(sonarConfig);
|
|
20242
|
-
|
|
20243
|
-
if (!config2 || config2.level === "off") {
|
|
20244
|
-
debugLog.info("Config missing or level=off, skipping");
|
|
20210
|
+
if (!config2 || config2.level === "off")
|
|
20245
20211
|
return;
|
|
20246
|
-
}
|
|
20247
20212
|
const dir = getDirectory();
|
|
20248
|
-
|
|
20249
|
-
const needsBoot = await needsBootstrap(dir);
|
|
20250
|
-
debugLog.info("needsBootstrap result", { needsBoot });
|
|
20251
|
-
if (needsBoot) {
|
|
20252
|
-
debugLog.info("Bootstrap needed, skipping initial check");
|
|
20213
|
+
if (await needsBootstrap(dir))
|
|
20253
20214
|
return;
|
|
20254
|
-
}
|
|
20255
|
-
debugLog.info("Loading project state");
|
|
20256
20215
|
const state = await getProjectState(dir);
|
|
20257
|
-
|
|
20258
|
-
hasState: !!state,
|
|
20259
|
-
projectKey: state?.projectKey,
|
|
20260
|
-
hasToken: !!state?.projectToken
|
|
20261
|
-
});
|
|
20262
|
-
if (!state || !state.projectKey) {
|
|
20263
|
-
debugLog.info("No state or projectKey, skipping");
|
|
20216
|
+
if (!state?.projectKey)
|
|
20264
20217
|
return;
|
|
20265
|
-
}
|
|
20266
|
-
debugLog.info("Creating API and fetching quality status", { projectKey: state.projectKey });
|
|
20267
20218
|
const api2 = createSonarQubeAPI(config2, state);
|
|
20268
20219
|
const [qgStatus, counts] = await Promise.all([
|
|
20269
20220
|
api2.qualityGate.getStatus(state.projectKey),
|
|
20270
20221
|
api2.issues.getCounts(state.projectKey)
|
|
20271
20222
|
]);
|
|
20272
|
-
debugLog.info("Quality status fetched", { qgStatus: qgStatus.projectStatus.status, counts });
|
|
20273
20223
|
const hasIssues = counts.blocker > 0 || counts.critical > 0 || counts.major > 0;
|
|
20274
20224
|
const qgFailed = qgStatus.projectStatus.status !== "OK";
|
|
20275
|
-
if (hasIssues
|
|
20276
|
-
|
|
20277
|
-
|
|
20278
|
-
|
|
20279
|
-
|
|
20280
|
-
|
|
20281
|
-
|
|
20282
|
-
|
|
20283
|
-
` : ""}
|
|
20284
|
-
Use \`sonarqube({ action: "issues" })\` to see details or \`sonarqube({ action: "analyze" })\` to re-analyze.`;
|
|
20285
|
-
await client.session.prompt({
|
|
20286
|
-
path: { id: sessionId },
|
|
20287
|
-
body: {
|
|
20288
|
-
noReply: true,
|
|
20289
|
-
parts: [{ type: "text", text: notification }]
|
|
20290
|
-
}
|
|
20291
|
-
});
|
|
20292
|
-
await showToast(qgFailed ? "SonarQube: Quality Gate Failed" : "SonarQube: Issues Found", qgFailed ? "error" : "info");
|
|
20293
|
-
}
|
|
20225
|
+
if (!hasIssues && !qgFailed)
|
|
20226
|
+
return;
|
|
20227
|
+
const notification = buildQualityNotification(qgStatus.projectStatus.status, counts, qgFailed);
|
|
20228
|
+
await client.session.prompt({
|
|
20229
|
+
path: { id: sessionId },
|
|
20230
|
+
body: { noReply: true, parts: [{ type: "text", text: notification }] }
|
|
20231
|
+
});
|
|
20232
|
+
await showToast(qgFailed ? "SonarQube: Quality Gate Failed" : "SonarQube: Issues Found", qgFailed ? "error" : "info");
|
|
20294
20233
|
} catch (error45) {
|
|
20295
|
-
|
|
20296
|
-
const { appendFileSync: appendFileSync5 } = await import("node:fs");
|
|
20297
|
-
appendFileSync5("/tmp/sonarqube-plugin-debug.log", `${new Date().toISOString()} [ERROR] performInitialQualityCheck FAILED: ${error45}
|
|
20298
|
-
`);
|
|
20299
|
-
} catch {}
|
|
20234
|
+
safeLog(`performInitialQualityCheck error: ${error45}`);
|
|
20300
20235
|
}
|
|
20301
20236
|
};
|
|
20302
20237
|
const showToast = async (message, variant = "info") => {
|
|
@@ -20462,7 +20397,7 @@ ${statusNote}`;
|
|
|
20462
20397
|
}
|
|
20463
20398
|
try {
|
|
20464
20399
|
const state = await getProjectState(getDirectory());
|
|
20465
|
-
if (!state
|
|
20400
|
+
if (!state?.projectKey)
|
|
20466
20401
|
return;
|
|
20467
20402
|
const api2 = createSonarQubeAPI(config2, state);
|
|
20468
20403
|
const counts = await api2.issues.getCounts(state.projectKey);
|
|
@@ -20625,9 +20560,8 @@ Git operation completed with changes. Consider running:
|
|
|
20625
20560
|
}
|
|
20626
20561
|
const dir = getDirectory();
|
|
20627
20562
|
const state = await getProjectState(dir);
|
|
20628
|
-
if (!state
|
|
20563
|
+
if (!state?.projectKey)
|
|
20629
20564
|
return;
|
|
20630
|
-
}
|
|
20631
20565
|
const api2 = createSonarQubeAPI(config2, state);
|
|
20632
20566
|
const [qgStatus, counts, newCodeResponse] = await Promise.all([
|
|
20633
20567
|
api2.qualityGate.getStatus(state.projectKey),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-sonarqube",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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": "0.2.
|
|
41
|
+
"opencode-sonarqube": "0.2.2",
|
|
42
42
|
"zod": "^3.24.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|