From cef2befdd156d3d4028f087a96e9c118b9174bf1 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Thu, 21 May 2026 17:33:39 +0000 Subject: [PATCH] fix: add dev base image provisioning path --- docs/artifact-catalog.md | 7 +- docs/dev-artifact-publish.md | 14 +- docs/dev-base-image-preflight.md | 40 +++++- reports/dev-gate/dev-artifacts.json | 144 ++++++++++++++----- scripts/dev-artifact-publish.mjs | 5 + scripts/src/dev-base-image-preflight.mjs | 173 +++++++++++++++++++++-- 6 files changed, 327 insertions(+), 56 deletions(-) diff --git a/docs/artifact-catalog.md b/docs/artifact-catalog.md index 0ad8f12b..4f43d7ac 100644 --- a/docs/artifact-catalog.md +++ b/docs/artifact-catalog.md @@ -78,8 +78,11 @@ npm run --silent dev-base-image:preflight The preflight is local-only and checks for an approved builder base image through `HWLAB_DEV_BASE_IMAGE`, a local `node:20-*` Docker image, or an explicit local HWLAB dev base tag such as `hwlab-dev-base:`. It prints the image source, -local tag, image ID, publish usability, blockers, and next steps. A blocked -result means artifact publish must stop and must not be reported as a successful +local tag, image ID, publish usability, blockers, next steps, and +machine-readable preload/provision commands. The recommended source is +`node:20-bookworm-slim`; the recommended HWLAB-local tag is +`127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim`. A blocked result +means artifact publish must stop and must not be reported as a successful publish. ## Validation diff --git a/docs/dev-artifact-publish.md b/docs/dev-artifact-publish.md index 239ed66a..aeb575d7 100644 --- a/docs/dev-artifact-publish.md +++ b/docs/dev-artifact-publish.md @@ -10,8 +10,11 @@ secrets, enable PROD, or push to GHCR/Docker Hub/other third-party registries. - Namespace: `hwlab-dev`. - Default registry prefix: `127.0.0.1:5000/hwlab`. - Base image: resolved by `scripts/preflight-dev-base-image.mjs` from - `HWLAB_DEV_BASE_IMAGE` or a local `node:20-*` Docker image. There is no - network pull or third-party fallback; the build runs with `--pull=false`. + `HWLAB_DEV_BASE_IMAGE`, a local `node:20-*` Docker image, or a local HWLAB + base tag such as + `127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim`. There is no + network pull or third-party fallback during publish; the build runs with + `--pull=false`. - Image tag: first seven characters of the Git commit used as build source. - Required image labels: - `hwlab.pikastech.local/repo` @@ -81,8 +84,11 @@ publish when build, push, base image, registry, contract, or safety checks fail. The report also includes `artifactPublish.baseImagePreflight` with the image source, local tag, local image ID, `publishUsable` gate, blockers, and next -steps. When that preflight is blocked, artifact publish is blocked and no build -or push is attempted. +steps. It also carries `recommendation`, `provision`, and `blockedReport` +objects so the blocked report includes the exact blocker, expected image tag, +recommended `HWLAB_DEV_BASE_IMAGE` value, and preload commands. When that +preflight is blocked, artifact publish is blocked and no build or push is +attempted. ## Known Implementation States diff --git a/docs/dev-base-image-preflight.md b/docs/dev-base-image-preflight.md index 1eb6c778..26a20cc3 100644 --- a/docs/dev-base-image-preflight.md +++ b/docs/dev-base-image-preflight.md @@ -46,7 +46,9 @@ Selection order is intentionally narrow: exists in the local Docker image cache. 2. If `HWLAB_DEV_BASE_IMAGE` is not set, the preflight looks for local `node:20-*` images and selects the first deterministic local tag. -3. If neither source is available, the result is a blocker. +3. If no `node:20-*` tag exists, the preflight also accepts a local HWLAB base + tag such as `127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim`. +4. If neither source is available, the result is a blocker. Explicit base image values must match local `node:20-*`, `hwlab-dev-base:*`, or `hwlab-node20-base:*` references. They must not reference UniDesk, Code Queue, @@ -55,6 +57,13 @@ service image. A service image such as `ghcr.io/pikastech/hwlab-cloud-api:` is an artifact output, not a builder base image. +The recommended D601 preload source is `node:20-bookworm-slim`. The recommended +HWLAB-specific local tag is: + +```sh +127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim +``` + ## Output Contract Important fields: @@ -66,9 +75,14 @@ Important fields: | `requestedImage` | The explicit environment value, when present. | | `localTag` | Local Docker tag selected for the builder base image. | | `imageId` | Local Docker image ID from `docker image inspect`. | +| `candidates` | Approved local builder base images found in Docker. | +| `rejectedCandidates` | Near misses such as local non-Node-20 Node images. | | `publishUsable` | Boolean gate for #35 artifact publish. | | `blockers` | Deterministic reasons artifact publish must stop. | | `nextSteps` | Operational fixes that do not require secret reads or runtime substitution. | +| `recommendation` | Machine-readable recommended source, env var, and base image tags. | +| `provision` | Copy-pasteable online, HWLAB-tag, offline import, and dry-run commands. | +| `blockedReport` | Minimal machine-readable blocked report: exact blocker, expected tag, env value, and next preload command. | ## Blocker Handling @@ -77,6 +91,30 @@ Docker cache, set `HWLAB_DEV_BASE_IMAGE` to the exact local tag if it is not a `node:20-*` tag, and rerun the preflight. Do not claim artifact publish success until the preflight returns `status: "ready"`. +Online D601 preload: + +```sh +docker pull node:20-bookworm-slim +HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/preflight-dev-base-image.mjs +``` + +Optional HWLAB-local tag provision: + +```sh +docker pull node:20-bookworm-slim +docker tag node:20-bookworm-slim 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim +HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim node scripts/preflight-dev-base-image.mjs +``` + +Offline preload path when D601 cannot pull directly: + +```sh +docker pull node:20-bookworm-slim +docker save node:20-bookworm-slim -o hwlab-node20-bookworm-slim.tar +docker load -i hwlab-node20-bookworm-slim.tar +HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/preflight-dev-base-image.mjs +``` + The preflight may use UniDesk and Code Queue as scheduling infrastructure only. It must not use their runtime images as HWLAB service images or builder base image substitutes. diff --git a/reports/dev-gate/dev-artifacts.json b/reports/dev-gate/dev-artifacts.json index fb4c53c6..6bf10d77 100644 --- a/reports/dev-gate/dev-artifacts.json +++ b/reports/dev-gate/dev-artifacts.json @@ -4,7 +4,7 @@ "reportVersion": "v1", "issue": "pikasTech/HWLAB#31", "taskId": "dev-artifact-publish", - "commitId": "ad903a7", + "commitId": "70bb916", "acceptanceLevel": "dev_artifact_publish", "devOnly": true, "prodDisabled": true, @@ -64,7 +64,7 @@ "type": "environment_blocker", "scope": "base-image", "status": "open", - "summary": "HWLAB_DEV_BASE_IMAGE is not set and no local node:20-* image was found in the Docker image cache." + "summary": "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim." }, { "type": "runtime_blocker", @@ -90,7 +90,7 @@ "status": "blocked", "mode": "preflight", "repo": "pikasTech/HWLAB", - "sourceCommitId": "ad903a76e93bc8fab5c95138df15bfcb852bc339", + "sourceCommitId": "70bb9168ead272ec1f77b752df8e178e70858df4", "registryPrefix": "127.0.0.1:5000/hwlab", "baseImage": null, "baseImagePreflight": { @@ -100,15 +100,91 @@ "localTag": null, "imageId": null, "repoDigests": [], + "candidates": [], + "rejectedCandidates": [ + { + "source": "local-docker-node", + "localTag": "node:22-bookworm-slim", + "imageId": "341b84210b33", + "reason": "local Node image is not Node 20 and is not approved as the HWLAB DEV builder base image" + } + ], "publishUsable": false, "blockers": [ - "HWLAB_DEV_BASE_IMAGE is not set and no local node:20-* image was found in the Docker image cache." + "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim." ], "nextSteps": [ - "Set HWLAB_DEV_BASE_IMAGE to an approved local DEV builder base image, or preload a local node:20-* image such as node:20-bookworm-slim.", + "Preload node:20-bookworm-slim into the D601 Docker cache, or tag it as 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim.", + "Set HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim or HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim when invoking artifact publish.", "Rerun this preflight and require status=ready before #35 artifact publish.", "Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes." ], + "recommendation": { + "source": "Preloaded D601 local Docker cache or D601-local registry tag.", + "envVar": "HWLAB_DEV_BASE_IMAGE", + "preferredImageTag": "node:20-bookworm-slim", + "preferredHwlabImageTag": "127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim", + "recommendedEnvValue": "node:20-bookworm-slim" + }, + "provision": { + "required": true, + "exactBlocker": "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim.", + "expectedImageTag": "node:20-bookworm-slim", + "recommendedEnv": { + "name": "HWLAB_DEV_BASE_IMAGE", + "value": "node:20-bookworm-slim", + "assignment": "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim" + }, + "recommendedHwlabImageTag": "127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim", + "nextHumanCommand": "docker pull node:20-bookworm-slim", + "commands": { + "d601OnlinePreload": [ + "docker pull node:20-bookworm-slim", + "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/preflight-dev-base-image.mjs" + ], + "d601HwlabTagProvision": [ + "docker pull node:20-bookworm-slim", + "docker tag node:20-bookworm-slim 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim", + "HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim node scripts/preflight-dev-base-image.mjs" + ], + "offlineSourceExport": [ + "docker pull node:20-bookworm-slim", + "docker save node:20-bookworm-slim -o hwlab-node20-bookworm-slim.tar" + ], + "offlineD601Import": [ + "docker load -i hwlab-node20-bookworm-slim.tar", + "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/preflight-dev-base-image.mjs" + ], + "artifactPublishDryRun": [ + "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/dev-artifact-publish.mjs --preflight --no-report" + ] + } + }, + "blockedReport": { + "exactBlocker": "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim.", + "expectedImageTag": "node:20-bookworm-slim", + "recommendedEnv": { + "name": "HWLAB_DEV_BASE_IMAGE", + "value": "node:20-bookworm-slim", + "assignment": "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim" + }, + "recommendedHwlabImageTag": "127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim", + "nextHumanCommand": "docker pull node:20-bookworm-slim", + "preloadCommands": [ + "docker pull node:20-bookworm-slim", + "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/preflight-dev-base-image.mjs" + ], + "offlinePreloadCommands": { + "sourceExport": [ + "docker pull node:20-bookworm-slim", + "docker save node:20-bookworm-slim -o hwlab-node20-bookworm-slim.tar" + ], + "d601Import": [ + "docker load -i hwlab-node20-bookworm-slim.tar", + "HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim node scripts/preflight-dev-base-image.mjs" + ] + } + }, "containerEngine": { "command": "docker", "available": true, @@ -121,13 +197,13 @@ "serviceCount": 13, "builtCount": 0, "publishedCount": 0, - "generatedAt": "2026-05-21T16:53:05.962Z", + "generatedAt": "2026-05-21T17:31:56.761Z", "services": [ { "serviceId": "hwlab-cloud-api", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -137,8 +213,8 @@ { "serviceId": "hwlab-cloud-web", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "cloud-web", @@ -148,8 +224,8 @@ { "serviceId": "hwlab-agent-mgr", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -159,8 +235,8 @@ { "serviceId": "hwlab-agent-worker", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -170,8 +246,8 @@ { "serviceId": "hwlab-gateway", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -181,8 +257,8 @@ { "serviceId": "hwlab-gateway-simu", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -192,8 +268,8 @@ { "serviceId": "hwlab-box-simu", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -203,8 +279,8 @@ { "serviceId": "hwlab-patch-panel", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "node-command", @@ -214,8 +290,8 @@ { "serviceId": "hwlab-router", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-router:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-router:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "health-placeholder", @@ -225,8 +301,8 @@ { "serviceId": "hwlab-tunnel-client", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "health-placeholder", @@ -236,8 +312,8 @@ { "serviceId": "hwlab-edge-proxy", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "health-placeholder", @@ -247,8 +323,8 @@ { "serviceId": "hwlab-cli", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "cli", @@ -258,8 +334,8 @@ { "serviceId": "hwlab-agent-skills", "status": "preflight_only", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:ad903a7", - "imageTag": "ad903a7", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:70bb916", + "imageTag": "70bb916", "digest": "not_published", "repositoryDigest": null, "runtimeKind": "skills-bundle", @@ -272,8 +348,8 @@ "type": "environment_blocker", "scope": "base-image", "status": "open", - "summary": "HWLAB_DEV_BASE_IMAGE is not set and no local node:20-* image was found in the Docker image cache.", - "next": "Set HWLAB_DEV_BASE_IMAGE to an approved local DEV builder base image, or preload a local node:20-* image such as node:20-bookworm-slim. Rerun this preflight and require status=ready before #35 artifact publish. Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes." + "summary": "HWLAB_DEV_BASE_IMAGE is not set and no approved local DEV builder base image was found in the Docker image cache; expected node:20-bookworm-slim or 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim.", + "next": "Preload node:20-bookworm-slim into the D601 Docker cache, or tag it as 127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim. Set HWLAB_DEV_BASE_IMAGE=node:20-bookworm-slim or HWLAB_DEV_BASE_IMAGE=127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim when invoking artifact publish. Rerun this preflight and require status=ready before #35 artifact publish. Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes." }, { "type": "runtime_blocker", diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index ed886e69..d781269c 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -146,9 +146,14 @@ function summarizeBaseImagePreflight(result) { localTag: result.localTag, imageId: result.imageId, repoDigests: result.repoDigests, + candidates: result.candidates, + rejectedCandidates: result.rejectedCandidates, publishUsable: result.publishUsable, blockers: result.blockers, nextSteps: result.nextSteps, + recommendation: result.recommendation, + provision: result.provision, + blockedReport: result.blockedReport, containerEngine: result.containerEngine }; } diff --git a/scripts/src/dev-base-image-preflight.mjs b/scripts/src/dev-base-image-preflight.mjs index 09595953..57e9b6db 100644 --- a/scripts/src/dev-base-image-preflight.mjs +++ b/scripts/src/dev-base-image-preflight.mjs @@ -6,6 +6,9 @@ const envVarName = "HWLAB_DEV_BASE_IMAGE"; const dockerCommand = "docker"; const dockerTimeoutMs = 15_000; const node20TagPattern = /^20-/u; +const recommendedNode20Image = "node:20-bookworm-slim"; +const recommendedHwlabBaseImage = "127.0.0.1:5000/hwlab/hwlab-dev-base:node20-bookworm-slim"; +const recommendedPreloadTar = "hwlab-node20-bookworm-slim.tar"; const forbiddenImageFragments = Object.freeze([ "unidesk", "backend-core", @@ -21,6 +24,69 @@ const allowedExplicitBaseImages = Object.freeze([ "*/hwlab-node20-base:*" ]); +function baseImageRecommendation(envValue = recommendedNode20Image) { + return { + source: "Preloaded D601 local Docker cache or D601-local registry tag.", + envVar: envVarName, + preferredImageTag: recommendedNode20Image, + preferredHwlabImageTag: recommendedHwlabBaseImage, + recommendedEnvValue: envValue + }; +} + +function provisionPlan({ exactBlocker = null, envValue = recommendedNode20Image, required = true } = {}) { + return { + required, + exactBlocker, + expectedImageTag: recommendedNode20Image, + recommendedEnv: { + name: envVarName, + value: envValue, + assignment: `${envVarName}=${envValue}` + }, + recommendedHwlabImageTag: recommendedHwlabBaseImage, + nextHumanCommand: required ? `docker pull ${recommendedNode20Image}` : null, + commands: { + d601OnlinePreload: [ + `docker pull ${recommendedNode20Image}`, + `${envVarName}=${recommendedNode20Image} node scripts/preflight-dev-base-image.mjs` + ], + d601HwlabTagProvision: [ + `docker pull ${recommendedNode20Image}`, + `docker tag ${recommendedNode20Image} ${recommendedHwlabBaseImage}`, + `${envVarName}=${recommendedHwlabBaseImage} node scripts/preflight-dev-base-image.mjs` + ], + offlineSourceExport: [ + `docker pull ${recommendedNode20Image}`, + `docker save ${recommendedNode20Image} -o ${recommendedPreloadTar}` + ], + offlineD601Import: [ + `docker load -i ${recommendedPreloadTar}`, + `${envVarName}=${recommendedNode20Image} node scripts/preflight-dev-base-image.mjs` + ], + artifactPublishDryRun: [ + `${envVarName}=${envValue} node scripts/dev-artifact-publish.mjs --preflight --no-report` + ] + } + }; +} + +function blockedReportFor(exactBlocker) { + const plan = provisionPlan({ exactBlocker }); + return { + exactBlocker, + expectedImageTag: plan.expectedImageTag, + recommendedEnv: plan.recommendedEnv, + recommendedHwlabImageTag: plan.recommendedHwlabImageTag, + nextHumanCommand: plan.nextHumanCommand, + preloadCommands: plan.commands.d601OnlinePreload, + offlinePreloadCommands: { + sourceExport: plan.commands.offlineSourceExport, + d601Import: plan.commands.offlineD601Import + } + }; +} + function runDocker(args) { const result = spawnSync(dockerCommand, args, { encoding: "utf8", @@ -68,6 +134,10 @@ function isHwlabDevBaseRef(imageRef) { return /(^|\/)(hwlab-dev-base|hwlab-node20-base)(?=[:@]|$)/u.test(lowerRef); } +function isTaggedImage(image) { + return image.Repository && image.Tag && image.Repository !== "" && image.Tag !== ""; +} + function approvalForBaseRef(imageRef) { if (isNode20BaseRef(imageRef)) { return { @@ -149,8 +219,12 @@ function baseResult(overrides) { imageId: null, repoDigests: [], candidates: [], + rejectedCandidates: [], blockers: [], nextSteps: [], + recommendation: baseImageRecommendation(), + provision: provisionPlan(), + blockedReport: null, constraints: { localOnly: true, doesNotPull: true, @@ -167,6 +241,10 @@ function baseResult(overrides) { } function blocked(overrides) { + const exactBlocker = overrides.blockers?.length + ? overrides.blockers.join("; ") + : "DEV builder base image preflight did not return ready."; + const envValue = overrides.recommendedEnvValue ?? overrides.requestedImage ?? recommendedNode20Image; return baseResult({ status: "blocked", publishUsable: false, @@ -175,6 +253,9 @@ function blocked(overrides) { usable: false, reason: "DEV artifact publish must stop until the blocker is resolved." }, + recommendation: baseImageRecommendation(envValue), + provision: provisionPlan({ exactBlocker, envValue }), + blockedReport: blockedReportFor(exactBlocker), ...overrides }); } @@ -195,6 +276,12 @@ function ready({ imageSource, approval, requestedImage, localTag, inspectDoc, ca imageId: inspectDoc.Id ?? null, repoDigests: Array.isArray(inspectDoc.RepoDigests) ? inspectDoc.RepoDigests : [], candidates, + recommendation: baseImageRecommendation(localTag), + provision: provisionPlan({ + envValue: localTag, + required: false + }), + blockedReport: null, blockers: [], nextSteps: [ "Use the reported localTag as the DEV builder base image for artifact publish.", @@ -241,21 +328,70 @@ function dockerUnavailableResult(versionResult) { }); } -function selectNode20Candidate(images) { - const candidates = images - .filter((image) => image.Repository === "node" && node20TagPattern.test(image.Tag)) - .map((image) => ({ - source: "local-docker-node:20-*", - approval: approvalForBaseRef(candidateTag(image)), - localTag: candidateTag(image), +function candidateSource(image) { + if (image.Repository === "node" && node20TagPattern.test(image.Tag)) { + return "local-docker-node:20-*"; + } + if (isHwlabDevBaseRef(candidateTag(image))) { + return "local-docker-hwlab-dev-base"; + } + return null; +} + +function candidatePriority(candidate) { + if (candidate.source === "local-docker-node:20-*") return 0; + if (candidate.source === "local-docker-hwlab-dev-base") return 1; + return 2; +} + +function rejectedBaseCandidate(image) { + if (!isTaggedImage(image)) return null; + const localTag = candidateTag(image); + if (image.Repository === "node" && !node20TagPattern.test(image.Tag)) { + return { + source: "local-docker-node", + localTag, imageId: image.ID ?? null, - size: image.Size ?? null, - createdSince: image.CreatedSince ?? null + reason: "local Node image is not Node 20 and is not approved as the HWLAB DEV builder base image" + }; + } + return null; +} + +function selectLocalBaseCandidate(images) { + const candidates = images + .filter(isTaggedImage) + .map((image) => { + const source = candidateSource(image); + if (!source) return null; + const localTag = candidateTag(image); + return { + source, + approval: approvalForBaseRef(localTag), + localTag, + imageId: image.ID ?? null, + size: image.Size ?? null, + createdSince: image.CreatedSince ?? null + }; + }) + .filter(Boolean) + .map((image) => ({ + ...image, + priority: candidatePriority(image) })) + .sort((left, right) => ( + left.priority - right.priority || left.localTag.localeCompare(right.localTag) + )) + .map(({ priority, ...candidate }) => candidate); + + const rejectedCandidates = images + .map(rejectedBaseCandidate) + .filter(Boolean) .sort((left, right) => left.localTag.localeCompare(right.localTag)); return { candidates, + rejectedCandidates, selected: candidates[0] ?? null }; } @@ -289,9 +425,10 @@ function readyFromExplicitImage({ requestedImage, inspectDoc, containerEngine }) localTag, imageId: inspectDoc.Id ?? null, blockers, + recommendedEnvValue: recommendedNode20Image, nextSteps: [ `Set ${envVarName} to an approved local builder base image, not a runtime service image.`, - "Allowed references are local node:20-* tags or HWLAB dev base tags such as hwlab-dev-base:.", + `Allowed references are local node:20-* tags or HWLAB dev base tags such as ${recommendedHwlabBaseImage}.`, "Do not continue #35 artifact publish with this image." ], containerEngine @@ -344,9 +481,10 @@ export async function runDevBaseImagePreflight({ env = process.env } = {}) { ...refBlockers, ...(requestedApproval.approved ? [] : [requestedApproval.reason]) ], + recommendedEnvValue: recommendedNode20Image, nextSteps: [ `Set ${envVarName} to an approved local builder base image, not a runtime service image.`, - "Allowed references are local node:20-* tags or HWLAB dev base tags such as hwlab-dev-base:.", + `Allowed references are local node:20-* tags or HWLAB dev base tags such as ${recommendedHwlabBaseImage}.`, "Do not continue #35 artifact publish with this image." ], containerEngine @@ -360,7 +498,7 @@ export async function runDevBaseImagePreflight({ env = process.env } = {}) { requestedImage, blockers: [`${envVarName} is set to ${requestedImage}, but that image is not present in the local Docker image cache.`], nextSteps: [ - "Preload or build the approved DEV builder base image locally before artifact publish.", + `Preload ${recommendedNode20Image} or tag it as ${recommendedHwlabBaseImage} before artifact publish.`, `Keep ${envVarName} pointed at the exact local tag once the image is available.`, "Do not pull from third-party hosting or substitute a UniDesk runtime image during this preflight." ], @@ -392,13 +530,15 @@ export async function runDevBaseImagePreflight({ env = process.env } = {}) { }); } - const { candidates, selected } = selectNode20Candidate(splitJsonLines(imageList.stdout)); + const { candidates, rejectedCandidates, selected } = selectLocalBaseCandidate(splitJsonLines(imageList.stdout)); if (!selected) { return blocked({ - blockers: [`${envVarName} is not set and no local node:20-* image was found in the Docker image cache.`], + blockers: [`${envVarName} is not set and no approved local DEV builder base image was found in the Docker image cache; expected ${recommendedNode20Image} or ${recommendedHwlabBaseImage}.`], candidates, + rejectedCandidates, nextSteps: [ - `Set ${envVarName} to an approved local DEV builder base image, or preload a local node:20-* image such as node:20-bookworm-slim.`, + `Preload ${recommendedNode20Image} into the D601 Docker cache, or tag it as ${recommendedHwlabBaseImage}.`, + `Set ${envVarName}=${recommendedNode20Image} or ${envVarName}=${recommendedHwlabBaseImage} when invoking artifact publish.`, "Rerun this preflight and require status=ready before #35 artifact publish.", "Do not use UniDesk runtime, Code Queue runner, backend-core, provider-gateway, or microservice-proxy images as substitutes." ], @@ -413,6 +553,7 @@ export async function runDevBaseImagePreflight({ env = process.env } = {}) { requestedImage: null, localTag: selected.localTag, candidates, + rejectedCandidates, blockers: [`Selected local candidate ${selected.localTag} could not be inspected.`], nextSteps: [ `Set ${envVarName} to a known-good approved local base image and rerun this preflight.`, @@ -428,6 +569,7 @@ export async function runDevBaseImagePreflight({ env = process.env } = {}) { requestedImage: null, localTag: selected.localTag, candidates, + rejectedCandidates, imageId: inspected.docs[0].Id ?? null, blockers: evidenceBlockers, nextSteps: [ @@ -447,6 +589,7 @@ export async function runDevBaseImagePreflight({ env = process.env } = {}) { inspectDoc: inspected.docs[0], candidates }), + rejectedCandidates, containerEngine }; }