javi-forge 1.7.0 → 1.8.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 +107 -105
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/package.json +1 -1
- package/ci-local/docker/python.Dockerfile +0 -7
package/dist/commands/ci.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import fs from "fs-extra";
|
|
4
4
|
import { CI_STACKS, findCIConfig, loadCIConfig, } from "../lib/ci-config.js";
|
|
5
5
|
import { refreshContextDir } from "../lib/context.js";
|
|
6
|
-
import { ensureImage,
|
|
6
|
+
import { ensureImage, isDockerAvailable, openShell, runInContainer, } from "../lib/docker.js";
|
|
7
7
|
import { execFileAsync } from "../lib/exec.js";
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Stack detection
|
|
@@ -349,6 +349,10 @@ export async function runCI(options, onStep) {
|
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
// ── Check Docker ─────────────────────────────────────────────────────────────
|
|
352
|
+
// Image resolved by the prologue for the auto path — threaded into the
|
|
353
|
+
// executor so nothing downstream re-derives it. Set iff
|
|
354
|
+
// `resolved.source === "auto" && !noDocker`.
|
|
355
|
+
let autoImage;
|
|
352
356
|
if (!noDocker) {
|
|
353
357
|
const stepDocker = "docker-check";
|
|
354
358
|
report(onStep, stepDocker, "Checking Docker", "running");
|
|
@@ -364,7 +368,7 @@ export async function runCI(options, onStep) {
|
|
|
364
368
|
const stepImage = "docker-image";
|
|
365
369
|
report(onStep, stepImage, `Building image for ${stackInfo.stackType}`, "running");
|
|
366
370
|
try {
|
|
367
|
-
await ensureImage({
|
|
371
|
+
autoImage = await ensureImage({
|
|
368
372
|
stack: stackInfo.stackType,
|
|
369
373
|
javaVersion: stackInfo.javaVersion,
|
|
370
374
|
});
|
|
@@ -393,30 +397,22 @@ export async function runCI(options, onStep) {
|
|
|
393
397
|
report(onStep, stepContext, "Refresh .context/ directory", "error", String(e));
|
|
394
398
|
}
|
|
395
399
|
// ── Runner execution ─────────────────────────────────────────────────────────
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
// ONE executor for every resolution source. Naming is a function of
|
|
401
|
+
// `resolved.source` alone — never of `resolved.runners.length`, which would
|
|
402
|
+
// silently rename a single-runner CONFIG.
|
|
403
|
+
const naming = resolved.source === "auto" ? NAMING_MODE.BARE : NAMING_MODE.SUFFIXED;
|
|
404
|
+
for (const runner of resolved.runners) {
|
|
405
|
+
await runRunner(runner, {
|
|
399
406
|
projectDir,
|
|
400
407
|
mode,
|
|
401
408
|
noDocker,
|
|
409
|
+
noSecurity,
|
|
402
410
|
timeout,
|
|
403
411
|
onStep,
|
|
412
|
+
naming,
|
|
413
|
+
preresolvedImage: resolved.source === "auto" ? autoImage : undefined,
|
|
404
414
|
});
|
|
405
415
|
}
|
|
406
|
-
else {
|
|
407
|
-
// Configured (or --stack override) runners execute in declared order,
|
|
408
|
-
// each in its own working directory.
|
|
409
|
-
for (const runner of resolved.runners) {
|
|
410
|
-
await runConfiguredRunner(runner, {
|
|
411
|
-
projectDir,
|
|
412
|
-
mode,
|
|
413
|
-
noDocker,
|
|
414
|
-
noSecurity,
|
|
415
|
-
timeout,
|
|
416
|
-
onStep,
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
416
|
// ── Security scan (full mode only) ──────────────────────────────────────────
|
|
421
417
|
if (mode === "full" && !noSecurity) {
|
|
422
418
|
const stepSecurity = "security";
|
|
@@ -456,90 +452,70 @@ export async function runCI(options, onStep) {
|
|
|
456
452
|
}
|
|
457
453
|
}
|
|
458
454
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
throw e;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
if (stackInfo.compileCmd) {
|
|
475
|
-
const stepCompile = "compile";
|
|
476
|
-
report(onStep, stepCompile, `Compile: ${stackInfo.compileCmd}`, "running");
|
|
477
|
-
try {
|
|
478
|
-
// Run as root inside the container to rm/build output dirs owned by any host user,
|
|
479
|
-
// then chown back to runner so subsequent test steps can read the output.
|
|
480
|
-
await runStep(stackInfo.compileCmd, projectDir, noDocker, timeout, runner, "root");
|
|
481
|
-
report(onStep, stepCompile, "Compile passed", "done");
|
|
482
|
-
}
|
|
483
|
-
catch (e) {
|
|
484
|
-
report(onStep, stepCompile, "Compile failed", "error", String(e));
|
|
485
|
-
throw e;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
if (mode === "full" && stackInfo.testCmd) {
|
|
489
|
-
const stepTest = "test";
|
|
490
|
-
report(onStep, stepTest, `Test: ${stackInfo.testCmd}`, "running");
|
|
491
|
-
try {
|
|
492
|
-
await runStep(stackInfo.testCmd, projectDir, noDocker, timeout, runner);
|
|
493
|
-
report(onStep, stepTest, "Tests passed", "done");
|
|
494
|
-
}
|
|
495
|
-
catch (e) {
|
|
496
|
-
report(onStep, stepTest, "Tests failed", "error", String(e));
|
|
497
|
-
throw e;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
455
|
+
// =============================================================================
|
|
456
|
+
// Step runners
|
|
457
|
+
// =============================================================================
|
|
458
|
+
/**
|
|
459
|
+
* Step-id and label naming, keyed on `resolved.source` and nothing else.
|
|
460
|
+
* `bare` is the zero-config (auto) presentation; `suffixed` carries the runner
|
|
461
|
+
* name, including for a CONFIG that happens to declare exactly one runner.
|
|
462
|
+
*/
|
|
463
|
+
const NAMING_MODE = {
|
|
464
|
+
BARE: "bare",
|
|
465
|
+
SUFFIXED: "suffixed",
|
|
466
|
+
};
|
|
501
467
|
/**
|
|
502
|
-
* Execute one
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
468
|
+
* Execute one resolved runner, whatever its source: resolve the image (already
|
|
469
|
+
* built by the prologue for auto, otherwise the default stack image, an
|
|
470
|
+
* explicit/digest-pinned image, or a deterministic build-context build), verify
|
|
471
|
+
* required tools fail-closed, then run setup/lint/compile/test/security
|
|
472
|
+
* commands in order, inside the runner's working directory.
|
|
506
473
|
* Any failure aborts the whole run — nothing is ever skipped silently.
|
|
507
474
|
*/
|
|
508
|
-
async function
|
|
509
|
-
const { projectDir, mode, noDocker, noSecurity, timeout, onStep } = ctx;
|
|
475
|
+
async function runRunner(runner, ctx) {
|
|
476
|
+
const { projectDir, mode, noDocker, noSecurity, timeout, onStep, naming } = ctx;
|
|
477
|
+
const bare = naming === NAMING_MODE.BARE;
|
|
510
478
|
// ── Image resolution (Docker mode only) ────────────────────────────────────
|
|
511
479
|
let imageName;
|
|
512
480
|
if (!noDocker) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
481
|
+
if (ctx.preresolvedImage) {
|
|
482
|
+
// Auto path: the prologue already built the image BEFORE the .context/
|
|
483
|
+
// refresh. Reuse it verbatim so no second `docker-image` step appears
|
|
484
|
+
// and the global step order is unchanged.
|
|
485
|
+
imageName = ctx.preresolvedImage;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const stepImage = `docker-image:${runner.name}`;
|
|
489
|
+
try {
|
|
490
|
+
if (runner.buildContext) {
|
|
491
|
+
report(onStep, stepImage, `Building image for ${runner.name} from ${runner.buildContext}`, "running");
|
|
492
|
+
imageName = await ensureImage({
|
|
493
|
+
stack: runner.stack,
|
|
494
|
+
buildContext: path.resolve(projectDir, runner.buildContext),
|
|
495
|
+
imageTag: `javi-forge-ci-${runner.name}`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else if (runner.image) {
|
|
499
|
+
// Explicit image (digest pins pass through verbatim).
|
|
500
|
+
imageName = runner.image;
|
|
501
|
+
report(onStep, stepImage, `Using configured image ${runner.image}`, "done");
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
report(onStep, stepImage, `Building image for ${runner.name} (${runner.stack})`, "running");
|
|
505
|
+
imageName = await ensureImage({
|
|
506
|
+
stack: runner.stack,
|
|
507
|
+
javaVersion: runner.javaVersion,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
if (!runner.image) {
|
|
511
|
+
report(onStep, stepImage, `Docker image ready (${imageName})`, "done");
|
|
512
|
+
}
|
|
534
513
|
}
|
|
535
|
-
|
|
536
|
-
report(onStep, stepImage,
|
|
514
|
+
catch (e) {
|
|
515
|
+
report(onStep, stepImage, "Building Docker image", "error", String(e));
|
|
516
|
+
throw e;
|
|
537
517
|
}
|
|
538
518
|
}
|
|
539
|
-
catch (e) {
|
|
540
|
-
report(onStep, stepImage, "Building Docker image", "error", String(e));
|
|
541
|
-
throw e;
|
|
542
|
-
}
|
|
543
519
|
}
|
|
544
520
|
// ── Required tools (fail closed, before any phase) ─────────────────────────
|
|
545
521
|
if (runner.requiredTools.length > 0) {
|
|
@@ -550,7 +526,14 @@ async function runConfiguredRunner(runner, ctx) {
|
|
|
550
526
|
report(onStep, stepTools, `Checking required tools [${runner.name}]: ${runner.requiredTools.join(", ")}`, "running");
|
|
551
527
|
for (const tool of runner.requiredTools) {
|
|
552
528
|
try {
|
|
553
|
-
await runStep(
|
|
529
|
+
await runStep({
|
|
530
|
+
command: `command -v ${tool}`,
|
|
531
|
+
projectDir,
|
|
532
|
+
noDocker,
|
|
533
|
+
timeout,
|
|
534
|
+
runner,
|
|
535
|
+
image: imageName,
|
|
536
|
+
});
|
|
554
537
|
}
|
|
555
538
|
catch {
|
|
556
539
|
const message = `runner "${runner.name}": required tool "${tool}" not found in ${envDesc} — ` +
|
|
@@ -574,6 +557,7 @@ async function runConfiguredRunner(runner, ctx) {
|
|
|
574
557
|
{
|
|
575
558
|
id: "test",
|
|
576
559
|
label: "Test",
|
|
560
|
+
doneLabel: "Tests",
|
|
577
561
|
cmds: runner.testCmds,
|
|
578
562
|
skip: mode !== "full",
|
|
579
563
|
},
|
|
@@ -587,21 +571,34 @@ async function runConfiguredRunner(runner, ctx) {
|
|
|
587
571
|
for (const phase of phases) {
|
|
588
572
|
if (phase.skip)
|
|
589
573
|
continue;
|
|
590
|
-
const stepId = `${phase.id}:${runner.name}`;
|
|
574
|
+
const stepId = bare ? phase.id : `${phase.id}:${runner.name}`;
|
|
575
|
+
// `doneLabel` is BARE-only: suffixed mode keeps the historical
|
|
576
|
+
// `${label} [${name}] passed` composition.
|
|
577
|
+
const subject = bare ? (phase.doneLabel ?? phase.label) : phase.label;
|
|
578
|
+
const suffix = bare ? "" : ` [${runner.name}]`;
|
|
591
579
|
for (const cmd of phase.cmds) {
|
|
592
|
-
report(onStep, stepId, `${phase.label}
|
|
580
|
+
report(onStep, stepId, `${phase.label}${suffix}: ${cmd}`, "running");
|
|
593
581
|
try {
|
|
594
|
-
await runStep(
|
|
595
|
-
|
|
582
|
+
await runStep({
|
|
583
|
+
command: cmd,
|
|
584
|
+
projectDir,
|
|
585
|
+
noDocker,
|
|
586
|
+
timeout,
|
|
587
|
+
runner,
|
|
588
|
+
user: phase.user,
|
|
589
|
+
image: imageName,
|
|
590
|
+
});
|
|
591
|
+
report(onStep, stepId, `${subject}${suffix} passed`, "done");
|
|
596
592
|
}
|
|
597
593
|
catch (e) {
|
|
598
|
-
report(onStep, stepId, `${
|
|
594
|
+
report(onStep, stepId, `${subject}${suffix} failed`, "error", String(e));
|
|
599
595
|
throw e;
|
|
600
596
|
}
|
|
601
597
|
}
|
|
602
598
|
}
|
|
603
599
|
}
|
|
604
|
-
async function runStep(
|
|
600
|
+
async function runStep(options) {
|
|
601
|
+
const { command, projectDir, noDocker, timeout, runner, user, image } = options;
|
|
605
602
|
if (noDocker) {
|
|
606
603
|
// Run natively, in the runner's working directory.
|
|
607
604
|
const cwd = path.join(projectDir, runner.directory);
|
|
@@ -618,10 +615,15 @@ async function runStep(command, projectDir, noDocker, timeout, runner, user, ima
|
|
|
618
615
|
});
|
|
619
616
|
}
|
|
620
617
|
else {
|
|
621
|
-
// The image is always resolved upstream (
|
|
622
|
-
// explicit config image, or the default
|
|
623
|
-
// receives it verbatim and never re-detects
|
|
624
|
-
|
|
618
|
+
// The image is always resolved upstream (prologue build for auto,
|
|
619
|
+
// per-runner ensureImage, explicit config image, or the default
|
|
620
|
+
// per-stack image). Docker receives it verbatim and never re-detects
|
|
621
|
+
// anything, so a missing image is a bug — fail loudly instead of
|
|
622
|
+
// silently re-deriving a name that may not be the one that was built.
|
|
623
|
+
if (!image) {
|
|
624
|
+
throw new Error(`internal invariant violated: no Docker image resolved for runner "${runner.name}" — ` +
|
|
625
|
+
"the image must be resolved before any step runs");
|
|
626
|
+
}
|
|
625
627
|
const workdir = runner.directory !== "."
|
|
626
628
|
? `/home/runner/work/${runner.directory}`
|
|
627
629
|
: "/home/runner/work";
|
package/dist/constants.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const MODULES_DIR: string;
|
|
|
9
9
|
export declare const WORKFLOWS_DIR: string;
|
|
10
10
|
/** CI-local directory */
|
|
11
11
|
export declare const CI_LOCAL_DIR: string;
|
|
12
|
+
/** Git hook assets directory (installed by `javi-forge ci init`) */
|
|
13
|
+
export declare const HOOK_ASSETS_DIR: string;
|
|
12
14
|
/** Security hooks template directory */
|
|
13
15
|
export declare const SECURITY_HOOKS_DIR: string;
|
|
14
16
|
/** Local AI stack template directory */
|
package/dist/constants.js
CHANGED
|
@@ -11,6 +11,8 @@ export const MODULES_DIR = path.join(FORGE_ROOT, "modules");
|
|
|
11
11
|
export const WORKFLOWS_DIR = path.join(FORGE_ROOT, "workflows");
|
|
12
12
|
/** CI-local directory */
|
|
13
13
|
export const CI_LOCAL_DIR = path.join(FORGE_ROOT, "ci-local");
|
|
14
|
+
/** Git hook assets directory (installed by `javi-forge ci init`) */
|
|
15
|
+
export const HOOK_ASSETS_DIR = path.join(FORGE_ROOT, "assets", "hooks");
|
|
14
16
|
/** Security hooks template directory */
|
|
15
17
|
export const SECURITY_HOOKS_DIR = path.join(TEMPLATES_DIR, "security-hooks");
|
|
16
18
|
/** Local AI stack template directory */
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
FROM python:3.12-slim
|
|
2
|
-
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
|
3
|
-
RUN pip install --no-cache-dir pytest ruff pylint poetry
|
|
4
|
-
RUN useradd -m -s /bin/bash runner
|
|
5
|
-
USER runner
|
|
6
|
-
WORKDIR /home/runner/work
|
|
7
|
-
ENTRYPOINT ["/bin/bash", "-c"]
|