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
package/src/core/job.ts
CHANGED
|
@@ -1,74 +1,84 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
CheckGateConfig,
|
|
3
|
+
LoadedConfig,
|
|
4
|
+
ReviewGateConfig,
|
|
5
|
+
ReviewPromptFrontmatter,
|
|
6
|
+
} from "../config/types.js";
|
|
7
|
+
import type { ExpandedEntryPoint } from "./entry-point.js";
|
|
3
8
|
|
|
4
|
-
export type JobType =
|
|
9
|
+
export type JobType = "check" | "review";
|
|
5
10
|
|
|
6
11
|
export interface Job {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
id: string; // unique id for logging/tracking
|
|
13
|
+
type: JobType;
|
|
14
|
+
name: string;
|
|
15
|
+
entryPoint: string;
|
|
16
|
+
gateConfig: CheckGateConfig | (ReviewGateConfig & ReviewPromptFrontmatter);
|
|
17
|
+
workingDirectory: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export class JobGenerator {
|
|
16
|
-
|
|
21
|
+
constructor(private config: LoadedConfig) {}
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
generateJobs(expandedEntryPoints: ExpandedEntryPoint[]): Job[] {
|
|
24
|
+
const jobs: Job[] = [];
|
|
25
|
+
const isCI =
|
|
26
|
+
process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
for (const ep of expandedEntryPoints) {
|
|
29
|
+
// 1. Process Checks
|
|
30
|
+
if (ep.config.checks) {
|
|
31
|
+
for (const checkName of ep.config.checks) {
|
|
32
|
+
const checkConfig = this.config.checks[checkName];
|
|
33
|
+
if (!checkConfig) {
|
|
34
|
+
console.warn(
|
|
35
|
+
`Warning: Check gate '${checkName}' configured in entry point '${ep.path}' but not found in checks definitions.`,
|
|
36
|
+
);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
31
39
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
// Filter based on environment
|
|
41
|
+
if (isCI && !checkConfig.run_in_ci) continue;
|
|
42
|
+
if (!isCI && !checkConfig.run_locally) continue;
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
jobs.push({
|
|
45
|
+
id: `check:${ep.path}:${checkName}`,
|
|
46
|
+
type: "check",
|
|
47
|
+
name: checkName,
|
|
48
|
+
entryPoint: ep.path,
|
|
49
|
+
gateConfig: checkConfig,
|
|
50
|
+
workingDirectory: checkConfig.working_directory || ep.path,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// 2. Process Reviews
|
|
56
|
+
if (ep.config.reviews) {
|
|
57
|
+
for (const reviewName of ep.config.reviews) {
|
|
58
|
+
const reviewConfig = this.config.reviews[reviewName];
|
|
59
|
+
if (!reviewConfig) {
|
|
60
|
+
console.warn(
|
|
61
|
+
`Warning: Review gate '${reviewName}' configured in entry point '${ep.path}' but not found in reviews definitions.`,
|
|
62
|
+
);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
55
65
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
// Filter based on environment
|
|
67
|
+
if (isCI && !reviewConfig.run_in_ci) continue;
|
|
68
|
+
if (!isCI && !reviewConfig.run_locally) continue;
|
|
59
69
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
jobs.push({
|
|
71
|
+
id: `review:${ep.path}:${reviewName}`,
|
|
72
|
+
type: "review",
|
|
73
|
+
name: reviewName,
|
|
74
|
+
entryPoint: ep.path,
|
|
75
|
+
gateConfig: reviewConfig,
|
|
76
|
+
workingDirectory: ep.path, // Reviews always run in context of entry point
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
return jobs;
|
|
83
|
+
}
|
|
74
84
|
}
|
package/src/core/runner.ts
CHANGED
|
@@ -1,235 +1,269 @@
|
|
|
1
|
-
import { exec } from
|
|
2
|
-
import {
|
|
3
|
-
import fs from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { getAdapter } from "../cli-adapters/index.js";
|
|
7
|
+
import type {
|
|
8
|
+
LoadedConfig,
|
|
9
|
+
ReviewGateConfig,
|
|
10
|
+
ReviewPromptFrontmatter,
|
|
11
|
+
} from "../config/types.js";
|
|
12
|
+
import { CheckGateExecutor } from "../gates/check.js";
|
|
13
|
+
import type { GateResult } from "../gates/result.js";
|
|
14
|
+
import { ReviewGateExecutor } from "../gates/review.js";
|
|
15
|
+
import type { ConsoleReporter } from "../output/console.js";
|
|
16
|
+
import type { Logger } from "../output/logger.js";
|
|
17
|
+
import type { PreviousViolation } from "../utils/log-parser.js";
|
|
18
|
+
import { sanitizeJobId } from "../utils/sanitizer.js";
|
|
19
|
+
import type { Job } from "./job.js";
|
|
16
20
|
|
|
17
21
|
const execAsync = promisify(exec);
|
|
18
22
|
|
|
19
23
|
export class Runner {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
24
|
+
private checkExecutor = new CheckGateExecutor();
|
|
25
|
+
private reviewExecutor = new ReviewGateExecutor();
|
|
26
|
+
private results: GateResult[] = [];
|
|
27
|
+
private shouldStop = false;
|
|
28
|
+
|
|
29
|
+
constructor(
|
|
30
|
+
private config: LoadedConfig,
|
|
31
|
+
private logger: Logger,
|
|
32
|
+
private reporter: ConsoleReporter,
|
|
33
|
+
private previousFailuresMap?: Map<string, Map<string, PreviousViolation[]>>,
|
|
34
|
+
private changeOptions?: { commit?: string; uncommitted?: boolean },
|
|
35
|
+
private baseBranchOverride?: string,
|
|
36
|
+
) {}
|
|
37
|
+
|
|
38
|
+
async run(jobs: Job[]): Promise<boolean> {
|
|
39
|
+
await this.logger.init();
|
|
40
|
+
|
|
41
|
+
const { runnableJobs, preflightResults } = await this.preflight(jobs);
|
|
42
|
+
this.results.push(...preflightResults);
|
|
43
|
+
|
|
44
|
+
const parallelEnabled = this.config.project.allow_parallel;
|
|
45
|
+
const parallelJobs = parallelEnabled
|
|
46
|
+
? runnableJobs.filter((j) => j.gateConfig.parallel)
|
|
47
|
+
: [];
|
|
48
|
+
const sequentialJobs = parallelEnabled
|
|
49
|
+
? runnableJobs.filter((j) => !j.gateConfig.parallel)
|
|
50
|
+
: runnableJobs;
|
|
51
|
+
|
|
52
|
+
// Start parallel jobs
|
|
53
|
+
const parallelPromises = parallelJobs.map((job) => this.executeJob(job));
|
|
54
|
+
|
|
55
|
+
// Start sequential jobs
|
|
56
|
+
// We run them one by one, but concurrently with the parallel batch
|
|
57
|
+
const sequentialPromise = (async () => {
|
|
58
|
+
for (const job of sequentialJobs) {
|
|
59
|
+
if (this.shouldStop) break;
|
|
60
|
+
await this.executeJob(job);
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
|
|
64
|
+
await Promise.all([...parallelPromises, sequentialPromise]);
|
|
65
|
+
|
|
66
|
+
await this.reporter.printSummary(this.results);
|
|
67
|
+
|
|
68
|
+
return this.results.every((r) => r.status === "pass");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async executeJob(job: Job): Promise<void> {
|
|
72
|
+
if (this.shouldStop) return;
|
|
73
|
+
|
|
74
|
+
this.reporter.onJobStart(job);
|
|
75
|
+
|
|
76
|
+
let result: GateResult;
|
|
77
|
+
|
|
78
|
+
if (job.type === "check") {
|
|
79
|
+
const logPath = this.logger.getLogPath(job.id);
|
|
80
|
+
const jobLogger = await this.logger.createJobLogger(job.id);
|
|
81
|
+
result = await this.checkExecutor.execute(
|
|
82
|
+
job.id,
|
|
83
|
+
job.gateConfig as CheckGateConfig,
|
|
84
|
+
job.workingDirectory,
|
|
85
|
+
jobLogger,
|
|
86
|
+
);
|
|
87
|
+
result.logPath = logPath;
|
|
88
|
+
} else {
|
|
89
|
+
// Use sanitized Job ID for lookup because that's what log-parser uses (based on filenames)
|
|
90
|
+
const safeJobId = sanitizeJobId(job.id);
|
|
91
|
+
const previousFailures = this.previousFailuresMap?.get(safeJobId);
|
|
92
|
+
const loggerFactory = this.logger.createLoggerFactory(job.id);
|
|
93
|
+
const effectiveBaseBranch =
|
|
94
|
+
this.baseBranchOverride || this.config.project.base_branch;
|
|
95
|
+
result = await this.reviewExecutor.execute(
|
|
96
|
+
job.id,
|
|
97
|
+
job.gateConfig as ReviewGateConfig & ReviewPromptFrontmatter,
|
|
98
|
+
job.entryPoint,
|
|
99
|
+
loggerFactory,
|
|
100
|
+
effectiveBaseBranch,
|
|
101
|
+
previousFailures,
|
|
102
|
+
this.changeOptions,
|
|
103
|
+
this.config.project.cli.check_usage_limit,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.results.push(result);
|
|
108
|
+
this.reporter.onJobComplete(job, result);
|
|
109
|
+
|
|
110
|
+
// Handle Fail Fast (only for checks, and only when parallel is false)
|
|
111
|
+
// fail_fast can only be set on checks when parallel is false (enforced by schema)
|
|
112
|
+
if (
|
|
113
|
+
result.status !== "pass" &&
|
|
114
|
+
job.type === "check" &&
|
|
115
|
+
job.gateConfig.fail_fast
|
|
116
|
+
) {
|
|
117
|
+
this.shouldStop = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async preflight(
|
|
122
|
+
jobs: Job[],
|
|
123
|
+
): Promise<{ runnableJobs: Job[]; preflightResults: GateResult[] }> {
|
|
124
|
+
const runnableJobs: Job[] = [];
|
|
125
|
+
const preflightResults: GateResult[] = [];
|
|
126
|
+
const cliCache = new Map<string, boolean>();
|
|
127
|
+
|
|
128
|
+
for (const job of jobs) {
|
|
129
|
+
if (this.shouldStop) break;
|
|
130
|
+
if (job.type === "check") {
|
|
131
|
+
const commandName = this.getCommandName(
|
|
132
|
+
(job.gateConfig as CheckGateConfig).command,
|
|
133
|
+
);
|
|
134
|
+
if (!commandName) {
|
|
135
|
+
preflightResults.push(
|
|
136
|
+
await this.recordPreflightFailure(job, "Unable to parse command"),
|
|
137
|
+
);
|
|
138
|
+
if (this.shouldFailFast(job)) this.shouldStop = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const available = await this.commandExists(
|
|
143
|
+
commandName,
|
|
144
|
+
job.workingDirectory,
|
|
145
|
+
);
|
|
146
|
+
if (!available) {
|
|
147
|
+
preflightResults.push(
|
|
148
|
+
await this.recordPreflightFailure(
|
|
149
|
+
job,
|
|
150
|
+
`Missing command: ${commandName}`,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
if (this.shouldFailFast(job)) this.shouldStop = true;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
const reviewConfig = job.gateConfig as ReviewGateConfig &
|
|
158
|
+
ReviewPromptFrontmatter;
|
|
159
|
+
const required = reviewConfig.num_reviews ?? 1;
|
|
160
|
+
const availableTools: string[] = [];
|
|
161
|
+
|
|
162
|
+
for (const toolName of reviewConfig.cli_preference || []) {
|
|
163
|
+
if (availableTools.length >= required) break;
|
|
164
|
+
const cached = cliCache.get(toolName);
|
|
165
|
+
const isAvailable = cached ?? (await this.checkAdapter(toolName));
|
|
166
|
+
cliCache.set(toolName, isAvailable);
|
|
167
|
+
if (isAvailable) availableTools.push(toolName);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (availableTools.length < required) {
|
|
171
|
+
preflightResults.push(
|
|
172
|
+
await this.recordPreflightFailure(
|
|
173
|
+
job,
|
|
174
|
+
`Missing CLI tools: need ${required}, found ${availableTools.length}`,
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
if (this.shouldFailFast(job)) this.shouldStop = true;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
runnableJobs.push(job);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { runnableJobs, preflightResults };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private async recordPreflightFailure(
|
|
189
|
+
job: Job,
|
|
190
|
+
message: string,
|
|
191
|
+
): Promise<GateResult> {
|
|
192
|
+
if (job.type === "check") {
|
|
193
|
+
const logPath = this.logger.getLogPath(job.id);
|
|
194
|
+
const jobLogger = await this.logger.createJobLogger(job.id);
|
|
195
|
+
await jobLogger(
|
|
196
|
+
`[${new Date().toISOString()}] Health check failed\n${message}\n`,
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
jobId: job.id,
|
|
200
|
+
status: "error",
|
|
201
|
+
duration: 0,
|
|
202
|
+
message,
|
|
203
|
+
logPath,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
jobId: job.id,
|
|
209
|
+
status: "error",
|
|
210
|
+
duration: 0,
|
|
211
|
+
message,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async checkAdapter(name: string): Promise<boolean> {
|
|
216
|
+
const adapter = getAdapter(name);
|
|
217
|
+
if (!adapter) return false;
|
|
218
|
+
const health = await adapter.checkHealth({
|
|
219
|
+
checkUsageLimit: this.config.project.cli.check_usage_limit,
|
|
220
|
+
});
|
|
221
|
+
return health.status === "healthy";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private getCommandName(command: string): string | null {
|
|
225
|
+
const tokens = this.tokenize(command);
|
|
226
|
+
for (const token of tokens) {
|
|
227
|
+
if (token === "env") continue;
|
|
228
|
+
if (this.isEnvAssignment(token)) continue;
|
|
229
|
+
return token;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private tokenize(command: string): string[] {
|
|
235
|
+
const matches = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
|
|
236
|
+
if (!matches) return [];
|
|
237
|
+
return matches.map((token) => token.replace(/^['"]|['"]$/g, ""));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private isEnvAssignment(token: string): boolean {
|
|
241
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async commandExists(command: string, cwd: string): Promise<boolean> {
|
|
245
|
+
if (command.includes("/") || command.startsWith(".")) {
|
|
246
|
+
const resolved = path.isAbsolute(command)
|
|
247
|
+
? command
|
|
248
|
+
: path.join(cwd, command);
|
|
249
|
+
try {
|
|
250
|
+
await fs.access(resolved, fsConstants.X_OK);
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await execAsync(`command -v ${command}`);
|
|
259
|
+
return true;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private shouldFailFast(job: Job): boolean {
|
|
266
|
+
// Only checks can have fail_fast, and only when parallel is false
|
|
267
|
+
return Boolean(job.type === "check" && job.gateConfig.fail_fast);
|
|
268
|
+
}
|
|
235
269
|
}
|