javi-forge 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/dispatch/ci.js
CHANGED
|
@@ -50,7 +50,8 @@ export async function handleCi(cli, ctx) {
|
|
|
50
50
|
if (gates.length > 0) {
|
|
51
51
|
console.log(` ${gates.length} gate(s):`);
|
|
52
52
|
for (const gate of gates) {
|
|
53
|
-
|
|
53
|
+
const image = gate.image !== undefined ? `, image: ${gate.image}` : "";
|
|
54
|
+
console.log(` - ${gate.id} (${gate.mode}, scope: ${gate.scope}${image})`);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -59,6 +59,9 @@ export async function validateCIConfig(projectDir, config) {
|
|
|
59
59
|
id: g.id,
|
|
60
60
|
mode: g.mode,
|
|
61
61
|
scope: g.scope,
|
|
62
|
+
// Surface `image` ONLY when declared, so an image-less gate summary
|
|
63
|
+
// stays byte-identical to today (no `image` key).
|
|
64
|
+
...(g.image !== undefined ? { image: g.image } : {}),
|
|
62
65
|
})),
|
|
63
66
|
};
|
|
64
67
|
}
|
package/dist/commands/ci.d.ts
CHANGED
|
@@ -80,6 +80,18 @@ export interface ResolveRunnerOptions {
|
|
|
80
80
|
*/
|
|
81
81
|
export declare function resolveCIRunners(projectDir: string, options?: ResolveRunnerOptions): Promise<ResolvedRunners>;
|
|
82
82
|
export declare function runCI(options: CIOptions, onStep: CIStepCallback, onGateOutcome?: (outcome: GateOutcome) => void): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Result of a single host-native gate command. `code` is the resolved exit code
|
|
85
|
+
* (a timed-out command resolves the `timeout(1)` sentinel 124); `timedOut` is
|
|
86
|
+
* `true` IFF the internal wall-clock `killTimer` fired. The flag exists so a
|
|
87
|
+
* caller can tell a wall-clock timeout apart from a child that itself exits 124
|
|
88
|
+
* (a `curl` op-timeout, a nested `timeout(1)`, a script returning 124) — both
|
|
89
|
+
* carry `code: 124`, but only a real timeout carries `timedOut: true` (R3-004).
|
|
90
|
+
*/
|
|
91
|
+
export interface GateRunResult {
|
|
92
|
+
code: number;
|
|
93
|
+
timedOut: boolean;
|
|
94
|
+
}
|
|
83
95
|
/**
|
|
84
96
|
* Execute a single gate command HOST-NATIVE via `bash -c`, at the repo root,
|
|
85
97
|
* with the provided env MAP. Modeled on `runSemgrep`/`runGhagga` (a spawned
|
|
@@ -112,7 +124,7 @@ export declare function runCI(options: CIOptions, onStep: CIStepCallback, onGate
|
|
|
112
124
|
* Env values arrive as discrete map entries — never string-spliced into the
|
|
113
125
|
* `bash -c` command — so metacharacters in a value cannot break out of the shell.
|
|
114
126
|
*/
|
|
115
|
-
export declare function runGateNative(cmd: string, cwd: string, env: Record<string, string>, timeoutSec?: number): Promise<
|
|
127
|
+
export declare function runGateNative(cmd: string, cwd: string, env: Record<string, string>, timeoutSec?: number): Promise<GateRunResult>;
|
|
116
128
|
/**
|
|
117
129
|
* A single gate's structured result, collected for the headless JSON run path.
|
|
118
130
|
* Mirrors the `{ id, mode, scope, status, blocking, changedFiles?, exitCode? }`
|
|
@@ -130,10 +142,12 @@ export interface GateOutcome {
|
|
|
130
142
|
/** First non-zero command code for a failed gate. */
|
|
131
143
|
exitCode?: number;
|
|
132
144
|
/**
|
|
133
|
-
* Human-readable cause of a degrade/skip, surfaced so the headless
|
|
134
|
-
* consumer sees
|
|
135
|
-
*
|
|
136
|
-
*
|
|
145
|
+
* Human-readable cause of a degrade/skip/timeout, surfaced so the headless
|
|
146
|
+
* JSON consumer sees it LOUDLY — not just in the Ink stream. Populated for the
|
|
147
|
+
* scope:changed skip variants (base ref null, changed-file resolution failure
|
|
148
|
+
* under a shallow clone / missing ref, empty changed-set skip) AND for a gate
|
|
149
|
+
* that timed out (so a 124 wall-clock kill is distinguishable from a command
|
|
150
|
+
* that itself exits 124).
|
|
137
151
|
*/
|
|
138
152
|
reason?: string;
|
|
139
153
|
}
|
package/dist/commands/ci.js
CHANGED
|
@@ -822,19 +822,25 @@ export async function runGateNative(cmd, cwd, env, timeoutSec) {
|
|
|
822
822
|
// INVARIANT: timedOut ⇒ non-zero, ALWAYS. Even a child that trapped the
|
|
823
823
|
// SIGTERM and exited 0 before the SIGKILL escalation is a timeout, not a
|
|
824
824
|
// pass. Resolve the GNU `timeout(1)` sentinel 124 ("command timed out"),
|
|
825
|
-
// semantically distinct from a signal-death 143/137.
|
|
826
|
-
|
|
825
|
+
// semantically distinct from a signal-death 143/137. The `timedOut` flag
|
|
826
|
+
// travels with the code so the caller can tell a wall-clock timeout apart
|
|
827
|
+
// from a child that itself exits 124 (both are 124, but only one is a
|
|
828
|
+
// timeout — R3-004 observability).
|
|
829
|
+
resolve({ code: GATE_TIMEOUT_EXIT_CODE, timedOut: true });
|
|
827
830
|
return;
|
|
828
831
|
}
|
|
829
832
|
if (code !== null) {
|
|
830
|
-
resolve(code);
|
|
833
|
+
resolve({ code, timedOut: false });
|
|
831
834
|
return;
|
|
832
835
|
}
|
|
833
836
|
// Signal death: map to a non-zero code so the collector records a
|
|
834
837
|
// blocking failure. `128 + signum` mirrors the shell; fall back to 1
|
|
835
838
|
// when the signal name is not resolvable.
|
|
836
839
|
const signum = signal ? os.constants.signals[signal] : undefined;
|
|
837
|
-
resolve(
|
|
840
|
+
resolve({
|
|
841
|
+
code: signum !== undefined ? 128 + signum : 1,
|
|
842
|
+
timedOut: false,
|
|
843
|
+
});
|
|
838
844
|
});
|
|
839
845
|
proc.on("error", (e) => {
|
|
840
846
|
clearTimers();
|
|
@@ -974,13 +980,16 @@ async function runGates(gates, projectDir, onStep, onOutcome) {
|
|
|
974
980
|
Object.assign(gateEnv, gate.env);
|
|
975
981
|
}
|
|
976
982
|
let exitCode = 0;
|
|
983
|
+
let timedOut = false;
|
|
977
984
|
let spawnError;
|
|
978
985
|
try {
|
|
979
986
|
for (const cmd of gate.run) {
|
|
980
987
|
// timeout is per-command (matches the fail-fast model): each command
|
|
981
988
|
// gets its own wall-clock budget. A timed-out command is killed and
|
|
982
989
|
// resolves non-zero, so fail-fast stops the gate here.
|
|
983
|
-
|
|
990
|
+
const result = await runGateNative(cmd, projectDir, gateEnv, gate.timeout);
|
|
991
|
+
exitCode = result.code;
|
|
992
|
+
timedOut = result.timedOut;
|
|
984
993
|
if (exitCode !== 0)
|
|
985
994
|
break; // fail-fast: skip the remaining commands
|
|
986
995
|
}
|
|
@@ -993,15 +1002,31 @@ async function runGates(gates, projectDir, onStep, onOutcome) {
|
|
|
993
1002
|
emit("done", { changedFiles: gateChangedFiles });
|
|
994
1003
|
continue;
|
|
995
1004
|
}
|
|
1005
|
+
// R3-004: a timed-out gate carries a `reason` naming the timeout so the
|
|
1006
|
+
// JSON/dashboard consumer can distinguish "this gate timed out (bump the
|
|
1007
|
+
// timeout)" from "the command failed with 124 (fix the command)". A
|
|
1008
|
+
// non-timeout failure leaves `reason` undefined. Never key on the 124 value
|
|
1009
|
+
// itself — that IS the ambiguity; only the real `timedOut` signal disambiguates.
|
|
1010
|
+
const timeoutReason = timedOut
|
|
1011
|
+
? `timed out after ${gate.timeout}s`
|
|
1012
|
+
: undefined;
|
|
996
1013
|
const detail = spawnError !== undefined ? String(spawnError) : `exit ${exitCode}`;
|
|
997
1014
|
if (blocking) {
|
|
998
1015
|
blockingFailures.push(gate.id);
|
|
999
1016
|
report(onStep, stepId, `${label} failed`, "error", detail);
|
|
1000
|
-
emit("error", {
|
|
1017
|
+
emit("error", {
|
|
1018
|
+
changedFiles: gateChangedFiles,
|
|
1019
|
+
exitCode,
|
|
1020
|
+
reason: timeoutReason,
|
|
1021
|
+
});
|
|
1001
1022
|
}
|
|
1002
1023
|
else {
|
|
1003
1024
|
report(onStep, stepId, `${label} failed (informative)`, "warning", detail);
|
|
1004
|
-
emit("warning", {
|
|
1025
|
+
emit("warning", {
|
|
1026
|
+
changedFiles: gateChangedFiles,
|
|
1027
|
+
exitCode,
|
|
1028
|
+
reason: timeoutReason,
|
|
1029
|
+
});
|
|
1005
1030
|
}
|
|
1006
1031
|
}
|
|
1007
1032
|
if (blockingFailures.length > 0) {
|
package/dist/lib/ci-config.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export interface CIGateConfig {
|
|
|
60
60
|
baseline?: string;
|
|
61
61
|
/** Optional env injected via the child-process env map (slice 4). */
|
|
62
62
|
env?: Record<string, string>;
|
|
63
|
+
/**
|
|
64
|
+
* Optional container image ref (containerized-gates). A plain,
|
|
65
|
+
* digest-pinnable image ref; when present the gate runs inside this image
|
|
66
|
+
* instead of host-native. Omitted → host-native (unchanged). Execution
|
|
67
|
+
* routing lands in a later slice; this slice validates the schema only.
|
|
68
|
+
*/
|
|
69
|
+
image?: string;
|
|
63
70
|
/**
|
|
64
71
|
* Optional per-command wall-clock timeout in seconds (GATE-2). When set, a
|
|
65
72
|
* command exceeding it is killed and the gate FAILS (non-zero). Omitted →
|
package/dist/lib/ci-config.js
CHANGED
|
@@ -221,6 +221,7 @@ const GATE_FIELDS = new Set([
|
|
|
221
221
|
"scope",
|
|
222
222
|
"baseline",
|
|
223
223
|
"env",
|
|
224
|
+
"image",
|
|
224
225
|
"timeout",
|
|
225
226
|
]);
|
|
226
227
|
function validateGate(raw, index, errors) {
|
|
@@ -318,6 +319,28 @@ function validateGate(raw, index, errors) {
|
|
|
318
319
|
env = raw.env;
|
|
319
320
|
}
|
|
320
321
|
}
|
|
322
|
+
let image;
|
|
323
|
+
if (raw.image !== undefined) {
|
|
324
|
+
if (typeof raw.image !== "string" || !raw.image.trim()) {
|
|
325
|
+
errors.push({
|
|
326
|
+
path: `${base}.image`,
|
|
327
|
+
message: "image must be a non-empty string",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else if (raw.image.trim().startsWith("-")) {
|
|
331
|
+
// Harden against docker-flag injection: an image ref like "--privileged"
|
|
332
|
+
// or "-v /:/host" would be parsed by `docker run` as a FLAG, not an
|
|
333
|
+
// image argument, if it reached argv. Reject leading-dash refs at
|
|
334
|
+
// validation with a named error (JDB-004).
|
|
335
|
+
errors.push({
|
|
336
|
+
path: `${base}.image`,
|
|
337
|
+
message: "image must not start with '-' (would be parsed as a docker flag)",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
image = raw.image;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
321
344
|
let timeout;
|
|
322
345
|
if (raw.timeout !== undefined) {
|
|
323
346
|
if (typeof raw.timeout !== "number" ||
|
|
@@ -339,6 +362,7 @@ function validateGate(raw, index, errors) {
|
|
|
339
362
|
scope,
|
|
340
363
|
baseline,
|
|
341
364
|
env,
|
|
365
|
+
image,
|
|
342
366
|
timeout,
|
|
343
367
|
};
|
|
344
368
|
}
|