opencode-sonarqube 1.5.0 → 2.0.0
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/dist/index.js +182 -70
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4039,7 +4039,7 @@ var init_types2 = __esm(() => {
|
|
|
4039
4039
|
projectName: exports_external2.string().optional().describe("SonarQube project display name"),
|
|
4040
4040
|
qualityGate: exports_external2.string().optional().describe("Quality gate to use"),
|
|
4041
4041
|
newCodeDefinition: exports_external2.enum(["previous_version", "number_of_days", "reference_branch", "specific_analysis"]).default("previous_version").describe("How to define 'new code' for analysis"),
|
|
4042
|
-
sources: exports_external2.string().
|
|
4042
|
+
sources: exports_external2.string().optional().describe("Source directories to analyze (auto-detected if not set)"),
|
|
4043
4043
|
tests: exports_external2.string().optional().describe("Test directories"),
|
|
4044
4044
|
exclusions: exports_external2.string().optional().describe("File exclusion patterns")
|
|
4045
4045
|
});
|
|
@@ -4422,11 +4422,29 @@ function getSubdirectories(path) {
|
|
|
4422
4422
|
const output = result.stdout.toString().trim();
|
|
4423
4423
|
if (!output)
|
|
4424
4424
|
return [];
|
|
4425
|
+
const SKIP_DIRS = new Set([
|
|
4426
|
+
"node_modules",
|
|
4427
|
+
"dist",
|
|
4428
|
+
"build",
|
|
4429
|
+
"out",
|
|
4430
|
+
"coverage",
|
|
4431
|
+
".next",
|
|
4432
|
+
".nuxt",
|
|
4433
|
+
".output",
|
|
4434
|
+
"target",
|
|
4435
|
+
"__pycache__",
|
|
4436
|
+
".venv",
|
|
4437
|
+
"venv",
|
|
4438
|
+
"vendor",
|
|
4439
|
+
"tmp",
|
|
4440
|
+
".turbo",
|
|
4441
|
+
".cache"
|
|
4442
|
+
]);
|
|
4425
4443
|
return output.split(`
|
|
4426
4444
|
`).filter((name) => {
|
|
4427
4445
|
if (name.startsWith("."))
|
|
4428
4446
|
return false;
|
|
4429
|
-
if (name
|
|
4447
|
+
if (SKIP_DIRS.has(name))
|
|
4430
4448
|
return false;
|
|
4431
4449
|
return directoryExists(`${path}/${name}`);
|
|
4432
4450
|
});
|
|
@@ -4576,7 +4594,7 @@ async function generatePropertiesContent(options, config2, directory) {
|
|
|
4576
4594
|
if (detection.languages.includes("go")) {
|
|
4577
4595
|
lines.push("", "# Go", "sonar.go.coverage.reportPaths=coverage.out");
|
|
4578
4596
|
}
|
|
4579
|
-
lines.push("", "# Analysis Settings", "sonar.sourceEncoding=UTF-8", "sonar.scm.provider=git", `sonar.cpd.exclusions=${getTestPatterns(detection.languages)}
|
|
4597
|
+
lines.push("", "# Analysis Settings", "sonar.sourceEncoding=UTF-8", "sonar.scm.provider=git", `sonar.cpd.exclusions=${getTestPatterns(detection.languages)}`);
|
|
4580
4598
|
if (options.additionalProperties) {
|
|
4581
4599
|
lines.push("", "# Additional Properties");
|
|
4582
4600
|
for (const [key, value] of Object.entries(options.additionalProperties)) {
|
|
@@ -19449,24 +19467,22 @@ async function runScanner(config3, state, options, directory) {
|
|
|
19449
19467
|
const exitCode = await proc.exited;
|
|
19450
19468
|
const output = stdout + (stderr ? `
|
|
19451
19469
|
${stderr}` : "");
|
|
19470
|
+
const taskId = extractTaskId(output);
|
|
19452
19471
|
if (exitCode === 0) {
|
|
19453
|
-
const taskId = extractTaskId(output);
|
|
19454
19472
|
logger4.info("Scanner completed successfully", { taskId });
|
|
19455
|
-
return {
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
19459
|
-
|
|
19460
|
-
};
|
|
19461
|
-
} else {
|
|
19462
|
-
logger4.error(`Scanner failed with exit code ${exitCode}`);
|
|
19463
|
-
return {
|
|
19464
|
-
success: false,
|
|
19465
|
-
output,
|
|
19466
|
-
error: `Scanner exited with code ${exitCode}`,
|
|
19467
|
-
exitCode
|
|
19468
|
-
};
|
|
19473
|
+
return { success: true, output, exitCode, taskId };
|
|
19474
|
+
}
|
|
19475
|
+
if (taskId) {
|
|
19476
|
+
logger4.warn(`Scanner exited with code ${exitCode} but analysis was uploaded`, { taskId });
|
|
19477
|
+
return { success: true, output, exitCode, taskId };
|
|
19469
19478
|
}
|
|
19479
|
+
logger4.error(`Scanner failed with exit code ${exitCode}`);
|
|
19480
|
+
return {
|
|
19481
|
+
success: false,
|
|
19482
|
+
output,
|
|
19483
|
+
error: `Scanner exited with code ${exitCode}`,
|
|
19484
|
+
exitCode
|
|
19485
|
+
};
|
|
19470
19486
|
} catch (error45) {
|
|
19471
19487
|
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
19472
19488
|
logger4.error(`Scanner execution failed: ${errorMessage}`);
|
|
@@ -19919,9 +19935,118 @@ function createHandlerContext(config3, state, projectKey, directory) {
|
|
|
19919
19935
|
// src/tools/handlers/setup.ts
|
|
19920
19936
|
init_bootstrap();
|
|
19921
19937
|
init_detection();
|
|
19922
|
-
init_logger();
|
|
19923
19938
|
import { mkdir } from "node:fs/promises";
|
|
19924
|
-
|
|
19939
|
+
|
|
19940
|
+
// src/scanner/config/validation.ts
|
|
19941
|
+
init_logger();
|
|
19942
|
+
init_patterns();
|
|
19943
|
+
init_detection();
|
|
19944
|
+
var logger8 = new Logger("config-validation");
|
|
19945
|
+
function sourcesNeedRepair(sources) {
|
|
19946
|
+
if (!sources)
|
|
19947
|
+
return false;
|
|
19948
|
+
const parts = sources.split(",").map((s) => s.trim());
|
|
19949
|
+
return parts.some((part) => MONOREPO_PATTERNS.includes(part));
|
|
19950
|
+
}
|
|
19951
|
+
async function validateAndRepairConfig(directory) {
|
|
19952
|
+
const configPath = `${directory}/.sonarqube/config.json`;
|
|
19953
|
+
const repairs = [];
|
|
19954
|
+
try {
|
|
19955
|
+
const file2 = Bun.file(configPath);
|
|
19956
|
+
if (!await file2.exists()) {
|
|
19957
|
+
return { valid: true, repairs };
|
|
19958
|
+
}
|
|
19959
|
+
const config3 = await file2.json();
|
|
19960
|
+
let changed = false;
|
|
19961
|
+
if (config3.sources && sourcesNeedRepair(config3.sources)) {
|
|
19962
|
+
const oldSources = config3.sources;
|
|
19963
|
+
const newSources = detectSourceDirectories(directory);
|
|
19964
|
+
if (newSources !== oldSources) {
|
|
19965
|
+
config3.sources = newSources;
|
|
19966
|
+
changed = true;
|
|
19967
|
+
repairs.push(`Updated sources: "${oldSources}" → "${newSources}" (avoid scanning build artifacts)`);
|
|
19968
|
+
}
|
|
19969
|
+
}
|
|
19970
|
+
if (config3.sources === "" || config3.sources === ".") {
|
|
19971
|
+
const detected = detectSourceDirectories(directory);
|
|
19972
|
+
if (detected !== ".") {
|
|
19973
|
+
config3.sources = detected;
|
|
19974
|
+
changed = true;
|
|
19975
|
+
repairs.push(`Detected sources: "${detected}"`);
|
|
19976
|
+
}
|
|
19977
|
+
}
|
|
19978
|
+
if (changed) {
|
|
19979
|
+
await Bun.write(configPath, JSON.stringify(config3, null, 2));
|
|
19980
|
+
logger8.info("Repaired config.json", { repairs });
|
|
19981
|
+
}
|
|
19982
|
+
return { valid: !changed, repairs };
|
|
19983
|
+
} catch (error45) {
|
|
19984
|
+
logger8.warn("Could not validate config.json", { error: error45 });
|
|
19985
|
+
return { valid: true, repairs };
|
|
19986
|
+
}
|
|
19987
|
+
}
|
|
19988
|
+
async function validateAndRepairProperties(directory) {
|
|
19989
|
+
const propertiesPath = `${directory}/sonar-project.properties`;
|
|
19990
|
+
const repairs = [];
|
|
19991
|
+
try {
|
|
19992
|
+
const file2 = Bun.file(propertiesPath);
|
|
19993
|
+
if (!await file2.exists()) {
|
|
19994
|
+
return { valid: true, repairs };
|
|
19995
|
+
}
|
|
19996
|
+
let content = await file2.text();
|
|
19997
|
+
let changed = false;
|
|
19998
|
+
const sourcesRegex = /^sonar\.sources=(.+)$/m;
|
|
19999
|
+
const sourcesMatch = sourcesRegex.exec(content);
|
|
20000
|
+
if (sourcesMatch) {
|
|
20001
|
+
const currentSources = sourcesMatch[1];
|
|
20002
|
+
if (sourcesNeedRepair(currentSources)) {
|
|
20003
|
+
const newSources = detectSourceDirectories(directory);
|
|
20004
|
+
if (newSources !== currentSources) {
|
|
20005
|
+
content = content.replace(sourcesRegex, `sonar.sources=${newSources}`);
|
|
20006
|
+
changed = true;
|
|
20007
|
+
repairs.push(`Updated sonar.sources: "${currentSources}" → "${newSources}"`);
|
|
20008
|
+
}
|
|
20009
|
+
}
|
|
20010
|
+
}
|
|
20011
|
+
const testsRegex = /^sonar\.tests=(.+)$/m;
|
|
20012
|
+
const testsMatch = testsRegex.exec(content);
|
|
20013
|
+
if (testsMatch) {
|
|
20014
|
+
const currentTests = testsMatch[1];
|
|
20015
|
+
if (sourcesNeedRepair(currentTests)) {
|
|
20016
|
+
const newTests = detectSourceDirectories(directory);
|
|
20017
|
+
if (newTests !== currentTests) {
|
|
20018
|
+
content = content.replace(testsRegex, `sonar.tests=${newTests}`);
|
|
20019
|
+
changed = true;
|
|
20020
|
+
repairs.push(`Updated sonar.tests: "${currentTests}" → "${newTests}"`);
|
|
20021
|
+
}
|
|
20022
|
+
}
|
|
20023
|
+
}
|
|
20024
|
+
if (changed) {
|
|
20025
|
+
await Bun.write(propertiesPath, content);
|
|
20026
|
+
logger8.info("Repaired sonar-project.properties", { repairs });
|
|
20027
|
+
}
|
|
20028
|
+
return { valid: !changed, repairs };
|
|
20029
|
+
} catch (error45) {
|
|
20030
|
+
logger8.warn("Could not validate sonar-project.properties", { error: error45 });
|
|
20031
|
+
return { valid: true, repairs };
|
|
20032
|
+
}
|
|
20033
|
+
}
|
|
20034
|
+
async function validateAndRepairAll(directory) {
|
|
20035
|
+
const configResult = await validateAndRepairConfig(directory);
|
|
20036
|
+
const propertiesResult = await validateAndRepairProperties(directory);
|
|
20037
|
+
const allRepairs = [...configResult.repairs, ...propertiesResult.repairs];
|
|
20038
|
+
const allValid = configResult.valid && propertiesResult.valid;
|
|
20039
|
+
if (allRepairs.length > 0) {
|
|
20040
|
+
logger8.info("Config validation completed with repairs", {
|
|
20041
|
+
repairCount: allRepairs.length
|
|
20042
|
+
});
|
|
20043
|
+
}
|
|
20044
|
+
return { valid: allValid, repairs: allRepairs };
|
|
20045
|
+
}
|
|
20046
|
+
|
|
20047
|
+
// src/tools/handlers/setup.ts
|
|
20048
|
+
init_logger();
|
|
20049
|
+
var logger9 = new Logger("sonarqube-handler-setup");
|
|
19925
20050
|
async function handleSetup(config3, directory, force = false) {
|
|
19926
20051
|
const result = await bootstrap({ config: config3, directory, force });
|
|
19927
20052
|
if (!result.success) {
|
|
@@ -19932,7 +20057,12 @@ ${result.message}`;
|
|
|
19932
20057
|
const sonarqubeDir = `${directory}/.sonarqube`;
|
|
19933
20058
|
const configPath = `${sonarqubeDir}/config.json`;
|
|
19934
20059
|
const configExists = await Bun.file(configPath).exists();
|
|
19935
|
-
if (
|
|
20060
|
+
if (configExists) {
|
|
20061
|
+
const validation = await validateAndRepairAll(directory);
|
|
20062
|
+
if (validation.repairs.length > 0) {
|
|
20063
|
+
logger9.info("Repaired existing config", { repairs: validation.repairs });
|
|
20064
|
+
}
|
|
20065
|
+
} else {
|
|
19936
20066
|
await mkdir(sonarqubeDir, { recursive: true });
|
|
19937
20067
|
const detectedSources = detectSourceDirectories(directory);
|
|
19938
20068
|
const defaultConfig = {
|
|
@@ -19942,7 +20072,7 @@ ${result.message}`;
|
|
|
19942
20072
|
newCodeDefinition: "previous_version"
|
|
19943
20073
|
};
|
|
19944
20074
|
await Bun.write(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
19945
|
-
|
|
20075
|
+
logger9.info("Created default config.json", { path: configPath });
|
|
19946
20076
|
}
|
|
19947
20077
|
const lines = [
|
|
19948
20078
|
"## SonarQube Project Initialized",
|
|
@@ -19962,56 +20092,38 @@ ${result.message}`;
|
|
|
19962
20092
|
`);
|
|
19963
20093
|
}
|
|
19964
20094
|
// src/tools/handlers/analyze.ts
|
|
19965
|
-
var COVERAGE_EXCLUDED_FILES = [
|
|
19966
|
-
"src/bootstrap/index.ts",
|
|
19967
|
-
"src/index.ts",
|
|
19968
|
-
"src/scanner/runner.ts",
|
|
19969
|
-
"src/tools/handlers/setup.ts",
|
|
19970
|
-
"src/cli.ts",
|
|
19971
|
-
"src/utils/config.ts",
|
|
19972
|
-
"src/utils/shared-state.ts"
|
|
19973
|
-
];
|
|
19974
|
-
async function filterLcovForCoverage(directory) {
|
|
19975
|
-
const lcovPath = `${directory}/coverage/lcov.info`;
|
|
19976
|
-
try {
|
|
19977
|
-
const content = await Bun.file(lcovPath).text();
|
|
19978
|
-
const lines = content.split(`
|
|
19979
|
-
`);
|
|
19980
|
-
const filteredLines = [];
|
|
19981
|
-
let skipCurrentFile = false;
|
|
19982
|
-
for (const line of lines) {
|
|
19983
|
-
if (line.startsWith("SF:")) {
|
|
19984
|
-
const filePath = line.substring(3);
|
|
19985
|
-
skipCurrentFile = COVERAGE_EXCLUDED_FILES.some((excluded) => filePath.endsWith(excluded) || filePath.includes(`/${excluded}`));
|
|
19986
|
-
}
|
|
19987
|
-
if (!skipCurrentFile) {
|
|
19988
|
-
filteredLines.push(line);
|
|
19989
|
-
}
|
|
19990
|
-
if (line === "end_of_record") {
|
|
19991
|
-
skipCurrentFile = false;
|
|
19992
|
-
}
|
|
19993
|
-
}
|
|
19994
|
-
await Bun.write(lcovPath, filteredLines.join(`
|
|
19995
|
-
`));
|
|
19996
|
-
} catch {}
|
|
19997
|
-
}
|
|
19998
20095
|
async function handleAnalyze(ctx, args) {
|
|
19999
20096
|
const { config: config3, state, projectKey, directory } = ctx;
|
|
20000
20097
|
const projectName = config3.projectName ?? projectKey;
|
|
20001
|
-
await
|
|
20002
|
-
|
|
20098
|
+
const validation = await validateAndRepairAll(directory);
|
|
20099
|
+
let effectiveSources = config3.sources;
|
|
20100
|
+
let effectiveTests = config3.tests;
|
|
20101
|
+
if (validation.repairs.length > 0) {
|
|
20102
|
+
try {
|
|
20103
|
+
const repairedConfig = await Bun.file(`${directory}/.sonarqube/config.json`).json();
|
|
20104
|
+
effectiveSources = repairedConfig.sources ?? effectiveSources;
|
|
20105
|
+
effectiveTests = repairedConfig.tests ?? effectiveTests;
|
|
20106
|
+
} catch {}
|
|
20107
|
+
}
|
|
20003
20108
|
const result = await runAnalysis(config3, state, {
|
|
20004
20109
|
projectKey,
|
|
20005
20110
|
projectName,
|
|
20006
|
-
sources:
|
|
20007
|
-
tests:
|
|
20008
|
-
exclusions: config3.exclusions
|
|
20009
|
-
additionalProperties: {
|
|
20010
|
-
"sonar.coverage.exclusions": coverageExclusions,
|
|
20011
|
-
"sonar.javascript.lcov.reportPaths": "coverage/lcov.info"
|
|
20012
|
-
}
|
|
20111
|
+
sources: effectiveSources,
|
|
20112
|
+
tests: effectiveTests,
|
|
20113
|
+
exclusions: config3.exclusions
|
|
20013
20114
|
}, directory);
|
|
20014
|
-
let output =
|
|
20115
|
+
let output = "";
|
|
20116
|
+
if (validation.repairs.length > 0) {
|
|
20117
|
+
output += `### Config Auto-Repair
|
|
20118
|
+
`;
|
|
20119
|
+
for (const repair of validation.repairs) {
|
|
20120
|
+
output += `- ${repair}
|
|
20121
|
+
`;
|
|
20122
|
+
}
|
|
20123
|
+
output += `
|
|
20124
|
+
`;
|
|
20125
|
+
}
|
|
20126
|
+
output += formatAnalysisResult(result);
|
|
20015
20127
|
if (args.fix && result.formattedIssues.length > 0) {
|
|
20016
20128
|
output += formatFixSuggestions(result.formattedIssues);
|
|
20017
20129
|
}
|
|
@@ -20370,7 +20482,7 @@ Run \`sonarqube({ action: "analyze" })\` to generate metrics.`));
|
|
|
20370
20482
|
return output;
|
|
20371
20483
|
}
|
|
20372
20484
|
// src/tools/sonarqube.ts
|
|
20373
|
-
var
|
|
20485
|
+
var logger10 = new Logger("sonarqube-tool");
|
|
20374
20486
|
var SonarQubeToolArgsSchema = exports_external2.object({
|
|
20375
20487
|
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)"),
|
|
20376
20488
|
scope: exports_external2.enum(["all", "new", "changed"]).optional().default("all").describe("Scope of analysis: all files, only new code, or changed files"),
|
|
@@ -20399,13 +20511,13 @@ Optional:
|
|
|
20399
20511
|
- SONAR_USER
|
|
20400
20512
|
- SONAR_PASSWORD`);
|
|
20401
20513
|
}
|
|
20402
|
-
|
|
20514
|
+
logger10.info(`Executing SonarQube tool: ${args.action}`, { directory });
|
|
20403
20515
|
try {
|
|
20404
20516
|
if (args.action === "init" || args.action === "setup") {
|
|
20405
20517
|
return await handleSetup(config3, directory, args.force);
|
|
20406
20518
|
}
|
|
20407
20519
|
if (await needsBootstrap(directory)) {
|
|
20408
|
-
|
|
20520
|
+
logger10.info("First run detected, running bootstrap");
|
|
20409
20521
|
const { bootstrap: bootstrap2 } = await Promise.resolve().then(() => (init_bootstrap(), exports_bootstrap));
|
|
20410
20522
|
const setupResult = await bootstrap2({ config: config3, directory });
|
|
20411
20523
|
if (!setupResult.success) {
|
|
@@ -20413,7 +20525,7 @@ Optional:
|
|
|
20413
20525
|
|
|
20414
20526
|
${setupResult.message}`);
|
|
20415
20527
|
}
|
|
20416
|
-
|
|
20528
|
+
logger10.info("Bootstrap completed", { projectKey: setupResult.projectKey });
|
|
20417
20529
|
}
|
|
20418
20530
|
const state = await getProjectState(directory);
|
|
20419
20531
|
if (!state) {
|
|
@@ -20453,7 +20565,7 @@ ${setupResult.message}`);
|
|
|
20453
20565
|
}
|
|
20454
20566
|
} catch (error45) {
|
|
20455
20567
|
const errorMessage = error45 instanceof Error ? error45.message : String(error45);
|
|
20456
|
-
|
|
20568
|
+
logger10.error(`Tool execution failed: ${errorMessage}`);
|
|
20457
20569
|
return formatError2(ErrorMessages.apiError(args.action, errorMessage, config3.url));
|
|
20458
20570
|
}
|
|
20459
20571
|
}
|