fix: add dev base image provisioning path

This commit is contained in:
HWLAB Code Queue
2026-05-21 17:33:39 +00:00
parent 3e26b6c435
commit cef2befdd1
6 changed files with 327 additions and 56 deletions
+5 -2
View File
@@ -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:<tag>`. 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
+10 -4
View File
@@ -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
+39 -1
View File
@@ -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:<sha>` 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.
+110 -34
View File
@@ -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",
+5
View File
@@ -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
};
}
+158 -15
View File
@@ -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 !== "<none>" && image.Tag !== "<none>";
}
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:<tag>.",
`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:<tag>.",
`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
};
}