javi-forge 1.17.0 → 1.18.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.js +48 -3
- package/package.json +1 -1
package/dist/commands/ci.js
CHANGED
|
@@ -316,6 +316,18 @@ function describeRunners(resolved) {
|
|
|
316
316
|
}
|
|
317
317
|
export async function runCI(options, onStep, onGateOutcome) {
|
|
318
318
|
const { projectDir = process.cwd(), mode = "full", noDocker = false, noGhagga = false, noSecurity = false, timeout = 600, } = options;
|
|
319
|
+
// ── Run-scoped Docker availability (lazy-memoized) ─────────────────────────
|
|
320
|
+
// Computed at most ONCE per run and only when an image gate needs it. The
|
|
321
|
+
// full/quick prologue below assigns its own `isDockerAvailable()` result back
|
|
322
|
+
// into this cache so `runGates` never re-probes; the gates-only path leaves it
|
|
323
|
+
// undefined until an image gate lazily triggers the probe (a native-only
|
|
324
|
+
// gates-only repo never touches Docker — behaves exactly as before slice 3).
|
|
325
|
+
let dockerAvailableCache;
|
|
326
|
+
const dockerAvailable = async () => (dockerAvailableCache ??= await isDockerAvailable());
|
|
327
|
+
const dockerGate = {
|
|
328
|
+
noDocker,
|
|
329
|
+
isAvailable: dockerAvailable,
|
|
330
|
+
};
|
|
319
331
|
// ── Resolve runners (once — nothing downstream re-detects) ─────────────────
|
|
320
332
|
const stepDetect = "detect";
|
|
321
333
|
report(onStep, stepDetect, "Detecting stack", "running");
|
|
@@ -343,7 +355,7 @@ export async function runCI(options, onStep, onGateOutcome) {
|
|
|
343
355
|
report(onStep, mode, `${mode} mode`, "error", detail);
|
|
344
356
|
throw new Error(`no runners resolved — ${mode} mode requires at least one runner`);
|
|
345
357
|
}
|
|
346
|
-
await runGates(resolved.gates, projectDir, onStep, onGateOutcome);
|
|
358
|
+
await runGates(resolved.gates, projectDir, onStep, dockerGate, onGateOutcome);
|
|
347
359
|
return;
|
|
348
360
|
}
|
|
349
361
|
// Legacy single-runner view for the zero-config auto path. Keeping this
|
|
@@ -409,6 +421,9 @@ export async function runCI(options, onStep, onGateOutcome) {
|
|
|
409
421
|
const stepDocker = "docker-check";
|
|
410
422
|
report(onStep, stepDocker, "Checking Docker", "running");
|
|
411
423
|
const dockerOk = await isDockerAvailable();
|
|
424
|
+
// Reuse this result for the gate fail-closed check — `runGates` must not
|
|
425
|
+
// run a second `docker info` (memoization seam).
|
|
426
|
+
dockerAvailableCache = dockerOk;
|
|
412
427
|
if (!dockerOk) {
|
|
413
428
|
report(onStep, stepDocker, "Docker not available", "error", "Start Docker or use --no-docker");
|
|
414
429
|
throw new Error("Docker is not available");
|
|
@@ -515,7 +530,7 @@ export async function runCI(options, onStep, onGateOutcome) {
|
|
|
515
530
|
// already `full` or `quick` here; the guard makes the contract explicit.
|
|
516
531
|
// `runGates` no-ops on an empty gate list (a v1 repo carries none).
|
|
517
532
|
if (mode === "full" || mode === "quick") {
|
|
518
|
-
await runGates(resolved.gates, projectDir, onStep, onGateOutcome);
|
|
533
|
+
await runGates(resolved.gates, projectDir, onStep, dockerGate, onGateOutcome);
|
|
519
534
|
}
|
|
520
535
|
}
|
|
521
536
|
// =============================================================================
|
|
@@ -938,7 +953,10 @@ async function runGateCommand(gate, cmd, projectDir, nativeEnv, containerEnv) {
|
|
|
938
953
|
timedOut: result.timedOut,
|
|
939
954
|
};
|
|
940
955
|
}
|
|
941
|
-
async function runGates(gates, projectDir, onStep,
|
|
956
|
+
async function runGates(gates, projectDir, onStep,
|
|
957
|
+
// REQUIRED, so it MUST precede the optional `onOutcome` (TS1016: a required
|
|
958
|
+
// parameter cannot follow an optional one). Both call sites pass it positionally.
|
|
959
|
+
docker, onOutcome) {
|
|
942
960
|
if (gates.length === 0)
|
|
943
961
|
return;
|
|
944
962
|
const blockingFailures = [];
|
|
@@ -989,6 +1007,33 @@ async function runGates(gates, projectDir, onStep, onOutcome) {
|
|
|
989
1007
|
...extra,
|
|
990
1008
|
});
|
|
991
1009
|
report(onStep, stepId, label, "running");
|
|
1010
|
+
// Fail-closed matrix (slice 3): an image gate that cannot reach Docker is
|
|
1011
|
+
// REFUSED — it MUST NOT fall through to native/unpinned execution and MUST
|
|
1012
|
+
// NOT be silently skipped/passed. Blocking → build failure (feeds the
|
|
1013
|
+
// aggregate throw); informative → `warning` (never a false-green). A gate
|
|
1014
|
+
// WITHOUT `image` is unaffected and runs native regardless of --no-docker.
|
|
1015
|
+
//
|
|
1016
|
+
// `isAvailable()` is touched ONLY for an image gate under Docker: the
|
|
1017
|
+
// `noDocker` short-circuit means an image-less set (or a --no-docker run)
|
|
1018
|
+
// never shells out to `docker info`, keeping the native path zero-cost.
|
|
1019
|
+
if (gate.image !== undefined) {
|
|
1020
|
+
if (docker.noDocker || !(await docker.isAvailable())) {
|
|
1021
|
+
const why = docker.noDocker
|
|
1022
|
+
? "--no-docker set"
|
|
1023
|
+
: "Docker not available";
|
|
1024
|
+
const reason = `gate "${gate.id}" requires image "${gate.image}" but ${why} — refusing (never runs native/unpinned)`;
|
|
1025
|
+
if (blocking) {
|
|
1026
|
+
blockingFailures.push(gate.id);
|
|
1027
|
+
report(onStep, stepId, `${label} failed`, "error", reason);
|
|
1028
|
+
emit("error", { reason });
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
report(onStep, stepId, `${label} failed (informative)`, "warning", reason);
|
|
1032
|
+
emit("warning", { reason });
|
|
1033
|
+
}
|
|
1034
|
+
continue; // NEVER falls through to native execution.
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
992
1037
|
// Per-gate env: build the INJECTED allowlist (engine keys + baseline) once,
|
|
993
1038
|
// then split into two maps (JDB-001):
|
|
994
1039
|
// - nativeEnv: full host env + injected + gate.env — a spawn env MAP (never
|