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.
@@ -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, getImageName, isDockerAvailable, openShell, runInContainer, } from "../lib/docker.js";
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
- if (resolved.source === "auto" && primary) {
397
- // Legacy zero-config path unchanged single-stack behavior.
398
- await runLegacySteps(stackInfo, primary, {
400
+ // ONE executor for every resolution source. Naming is a function of
401
+ // `resolved.source` alonenever 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
- /** Legacy zero-config execution — identical to the pre-mixed-stack behavior. */
460
- async function runLegacySteps(stackInfo, runner, ctx) {
461
- const { projectDir, mode, noDocker, timeout, onStep } = ctx;
462
- if (stackInfo.lintCmd) {
463
- const stepLint = "lint";
464
- report(onStep, stepLint, `Lint: ${stackInfo.lintCmd}`, "running");
465
- try {
466
- await runStep(stackInfo.lintCmd, projectDir, noDocker, timeout, runner);
467
- report(onStep, stepLint, "Lint passed", "done");
468
- }
469
- catch (e) {
470
- report(onStep, stepLint, "Lint failed", "error", String(e));
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 configured runner: resolve the image (default stack image,
503
- * explicit/digest-pinned image, or deterministic build-context build),
504
- * verify required tools fail-closed, then run setup/lint/compile/test/
505
- * security commands in order, inside the runner's working directory.
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 runConfiguredRunner(runner, ctx) {
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
- const stepImage = `docker-image:${runner.name}`;
514
- try {
515
- if (runner.buildContext) {
516
- report(onStep, stepImage, `Building image for ${runner.name} from ${runner.buildContext}`, "running");
517
- imageName = await ensureImage({
518
- stack: runner.stack,
519
- buildContext: path.resolve(projectDir, runner.buildContext),
520
- imageTag: `javi-forge-ci-${runner.name}`,
521
- });
522
- }
523
- else if (runner.image) {
524
- // Explicit image (digest pins pass through verbatim).
525
- imageName = runner.image;
526
- report(onStep, stepImage, `Using configured image ${runner.image}`, "done");
527
- }
528
- else {
529
- report(onStep, stepImage, `Building image for ${runner.name} (${runner.stack})`, "running");
530
- imageName = await ensureImage({
531
- stack: runner.stack,
532
- javaVersion: runner.javaVersion,
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
- if (!runner.image) {
536
- report(onStep, stepImage, `Docker image ready (${imageName})`, "done");
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(`command -v ${tool}`, projectDir, noDocker, timeout, runner, undefined, imageName);
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} [${runner.name}]: ${cmd}`, "running");
580
+ report(onStep, stepId, `${phase.label}${suffix}: ${cmd}`, "running");
593
581
  try {
594
- await runStep(cmd, projectDir, noDocker, timeout, runner, phase.user, imageName);
595
- report(onStep, stepId, `${phase.label} [${runner.name}] passed`, "done");
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, `${phase.label} [${runner.name}] failed`, "error", String(e));
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(command, projectDir, noDocker, timeout, runner, user, imageOverride) {
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 (per-runner ensureImage,
622
- // explicit config image, or the default per-stack image). Docker
623
- // receives it verbatim and never re-detects anything.
624
- const image = imageOverride ?? runner.image ?? getImageName(runner.stack);
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";
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "javi-forge",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Project scaffolding and AI-ready CI bootstrap",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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"]