javi-forge 1.14.0 → 1.15.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/commands/ci.d.ts +14 -1
- package/dist/commands/ci.js +68 -4
- package/dist/lib/ci-config.d.ts +6 -0
- package/dist/lib/ci-config.js +24 -1
- package/package.json +1 -1
package/dist/commands/ci.d.ts
CHANGED
|
@@ -96,10 +96,23 @@ export declare function runCI(options: CIOptions, onStep: CIStepCallback, onGate
|
|
|
96
96
|
* eliminate. A null code therefore resolves to a NON-ZERO code using the shell
|
|
97
97
|
* convention `128 + <signal number>` when the signal is resolvable, else 1.
|
|
98
98
|
*
|
|
99
|
+
* TIMEOUT (GATE-2): when `timeoutSec` is set, a command still running after that
|
|
100
|
+
* many wall-clock seconds is killed — SIGTERM first, escalated to SIGKILL after a
|
|
101
|
+
* short grace if it ignores SIGTERM. A `timedOut` flag is set BEFORE the SIGTERM
|
|
102
|
+
* lands, and on `close` it OVERRIDES the child's reported code with the timeout
|
|
103
|
+
* sentinel 124 — REGARDLESS of what the child reported. This closes the false-green
|
|
104
|
+
* where a child that TRAPS SIGTERM and exits 0 gracefully (dev servers, watchers,
|
|
105
|
+
* SIGTERM-handling CLIs) would otherwise resolve 0 and PASS a timed-out blocking
|
|
106
|
+
* gate. INVARIANT: `timedOut ⇒ non-zero, ALWAYS` (124, the GNU `timeout(1)`
|
|
107
|
+
* convention — distinct from a signal-death 143/137). Timers are cleared on
|
|
108
|
+
* `close`/`error` so no dangling handle keeps the process alive. Omitting
|
|
109
|
+
* `timeoutSec` preserves the pre-GATE-2 behavior exactly (no timer, runs to
|
|
110
|
+
* completion).
|
|
111
|
+
*
|
|
99
112
|
* Env values arrive as discrete map entries — never string-spliced into the
|
|
100
113
|
* `bash -c` command — so metacharacters in a value cannot break out of the shell.
|
|
101
114
|
*/
|
|
102
|
-
export declare function runGateNative(cmd: string, cwd: string, env: Record<string, string
|
|
115
|
+
export declare function runGateNative(cmd: string, cwd: string, env: Record<string, string>, timeoutSec?: number): Promise<number>;
|
|
103
116
|
/**
|
|
104
117
|
* A single gate's structured result, collected for the headless JSON run path.
|
|
105
118
|
* Mirrors the `{ id, mode, scope, status, blocking, changedFiles?, exitCode? }`
|
package/dist/commands/ci.js
CHANGED
|
@@ -773,13 +773,59 @@ function filterDefinedEnv(env) {
|
|
|
773
773
|
* eliminate. A null code therefore resolves to a NON-ZERO code using the shell
|
|
774
774
|
* convention `128 + <signal number>` when the signal is resolvable, else 1.
|
|
775
775
|
*
|
|
776
|
+
* TIMEOUT (GATE-2): when `timeoutSec` is set, a command still running after that
|
|
777
|
+
* many wall-clock seconds is killed — SIGTERM first, escalated to SIGKILL after a
|
|
778
|
+
* short grace if it ignores SIGTERM. A `timedOut` flag is set BEFORE the SIGTERM
|
|
779
|
+
* lands, and on `close` it OVERRIDES the child's reported code with the timeout
|
|
780
|
+
* sentinel 124 — REGARDLESS of what the child reported. This closes the false-green
|
|
781
|
+
* where a child that TRAPS SIGTERM and exits 0 gracefully (dev servers, watchers,
|
|
782
|
+
* SIGTERM-handling CLIs) would otherwise resolve 0 and PASS a timed-out blocking
|
|
783
|
+
* gate. INVARIANT: `timedOut ⇒ non-zero, ALWAYS` (124, the GNU `timeout(1)`
|
|
784
|
+
* convention — distinct from a signal-death 143/137). Timers are cleared on
|
|
785
|
+
* `close`/`error` so no dangling handle keeps the process alive. Omitting
|
|
786
|
+
* `timeoutSec` preserves the pre-GATE-2 behavior exactly (no timer, runs to
|
|
787
|
+
* completion).
|
|
788
|
+
*
|
|
776
789
|
* Env values arrive as discrete map entries — never string-spliced into the
|
|
777
790
|
* `bash -c` command — so metacharacters in a value cannot break out of the shell.
|
|
778
791
|
*/
|
|
779
|
-
export async function runGateNative(cmd, cwd, env) {
|
|
792
|
+
export async function runGateNative(cmd, cwd, env, timeoutSec) {
|
|
780
793
|
return await new Promise((resolve, reject) => {
|
|
781
794
|
const proc = spawn("bash", ["-c", cmd], { cwd, env, stdio: "inherit" });
|
|
795
|
+
let killTimer;
|
|
796
|
+
let graceTimer;
|
|
797
|
+
let timedOut = false;
|
|
798
|
+
const clearTimers = () => {
|
|
799
|
+
if (killTimer !== undefined)
|
|
800
|
+
clearTimeout(killTimer);
|
|
801
|
+
if (graceTimer !== undefined)
|
|
802
|
+
clearTimeout(graceTimer);
|
|
803
|
+
};
|
|
804
|
+
if (timeoutSec !== undefined) {
|
|
805
|
+
killTimer = setTimeout(() => {
|
|
806
|
+
// The wall-clock budget expired: mark the run as timed out BEFORE the
|
|
807
|
+
// SIGTERM lands. A child that traps SIGTERM and exits 0 gracefully would
|
|
808
|
+
// otherwise report code=0 → a false-green; the `timedOut` flag overrides
|
|
809
|
+
// that below so a timed-out gate is ALWAYS a FAILURE.
|
|
810
|
+
timedOut = true;
|
|
811
|
+
// Ask politely, then force: SIGKILL if the child is still alive after
|
|
812
|
+
// the grace window.
|
|
813
|
+
proc.kill("SIGTERM");
|
|
814
|
+
graceTimer = setTimeout(() => {
|
|
815
|
+
proc.kill("SIGKILL");
|
|
816
|
+
}, GATE_TIMEOUT_GRACE_MS);
|
|
817
|
+
}, timeoutSec * 1000);
|
|
818
|
+
}
|
|
782
819
|
proc.on("close", (code, signal) => {
|
|
820
|
+
clearTimers();
|
|
821
|
+
if (timedOut) {
|
|
822
|
+
// INVARIANT: timedOut ⇒ non-zero, ALWAYS. Even a child that trapped the
|
|
823
|
+
// SIGTERM and exited 0 before the SIGKILL escalation is a timeout, not a
|
|
824
|
+
// pass. Resolve the GNU `timeout(1)` sentinel 124 ("command timed out"),
|
|
825
|
+
// semantically distinct from a signal-death 143/137.
|
|
826
|
+
resolve(GATE_TIMEOUT_EXIT_CODE);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
783
829
|
if (code !== null) {
|
|
784
830
|
resolve(code);
|
|
785
831
|
return;
|
|
@@ -790,9 +836,21 @@ export async function runGateNative(cmd, cwd, env) {
|
|
|
790
836
|
const signum = signal ? os.constants.signals[signal] : undefined;
|
|
791
837
|
resolve(signum !== undefined ? 128 + signum : 1);
|
|
792
838
|
});
|
|
793
|
-
proc.on("error",
|
|
839
|
+
proc.on("error", (e) => {
|
|
840
|
+
clearTimers();
|
|
841
|
+
reject(e);
|
|
842
|
+
});
|
|
794
843
|
});
|
|
795
844
|
}
|
|
845
|
+
/** Grace between the timeout SIGTERM and the escalated SIGKILL. */
|
|
846
|
+
const GATE_TIMEOUT_GRACE_MS = 2000;
|
|
847
|
+
/**
|
|
848
|
+
* Exit code resolved for a timed-out gate, regardless of how the child died.
|
|
849
|
+
* 124 is the GNU `timeout(1)` convention for "command timed out" — semantically
|
|
850
|
+
* distinct from a signal-death 143 (SIGTERM) / 137 (SIGKILL), and it CANNOT be a
|
|
851
|
+
* false-green because it is non-zero. Enforces `timedOut ⇒ non-zero, always`.
|
|
852
|
+
*/
|
|
853
|
+
const GATE_TIMEOUT_EXIT_CODE = 124;
|
|
796
854
|
/**
|
|
797
855
|
* Env var carrying a scope:changed gate's newline-joined, root-relative paths.
|
|
798
856
|
*
|
|
@@ -820,7 +878,10 @@ const BASELINE_ENV = "JAVI_FORGE_BASELINE";
|
|
|
820
878
|
* contribute to the accumulator, so the exit code stays 0.
|
|
821
879
|
*
|
|
822
880
|
* Multi-command gates run in order and STOP at the first non-zero exit
|
|
823
|
-
* (fail-fast, matching the runner precedent); that first code is reported.
|
|
881
|
+
* (fail-fast, matching the runner precedent); that first code is reported. An
|
|
882
|
+
* optional per-gate `timeout` (seconds) is applied PER COMMAND: a command that
|
|
883
|
+
* exceeds it is killed and resolves non-zero, so the timed-out gate fails
|
|
884
|
+
* (blocking) or warns (informative) — never a false-green.
|
|
824
885
|
*
|
|
825
886
|
* `scope: changed` consumes the injectable `git-diff.ts` engine: the base ref is
|
|
826
887
|
* resolved and the changed set computed ONCE, then shared. A non-empty set runs
|
|
@@ -916,7 +977,10 @@ async function runGates(gates, projectDir, onStep, onOutcome) {
|
|
|
916
977
|
let spawnError;
|
|
917
978
|
try {
|
|
918
979
|
for (const cmd of gate.run) {
|
|
919
|
-
|
|
980
|
+
// timeout is per-command (matches the fail-fast model): each command
|
|
981
|
+
// gets its own wall-clock budget. A timed-out command is killed and
|
|
982
|
+
// resolves non-zero, so fail-fast stops the gate here.
|
|
983
|
+
exitCode = await runGateNative(cmd, projectDir, gateEnv, gate.timeout);
|
|
920
984
|
if (exitCode !== 0)
|
|
921
985
|
break; // fail-fast: skip the remaining commands
|
|
922
986
|
}
|
package/dist/lib/ci-config.d.ts
CHANGED
|
@@ -60,6 +60,12 @@ 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 per-command wall-clock timeout in seconds (GATE-2). When set, a
|
|
65
|
+
* command exceeding it is killed and the gate FAILS (non-zero). Omitted →
|
|
66
|
+
* no timeout (unchanged behavior).
|
|
67
|
+
*/
|
|
68
|
+
timeout?: number;
|
|
63
69
|
}
|
|
64
70
|
export interface CIConfig {
|
|
65
71
|
version: number;
|
package/dist/lib/ci-config.js
CHANGED
|
@@ -214,7 +214,15 @@ function validateRunner(raw, index, errors) {
|
|
|
214
214
|
requires,
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
|
-
const GATE_FIELDS = new Set([
|
|
217
|
+
const GATE_FIELDS = new Set([
|
|
218
|
+
"id",
|
|
219
|
+
"run",
|
|
220
|
+
"mode",
|
|
221
|
+
"scope",
|
|
222
|
+
"baseline",
|
|
223
|
+
"env",
|
|
224
|
+
"timeout",
|
|
225
|
+
]);
|
|
218
226
|
function validateGate(raw, index, errors) {
|
|
219
227
|
const base = `gates[${index}]`;
|
|
220
228
|
if (!isRecord(raw)) {
|
|
@@ -310,6 +318,20 @@ function validateGate(raw, index, errors) {
|
|
|
310
318
|
env = raw.env;
|
|
311
319
|
}
|
|
312
320
|
}
|
|
321
|
+
let timeout;
|
|
322
|
+
if (raw.timeout !== undefined) {
|
|
323
|
+
if (typeof raw.timeout !== "number" ||
|
|
324
|
+
!Number.isFinite(raw.timeout) ||
|
|
325
|
+
raw.timeout <= 0) {
|
|
326
|
+
errors.push({
|
|
327
|
+
path: `${base}.timeout`,
|
|
328
|
+
message: `timeout must be a positive number of seconds (got "${String(raw.timeout)}")`,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
timeout = raw.timeout;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
313
335
|
return {
|
|
314
336
|
id: typeof id === "string" ? id.trim() : "",
|
|
315
337
|
run,
|
|
@@ -317,6 +339,7 @@ function validateGate(raw, index, errors) {
|
|
|
317
339
|
scope,
|
|
318
340
|
baseline,
|
|
319
341
|
env,
|
|
342
|
+
timeout,
|
|
320
343
|
};
|
|
321
344
|
}
|
|
322
345
|
/**
|