greenbump 0.2.1 → 0.5.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/CHANGELOG.md +82 -0
- package/README.md +2 -0
- package/action-dist/index.cjs +1054 -19
- package/competitive-analysis.md +304 -0
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/engine/change-detector.js +152 -0
- package/dist/engine/change-detector.js.map +1 -0
- package/dist/engine/dep-graph.js +101 -0
- package/dist/engine/dep-graph.js.map +1 -0
- package/dist/engine/git.js +10 -0
- package/dist/engine/git.js.map +1 -1
- package/dist/engine/perf/metrics.js +171 -0
- package/dist/engine/perf/metrics.js.map +1 -0
- package/dist/engine/perf/regression.js +115 -0
- package/dist/engine/perf/regression.js.map +1 -0
- package/dist/engine/run.js +71 -2
- package/dist/engine/run.js.map +1 -1
- package/dist/engine/sandbox/docker-manager.js +215 -0
- package/dist/engine/sandbox/docker-manager.js.map +1 -0
- package/dist/engine/sandbox/orchestrator.js +107 -0
- package/dist/engine/sandbox/orchestrator.js.map +1 -0
- package/dist/engine/sandbox/service-detector.js +147 -0
- package/dist/engine/sandbox/service-detector.js.map +1 -0
- package/dist/engine/sandbox/template-generator.js +241 -0
- package/dist/engine/sandbox/template-generator.js.map +1 -0
- package/dist/engine/staged-fix.js +108 -0
- package/dist/engine/staged-fix.js.map +1 -0
- package/dist/engine/stages.js +125 -0
- package/dist/engine/stages.js.map +1 -0
- package/dist/engine/verify.js +121 -0
- package/dist/engine/verify.js.map +1 -0
- package/dist/format.js +20 -0
- package/dist/format.js.map +1 -1
- package/optimization-roadmap.md +772 -0
- package/package.json +4 -1
package/action-dist/index.cjs
CHANGED
|
@@ -6503,8 +6503,8 @@ async function pathExists(p2) {
|
|
|
6503
6503
|
// dist/engine/ecosystems/npm.js
|
|
6504
6504
|
async function readScripts(cwd) {
|
|
6505
6505
|
try {
|
|
6506
|
-
const { readFile:
|
|
6507
|
-
const raw = await
|
|
6506
|
+
const { readFile: readFile13 } = await import("node:fs/promises");
|
|
6507
|
+
const raw = await readFile13((0, import_node_path.join)(cwd, "package.json"), "utf8");
|
|
6508
6508
|
const pkg = JSON.parse(raw);
|
|
6509
6509
|
return { build: pkg.scripts?.build, test: pkg.scripts?.test };
|
|
6510
6510
|
} catch {
|
|
@@ -6546,8 +6546,8 @@ var npmAdapter = {
|
|
|
6546
6546
|
var import_node_path2 = require("node:path");
|
|
6547
6547
|
async function readScripts2(cwd) {
|
|
6548
6548
|
try {
|
|
6549
|
-
const { readFile:
|
|
6550
|
-
const raw = await
|
|
6549
|
+
const { readFile: readFile13 } = await import("node:fs/promises");
|
|
6550
|
+
const raw = await readFile13((0, import_node_path2.join)(cwd, "package.json"), "utf8");
|
|
6551
6551
|
const pkg = JSON.parse(raw);
|
|
6552
6552
|
return { build: pkg.scripts?.build, test: pkg.scripts?.test };
|
|
6553
6553
|
} catch {
|
|
@@ -6598,8 +6598,8 @@ var yarnAdapter = {
|
|
|
6598
6598
|
var import_node_path3 = require("node:path");
|
|
6599
6599
|
async function readScripts3(cwd) {
|
|
6600
6600
|
try {
|
|
6601
|
-
const { readFile:
|
|
6602
|
-
const raw = await
|
|
6601
|
+
const { readFile: readFile13 } = await import("node:fs/promises");
|
|
6602
|
+
const raw = await readFile13((0, import_node_path3.join)(cwd, "package.json"), "utf8");
|
|
6603
6603
|
const pkg = JSON.parse(raw);
|
|
6604
6604
|
return { build: pkg.scripts?.build, test: pkg.scripts?.test };
|
|
6605
6605
|
} catch {
|
|
@@ -6992,14 +6992,14 @@ var bundlerAdapter = {
|
|
|
6992
6992
|
}
|
|
6993
6993
|
};
|
|
6994
6994
|
async function pinInGemfile(cwd, name, version, fallback) {
|
|
6995
|
-
const { readFile:
|
|
6995
|
+
const { readFile: readFile13, writeFile: writeFile10 } = await import("node:fs/promises");
|
|
6996
6996
|
try {
|
|
6997
6997
|
const path2 = (0, import_node_path11.join)(cwd, "Gemfile");
|
|
6998
|
-
const raw = await
|
|
6998
|
+
const raw = await readFile13(path2, "utf8");
|
|
6999
6999
|
const re2 = new RegExp(`^(\\s*gem\\s+["']${name}["'])(.*)$`, "m");
|
|
7000
7000
|
if (re2.test(raw)) {
|
|
7001
7001
|
const next = raw.replace(re2, `$1, "= ${version}"`);
|
|
7002
|
-
await
|
|
7002
|
+
await writeFile10(path2, next, "utf8");
|
|
7003
7003
|
}
|
|
7004
7004
|
} catch {
|
|
7005
7005
|
}
|
|
@@ -7087,7 +7087,7 @@ var mavenAdapter = {
|
|
|
7087
7087
|
return out;
|
|
7088
7088
|
},
|
|
7089
7089
|
async install(cwd, name, version) {
|
|
7090
|
-
const { readFile: rf, writeFile:
|
|
7090
|
+
const { readFile: rf, writeFile: writeFile10 } = await import("node:fs/promises");
|
|
7091
7091
|
const path2 = (0, import_node_path13.join)(cwd, "pom.xml");
|
|
7092
7092
|
const [, artifactId] = name.split(":");
|
|
7093
7093
|
const raw = await rf(path2, "utf8");
|
|
@@ -7095,7 +7095,7 @@ var mavenAdapter = {
|
|
|
7095
7095
|
if (!re2.test(raw)) {
|
|
7096
7096
|
return { code: 1, stdout: "", stderr: `could not find ${name} in pom.xml`, combined: `could not find ${name} in pom.xml` };
|
|
7097
7097
|
}
|
|
7098
|
-
await
|
|
7098
|
+
await writeFile10(path2, raw.replace(re2, `$1${version}$3`), "utf8");
|
|
7099
7099
|
return { code: 0, stdout: `pinned ${name} to ${version} in pom.xml`, stderr: "", combined: "" };
|
|
7100
7100
|
},
|
|
7101
7101
|
async defaultCheckCommands() {
|
|
@@ -7278,14 +7278,14 @@ var mixAdapter = {
|
|
|
7278
7278
|
return parseMixOutdated(r2.stdout);
|
|
7279
7279
|
},
|
|
7280
7280
|
async install(cwd, name, version) {
|
|
7281
|
-
const { readFile:
|
|
7281
|
+
const { readFile: readFile13, writeFile: writeFile10 } = await import("node:fs/promises");
|
|
7282
7282
|
const path2 = (0, import_node_path15.join)(cwd, "mix.exs");
|
|
7283
|
-
const raw = await
|
|
7283
|
+
const raw = await readFile13(path2, "utf8");
|
|
7284
7284
|
const re2 = new RegExp(`(\\{:${name},\\s*)"[^"]+"`);
|
|
7285
7285
|
if (!re2.test(raw)) {
|
|
7286
7286
|
return { code: 1, stdout: "", stderr: `could not find :${name} in mix.exs`, combined: `could not find :${name} in mix.exs` };
|
|
7287
7287
|
}
|
|
7288
|
-
await
|
|
7288
|
+
await writeFile10(path2, raw.replace(re2, `$1"== ${version}"`), "utf8");
|
|
7289
7289
|
return { code: 0, stdout: `pinned ${name} to ${version} in mix.exs`, stderr: "", combined: "" };
|
|
7290
7290
|
},
|
|
7291
7291
|
async defaultCheckCommands() {
|
|
@@ -7616,8 +7616,8 @@ var elmAdapter = {
|
|
|
7616
7616
|
return { code: 1, stdout: "", stderr: `${name} is not a direct dependency in elm.json`, combined: `${name} is not a direct dependency in elm.json` };
|
|
7617
7617
|
}
|
|
7618
7618
|
json.dependencies.direct[name] = version;
|
|
7619
|
-
const { writeFile:
|
|
7620
|
-
await
|
|
7619
|
+
const { writeFile: writeFile10 } = await import("node:fs/promises");
|
|
7620
|
+
await writeFile10(path2, JSON.stringify(json, null, 4), "utf8");
|
|
7621
7621
|
return { code: 0, stdout: `pinned ${name} to ${version} in elm.json`, stderr: "", combined: "" };
|
|
7622
7622
|
},
|
|
7623
7623
|
async defaultCheckCommands() {
|
|
@@ -19295,6 +19295,986 @@ ${listProviders()}`);
|
|
|
19295
19295
|
return new OpenAICompatProvider(presetName, apiKey, baseURL, model);
|
|
19296
19296
|
}
|
|
19297
19297
|
|
|
19298
|
+
// dist/engine/verify.js
|
|
19299
|
+
var import_node_child_process2 = require("node:child_process");
|
|
19300
|
+
var import_node_util = require("node:util");
|
|
19301
|
+
var import_node_path22 = require("node:path");
|
|
19302
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
|
|
19303
|
+
async function runStaticAnalysis(cwd, options = {}) {
|
|
19304
|
+
const { checkTypes = true, checkLint = true, strictLint = false } = options;
|
|
19305
|
+
const results = [];
|
|
19306
|
+
if (checkTypes && await hasTsConfig(cwd)) {
|
|
19307
|
+
try {
|
|
19308
|
+
const { stdout, stderr } = await execAsync("npx tsc --noEmit", {
|
|
19309
|
+
cwd,
|
|
19310
|
+
timeout: 6e4
|
|
19311
|
+
// 1 minute timeout
|
|
19312
|
+
});
|
|
19313
|
+
results.push({
|
|
19314
|
+
passed: true,
|
|
19315
|
+
stage: "types",
|
|
19316
|
+
output: stdout + stderr
|
|
19317
|
+
});
|
|
19318
|
+
} catch (err) {
|
|
19319
|
+
const output = err.stdout + err.stderr;
|
|
19320
|
+
results.push({
|
|
19321
|
+
passed: false,
|
|
19322
|
+
stage: "types",
|
|
19323
|
+
output,
|
|
19324
|
+
warnings: parseTypeErrors(output)
|
|
19325
|
+
});
|
|
19326
|
+
}
|
|
19327
|
+
}
|
|
19328
|
+
if (checkLint && await hasEslintConfig(cwd)) {
|
|
19329
|
+
try {
|
|
19330
|
+
const { stdout, stderr } = await execAsync("npx eslint . --ext .ts,.tsx,.js,.jsx --format compact", {
|
|
19331
|
+
cwd,
|
|
19332
|
+
timeout: 6e4
|
|
19333
|
+
});
|
|
19334
|
+
results.push({
|
|
19335
|
+
passed: true,
|
|
19336
|
+
stage: "lint",
|
|
19337
|
+
output: stdout + stderr
|
|
19338
|
+
});
|
|
19339
|
+
} catch (err) {
|
|
19340
|
+
const output = err.stdout + err.stderr;
|
|
19341
|
+
results.push({
|
|
19342
|
+
passed: strictLint ? false : true,
|
|
19343
|
+
// Lint warnings don't fail by default
|
|
19344
|
+
stage: "lint",
|
|
19345
|
+
output,
|
|
19346
|
+
warnings: parseLintWarnings(output)
|
|
19347
|
+
});
|
|
19348
|
+
}
|
|
19349
|
+
}
|
|
19350
|
+
return results;
|
|
19351
|
+
}
|
|
19352
|
+
async function hasTsConfig(cwd) {
|
|
19353
|
+
return await pathExists((0, import_node_path22.join)(cwd, "tsconfig.json"));
|
|
19354
|
+
}
|
|
19355
|
+
async function hasEslintConfig(cwd) {
|
|
19356
|
+
const configs = [
|
|
19357
|
+
".eslintrc.js",
|
|
19358
|
+
".eslintrc.cjs",
|
|
19359
|
+
".eslintrc.json",
|
|
19360
|
+
".eslintrc.yml",
|
|
19361
|
+
".eslintrc.yaml"
|
|
19362
|
+
];
|
|
19363
|
+
for (const cfg of configs) {
|
|
19364
|
+
if (await pathExists((0, import_node_path22.join)(cwd, cfg)))
|
|
19365
|
+
return true;
|
|
19366
|
+
}
|
|
19367
|
+
try {
|
|
19368
|
+
const pkgPath = (0, import_node_path22.join)(cwd, "package.json");
|
|
19369
|
+
if (await pathExists(pkgPath)) {
|
|
19370
|
+
const pkg = JSON.parse(await import("node:fs/promises").then((fs2) => fs2.readFile(pkgPath, "utf8")));
|
|
19371
|
+
if (pkg.eslintConfig)
|
|
19372
|
+
return true;
|
|
19373
|
+
}
|
|
19374
|
+
} catch {
|
|
19375
|
+
}
|
|
19376
|
+
return false;
|
|
19377
|
+
}
|
|
19378
|
+
function parseTypeErrors(output) {
|
|
19379
|
+
const lines = output.split("\n");
|
|
19380
|
+
const errors = [];
|
|
19381
|
+
for (const line of lines) {
|
|
19382
|
+
if (/error TS\d+:/.test(line)) {
|
|
19383
|
+
const match = line.match(/^(.+?\.\w+)\(\d+,\d+\): (.+)$/);
|
|
19384
|
+
if (match) {
|
|
19385
|
+
errors.push(`${match[1]}: ${match[2]}`);
|
|
19386
|
+
} else {
|
|
19387
|
+
errors.push(line.trim());
|
|
19388
|
+
}
|
|
19389
|
+
}
|
|
19390
|
+
}
|
|
19391
|
+
return errors.slice(0, 10);
|
|
19392
|
+
}
|
|
19393
|
+
function parseLintWarnings(output) {
|
|
19394
|
+
const lines = output.split("\n");
|
|
19395
|
+
const warnings = [];
|
|
19396
|
+
for (const line of lines) {
|
|
19397
|
+
if (line.includes("Error") || line.includes("Warning")) {
|
|
19398
|
+
warnings.push(line.trim());
|
|
19399
|
+
}
|
|
19400
|
+
}
|
|
19401
|
+
return warnings.slice(0, 10);
|
|
19402
|
+
}
|
|
19403
|
+
|
|
19404
|
+
// dist/engine/change-detector.js
|
|
19405
|
+
var import_node_child_process3 = require("node:child_process");
|
|
19406
|
+
var import_node_util2 = require("node:util");
|
|
19407
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process3.exec);
|
|
19408
|
+
async function detectSuspiciousChanges(cwd) {
|
|
19409
|
+
const changes = [];
|
|
19410
|
+
try {
|
|
19411
|
+
const { stdout: diff } = await execAsync2("git diff HEAD", { cwd });
|
|
19412
|
+
if (!diff.trim())
|
|
19413
|
+
return [];
|
|
19414
|
+
const testFileChanges = detectTestFileChanges(diff);
|
|
19415
|
+
changes.push(...testFileChanges);
|
|
19416
|
+
const largeDeletions = detectLargeDeletions(diff);
|
|
19417
|
+
changes.push(...largeDeletions);
|
|
19418
|
+
const commentedTests = detectCommentedTests(diff);
|
|
19419
|
+
changes.push(...commentedTests);
|
|
19420
|
+
const removedTests = detectRemovedTests(diff);
|
|
19421
|
+
changes.push(...removedTests);
|
|
19422
|
+
} catch (err) {
|
|
19423
|
+
}
|
|
19424
|
+
return changes;
|
|
19425
|
+
}
|
|
19426
|
+
function detectTestFileChanges(diff) {
|
|
19427
|
+
const changes = [];
|
|
19428
|
+
const testFilePattern = /\.(test|spec)\.(ts|js|tsx|jsx)$/;
|
|
19429
|
+
const fileHeaderPattern = /^diff --git a\/(.+?) b\/(.+?)$/gm;
|
|
19430
|
+
let match;
|
|
19431
|
+
while ((match = fileHeaderPattern.exec(diff)) !== null) {
|
|
19432
|
+
const filePath = match[2];
|
|
19433
|
+
if (testFilePattern.test(filePath)) {
|
|
19434
|
+
changes.push({
|
|
19435
|
+
type: "test-modified",
|
|
19436
|
+
file: filePath,
|
|
19437
|
+
description: "Test file was modified during fix loop",
|
|
19438
|
+
severity: "critical"
|
|
19439
|
+
});
|
|
19440
|
+
}
|
|
19441
|
+
}
|
|
19442
|
+
return changes;
|
|
19443
|
+
}
|
|
19444
|
+
function detectLargeDeletions(diff) {
|
|
19445
|
+
const changes = [];
|
|
19446
|
+
const fileDiffs = diff.split(/^diff --git /m).slice(1);
|
|
19447
|
+
for (const fileDiff of fileDiffs) {
|
|
19448
|
+
const fileMatch = fileDiff.match(/^a\/(.+?) b\/(.+?)$/m);
|
|
19449
|
+
if (!fileMatch)
|
|
19450
|
+
continue;
|
|
19451
|
+
const filePath = fileMatch[2];
|
|
19452
|
+
const lines = fileDiff.split("\n");
|
|
19453
|
+
let deletions = 0;
|
|
19454
|
+
for (const line of lines) {
|
|
19455
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
19456
|
+
deletions++;
|
|
19457
|
+
}
|
|
19458
|
+
}
|
|
19459
|
+
if (deletions > 50) {
|
|
19460
|
+
changes.push({
|
|
19461
|
+
type: "large-deletion",
|
|
19462
|
+
file: filePath,
|
|
19463
|
+
description: `${deletions} lines deleted (possible over-aggressive fix)`,
|
|
19464
|
+
severity: "warning"
|
|
19465
|
+
});
|
|
19466
|
+
}
|
|
19467
|
+
}
|
|
19468
|
+
return changes;
|
|
19469
|
+
}
|
|
19470
|
+
function detectCommentedTests(diff) {
|
|
19471
|
+
const changes = [];
|
|
19472
|
+
const lines = diff.split("\n");
|
|
19473
|
+
for (let i2 = 0; i2 < lines.length - 1; i2++) {
|
|
19474
|
+
const prevLine = lines[i2];
|
|
19475
|
+
const currLine = lines[i2 + 1];
|
|
19476
|
+
if (prevLine.startsWith("-") && /\b(it|test|describe)\(/.test(prevLine) && currLine.startsWith("+") && (/^[+]\s*(\/\/|\/\*)/.test(currLine) || currLine.includes("// it(") || currLine.includes("// test(") || currLine.includes("/* it(") || currLine.includes("/* test("))) {
|
|
19477
|
+
const fileContext = findFileContext(diff, i2);
|
|
19478
|
+
changes.push({
|
|
19479
|
+
type: "test-commented",
|
|
19480
|
+
file: fileContext || "unknown",
|
|
19481
|
+
description: "Test case appears to have been commented out",
|
|
19482
|
+
severity: "critical"
|
|
19483
|
+
});
|
|
19484
|
+
}
|
|
19485
|
+
}
|
|
19486
|
+
return changes;
|
|
19487
|
+
}
|
|
19488
|
+
function detectRemovedTests(diff) {
|
|
19489
|
+
const changes = [];
|
|
19490
|
+
const lines = diff.split("\n");
|
|
19491
|
+
let removedTestCount = 0;
|
|
19492
|
+
let currentFile = "unknown";
|
|
19493
|
+
for (const line of lines) {
|
|
19494
|
+
const fileMatch = line.match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
19495
|
+
if (fileMatch) {
|
|
19496
|
+
currentFile = fileMatch[2];
|
|
19497
|
+
removedTestCount = 0;
|
|
19498
|
+
}
|
|
19499
|
+
if (line.startsWith("-") && !line.startsWith("---") && /\b(it|test)\s*\(/.test(line)) {
|
|
19500
|
+
removedTestCount++;
|
|
19501
|
+
}
|
|
19502
|
+
}
|
|
19503
|
+
if (removedTestCount > 0) {
|
|
19504
|
+
changes.push({
|
|
19505
|
+
type: "test-removed",
|
|
19506
|
+
file: currentFile,
|
|
19507
|
+
description: `${removedTestCount} test case(s) were deleted`,
|
|
19508
|
+
severity: "critical"
|
|
19509
|
+
});
|
|
19510
|
+
}
|
|
19511
|
+
return changes;
|
|
19512
|
+
}
|
|
19513
|
+
function findFileContext(diff, lineIndex) {
|
|
19514
|
+
const lines = diff.split("\n");
|
|
19515
|
+
for (let i2 = lineIndex; i2 >= 0; i2--) {
|
|
19516
|
+
const match = lines[i2].match(/^diff --git a\/(.+?) b\/(.+?)$/);
|
|
19517
|
+
if (match)
|
|
19518
|
+
return match[2];
|
|
19519
|
+
}
|
|
19520
|
+
return null;
|
|
19521
|
+
}
|
|
19522
|
+
|
|
19523
|
+
// dist/engine/sandbox/orchestrator.js
|
|
19524
|
+
var import_path12 = require("path");
|
|
19525
|
+
var import_promises14 = require("fs/promises");
|
|
19526
|
+
|
|
19527
|
+
// dist/engine/sandbox/docker-manager.js
|
|
19528
|
+
var import_fs2 = require("fs");
|
|
19529
|
+
var import_path9 = require("path");
|
|
19530
|
+
var import_promises12 = require("fs/promises");
|
|
19531
|
+
async function checkDockerAvailable() {
|
|
19532
|
+
const info2 = {
|
|
19533
|
+
available: false,
|
|
19534
|
+
composeAvailable: false
|
|
19535
|
+
};
|
|
19536
|
+
try {
|
|
19537
|
+
const versionResult = await exec("docker", ["--version"], { cwd: process.cwd() });
|
|
19538
|
+
info2.version = versionResult.stdout.trim();
|
|
19539
|
+
await exec("docker", ["ps"], { cwd: process.cwd() });
|
|
19540
|
+
info2.available = true;
|
|
19541
|
+
try {
|
|
19542
|
+
await exec("docker-compose", ["--version"], { cwd: process.cwd() });
|
|
19543
|
+
info2.composeAvailable = true;
|
|
19544
|
+
} catch {
|
|
19545
|
+
}
|
|
19546
|
+
} catch {
|
|
19547
|
+
}
|
|
19548
|
+
return info2;
|
|
19549
|
+
}
|
|
19550
|
+
async function buildDockerImage(cwd, imageName, dockerfilePath) {
|
|
19551
|
+
console.log(`\u{1F433} Building Docker image: ${imageName}...`);
|
|
19552
|
+
await exec("docker", [
|
|
19553
|
+
"build",
|
|
19554
|
+
"-t",
|
|
19555
|
+
imageName,
|
|
19556
|
+
"-f",
|
|
19557
|
+
dockerfilePath,
|
|
19558
|
+
"."
|
|
19559
|
+
], { cwd });
|
|
19560
|
+
}
|
|
19561
|
+
async function runVerificationInContainer(cwd, options) {
|
|
19562
|
+
const args = [
|
|
19563
|
+
"run",
|
|
19564
|
+
"--rm",
|
|
19565
|
+
"-v",
|
|
19566
|
+
`${cwd}:/workspace`,
|
|
19567
|
+
"-w",
|
|
19568
|
+
"/workspace"
|
|
19569
|
+
];
|
|
19570
|
+
if (options.networkMode) {
|
|
19571
|
+
args.push("--network", options.networkMode);
|
|
19572
|
+
}
|
|
19573
|
+
if (options.env) {
|
|
19574
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
19575
|
+
args.push("-e", `${key}=${value}`);
|
|
19576
|
+
}
|
|
19577
|
+
}
|
|
19578
|
+
args.push(options.image);
|
|
19579
|
+
args.push("sh", "-c", "npm install && npm run build 2>&1 && npm test 2>&1");
|
|
19580
|
+
let exitCode = 0;
|
|
19581
|
+
let stdout = "";
|
|
19582
|
+
let stderr = "";
|
|
19583
|
+
let buildSuccess = false;
|
|
19584
|
+
let testsPassed = false;
|
|
19585
|
+
try {
|
|
19586
|
+
const result = await exec("docker", args, {
|
|
19587
|
+
cwd,
|
|
19588
|
+
timeout: options.timeout || 6e5
|
|
19589
|
+
// 10 minutes default
|
|
19590
|
+
});
|
|
19591
|
+
stdout = result.stdout;
|
|
19592
|
+
stderr = result.stderr;
|
|
19593
|
+
exitCode = 0;
|
|
19594
|
+
buildSuccess = true;
|
|
19595
|
+
testsPassed = true;
|
|
19596
|
+
} catch (error) {
|
|
19597
|
+
exitCode = error.exitCode || 1;
|
|
19598
|
+
stdout = error.stdout || "";
|
|
19599
|
+
stderr = error.stderr || "";
|
|
19600
|
+
buildSuccess = !stderr.includes("npm run build") && !stdout.includes("build failed");
|
|
19601
|
+
testsPassed = false;
|
|
19602
|
+
}
|
|
19603
|
+
return {
|
|
19604
|
+
exitCode,
|
|
19605
|
+
stdout,
|
|
19606
|
+
stderr,
|
|
19607
|
+
buildSuccess,
|
|
19608
|
+
testsPassed
|
|
19609
|
+
};
|
|
19610
|
+
}
|
|
19611
|
+
async function startComposeServices(cwd, composeFile) {
|
|
19612
|
+
console.log("\u{1F680} Starting docker-compose services...");
|
|
19613
|
+
const projectName = `greenbump-${Date.now()}`;
|
|
19614
|
+
await exec("docker-compose", [
|
|
19615
|
+
"-f",
|
|
19616
|
+
composeFile,
|
|
19617
|
+
"-p",
|
|
19618
|
+
projectName,
|
|
19619
|
+
"up",
|
|
19620
|
+
"-d"
|
|
19621
|
+
], { cwd });
|
|
19622
|
+
return projectName;
|
|
19623
|
+
}
|
|
19624
|
+
async function checkServiceHealth(projectName, serviceName) {
|
|
19625
|
+
try {
|
|
19626
|
+
const result = await exec("docker-compose", ["-p", projectName, "ps", "--filter", `name=${serviceName}`, "--format", "json"], { cwd: process.cwd() });
|
|
19627
|
+
const output = result.stdout.trim();
|
|
19628
|
+
if (!output)
|
|
19629
|
+
return false;
|
|
19630
|
+
return output.includes('"State":"running"') || output.includes("healthy");
|
|
19631
|
+
} catch {
|
|
19632
|
+
return false;
|
|
19633
|
+
}
|
|
19634
|
+
}
|
|
19635
|
+
async function waitForServices(projectName, services, timeout = 60) {
|
|
19636
|
+
console.log("\u23F3 Waiting for services to become healthy...");
|
|
19637
|
+
const deadline = Date.now() + timeout * 1e3;
|
|
19638
|
+
const healthyServices = /* @__PURE__ */ new Set();
|
|
19639
|
+
while (Date.now() < deadline && healthyServices.size < services.length) {
|
|
19640
|
+
for (const service of services) {
|
|
19641
|
+
if (healthyServices.has(service))
|
|
19642
|
+
continue;
|
|
19643
|
+
const healthy = await checkServiceHealth(projectName, service);
|
|
19644
|
+
if (healthy) {
|
|
19645
|
+
console.log(`\u2713 ${service} is healthy`);
|
|
19646
|
+
healthyServices.add(service);
|
|
19647
|
+
}
|
|
19648
|
+
}
|
|
19649
|
+
if (healthyServices.size < services.length) {
|
|
19650
|
+
await sleep3(2e3);
|
|
19651
|
+
}
|
|
19652
|
+
}
|
|
19653
|
+
if (healthyServices.size < services.length) {
|
|
19654
|
+
const unhealthy = services.filter((s2) => !healthyServices.has(s2));
|
|
19655
|
+
throw new Error(`Timeout waiting for services to become healthy: ${unhealthy.join(", ")}`);
|
|
19656
|
+
}
|
|
19657
|
+
}
|
|
19658
|
+
async function stopComposeServices(cwd, composeFile, projectName) {
|
|
19659
|
+
console.log("\u{1F6D1} Stopping docker-compose services...");
|
|
19660
|
+
try {
|
|
19661
|
+
await exec("docker-compose", [
|
|
19662
|
+
"-f",
|
|
19663
|
+
composeFile,
|
|
19664
|
+
"-p",
|
|
19665
|
+
projectName,
|
|
19666
|
+
"down",
|
|
19667
|
+
"-v"
|
|
19668
|
+
], { cwd });
|
|
19669
|
+
} catch (error) {
|
|
19670
|
+
console.warn("Warning: Failed to stop compose services:", error);
|
|
19671
|
+
}
|
|
19672
|
+
}
|
|
19673
|
+
async function removeDockerImage(imageName) {
|
|
19674
|
+
try {
|
|
19675
|
+
await exec("docker", ["rmi", imageName], { cwd: process.cwd() });
|
|
19676
|
+
} catch {
|
|
19677
|
+
}
|
|
19678
|
+
}
|
|
19679
|
+
async function cleanupDocker(cwd, imageName, composeFile, projectName) {
|
|
19680
|
+
console.log("\u{1F9F9} Cleaning up Docker resources...");
|
|
19681
|
+
if (composeFile && projectName) {
|
|
19682
|
+
await stopComposeServices(cwd, composeFile, projectName);
|
|
19683
|
+
}
|
|
19684
|
+
await removeDockerImage(imageName);
|
|
19685
|
+
const tempFiles = [
|
|
19686
|
+
(0, import_path9.join)(cwd, ".greenbump-Dockerfile"),
|
|
19687
|
+
(0, import_path9.join)(cwd, ".greenbump-compose.yml")
|
|
19688
|
+
];
|
|
19689
|
+
for (const file of tempFiles) {
|
|
19690
|
+
try {
|
|
19691
|
+
if ((0, import_fs2.existsSync)(file)) {
|
|
19692
|
+
await (0, import_promises12.rm)(file);
|
|
19693
|
+
}
|
|
19694
|
+
} catch {
|
|
19695
|
+
}
|
|
19696
|
+
}
|
|
19697
|
+
}
|
|
19698
|
+
function sleep3(ms) {
|
|
19699
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
19700
|
+
}
|
|
19701
|
+
|
|
19702
|
+
// dist/engine/sandbox/template-generator.js
|
|
19703
|
+
var import_fs3 = require("fs");
|
|
19704
|
+
var import_path10 = require("path");
|
|
19705
|
+
async function detectProjectType(cwd) {
|
|
19706
|
+
if ((0, import_fs3.existsSync)((0, import_path10.join)(cwd, "package.json"))) {
|
|
19707
|
+
return "nodejs";
|
|
19708
|
+
}
|
|
19709
|
+
if ((0, import_fs3.existsSync)((0, import_path10.join)(cwd, "requirements.txt")) || (0, import_fs3.existsSync)((0, import_path10.join)(cwd, "pyproject.toml")) || (0, import_fs3.existsSync)((0, import_path10.join)(cwd, "setup.py"))) {
|
|
19710
|
+
return "python";
|
|
19711
|
+
}
|
|
19712
|
+
return "unknown";
|
|
19713
|
+
}
|
|
19714
|
+
async function generateDockerfile(projectType, cwd) {
|
|
19715
|
+
if (projectType === "nodejs") {
|
|
19716
|
+
return await generateNodeJsDockerfile(cwd);
|
|
19717
|
+
}
|
|
19718
|
+
if (projectType === "python") {
|
|
19719
|
+
return await generatePythonDockerfile(cwd);
|
|
19720
|
+
}
|
|
19721
|
+
return await generateNodeJsDockerfile(cwd);
|
|
19722
|
+
}
|
|
19723
|
+
async function generateNodeJsDockerfile(cwd) {
|
|
19724
|
+
const hasPnpm = (0, import_fs3.existsSync)((0, import_path10.join)(cwd, "pnpm-lock.yaml"));
|
|
19725
|
+
const hasYarn = (0, import_fs3.existsSync)((0, import_path10.join)(cwd, "yarn.lock"));
|
|
19726
|
+
let installCmd = "npm ci";
|
|
19727
|
+
let copyLockFile = "COPY package*.json ./";
|
|
19728
|
+
if (hasPnpm) {
|
|
19729
|
+
installCmd = "corepack enable && pnpm install --frozen-lockfile";
|
|
19730
|
+
copyLockFile = "COPY package*.json pnpm-lock.yaml ./";
|
|
19731
|
+
} else if (hasYarn) {
|
|
19732
|
+
installCmd = "yarn install --frozen-lockfile";
|
|
19733
|
+
copyLockFile = "COPY package*.json yarn.lock ./";
|
|
19734
|
+
}
|
|
19735
|
+
return `FROM node:20-slim
|
|
19736
|
+
|
|
19737
|
+
WORKDIR /workspace
|
|
19738
|
+
|
|
19739
|
+
# Copy package files
|
|
19740
|
+
${copyLockFile}
|
|
19741
|
+
|
|
19742
|
+
# Install dependencies
|
|
19743
|
+
RUN ${installCmd}
|
|
19744
|
+
|
|
19745
|
+
# Copy source code
|
|
19746
|
+
COPY . .
|
|
19747
|
+
|
|
19748
|
+
# Build if build script exists
|
|
19749
|
+
RUN if grep -q '"build":' package.json; then npm run build || true; fi
|
|
19750
|
+
|
|
19751
|
+
# Default command: run tests
|
|
19752
|
+
CMD ["npm", "test"]
|
|
19753
|
+
`;
|
|
19754
|
+
}
|
|
19755
|
+
async function generatePythonDockerfile(cwd) {
|
|
19756
|
+
const hasRequirements = (0, import_fs3.existsSync)((0, import_path10.join)(cwd, "requirements.txt"));
|
|
19757
|
+
const hasPyproject = (0, import_fs3.existsSync)((0, import_path10.join)(cwd, "pyproject.toml"));
|
|
19758
|
+
let installCmd = "";
|
|
19759
|
+
let copyFiles = "";
|
|
19760
|
+
if (hasRequirements) {
|
|
19761
|
+
copyFiles = "COPY requirements*.txt ./";
|
|
19762
|
+
installCmd = "pip install --no-cache-dir -r requirements.txt";
|
|
19763
|
+
} else if (hasPyproject) {
|
|
19764
|
+
copyFiles = "COPY pyproject.toml setup.py* ./";
|
|
19765
|
+
installCmd = "pip install --no-cache-dir -e .";
|
|
19766
|
+
}
|
|
19767
|
+
return `FROM python:3.11-slim
|
|
19768
|
+
|
|
19769
|
+
WORKDIR /workspace
|
|
19770
|
+
|
|
19771
|
+
# Copy dependency files
|
|
19772
|
+
${copyFiles}
|
|
19773
|
+
|
|
19774
|
+
# Install dependencies
|
|
19775
|
+
RUN ${installCmd}
|
|
19776
|
+
|
|
19777
|
+
# Copy source code
|
|
19778
|
+
COPY . .
|
|
19779
|
+
|
|
19780
|
+
# Default command: run tests
|
|
19781
|
+
CMD ["pytest"]
|
|
19782
|
+
`;
|
|
19783
|
+
}
|
|
19784
|
+
async function generateComposeFile(services) {
|
|
19785
|
+
const serviceConfigs = services.map(getServiceConfig).filter((config) => config !== null);
|
|
19786
|
+
if (serviceConfigs.length === 0) {
|
|
19787
|
+
return "";
|
|
19788
|
+
}
|
|
19789
|
+
const servicesYaml = serviceConfigs.map(formatServiceConfig).join("\n");
|
|
19790
|
+
return `version: '3.8'
|
|
19791
|
+
|
|
19792
|
+
services:
|
|
19793
|
+
${servicesYaml}
|
|
19794
|
+
|
|
19795
|
+
networks:
|
|
19796
|
+
default:
|
|
19797
|
+
name: greenbump_network
|
|
19798
|
+
`;
|
|
19799
|
+
}
|
|
19800
|
+
function getServiceConfig(serviceName) {
|
|
19801
|
+
const configs = {
|
|
19802
|
+
postgres: {
|
|
19803
|
+
name: "postgres",
|
|
19804
|
+
image: "postgres:16-alpine",
|
|
19805
|
+
ports: ["5432:5432"],
|
|
19806
|
+
environment: {
|
|
19807
|
+
POSTGRES_USER: "test",
|
|
19808
|
+
POSTGRES_PASSWORD: "test",
|
|
19809
|
+
POSTGRES_DB: "test"
|
|
19810
|
+
},
|
|
19811
|
+
healthcheck: {
|
|
19812
|
+
test: ["CMD-SHELL", "pg_isready -U test"],
|
|
19813
|
+
interval: "5s",
|
|
19814
|
+
timeout: "5s",
|
|
19815
|
+
retries: 5
|
|
19816
|
+
}
|
|
19817
|
+
},
|
|
19818
|
+
mysql: {
|
|
19819
|
+
name: "mysql",
|
|
19820
|
+
image: "mysql:8",
|
|
19821
|
+
ports: ["3306:3306"],
|
|
19822
|
+
environment: {
|
|
19823
|
+
MYSQL_ROOT_PASSWORD: "test",
|
|
19824
|
+
MYSQL_DATABASE: "test"
|
|
19825
|
+
},
|
|
19826
|
+
healthcheck: {
|
|
19827
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"],
|
|
19828
|
+
interval: "5s",
|
|
19829
|
+
timeout: "5s",
|
|
19830
|
+
retries: 5
|
|
19831
|
+
}
|
|
19832
|
+
},
|
|
19833
|
+
redis: {
|
|
19834
|
+
name: "redis",
|
|
19835
|
+
image: "redis:7-alpine",
|
|
19836
|
+
ports: ["6379:6379"],
|
|
19837
|
+
healthcheck: {
|
|
19838
|
+
test: ["CMD", "redis-cli", "ping"],
|
|
19839
|
+
interval: "5s",
|
|
19840
|
+
timeout: "5s",
|
|
19841
|
+
retries: 5
|
|
19842
|
+
}
|
|
19843
|
+
},
|
|
19844
|
+
mongodb: {
|
|
19845
|
+
name: "mongodb",
|
|
19846
|
+
image: "mongo:7",
|
|
19847
|
+
ports: ["27017:27017"],
|
|
19848
|
+
environment: {
|
|
19849
|
+
MONGO_INITDB_ROOT_USERNAME: "test",
|
|
19850
|
+
MONGO_INITDB_ROOT_PASSWORD: "test"
|
|
19851
|
+
},
|
|
19852
|
+
healthcheck: {
|
|
19853
|
+
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"],
|
|
19854
|
+
interval: "5s",
|
|
19855
|
+
timeout: "5s",
|
|
19856
|
+
retries: 5
|
|
19857
|
+
}
|
|
19858
|
+
}
|
|
19859
|
+
};
|
|
19860
|
+
return configs[serviceName] || null;
|
|
19861
|
+
}
|
|
19862
|
+
function formatServiceConfig(config) {
|
|
19863
|
+
const lines = [` ${config.name}:`];
|
|
19864
|
+
lines.push(` image: ${config.image}`);
|
|
19865
|
+
if (config.environment) {
|
|
19866
|
+
lines.push(" environment:");
|
|
19867
|
+
for (const [key, value] of Object.entries(config.environment)) {
|
|
19868
|
+
lines.push(` ${key}: ${value}`);
|
|
19869
|
+
}
|
|
19870
|
+
}
|
|
19871
|
+
if (config.ports.length > 0) {
|
|
19872
|
+
lines.push(" ports:");
|
|
19873
|
+
for (const port of config.ports) {
|
|
19874
|
+
lines.push(` - "${port}"`);
|
|
19875
|
+
}
|
|
19876
|
+
}
|
|
19877
|
+
lines.push(" healthcheck:");
|
|
19878
|
+
lines.push(` test: ${JSON.stringify(config.healthcheck.test)}`);
|
|
19879
|
+
lines.push(` interval: ${config.healthcheck.interval}`);
|
|
19880
|
+
lines.push(` timeout: ${config.healthcheck.timeout}`);
|
|
19881
|
+
lines.push(` retries: ${config.healthcheck.retries}`);
|
|
19882
|
+
return lines.join("\n");
|
|
19883
|
+
}
|
|
19884
|
+
|
|
19885
|
+
// dist/engine/sandbox/service-detector.js
|
|
19886
|
+
var import_fs4 = require("fs");
|
|
19887
|
+
var import_path11 = require("path");
|
|
19888
|
+
var import_promises13 = require("fs/promises");
|
|
19889
|
+
async function detectRequiredServices(cwd) {
|
|
19890
|
+
const services = /* @__PURE__ */ new Set();
|
|
19891
|
+
await detectFromPackageJson(cwd, services);
|
|
19892
|
+
await detectFromDockerCompose(cwd, services);
|
|
19893
|
+
await detectFromEnvFiles(cwd, services);
|
|
19894
|
+
return Array.from(services);
|
|
19895
|
+
}
|
|
19896
|
+
async function detectFromPackageJson(cwd, services) {
|
|
19897
|
+
const pkgPath = (0, import_path11.join)(cwd, "package.json");
|
|
19898
|
+
if (!(0, import_fs4.existsSync)(pkgPath))
|
|
19899
|
+
return;
|
|
19900
|
+
try {
|
|
19901
|
+
const content = await (0, import_promises13.readFile)(pkgPath, "utf-8");
|
|
19902
|
+
const pkg = JSON.parse(content);
|
|
19903
|
+
const allDeps = {
|
|
19904
|
+
...pkg.dependencies,
|
|
19905
|
+
...pkg.devDependencies
|
|
19906
|
+
};
|
|
19907
|
+
if (allDeps.pg || allDeps["pg-promise"] || allDeps.postgres || allDeps.sequelize) {
|
|
19908
|
+
services.add("postgres");
|
|
19909
|
+
}
|
|
19910
|
+
if (allDeps.mysql || allDeps.mysql2) {
|
|
19911
|
+
services.add("mysql");
|
|
19912
|
+
}
|
|
19913
|
+
if (allDeps.redis || allDeps.ioredis) {
|
|
19914
|
+
services.add("redis");
|
|
19915
|
+
}
|
|
19916
|
+
if (allDeps.mongodb || allDeps.mongoose) {
|
|
19917
|
+
services.add("mongodb");
|
|
19918
|
+
}
|
|
19919
|
+
} catch {
|
|
19920
|
+
}
|
|
19921
|
+
}
|
|
19922
|
+
async function detectFromDockerCompose(cwd, services) {
|
|
19923
|
+
const composeFiles = [
|
|
19924
|
+
"docker-compose.yml",
|
|
19925
|
+
"docker-compose.yaml",
|
|
19926
|
+
"docker-compose.test.yml",
|
|
19927
|
+
"docker-compose.test.yaml"
|
|
19928
|
+
];
|
|
19929
|
+
for (const file of composeFiles) {
|
|
19930
|
+
const composePath = (0, import_path11.join)(cwd, file);
|
|
19931
|
+
if (!(0, import_fs4.existsSync)(composePath))
|
|
19932
|
+
continue;
|
|
19933
|
+
try {
|
|
19934
|
+
const content = await (0, import_promises13.readFile)(composePath, "utf-8");
|
|
19935
|
+
if (/image:\s*postgres/i.test(content)) {
|
|
19936
|
+
services.add("postgres");
|
|
19937
|
+
}
|
|
19938
|
+
if (/image:\s*mysql/i.test(content)) {
|
|
19939
|
+
services.add("mysql");
|
|
19940
|
+
}
|
|
19941
|
+
if (/image:\s*redis/i.test(content)) {
|
|
19942
|
+
services.add("redis");
|
|
19943
|
+
}
|
|
19944
|
+
if (/image:\s*mongo/i.test(content)) {
|
|
19945
|
+
services.add("mongodb");
|
|
19946
|
+
}
|
|
19947
|
+
} catch {
|
|
19948
|
+
}
|
|
19949
|
+
}
|
|
19950
|
+
}
|
|
19951
|
+
async function detectFromEnvFiles(cwd, services) {
|
|
19952
|
+
const envFiles = [
|
|
19953
|
+
".env",
|
|
19954
|
+
".env.test",
|
|
19955
|
+
".env.local",
|
|
19956
|
+
".env.development"
|
|
19957
|
+
];
|
|
19958
|
+
for (const file of envFiles) {
|
|
19959
|
+
const envPath = (0, import_path11.join)(cwd, file);
|
|
19960
|
+
if (!(0, import_fs4.existsSync)(envPath))
|
|
19961
|
+
continue;
|
|
19962
|
+
try {
|
|
19963
|
+
const content = await (0, import_promises13.readFile)(envPath, "utf-8");
|
|
19964
|
+
if (/postgres:\/\//i.test(content) || /postgresql:\/\//i.test(content)) {
|
|
19965
|
+
services.add("postgres");
|
|
19966
|
+
}
|
|
19967
|
+
if (/mysql:\/\//i.test(content)) {
|
|
19968
|
+
services.add("mysql");
|
|
19969
|
+
}
|
|
19970
|
+
if (/redis:\/\//i.test(content)) {
|
|
19971
|
+
services.add("redis");
|
|
19972
|
+
}
|
|
19973
|
+
if (/mongodb:\/\//i.test(content)) {
|
|
19974
|
+
services.add("mongodb");
|
|
19975
|
+
}
|
|
19976
|
+
if (/POSTGRES_HOST|PGHOST|DATABASE_URL.*postgres/i.test(content)) {
|
|
19977
|
+
services.add("postgres");
|
|
19978
|
+
}
|
|
19979
|
+
if (/MYSQL_HOST|DATABASE_URL.*mysql/i.test(content)) {
|
|
19980
|
+
services.add("mysql");
|
|
19981
|
+
}
|
|
19982
|
+
if (/REDIS_HOST|REDIS_URL/i.test(content)) {
|
|
19983
|
+
services.add("redis");
|
|
19984
|
+
}
|
|
19985
|
+
if (/MONGO_HOST|MONGODB_URL|MONGO_URL/i.test(content)) {
|
|
19986
|
+
services.add("mongodb");
|
|
19987
|
+
}
|
|
19988
|
+
} catch {
|
|
19989
|
+
}
|
|
19990
|
+
}
|
|
19991
|
+
}
|
|
19992
|
+
function getServiceDisplayName(service) {
|
|
19993
|
+
const names = {
|
|
19994
|
+
postgres: "PostgreSQL",
|
|
19995
|
+
mysql: "MySQL",
|
|
19996
|
+
redis: "Redis",
|
|
19997
|
+
mongodb: "MongoDB"
|
|
19998
|
+
};
|
|
19999
|
+
return names[service] || service;
|
|
20000
|
+
}
|
|
20001
|
+
|
|
20002
|
+
// dist/engine/sandbox/orchestrator.js
|
|
20003
|
+
async function runInSandbox(cwd, options) {
|
|
20004
|
+
const dockerInfo = await checkDockerAvailable();
|
|
20005
|
+
if (!dockerInfo.available) {
|
|
20006
|
+
console.warn("\u26A0\uFE0F Docker not available, skipping sandbox verification");
|
|
20007
|
+
console.warn(" Install Docker for isolated testing: https://docs.docker.com/get-docker/");
|
|
20008
|
+
return {
|
|
20009
|
+
passed: false,
|
|
20010
|
+
buildSuccess: false,
|
|
20011
|
+
testsPassed: false,
|
|
20012
|
+
services: [],
|
|
20013
|
+
logs: "",
|
|
20014
|
+
exitCode: -1,
|
|
20015
|
+
skipped: true,
|
|
20016
|
+
skipReason: "docker_unavailable"
|
|
20017
|
+
};
|
|
20018
|
+
}
|
|
20019
|
+
console.log("\u{1F433} Starting sandbox environment...");
|
|
20020
|
+
console.log(` Docker version: ${dockerInfo.version}`);
|
|
20021
|
+
const projectType = await detectProjectType(cwd);
|
|
20022
|
+
console.log(` Project type: ${projectType}`);
|
|
20023
|
+
const services = options.services || await detectRequiredServices(cwd);
|
|
20024
|
+
if (services.length > 0) {
|
|
20025
|
+
console.log(` Required services: ${services.map(getServiceDisplayName).join(", ")}`);
|
|
20026
|
+
}
|
|
20027
|
+
const imageName = options.dockerImage || "greenbump-verify";
|
|
20028
|
+
const dockerfilePath = (0, import_path12.join)(cwd, ".greenbump-Dockerfile");
|
|
20029
|
+
const composeFilePath = (0, import_path12.join)(cwd, ".greenbump-compose.yml");
|
|
20030
|
+
let projectName;
|
|
20031
|
+
const serviceHealths = [];
|
|
20032
|
+
try {
|
|
20033
|
+
const dockerfile = await generateDockerfile(projectType, cwd);
|
|
20034
|
+
await (0, import_promises14.writeFile)(dockerfilePath, dockerfile);
|
|
20035
|
+
let composeFile = "";
|
|
20036
|
+
if (services.length > 0) {
|
|
20037
|
+
composeFile = await generateComposeFile(services);
|
|
20038
|
+
if (composeFile) {
|
|
20039
|
+
await (0, import_promises14.writeFile)(composeFilePath, composeFile);
|
|
20040
|
+
}
|
|
20041
|
+
}
|
|
20042
|
+
await buildDockerImage(cwd, imageName, dockerfilePath);
|
|
20043
|
+
if (services.length > 0 && composeFile && dockerInfo.composeAvailable) {
|
|
20044
|
+
projectName = await startComposeServices(cwd, composeFilePath);
|
|
20045
|
+
await waitForServices(projectName, services, options.timeout ? options.timeout / 1e3 : 60);
|
|
20046
|
+
for (const service of services) {
|
|
20047
|
+
serviceHealths.push({
|
|
20048
|
+
name: service,
|
|
20049
|
+
healthy: true,
|
|
20050
|
+
displayName: getServiceDisplayName(service)
|
|
20051
|
+
});
|
|
20052
|
+
}
|
|
20053
|
+
} else if (services.length > 0 && !dockerInfo.composeAvailable) {
|
|
20054
|
+
console.warn("\u26A0\uFE0F docker-compose not available, skipping service startup");
|
|
20055
|
+
}
|
|
20056
|
+
console.log("\u{1F50D} Running verification in container...");
|
|
20057
|
+
const networkMode = projectName ? `${projectName}_default` : "bridge";
|
|
20058
|
+
const result = await runVerificationInContainer(cwd, {
|
|
20059
|
+
image: imageName,
|
|
20060
|
+
networkMode,
|
|
20061
|
+
timeout: options.timeout
|
|
20062
|
+
});
|
|
20063
|
+
if (result.exitCode === 0) {
|
|
20064
|
+
console.log("\u2705 Verification passed in sandbox");
|
|
20065
|
+
} else {
|
|
20066
|
+
console.error("\u274C Verification failed in sandbox");
|
|
20067
|
+
console.error(result.stderr);
|
|
20068
|
+
}
|
|
20069
|
+
return {
|
|
20070
|
+
passed: result.exitCode === 0,
|
|
20071
|
+
buildSuccess: result.buildSuccess,
|
|
20072
|
+
testsPassed: result.testsPassed,
|
|
20073
|
+
services: serviceHealths,
|
|
20074
|
+
logs: result.stdout + "\n" + result.stderr,
|
|
20075
|
+
exitCode: result.exitCode
|
|
20076
|
+
};
|
|
20077
|
+
} finally {
|
|
20078
|
+
if (!options.keepContainer) {
|
|
20079
|
+
await cleanupDocker(cwd, imageName, composeFilePath, projectName);
|
|
20080
|
+
} else {
|
|
20081
|
+
console.log("\u{1F527} Container kept for debugging (use --keep-container=false to cleanup)");
|
|
20082
|
+
}
|
|
20083
|
+
}
|
|
20084
|
+
}
|
|
20085
|
+
|
|
20086
|
+
// dist/engine/perf/metrics.js
|
|
20087
|
+
var import_fs5 = require("fs");
|
|
20088
|
+
var import_path13 = require("path");
|
|
20089
|
+
var import_promises15 = require("fs/promises");
|
|
20090
|
+
async function hasBuildScript(cwd) {
|
|
20091
|
+
const pkgPath = (0, import_path13.join)(cwd, "package.json");
|
|
20092
|
+
if (!(0, import_fs5.existsSync)(pkgPath))
|
|
20093
|
+
return false;
|
|
20094
|
+
try {
|
|
20095
|
+
const content = await (0, import_promises15.readFile)(pkgPath, "utf-8");
|
|
20096
|
+
const pkg = JSON.parse(content);
|
|
20097
|
+
return pkg.scripts?.build !== void 0;
|
|
20098
|
+
} catch {
|
|
20099
|
+
return false;
|
|
20100
|
+
}
|
|
20101
|
+
}
|
|
20102
|
+
async function hasTestScript(cwd) {
|
|
20103
|
+
const pkgPath = (0, import_path13.join)(cwd, "package.json");
|
|
20104
|
+
if (!(0, import_fs5.existsSync)(pkgPath))
|
|
20105
|
+
return false;
|
|
20106
|
+
try {
|
|
20107
|
+
const content = await (0, import_promises15.readFile)(pkgPath, "utf-8");
|
|
20108
|
+
const pkg = JSON.parse(content);
|
|
20109
|
+
return pkg.scripts?.test !== void 0;
|
|
20110
|
+
} catch {
|
|
20111
|
+
return false;
|
|
20112
|
+
}
|
|
20113
|
+
}
|
|
20114
|
+
async function getTotalSize(dirPath) {
|
|
20115
|
+
if (!(0, import_fs5.existsSync)(dirPath))
|
|
20116
|
+
return 0;
|
|
20117
|
+
try {
|
|
20118
|
+
const result = await exec("du", ["-sb", dirPath], { cwd: dirPath });
|
|
20119
|
+
const match = result.stdout.match(/^(\d+)/);
|
|
20120
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
20121
|
+
} catch {
|
|
20122
|
+
return 0;
|
|
20123
|
+
}
|
|
20124
|
+
}
|
|
20125
|
+
async function captureBaseline(cwd) {
|
|
20126
|
+
console.log("\u{1F4CA} Capturing performance baseline...");
|
|
20127
|
+
const metrics = {
|
|
20128
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
20129
|
+
};
|
|
20130
|
+
try {
|
|
20131
|
+
console.log(" Measuring install time...");
|
|
20132
|
+
const installStart = Date.now();
|
|
20133
|
+
await exec("npm", ["install"], { cwd });
|
|
20134
|
+
metrics.installTime = (Date.now() - installStart) / 1e3;
|
|
20135
|
+
console.log(` \u2713 Install: ${metrics.installTime.toFixed(1)}s`);
|
|
20136
|
+
if (await hasBuildScript(cwd)) {
|
|
20137
|
+
console.log(" Measuring build time...");
|
|
20138
|
+
const buildStart = Date.now();
|
|
20139
|
+
await exec("npm", ["run", "build"], { cwd });
|
|
20140
|
+
metrics.buildTime = (Date.now() - buildStart) / 1e3;
|
|
20141
|
+
console.log(` \u2713 Build: ${metrics.buildTime.toFixed(1)}s`);
|
|
20142
|
+
const distDir = (0, import_path13.join)(cwd, "dist");
|
|
20143
|
+
if ((0, import_fs5.existsSync)(distDir)) {
|
|
20144
|
+
metrics.bundleSize = await getTotalSize(distDir);
|
|
20145
|
+
console.log(` \u2713 Bundle: ${(metrics.bundleSize / 1024).toFixed(0)} KB`);
|
|
20146
|
+
}
|
|
20147
|
+
}
|
|
20148
|
+
if (await hasTestScript(cwd)) {
|
|
20149
|
+
console.log(" Measuring test time...");
|
|
20150
|
+
const testStart = Date.now();
|
|
20151
|
+
await exec("npm", ["test"], { cwd });
|
|
20152
|
+
metrics.testTime = (Date.now() - testStart) / 1e3;
|
|
20153
|
+
console.log(` \u2713 Test: ${metrics.testTime.toFixed(1)}s`);
|
|
20154
|
+
}
|
|
20155
|
+
} catch (error) {
|
|
20156
|
+
console.warn("\u26A0\uFE0F Warning: Failed to capture complete baseline:", error.message);
|
|
20157
|
+
}
|
|
20158
|
+
const greenbumpDir = (0, import_path13.join)(cwd, ".greenbump");
|
|
20159
|
+
if (!(0, import_fs5.existsSync)(greenbumpDir)) {
|
|
20160
|
+
await (0, import_promises15.mkdir)(greenbumpDir, { recursive: true });
|
|
20161
|
+
}
|
|
20162
|
+
const baselinePath = (0, import_path13.join)(greenbumpDir, "perf-baseline.json");
|
|
20163
|
+
await (0, import_promises15.writeFile)(baselinePath, JSON.stringify(metrics, null, 2));
|
|
20164
|
+
return metrics;
|
|
20165
|
+
}
|
|
20166
|
+
async function loadBaseline(cwd) {
|
|
20167
|
+
const baselinePath = (0, import_path13.join)(cwd, ".greenbump", "perf-baseline.json");
|
|
20168
|
+
if (!(0, import_fs5.existsSync)(baselinePath)) {
|
|
20169
|
+
return null;
|
|
20170
|
+
}
|
|
20171
|
+
try {
|
|
20172
|
+
const content = await (0, import_promises15.readFile)(baselinePath, "utf-8");
|
|
20173
|
+
const baseline = JSON.parse(content);
|
|
20174
|
+
if (baseline.timestamp) {
|
|
20175
|
+
baseline.timestamp = new Date(baseline.timestamp);
|
|
20176
|
+
}
|
|
20177
|
+
return baseline;
|
|
20178
|
+
} catch {
|
|
20179
|
+
return null;
|
|
20180
|
+
}
|
|
20181
|
+
}
|
|
20182
|
+
|
|
20183
|
+
// dist/engine/perf/regression.js
|
|
20184
|
+
var DEFAULT_THRESHOLDS = {
|
|
20185
|
+
installTime: 0.3,
|
|
20186
|
+
// 30% slower
|
|
20187
|
+
buildTime: 0.2,
|
|
20188
|
+
// 20% slower
|
|
20189
|
+
testTime: 0.3,
|
|
20190
|
+
// 30% slower
|
|
20191
|
+
bundleSize: 0.15,
|
|
20192
|
+
// 15% larger
|
|
20193
|
+
memoryPeak: 0.4
|
|
20194
|
+
// 40% more memory
|
|
20195
|
+
};
|
|
20196
|
+
function comparePerformance(baseline, current, thresholds = {}) {
|
|
20197
|
+
const mergedThresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };
|
|
20198
|
+
const comparisons = [];
|
|
20199
|
+
for (const [metric, threshold] of Object.entries(mergedThresholds)) {
|
|
20200
|
+
const before = baseline[metric];
|
|
20201
|
+
const after = current[metric];
|
|
20202
|
+
if (before === void 0 || after === void 0)
|
|
20203
|
+
continue;
|
|
20204
|
+
if (before === 0)
|
|
20205
|
+
continue;
|
|
20206
|
+
const delta = after - before;
|
|
20207
|
+
const percentChange = delta / before * 100;
|
|
20208
|
+
const isRegression = percentChange > threshold * 100;
|
|
20209
|
+
comparisons.push({
|
|
20210
|
+
metric,
|
|
20211
|
+
before,
|
|
20212
|
+
after,
|
|
20213
|
+
delta,
|
|
20214
|
+
percentChange,
|
|
20215
|
+
isRegression,
|
|
20216
|
+
severity: isRegression ? percentChange > threshold * 200 ? "critical" : "warning" : "ok"
|
|
20217
|
+
});
|
|
20218
|
+
}
|
|
20219
|
+
return comparisons;
|
|
20220
|
+
}
|
|
20221
|
+
function hasRegressions(comparisons) {
|
|
20222
|
+
return comparisons.some((c2) => c2.isRegression);
|
|
20223
|
+
}
|
|
20224
|
+
function getMetricDisplayName(metric) {
|
|
20225
|
+
const names = {
|
|
20226
|
+
installTime: "Install time",
|
|
20227
|
+
buildTime: "Build time",
|
|
20228
|
+
testTime: "Test time",
|
|
20229
|
+
bundleSize: "Bundle size",
|
|
20230
|
+
memoryPeak: "Memory usage"
|
|
20231
|
+
};
|
|
20232
|
+
return names[metric] || metric;
|
|
20233
|
+
}
|
|
20234
|
+
function formatMetricValue(metric, value) {
|
|
20235
|
+
if (metric === "bundleSize") {
|
|
20236
|
+
if (value < 1024 * 1024) {
|
|
20237
|
+
return `${(value / 1024).toFixed(0)} KB`;
|
|
20238
|
+
}
|
|
20239
|
+
return `${(value / (1024 * 1024)).toFixed(1)} MB`;
|
|
20240
|
+
}
|
|
20241
|
+
if (metric === "memoryPeak") {
|
|
20242
|
+
return `${value.toFixed(0)} MB`;
|
|
20243
|
+
}
|
|
20244
|
+
return `${value.toFixed(1)}s`;
|
|
20245
|
+
}
|
|
20246
|
+
function formatPercentChange(percentChange) {
|
|
20247
|
+
const sign = percentChange > 0 ? "+" : "";
|
|
20248
|
+
return `${sign}${percentChange.toFixed(1)}%`;
|
|
20249
|
+
}
|
|
20250
|
+
function displayPerformanceComparison(comparisons) {
|
|
20251
|
+
if (comparisons.length === 0) {
|
|
20252
|
+
console.log("\u{1F4CA} No performance metrics to compare");
|
|
20253
|
+
return;
|
|
20254
|
+
}
|
|
20255
|
+
console.log("\n\u{1F4CA} Performance Comparison:");
|
|
20256
|
+
for (const comparison of comparisons) {
|
|
20257
|
+
const name = getMetricDisplayName(comparison.metric);
|
|
20258
|
+
const before = formatMetricValue(comparison.metric, comparison.before);
|
|
20259
|
+
const after = formatMetricValue(comparison.metric, comparison.after);
|
|
20260
|
+
const change = formatPercentChange(comparison.percentChange);
|
|
20261
|
+
if (comparison.isRegression) {
|
|
20262
|
+
const icon = comparison.severity === "critical" ? "\u{1F534}" : "\u26A0\uFE0F";
|
|
20263
|
+
console.log(` ${icon} ${name}: ${before} \u2192 ${after} (${change}) - REGRESSION`);
|
|
20264
|
+
} else {
|
|
20265
|
+
console.log(` \u2713 ${name}: ${before} \u2192 ${after} (${change})`);
|
|
20266
|
+
}
|
|
20267
|
+
}
|
|
20268
|
+
const regressionCount = comparisons.filter((c2) => c2.isRegression).length;
|
|
20269
|
+
if (regressionCount > 0) {
|
|
20270
|
+
console.log(`
|
|
20271
|
+
\u26A0\uFE0F ${regressionCount} performance regression(s) detected!`);
|
|
20272
|
+
console.log(" Review changes before committing.\n");
|
|
20273
|
+
} else {
|
|
20274
|
+
console.log("\n\u2705 No performance regressions detected\n");
|
|
20275
|
+
}
|
|
20276
|
+
}
|
|
20277
|
+
|
|
19298
20278
|
// dist/engine/run.js
|
|
19299
20279
|
var RunError = class extends Error {
|
|
19300
20280
|
};
|
|
@@ -19338,6 +20318,11 @@ async function run(opts) {
|
|
|
19338
20318
|
await createBranch(cwd, branch);
|
|
19339
20319
|
log(`created branch ${branch}`);
|
|
19340
20320
|
}
|
|
20321
|
+
let perfBaseline;
|
|
20322
|
+
if (opts.detectRegressions) {
|
|
20323
|
+
log("capturing performance baseline\u2026");
|
|
20324
|
+
perfBaseline = await captureBaseline(cwd);
|
|
20325
|
+
}
|
|
19341
20326
|
log("running baseline build + tests\u2026");
|
|
19342
20327
|
const baseline = await runChecks(pm, cwd, checkOverrides);
|
|
19343
20328
|
if (!baseline.ok && !baseline.unverifiable) {
|
|
@@ -19412,13 +20397,63 @@ ${up.output}`);
|
|
|
19412
20397
|
summary.usage = fix.usage;
|
|
19413
20398
|
summary.editedFiles = fix.editedFiles;
|
|
19414
20399
|
summary.testFilesTouched = fix.editedFiles.filter((f2) => /(^|\/)(test|tests|__tests__|spec)(\/|\.)|\.(test|spec)\./i.test(f2));
|
|
20400
|
+
log("running static analysis\u2026");
|
|
20401
|
+
const staticResults = await runStaticAnalysis(cwd, { strictLint: false });
|
|
20402
|
+
const typesFailed = staticResults.some((r2) => r2.stage === "types" && !r2.passed);
|
|
20403
|
+
if (typesFailed) {
|
|
20404
|
+
log("\u26A0\uFE0F TypeScript type check failed after fix");
|
|
20405
|
+
summary.needsReview = true;
|
|
20406
|
+
summary.staticAnalysisWarnings = staticResults.filter((r2) => !r2.passed).flatMap((r2) => r2.warnings || []);
|
|
20407
|
+
}
|
|
20408
|
+
const suspiciousChanges = await detectSuspiciousChanges(cwd);
|
|
20409
|
+
const criticalChanges = suspiciousChanges.filter((c2) => c2.severity === "critical");
|
|
20410
|
+
if (criticalChanges.length > 0) {
|
|
20411
|
+
log("\u26A0\uFE0F Suspicious changes detected (test modifications, large deletions)");
|
|
20412
|
+
summary.needsReview = true;
|
|
20413
|
+
summary.suspiciousChanges = suspiciousChanges.map((c2) => `${c2.type}: ${c2.file} - ${c2.description}`);
|
|
20414
|
+
}
|
|
20415
|
+
if (opts.sandbox) {
|
|
20416
|
+
log("running sandbox verification\u2026");
|
|
20417
|
+
const sandboxResult = await runInSandbox(cwd, {
|
|
20418
|
+
enabled: true,
|
|
20419
|
+
services: opts.services,
|
|
20420
|
+
keepContainer: opts.keepContainer,
|
|
20421
|
+
timeout: 600
|
|
20422
|
+
});
|
|
20423
|
+
if (!sandboxResult.skipped) {
|
|
20424
|
+
summary.sandboxResult = {
|
|
20425
|
+
passed: sandboxResult.passed,
|
|
20426
|
+
services: sandboxResult.services.map((s2) => s2.displayName)
|
|
20427
|
+
};
|
|
20428
|
+
if (!sandboxResult.passed) {
|
|
20429
|
+
log("\u26A0\uFE0F Sandbox verification failed");
|
|
20430
|
+
summary.needsReview = true;
|
|
20431
|
+
}
|
|
20432
|
+
}
|
|
20433
|
+
}
|
|
20434
|
+
if (opts.detectRegressions && perfBaseline) {
|
|
20435
|
+
log("checking for performance regressions\u2026");
|
|
20436
|
+
const currentMetrics = await loadBaseline(cwd);
|
|
20437
|
+
if (currentMetrics) {
|
|
20438
|
+
const comparisons = comparePerformance(perfBaseline, currentMetrics);
|
|
20439
|
+
displayPerformanceComparison(comparisons);
|
|
20440
|
+
if (hasRegressions(comparisons)) {
|
|
20441
|
+
log("\u26A0\uFE0F Performance regressions detected");
|
|
20442
|
+
summary.needsReview = true;
|
|
20443
|
+
summary.performanceRegression = {
|
|
20444
|
+
detected: true,
|
|
20445
|
+
details: comparisons.filter((c2) => c2.isRegression).map((c2) => `${c2.metric}: ${c2.percentChange.toFixed(1)}% slower/larger`)
|
|
20446
|
+
};
|
|
20447
|
+
}
|
|
20448
|
+
}
|
|
20449
|
+
}
|
|
19415
20450
|
summary.needsReview = computeNeedsReview({
|
|
19416
20451
|
unverifiable: false,
|
|
19417
20452
|
neededFix: true,
|
|
19418
20453
|
fixed: fix.fixed,
|
|
19419
20454
|
testFilesTouched: summary.testFilesTouched,
|
|
19420
20455
|
budgetExceeded: fix.budgetExceeded
|
|
19421
|
-
});
|
|
20456
|
+
}) || summary.needsReview;
|
|
19422
20457
|
await maybeCommit(cwd, summary, fix.fixed);
|
|
19423
20458
|
summary.durationMs = Date.now() - startedAt;
|
|
19424
20459
|
return summary;
|
|
@@ -19494,7 +20529,7 @@ function renderPrBody(s2) {
|
|
|
19494
20529
|
}
|
|
19495
20530
|
|
|
19496
20531
|
// dist/report.js
|
|
19497
|
-
var
|
|
20532
|
+
var import_promises16 = require("node:fs/promises");
|
|
19498
20533
|
var REPORT_SCHEMA_VERSION = 1;
|
|
19499
20534
|
function buildReport(runs) {
|
|
19500
20535
|
return {
|
|
@@ -19504,7 +20539,7 @@ function buildReport(runs) {
|
|
|
19504
20539
|
};
|
|
19505
20540
|
}
|
|
19506
20541
|
async function writeReport(path2, envelope) {
|
|
19507
|
-
await (0,
|
|
20542
|
+
await (0, import_promises16.writeFile)(path2, JSON.stringify(envelope, null, 2) + "\n", "utf8");
|
|
19508
20543
|
}
|
|
19509
20544
|
|
|
19510
20545
|
// dist/action/github.js
|