feat: add dev base image preflight
This commit is contained in:
@@ -12,11 +12,12 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
|
||||
import { runDevBaseImagePreflight } from "./src/dev-base-image-preflight.mjs";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultRegistryPrefix =
|
||||
process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
||||
const defaultBaseImage = process.env.HWLAB_DEV_BASE_IMAGE || "node:22-bookworm-slim";
|
||||
const defaultBaseImage = process.env.HWLAB_DEV_BASE_IMAGE || null;
|
||||
const defaultReportPath = "reports/dev-gate/dev-artifacts.json";
|
||||
const catalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const deployPath = "deploy/deploy.json";
|
||||
@@ -93,7 +94,7 @@ function printHelp() {
|
||||
"",
|
||||
"options:",
|
||||
" --registry-prefix PREFIX default: 127.0.0.1:5000/hwlab",
|
||||
" --base-image IMAGE default: node:22-bookworm-slim",
|
||||
" --base-image IMAGE override HWLAB_DEV_BASE_IMAGE for this run; must already be local",
|
||||
" --services LIST comma-separated service IDs; default: all frozen DEV services",
|
||||
" --report PATH default: reports/dev-gate/dev-artifacts.json",
|
||||
" --no-report print JSON without updating the report file"
|
||||
@@ -127,6 +128,44 @@ function blocker({ type, scope, summary, next }) {
|
||||
};
|
||||
}
|
||||
|
||||
function preflightEnvForBaseImage(baseImage) {
|
||||
if (!baseImage) {
|
||||
return process.env;
|
||||
}
|
||||
return {
|
||||
...process.env,
|
||||
HWLAB_DEV_BASE_IMAGE: baseImage
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeBaseImagePreflight(result) {
|
||||
return {
|
||||
status: result.status,
|
||||
imageSource: result.imageSource,
|
||||
requestedImage: result.requestedImage,
|
||||
localTag: result.localTag,
|
||||
imageId: result.imageId,
|
||||
repoDigests: result.repoDigests,
|
||||
publishUsable: result.publishUsable,
|
||||
blockers: result.blockers,
|
||||
nextSteps: result.nextSteps,
|
||||
containerEngine: result.containerEngine
|
||||
};
|
||||
}
|
||||
|
||||
function baseImagePreflightBlocker(result) {
|
||||
return blocker({
|
||||
type: "environment_blocker",
|
||||
scope: "base-image",
|
||||
summary: result.blockers.length
|
||||
? result.blockers.join("; ")
|
||||
: "DEV builder base image preflight did not return ready.",
|
||||
next: result.nextSteps.length
|
||||
? result.nextSteps.join(" ")
|
||||
: "Provide an approved local Node 20 or HWLAB DEV builder base image before artifact publish."
|
||||
});
|
||||
}
|
||||
|
||||
function commandLine(command, args) {
|
||||
return [command, ...args].map((part) => (/\s/u.test(part) ? JSON.stringify(part) : part)).join(" ");
|
||||
}
|
||||
@@ -460,10 +499,16 @@ function tailText(value, maxLength = 2500) {
|
||||
return text.slice(text.length - maxLength);
|
||||
}
|
||||
|
||||
async function preflight({ args, services, catalog, deployManifest }) {
|
||||
async function preflight({ args, services, catalog, deployManifest, baseImagePreflight }) {
|
||||
const blockers = [];
|
||||
let registryPrefix = args.registryPrefix;
|
||||
|
||||
if (baseImagePreflight.publishUsable) {
|
||||
args.baseImage = baseImagePreflight.localTag;
|
||||
} else {
|
||||
blockers.push(baseImagePreflightBlocker(baseImagePreflight));
|
||||
}
|
||||
|
||||
try {
|
||||
registryPrefix = validateRegistryPrefix(args.registryPrefix);
|
||||
} catch (error) {
|
||||
@@ -490,7 +535,8 @@ async function preflight({ args, services, catalog, deployManifest }) {
|
||||
);
|
||||
}
|
||||
|
||||
for (const tag of [args.baseImage.split(":").at(-1), args.services.length === 1 ? args.services[0] : ""]) {
|
||||
const baseImageTag = args.baseImage ? args.baseImage.split(":").at(-1) : "";
|
||||
for (const tag of [baseImageTag, args.services.length === 1 ? args.services[0] : ""]) {
|
||||
if (mutableTags.has(tag)) {
|
||||
blockers.push(
|
||||
blocker({
|
||||
@@ -503,29 +549,7 @@ async function preflight({ args, services, catalog, deployManifest }) {
|
||||
}
|
||||
}
|
||||
|
||||
const dockerVersion = await run("docker", ["version", "--format", "{{json .}}"]);
|
||||
if (dockerVersion.code !== 0) {
|
||||
blockers.push(
|
||||
blocker({
|
||||
type: "environment_blocker",
|
||||
scope: "docker",
|
||||
summary: "Docker CLI or daemon is unavailable.",
|
||||
next: "Make the D601 runner Docker daemon reachable, then rerun the publish command."
|
||||
})
|
||||
);
|
||||
} else {
|
||||
const baseInspect = await run("docker", ["image", "inspect", args.baseImage, "--format", "{{.Id}}"]);
|
||||
if (baseInspect.code !== 0) {
|
||||
blockers.push(
|
||||
blocker({
|
||||
type: "environment_blocker",
|
||||
scope: "base-image",
|
||||
summary: `base image ${args.baseImage} is not present locally`,
|
||||
next: "Preload an internal/local Node base image or set HWLAB_DEV_BASE_IMAGE to an already-local image; the publish path uses --pull=false."
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (baseImagePreflight.containerEngine?.available) {
|
||||
if (registryPrefix.startsWith("127.0.0.1:5000/") || registryPrefix.startsWith("localhost:5000/")) {
|
||||
const ps = await run("docker", ["ps", "--format", "{{json .}}"]);
|
||||
if (ps.code !== 0 || !dockerPsShowsRegistry5000(ps.stdout)) {
|
||||
@@ -755,7 +779,7 @@ function devGateBlockers(blockers) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function createReport({ args, repo, commitId, shortCommit, mode, artifacts, blockers }) {
|
||||
function createReport({ args, repo, commitId, shortCommit, mode, artifacts, blockers, baseImagePreflight }) {
|
||||
const status = reportStatus(mode, artifacts, blockers);
|
||||
const fatalBlocked = hasFatalBlocker(blockers);
|
||||
const publishedCount = artifacts.filter((artifact) => artifact.status === "published").length;
|
||||
@@ -777,12 +801,16 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
|
||||
"docs/dev-acceptance-matrix.md",
|
||||
"docs/m0-contract-audit.md",
|
||||
"docs/artifact-catalog.md",
|
||||
"docs/dev-artifact-publish.md"
|
||||
"docs/dev-artifact-publish.md",
|
||||
"docs/dev-base-image-preflight.md"
|
||||
],
|
||||
summary: "DEV artifact publish evidence for pikasTech/HWLAB#35, stored in the DEV gate report envelope."
|
||||
},
|
||||
validationCommands: [
|
||||
"node --check scripts/dev-artifact-publish.mjs",
|
||||
"node --check scripts/preflight-dev-base-image.mjs",
|
||||
"node --check scripts/src/dev-base-image-preflight.mjs",
|
||||
"node scripts/preflight-dev-base-image.mjs",
|
||||
"node scripts/dev-artifact-publish.mjs --preflight --no-report",
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
@@ -811,6 +839,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
|
||||
status: fatalBlocked ? "blocked" : "pass",
|
||||
requirements: [
|
||||
"Registry prefix is localhost or private/internal only.",
|
||||
"DEV builder base image preflight returns ready before build or publish.",
|
||||
"Environment remains dev and namespace remains hwlab-dev.",
|
||||
"PROD profile remains disabled.",
|
||||
"No secret or token material is required by this workflow."
|
||||
@@ -827,7 +856,8 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
|
||||
repo,
|
||||
sourceCommitId: commitId,
|
||||
registryPrefix: args.registryPrefix,
|
||||
baseImage: args.baseImage,
|
||||
baseImage: args.baseImage ?? null,
|
||||
baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight),
|
||||
environment: ENVIRONMENT_DEV,
|
||||
serviceCount: artifacts.length,
|
||||
builtCount,
|
||||
@@ -865,6 +895,12 @@ async function main() {
|
||||
}
|
||||
|
||||
args.registryPrefix = validateRegistryPrefix(args.registryPrefix);
|
||||
const baseImagePreflight = await runDevBaseImagePreflight({
|
||||
env: preflightEnvForBaseImage(args.baseImage)
|
||||
});
|
||||
if (baseImagePreflight.publishUsable) {
|
||||
args.baseImage = baseImagePreflight.localTag;
|
||||
}
|
||||
const [catalog, deployManifest, commitId, remoteUrl] = await Promise.all([
|
||||
readJson(catalogPath),
|
||||
readJson(deployPath),
|
||||
@@ -875,7 +911,7 @@ async function main() {
|
||||
const repo = repoLabelFromRemote(remoteUrl);
|
||||
const services = await resolveServices(args.services);
|
||||
|
||||
let blockers = await preflight({ args, services, catalog, deployManifest });
|
||||
let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight });
|
||||
let artifacts = services.map((service) => ({
|
||||
...service,
|
||||
status: "preflight_only",
|
||||
@@ -912,7 +948,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
const report = createReport({ args, repo, commitId, shortCommit, mode: args.mode, artifacts, blockers });
|
||||
const report = createReport({ args, repo, commitId, shortCommit, mode: args.mode, artifacts, blockers, baseImagePreflight });
|
||||
if (args.emitReport) {
|
||||
await writeReport(args.reportPath, report);
|
||||
}
|
||||
@@ -923,6 +959,7 @@ async function main() {
|
||||
reportPath: args.emitReport ? args.reportPath : null,
|
||||
sourceCommitId: commitId,
|
||||
registryPrefix: args.registryPrefix,
|
||||
baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight),
|
||||
services: report.artifactPublish.services.map((service) => ({
|
||||
serviceId: service.serviceId,
|
||||
status: service.status,
|
||||
@@ -932,7 +969,9 @@ async function main() {
|
||||
blockers
|
||||
}, null, 2));
|
||||
|
||||
if ((args.mode === "build" || args.mode === "publish") && artifacts.some((artifact) => artifact.status.endsWith("_failed"))) {
|
||||
if (hasFatalBlocker(blockers)) {
|
||||
process.exitCode = 2;
|
||||
} else if ((args.mode === "build" || args.mode === "publish") && artifacts.some((artifact) => artifact.status.endsWith("_failed"))) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user