greenbump 0.4.0 → 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 +44 -1
- package/action-dist/index.cjs +813 -18
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/engine/git.js +1 -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 +49 -1
- 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/package.json +1 -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() {
|
|
@@ -19520,6 +19520,761 @@ function findFileContext(diff, lineIndex) {
|
|
|
19520
19520
|
return null;
|
|
19521
19521
|
}
|
|
19522
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
|
+
|
|
19523
20278
|
// dist/engine/run.js
|
|
19524
20279
|
var RunError = class extends Error {
|
|
19525
20280
|
};
|
|
@@ -19563,6 +20318,11 @@ async function run(opts) {
|
|
|
19563
20318
|
await createBranch(cwd, branch);
|
|
19564
20319
|
log(`created branch ${branch}`);
|
|
19565
20320
|
}
|
|
20321
|
+
let perfBaseline;
|
|
20322
|
+
if (opts.detectRegressions) {
|
|
20323
|
+
log("capturing performance baseline\u2026");
|
|
20324
|
+
perfBaseline = await captureBaseline(cwd);
|
|
20325
|
+
}
|
|
19566
20326
|
log("running baseline build + tests\u2026");
|
|
19567
20327
|
const baseline = await runChecks(pm, cwd, checkOverrides);
|
|
19568
20328
|
if (!baseline.ok && !baseline.unverifiable) {
|
|
@@ -19652,6 +20412,41 @@ ${up.output}`);
|
|
|
19652
20412
|
summary.needsReview = true;
|
|
19653
20413
|
summary.suspiciousChanges = suspiciousChanges.map((c2) => `${c2.type}: ${c2.file} - ${c2.description}`);
|
|
19654
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
|
+
}
|
|
19655
20450
|
summary.needsReview = computeNeedsReview({
|
|
19656
20451
|
unverifiable: false,
|
|
19657
20452
|
neededFix: true,
|
|
@@ -19734,7 +20529,7 @@ function renderPrBody(s2) {
|
|
|
19734
20529
|
}
|
|
19735
20530
|
|
|
19736
20531
|
// dist/report.js
|
|
19737
|
-
var
|
|
20532
|
+
var import_promises16 = require("node:fs/promises");
|
|
19738
20533
|
var REPORT_SCHEMA_VERSION = 1;
|
|
19739
20534
|
function buildReport(runs) {
|
|
19740
20535
|
return {
|
|
@@ -19744,7 +20539,7 @@ function buildReport(runs) {
|
|
|
19744
20539
|
};
|
|
19745
20540
|
}
|
|
19746
20541
|
async function writeReport(path2, envelope) {
|
|
19747
|
-
await (0,
|
|
20542
|
+
await (0, import_promises16.writeFile)(path2, JSON.stringify(envelope, null, 2) + "\n", "utf8");
|
|
19748
20543
|
}
|
|
19749
20544
|
|
|
19750
20545
|
// dist/action/github.js
|