agent-gauntlet 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -87
- package/package.json +4 -2
- package/src/bun-plugins.d.ts +4 -0
- package/src/cli-adapters/claude.ts +139 -108
- package/src/cli-adapters/codex.ts +141 -117
- package/src/cli-adapters/cursor.ts +152 -0
- package/src/cli-adapters/gemini.ts +171 -139
- package/src/cli-adapters/github-copilot.ts +153 -0
- package/src/cli-adapters/index.ts +77 -48
- package/src/commands/check.test.ts +24 -20
- package/src/commands/check.ts +86 -59
- package/src/commands/ci/index.ts +15 -0
- package/src/commands/ci/init.ts +96 -0
- package/src/commands/ci/list-jobs.ts +78 -0
- package/src/commands/detect.test.ts +38 -32
- package/src/commands/detect.ts +89 -61
- package/src/commands/health.test.ts +67 -53
- package/src/commands/health.ts +167 -145
- package/src/commands/help.test.ts +37 -37
- package/src/commands/help.ts +31 -22
- package/src/commands/index.ts +10 -9
- package/src/commands/init.test.ts +120 -107
- package/src/commands/init.ts +514 -417
- package/src/commands/list.test.ts +87 -70
- package/src/commands/list.ts +28 -24
- package/src/commands/rerun.ts +157 -119
- package/src/commands/review.test.ts +26 -20
- package/src/commands/review.ts +86 -59
- package/src/commands/run.test.ts +22 -20
- package/src/commands/run.ts +85 -58
- package/src/commands/shared.ts +44 -35
- package/src/config/ci-loader.ts +33 -0
- package/src/config/ci-schema.ts +52 -0
- package/src/config/loader.test.ts +112 -90
- package/src/config/loader.ts +132 -123
- package/src/config/schema.ts +48 -47
- package/src/config/types.ts +28 -13
- package/src/config/validator.ts +521 -454
- package/src/core/change-detector.ts +122 -104
- package/src/core/entry-point.test.ts +60 -62
- package/src/core/entry-point.ts +120 -74
- package/src/core/job.ts +69 -59
- package/src/core/runner.ts +264 -230
- package/src/gates/check.ts +78 -69
- package/src/gates/result.ts +7 -7
- package/src/gates/review.test.ts +277 -138
- package/src/gates/review.ts +724 -561
- package/src/index.ts +18 -15
- package/src/output/console.ts +253 -214
- package/src/output/logger.ts +66 -52
- package/src/templates/run_gauntlet.template.md +18 -0
- package/src/templates/workflow.yml +77 -0
- package/src/utils/diff-parser.ts +64 -62
- package/src/utils/log-parser.ts +227 -206
- package/src/utils/sanitizer.ts +1 -1
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { registerCheckCommand } from
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerCheckCommand } from "./check.js";
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
|
|
5
|
+
describe("Check Command", () => {
|
|
6
|
+
let program: Command;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
program = new Command();
|
|
10
|
+
registerCheckCommand(program);
|
|
11
|
+
});
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
it("should register the check command", () => {
|
|
14
|
+
const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
|
|
15
|
+
expect(checkCmd).toBeDefined();
|
|
16
|
+
expect(checkCmd?.description()).toBe(
|
|
17
|
+
"Run only applicable checks for detected changes",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
it("should have correct options", () => {
|
|
22
|
+
const checkCmd = program.commands.find((cmd) => cmd.name() === "check");
|
|
23
|
+
expect(checkCmd?.options.some((opt) => opt.long === "--gate")).toBe(true);
|
|
24
|
+
expect(checkCmd?.options.some((opt) => opt.long === "--commit")).toBe(true);
|
|
25
|
+
expect(checkCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
|
|
26
|
+
true,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
25
29
|
});
|
package/src/commands/check.ts
CHANGED
|
@@ -1,72 +1,99 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { loadConfig } from
|
|
4
|
-
import { ChangeDetector } from
|
|
5
|
-
import { EntryPointExpander } from
|
|
6
|
-
import { JobGenerator } from
|
|
7
|
-
import { Runner } from
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { rotateLogs } from
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { loadConfig } from "../config/loader.js";
|
|
4
|
+
import { ChangeDetector } from "../core/change-detector.js";
|
|
5
|
+
import { EntryPointExpander } from "../core/entry-point.js";
|
|
6
|
+
import { JobGenerator } from "../core/job.js";
|
|
7
|
+
import { Runner } from "../core/runner.js";
|
|
8
|
+
import { ConsoleReporter } from "../output/console.js";
|
|
9
|
+
import { Logger } from "../output/logger.js";
|
|
10
|
+
import { rotateLogs } from "./shared.js";
|
|
11
11
|
|
|
12
12
|
export function registerCheckCommand(program: Command): void {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
program
|
|
14
|
+
.command("check")
|
|
15
|
+
.description("Run only applicable checks for detected changes")
|
|
16
|
+
.option(
|
|
17
|
+
"-b, --base-branch <branch>",
|
|
18
|
+
"Override base branch for change detection",
|
|
19
|
+
)
|
|
20
|
+
.option("-g, --gate <name>", "Run specific check gate only")
|
|
21
|
+
.option("-c, --commit <sha>", "Use diff for a specific commit")
|
|
22
|
+
.option(
|
|
23
|
+
"-u, --uncommitted",
|
|
24
|
+
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
25
|
+
)
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
try {
|
|
28
|
+
const config = await loadConfig();
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
// Rotate logs before starting
|
|
31
|
+
await rotateLogs(config.project.log_dir);
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (changes.length === 0) {
|
|
37
|
-
console.log(chalk.green('No changes detected.'));
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
33
|
+
// Determine effective base branch
|
|
34
|
+
// Priority: CLI override > CI env var > config
|
|
35
|
+
const effectiveBaseBranch =
|
|
36
|
+
options.baseBranch ||
|
|
37
|
+
(process.env.GITHUB_BASE_REF &&
|
|
38
|
+
(process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
|
|
39
|
+
? process.env.GITHUB_BASE_REF
|
|
40
|
+
: null) ||
|
|
41
|
+
config.project.base_branch;
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const changeDetector = new ChangeDetector(effectiveBaseBranch, {
|
|
44
|
+
commit: options.commit,
|
|
45
|
+
uncommitted: options.uncommitted,
|
|
46
|
+
});
|
|
47
|
+
const expander = new EntryPointExpander();
|
|
48
|
+
const jobGen = new JobGenerator(config);
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
console.log(chalk.dim("Detecting changes..."));
|
|
51
|
+
const changes = await changeDetector.getChangedFiles();
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
if (changes.length === 0) {
|
|
54
|
+
console.log(chalk.green("No changes detected."));
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
console.log(chalk.yellow('No applicable checks for these changes.'));
|
|
55
|
-
process.exit(0);
|
|
56
|
-
}
|
|
58
|
+
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
const entryPoints = await expander.expand(
|
|
61
|
+
config.project.entry_points,
|
|
62
|
+
changes,
|
|
63
|
+
);
|
|
64
|
+
let jobs = jobGen.generateJobs(entryPoints);
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const runner = new Runner(config, logger, reporter);
|
|
66
|
+
// Filter to only checks
|
|
67
|
+
jobs = jobs.filter((j) => j.type === "check");
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
if (options.gate) {
|
|
70
|
+
jobs = jobs.filter((j) => j.name === options.gate);
|
|
71
|
+
}
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
if (jobs.length === 0) {
|
|
74
|
+
console.log(chalk.yellow("No applicable checks for these changes."));
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(chalk.dim(`Running ${jobs.length} check(s)...`));
|
|
79
|
+
|
|
80
|
+
const logger = new Logger(config.project.log_dir);
|
|
81
|
+
const reporter = new ConsoleReporter();
|
|
82
|
+
const runner = new Runner(
|
|
83
|
+
config,
|
|
84
|
+
logger,
|
|
85
|
+
reporter,
|
|
86
|
+
undefined,
|
|
87
|
+
undefined,
|
|
88
|
+
effectiveBaseBranch,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const success = await runner.run(jobs);
|
|
92
|
+
process.exit(success ? 0 : 1);
|
|
93
|
+
} catch (error: unknown) {
|
|
94
|
+
const err = error as { message?: string };
|
|
95
|
+
console.error(chalk.red("Error:"), err.message);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
72
99
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { initCI } from "./init.js";
|
|
3
|
+
import { listJobs } from "./list-jobs.js";
|
|
4
|
+
|
|
5
|
+
export function registerCICommand(program: Command): void {
|
|
6
|
+
const ci = program.command("ci").description("Manage CI integration");
|
|
7
|
+
|
|
8
|
+
ci.command("init")
|
|
9
|
+
.description("Initialize CI workflow and configuration")
|
|
10
|
+
.action(initCI);
|
|
11
|
+
|
|
12
|
+
ci.command("list-jobs")
|
|
13
|
+
.description("List CI jobs (used by workflow)")
|
|
14
|
+
.action(listJobs);
|
|
15
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { loadCIConfig } from "../../config/ci-loader.js";
|
|
6
|
+
import type { CIConfig } from "../../config/types.js";
|
|
7
|
+
import workflowTemplate from "../../templates/workflow.yml" with {
|
|
8
|
+
type: "text",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function initCI(): Promise<void> {
|
|
12
|
+
const workflowDir = path.join(process.cwd(), ".github", "workflows");
|
|
13
|
+
const workflowPath = path.join(workflowDir, "gauntlet.yml");
|
|
14
|
+
const gauntletDir = path.join(process.cwd(), ".gauntlet");
|
|
15
|
+
const ciConfigPath = path.join(gauntletDir, "ci.yml");
|
|
16
|
+
|
|
17
|
+
// 1. Ensure .gauntlet/ci.yml exists
|
|
18
|
+
if (!(await fileExists(ciConfigPath))) {
|
|
19
|
+
console.log(chalk.yellow("Creating starter .gauntlet/ci.yml..."));
|
|
20
|
+
await fs.mkdir(gauntletDir, { recursive: true });
|
|
21
|
+
const starterContent = `# CI Configuration for Agent Gauntlet
|
|
22
|
+
# Define runtimes, services, and which checks to run in CI.
|
|
23
|
+
|
|
24
|
+
runtimes:
|
|
25
|
+
# ruby:
|
|
26
|
+
# version: "3.3"
|
|
27
|
+
# bundler_cache: true
|
|
28
|
+
|
|
29
|
+
services:
|
|
30
|
+
# postgres:
|
|
31
|
+
# image: postgres:16
|
|
32
|
+
# ports: ["5432:5432"]
|
|
33
|
+
|
|
34
|
+
setup:
|
|
35
|
+
# - name: Global Setup
|
|
36
|
+
# run: echo "Setting up..."
|
|
37
|
+
|
|
38
|
+
checks:
|
|
39
|
+
# - name: linter
|
|
40
|
+
# requires_runtimes: [ruby]
|
|
41
|
+
`;
|
|
42
|
+
await fs.writeFile(ciConfigPath, starterContent);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(chalk.dim("Found existing .gauntlet/ci.yml"));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Load CI config to get services
|
|
48
|
+
let ciConfig: CIConfig | undefined;
|
|
49
|
+
try {
|
|
50
|
+
ciConfig = await loadCIConfig();
|
|
51
|
+
} catch (_e) {
|
|
52
|
+
console.warn(
|
|
53
|
+
chalk.yellow(
|
|
54
|
+
"Could not load CI config to inject services. Workflow will have no services defined.",
|
|
55
|
+
),
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Generate workflow file
|
|
60
|
+
console.log(chalk.dim(`Generating ${workflowPath}...`));
|
|
61
|
+
await fs.mkdir(workflowDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
let templateContent = workflowTemplate;
|
|
64
|
+
|
|
65
|
+
// Inject services
|
|
66
|
+
if (ciConfig?.services && Object.keys(ciConfig.services).length > 0) {
|
|
67
|
+
const servicesYaml = YAML.stringify({ services: ciConfig.services });
|
|
68
|
+
// Indent services
|
|
69
|
+
const indentedServices = servicesYaml
|
|
70
|
+
.split("\n")
|
|
71
|
+
.map((line) => (line.trim() ? ` ${line}` : line))
|
|
72
|
+
.join("\n");
|
|
73
|
+
|
|
74
|
+
templateContent = templateContent.replace(
|
|
75
|
+
"# Services will be injected here by agent-gauntlet",
|
|
76
|
+
indentedServices,
|
|
77
|
+
);
|
|
78
|
+
} else {
|
|
79
|
+
templateContent = templateContent.replace(
|
|
80
|
+
" # Services will be injected here by agent-gauntlet\n",
|
|
81
|
+
"",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await fs.writeFile(workflowPath, templateContent);
|
|
86
|
+
console.log(chalk.green("Successfully generated GitHub Actions workflow!"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
90
|
+
try {
|
|
91
|
+
const stat = await fs.stat(path);
|
|
92
|
+
return stat.isFile();
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { loadCIConfig } from "../../config/ci-loader.js";
|
|
2
|
+
import { loadConfig } from "../../config/loader.js";
|
|
3
|
+
import type { CISetupStep } from "../../config/types.js";
|
|
4
|
+
import { EntryPointExpander } from "../../core/entry-point.js";
|
|
5
|
+
|
|
6
|
+
export async function listJobs(): Promise<void> {
|
|
7
|
+
try {
|
|
8
|
+
const config = await loadConfig();
|
|
9
|
+
const ciConfig = await loadCIConfig();
|
|
10
|
+
const expander = new EntryPointExpander();
|
|
11
|
+
const expandedEntryPoints = await expander.expandAll(
|
|
12
|
+
config.project.entry_points,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const matrixJobs = [];
|
|
16
|
+
|
|
17
|
+
const globalSetup = formatSetup(ciConfig.setup || undefined);
|
|
18
|
+
|
|
19
|
+
if (ciConfig.checks) {
|
|
20
|
+
for (const ep of expandedEntryPoints) {
|
|
21
|
+
// Get checks enabled for this entry point
|
|
22
|
+
const allowedChecks = new Set(ep.config.checks || []);
|
|
23
|
+
|
|
24
|
+
for (const check of ciConfig.checks) {
|
|
25
|
+
if (allowedChecks.has(check.name)) {
|
|
26
|
+
// Check definition from .gauntlet/checks/*.yml
|
|
27
|
+
const checkDef = config.checks[check.name];
|
|
28
|
+
if (!checkDef) {
|
|
29
|
+
console.warn(
|
|
30
|
+
`Warning: Check '${check.name}' found in CI config but not defined in checks/*.yml`,
|
|
31
|
+
);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const id = `${check.name}-${ep.path.replace(/\//g, "-")}`;
|
|
36
|
+
|
|
37
|
+
matrixJobs.push({
|
|
38
|
+
id,
|
|
39
|
+
name: check.name,
|
|
40
|
+
entry_point: ep.path,
|
|
41
|
+
working_directory: checkDef.working_directory || ep.path,
|
|
42
|
+
command: checkDef.command,
|
|
43
|
+
runtimes: check.requires_runtimes || [],
|
|
44
|
+
services: check.requires_services || [],
|
|
45
|
+
setup: formatSetup(check.setup || undefined),
|
|
46
|
+
global_setup: globalSetup,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const output = {
|
|
54
|
+
matrix: matrixJobs,
|
|
55
|
+
services: ciConfig.services || {},
|
|
56
|
+
runtimes: ciConfig.runtimes || {},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
console.log(JSON.stringify(output));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("Error generating CI jobs:", e);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const formatSetup = (steps: CISetupStep[] | null | undefined): string => {
|
|
67
|
+
if (!steps || steps.length === 0) return "";
|
|
68
|
+
return steps
|
|
69
|
+
.map((s) => {
|
|
70
|
+
const cmd = s.working_directory
|
|
71
|
+
? `(cd "${s.working_directory}" && ${s.run})`
|
|
72
|
+
: s.run;
|
|
73
|
+
return `echo "::group::${s.name}"
|
|
74
|
+
${cmd}
|
|
75
|
+
echo "::endgroup::"`;
|
|
76
|
+
})
|
|
77
|
+
.join("\n");
|
|
78
|
+
};
|
|
@@ -1,37 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { registerDetectCommand } from
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerDetectCommand } from "./detect.js";
|
|
4
4
|
|
|
5
|
-
describe(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
describe("Detect Command", () => {
|
|
6
|
+
let program: Command;
|
|
7
|
+
const originalConsoleLog = console.log;
|
|
8
|
+
const originalConsoleError = console.error;
|
|
9
|
+
let logs: string[];
|
|
10
|
+
let errors: string[];
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
program = new Command();
|
|
14
|
+
registerDetectCommand(program);
|
|
15
|
+
logs = [];
|
|
16
|
+
errors = [];
|
|
17
|
+
console.log = (...args: unknown[]) => {
|
|
18
|
+
logs.push(args.join(" "));
|
|
19
|
+
};
|
|
20
|
+
console.error = (...args: unknown[]) => {
|
|
21
|
+
errors.push(args.join(" "));
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
console.log = originalConsoleLog;
|
|
27
|
+
console.error = originalConsoleError;
|
|
28
|
+
});
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
it("should register the detect command", () => {
|
|
31
|
+
const detectCmd = program.commands.find((cmd) => cmd.name() === "detect");
|
|
32
|
+
expect(detectCmd).toBeDefined();
|
|
33
|
+
expect(detectCmd?.description()).toBe(
|
|
34
|
+
"Show what gates would run for detected changes (without executing them)",
|
|
35
|
+
);
|
|
36
|
+
expect(detectCmd?.options.some((opt) => opt.long === "--commit")).toBe(
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
expect(detectCmd?.options.some((opt) => opt.long === "--uncommitted")).toBe(
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
37
43
|
});
|
package/src/commands/detect.ts
CHANGED
|
@@ -1,69 +1,97 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { loadConfig } from
|
|
4
|
-
import { ChangeDetector } from
|
|
5
|
-
import { EntryPointExpander } from
|
|
6
|
-
import {
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { loadConfig } from "../config/loader.js";
|
|
4
|
+
import { ChangeDetector } from "../core/change-detector.js";
|
|
5
|
+
import { EntryPointExpander } from "../core/entry-point.js";
|
|
6
|
+
import { type Job, JobGenerator } from "../core/job.js";
|
|
7
7
|
|
|
8
8
|
export function registerDetectCommand(program: Command): void {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (changes.length === 0) {
|
|
28
|
-
console.log(chalk.green('No changes detected.'));
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
console.log(chalk.dim(`Found ${changes.length} changed files:`));
|
|
33
|
-
changes.forEach(file => console.log(chalk.dim(` - ${file}`)));
|
|
34
|
-
console.log();
|
|
9
|
+
program
|
|
10
|
+
.command("detect")
|
|
11
|
+
.description(
|
|
12
|
+
"Show what gates would run for detected changes (without executing them)",
|
|
13
|
+
)
|
|
14
|
+
.option(
|
|
15
|
+
"-b, --base-branch <branch>",
|
|
16
|
+
"Override base branch for change detection",
|
|
17
|
+
)
|
|
18
|
+
.option("-c, --commit <sha>", "Use diff for a specific commit")
|
|
19
|
+
.option(
|
|
20
|
+
"-u, --uncommitted",
|
|
21
|
+
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
22
|
+
)
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
try {
|
|
25
|
+
const config = await loadConfig();
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
// Determine effective base branch
|
|
28
|
+
// Priority: CLI override > CI env var > config
|
|
29
|
+
const effectiveBaseBranch =
|
|
30
|
+
options.baseBranch ||
|
|
31
|
+
(process.env.GITHUB_BASE_REF &&
|
|
32
|
+
(process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
|
|
33
|
+
? process.env.GITHUB_BASE_REF
|
|
34
|
+
: null) ||
|
|
35
|
+
config.project.base_branch;
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const changeDetector = new ChangeDetector(effectiveBaseBranch, {
|
|
38
|
+
commit: options.commit,
|
|
39
|
+
uncommitted: options.uncommitted,
|
|
40
|
+
});
|
|
41
|
+
const expander = new EntryPointExpander();
|
|
42
|
+
const jobGen = new JobGenerator(config);
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// Group jobs by entry point for better display
|
|
47
|
-
const jobsByEntryPoint = new Map<string, Job[]>();
|
|
48
|
-
for (const job of jobs) {
|
|
49
|
-
if (!jobsByEntryPoint.has(job.entryPoint)) {
|
|
50
|
-
jobsByEntryPoint.set(job.entryPoint, []);
|
|
51
|
-
}
|
|
52
|
-
jobsByEntryPoint.get(job.entryPoint)!.push(job);
|
|
53
|
-
}
|
|
44
|
+
console.log(chalk.dim("Detecting changes..."));
|
|
45
|
+
const changes = await changeDetector.getChangedFiles();
|
|
54
46
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
console.log(` ${typeLabel} ${chalk.bold(job.name)}`);
|
|
60
|
-
}
|
|
61
|
-
console.log();
|
|
62
|
-
}
|
|
47
|
+
if (changes.length === 0) {
|
|
48
|
+
console.log(chalk.green("No changes detected."));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
52
|
+
console.log(chalk.dim(`Found ${changes.length} changed files:`));
|
|
53
|
+
changes.forEach((file) => {
|
|
54
|
+
console.log(chalk.dim(` - ${file}`));
|
|
55
|
+
});
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
const entryPoints = await expander.expand(
|
|
59
|
+
config.project.entry_points,
|
|
60
|
+
changes,
|
|
61
|
+
);
|
|
62
|
+
const jobs = jobGen.generateJobs(entryPoints);
|
|
63
|
+
|
|
64
|
+
if (jobs.length === 0) {
|
|
65
|
+
console.log(chalk.yellow("No applicable gates for these changes."));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(chalk.bold(`Would run ${jobs.length} gate(s):\n`));
|
|
70
|
+
|
|
71
|
+
// Group jobs by entry point for better display
|
|
72
|
+
const jobsByEntryPoint = new Map<string, Job[]>();
|
|
73
|
+
for (const job of jobs) {
|
|
74
|
+
if (!jobsByEntryPoint.has(job.entryPoint)) {
|
|
75
|
+
jobsByEntryPoint.set(job.entryPoint, []);
|
|
76
|
+
}
|
|
77
|
+
jobsByEntryPoint.get(job.entryPoint)?.push(job);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [entryPoint, entryJobs] of jobsByEntryPoint.entries()) {
|
|
81
|
+
console.log(chalk.cyan(`Entry point: ${entryPoint}`));
|
|
82
|
+
for (const job of entryJobs) {
|
|
83
|
+
const typeLabel =
|
|
84
|
+
job.type === "check"
|
|
85
|
+
? chalk.yellow("check")
|
|
86
|
+
: chalk.blue("review");
|
|
87
|
+
console.log(` ${typeLabel} ${chalk.bold(job.name)}`);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
} catch (error: unknown) {
|
|
92
|
+
const err = error as { message?: string };
|
|
93
|
+
console.error(chalk.red("Error:"), err.message);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
69
97
|
}
|