fix: add dev artifact entrypoints

This commit is contained in:
HWLAB Code Queue
2026-05-21 18:07:40 +00:00
parent ef35856bb0
commit 8807296d7d
12 changed files with 483 additions and 103 deletions
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env node
import {
healthPayload,
listen,
proxyHttpRequest,
resolveHostPort,
sendJson
} from "../../internal/dev-entrypoint/http.mjs";
import { createServer } from "node:http";
const serviceId = "hwlab-edge-proxy";
const upstream = process.env.HWLAB_EDGE_UPSTREAM || "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667";
const { host, port } = resolveHostPort({
listenEnv: "HWLAB_EDGE_LISTEN",
hostEnv: "HWLAB_EDGE_HOST",
portEnv: "HWLAB_EDGE_PORT",
fallbackPort: 6667
});
function proxyDetails() {
return {
upstream,
mode: "dev-edge-proxy"
};
}
const server = createServer((request, response) => {
const url = new URL(request.url || "/", "http://hwlab-edge-proxy.local");
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
sendJson(response, 200, healthPayload({
serviceId,
role: "public-dev-ingress",
details: proxyDetails()
}));
return;
}
if (request.method === "GET" && (url.pathname === "/status" || url.pathname === "/routes")) {
sendJson(response, 200, {
serviceId,
environment: "dev",
status: "ok",
upstream,
routes: [
{
pathPrefix: "/",
upstream,
mode: "http-proxy"
}
]
});
return;
}
if (request.method === "GET" && url.pathname === "/help") {
sendJson(response, 200, {
serviceId,
commands: ["GET /health", "GET /health/live", "GET /status", "GET /routes"]
});
return;
}
proxyHttpRequest({ request, response, upstream });
});
listen(server, { serviceId, host, port });
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env node
import {
createServiceServer,
listen,
resolveHostPort
} from "../../internal/dev-entrypoint/http.mjs";
const serviceId = "hwlab-router";
const mode = process.env.HWLAB_ROUTER_MODE || "edge-reachability";
const edgeProxyService = process.env.HWLAB_EDGE_PROXY_SERVICE || "hwlab-edge-proxy";
const cloudApiService = process.env.HWLAB_CLOUD_API_SERVICE || "hwlab-cloud-api";
const { host, port } = resolveHostPort({
listenEnv: "HWLAB_ROUTER_LISTEN",
hostEnv: "HWLAB_ROUTER_HOST",
portEnv: "HWLAB_ROUTER_PORT",
fallbackPort: 7401
});
const routeTable = [
{
pathPrefix: "/health",
targetServiceId: cloudApiService,
mode: "local-health"
},
{
pathPrefix: "/",
targetServiceId: cloudApiService,
via: edgeProxyService,
mode: "dev-edge-reachability"
}
];
const routes = new Map();
routes.set("GET /status", () => ({
serviceId,
environment: "dev",
status: "ok",
mode,
edgeProxyService,
cloudApiService,
routeTable
}));
routes.set("GET /routes", () => ({
serviceId,
routeTable
}));
listen(
createServiceServer({
serviceId,
role: "dev-router",
routes,
details: () => ({
mode,
edgeProxyService,
cloudApiService,
routeCount: routeTable.length
})
}),
{ serviceId, host, port }
);
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env node
import {
createServiceServer,
listen,
parsePort,
resolveHostPort
} from "../../internal/dev-entrypoint/http.mjs";
const serviceId = "hwlab-tunnel-client";
const mode = process.env.HWLAB_TUNNEL_MODE || "frp-client";
const serverAddress = process.env.HWLAB_FRP_SERVER_ADDR || "74.48.78.17";
const publicPort = parsePort(process.env.HWLAB_FRP_PUBLIC_PORT, 6667);
const localService = process.env.HWLAB_TUNNEL_LOCAL_SERVICE || "hwlab-edge-proxy";
const localPort = parsePort(process.env.HWLAB_TUNNEL_LOCAL_PORT, 6667);
const { host, port } = resolveHostPort({
listenEnv: "HWLAB_TUNNEL_LISTEN",
hostEnv: "HWLAB_TUNNEL_HOST",
portEnv: "HWLAB_TUNNEL_PORT",
fallbackPort: 7402
});
function tunnelState() {
return {
serviceId,
environment: "dev",
status: "configured",
mode,
reverseLink: {
direction: "d601-to-master",
serverAddress,
publicPort,
localService,
localPort
},
liveConnection: "not_started_by_entrypoint"
};
}
const routes = new Map();
routes.set("GET /status", () => tunnelState());
routes.set("GET /tunnel", () => tunnelState().reverseLink);
listen(
createServiceServer({
serviceId,
role: "frp-client",
routes,
details: () => tunnelState()
}),
{ serviceId, host, port }
);
+26 -13
View File
@@ -73,7 +73,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-cloud-web",
@@ -84,7 +85,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-agent-mgr",
@@ -95,7 +97,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-agent-worker",
@@ -106,7 +109,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-gateway",
@@ -117,7 +121,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-gateway-simu",
@@ -128,7 +133,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-box-simu",
@@ -139,7 +145,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-patch-panel",
@@ -150,7 +157,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-router",
@@ -161,7 +169,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-tunnel-client",
@@ -172,7 +181,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-edge-proxy",
@@ -183,7 +193,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-cli",
@@ -194,7 +205,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
},
{
"serviceId": "hwlab-agent-skills",
@@ -205,7 +217,8 @@
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live"
"healthPath": "/health/live",
"sourceState": "source-present"
}
]
}
+1
View File
@@ -56,6 +56,7 @@ rules:
| `namespace` | Must be `hwlab-dev`. |
| `healthPath` | Must be `/health/live` and match the DEV health contract. |
| `publishState` | Must be `skeleton-only` in a blocked skeleton catalog, or `published` only when the catalog was refreshed from a successful publish report. |
| `sourceState` | Must be `source-present` for MVP services with repository source, or `intentionally-disabled` for zero-replica services excluded from MVP publication. |
| `digest` | Must be `not_published` in the skeleton catalog; it may be a registry `sha256:<64 hex>` digest only after publish evidence exists. |
The catalog `commitId` is the artifact source commit, not proof that a registry
+8
View File
@@ -77,6 +77,7 @@ Each service record contains:
- `digest`
- `runtimeKind`
- `implementationState`
- `sourceState`
- `entrypoint`
`digest` is only set to a registry digest after `docker push` succeeds and the
@@ -117,5 +118,12 @@ attempted.
- `missing-runtime-entrypoint`: the image is only a health placeholder and is
not a real service implementation.
## Known Source States
- `source-present`: the service remains in the MVP artifact catalog and has a
repository-owned source path or entrypoint conclusion.
- `intentionally-disabled`: the service is explicitly out of the MVP artifact
publish set until it is given source. It must not remain a vague blocker.
Runtime implementation blockers do not fake a service implementation. They are
recorded as the next minimum work needed after artifact publication.
+134
View File
@@ -0,0 +1,134 @@
import { createServer, request as httpRequest } from "node:http";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../protocol/index.mjs";
export function parsePort(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
if (Number.isInteger(parsed) && parsed > 0 && parsed < 65536) {
return parsed;
}
return fallback;
}
export function resolveHostPort({ listenEnv, hostEnv, portEnv, fallbackPort }) {
const listen = process.env[listenEnv] ?? "";
const [listenHost, listenPort] = listen.includes(":") ? listen.split(":") : ["", listen];
const host = process.env[hostEnv] || listenHost || "0.0.0.0";
const port = parsePort(process.env[portEnv] || listenPort, fallbackPort);
return { host, port };
}
export function healthPayload({ serviceId, role, details = {} }) {
const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA || "unknown";
const imageReference = process.env.HWLAB_IMAGE || `ghcr.io/pikastech/${serviceId}:${commitId.slice(0, 7) || "unknown"}`;
const imageTag = process.env.HWLAB_IMAGE_TAG || commitId.slice(0, 7) || "unknown";
return {
serviceId,
environment: ENVIRONMENT_DEV,
status: "ok",
service: {
id: serviceId,
role,
healthPath: "/health",
livePath: "/health/live"
},
commit: {
id: commitId,
source: process.env.HWLAB_BUILD_SOURCE || "runtime-env"
},
image: {
reference: imageReference,
tag: imageTag,
digest: process.env.HWLAB_IMAGE_DIGEST || "unknown"
},
endpoint: process.env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
details
};
}
export function sendJson(response, statusCode, body) {
const payload = JSON.stringify(body, null, 2);
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"content-length": Buffer.byteLength(payload)
});
response.end(payload);
}
export function createServiceServer({ serviceId, role, routes = new Map(), details = () => ({}) }) {
return createServer(async (request, response) => {
try {
const url = new URL(request.url || "/", `http://${serviceId}.local`);
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
sendJson(response, 200, healthPayload({ serviceId, role, details: details() }));
return;
}
if (request.method === "GET" && url.pathname === "/help") {
sendJson(response, 200, {
serviceId,
commands: ["GET /health", "GET /health/live", ...routes.keys()]
});
return;
}
const handler = routes.get(`${request.method ?? "GET"} ${url.pathname}`);
if (!handler) {
sendJson(response, 404, { error: "not_found", serviceId, path: url.pathname });
return;
}
sendJson(response, 200, await handler({ request, url }));
} catch (error) {
sendJson(response, 500, {
error: "internal_error",
serviceId,
message: error instanceof Error ? error.message : String(error)
});
}
});
}
export function listen(server, { serviceId, host, port }) {
server.listen(port, host, () => {
process.stdout.write(`${JSON.stringify({ serviceId, status: "listening", host, port })}\n`);
});
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => {
server.close(() => {
process.exit(0);
});
});
}
}
export function proxyHttpRequest({ request, response, upstream }) {
const target = new URL(request.url || "/", upstream);
const proxy = httpRequest(
target,
{
method: request.method,
headers: {
...request.headers,
host: target.host
}
},
(upstreamResponse) => {
response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
upstreamResponse.pipe(response);
}
);
proxy.on("error", (error) => {
sendJson(response, 502, {
error: "upstream_unavailable",
upstream,
message: error.message
});
});
request.pipe(proxy);
}
+51 -77
View File
@@ -4,7 +4,7 @@
"reportVersion": "v1",
"issue": "pikasTech/HWLAB#35",
"taskId": "dev-artifact-publish",
"commitId": "70bb916",
"commitId": "7e0ccb0",
"acceptanceLevel": "dev_artifact_publish",
"devOnly": true,
"prodDisabled": true,
@@ -65,24 +65,6 @@
"scope": "base-image",
"status": "open",
"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",
"scope": "hwlab-router",
"status": "open",
"summary": "hwlab-router has no cmd/hwlab-router/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation."
},
{
"type": "runtime_blocker",
"scope": "hwlab-tunnel-client",
"status": "open",
"summary": "hwlab-tunnel-client has no cmd/hwlab-tunnel-client/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation."
},
{
"type": "runtime_blocker",
"scope": "hwlab-edge-proxy",
"status": "open",
"summary": "hwlab-edge-proxy has no cmd/hwlab-edge-proxy/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation."
}
],
"artifactPublish": {
@@ -90,7 +72,7 @@
"status": "blocked",
"mode": "preflight",
"repo": "pikasTech/HWLAB",
"sourceCommitId": "70bb9168ead272ec1f77b752df8e178e70858df4",
"sourceCommitId": "7e0ccb037f5680a21ee454276182da2012693e61",
"registryPrefix": "127.0.0.1:5000/hwlab",
"baseImage": null,
"baseImagePreflight": {
@@ -197,149 +179,162 @@
"serviceCount": 13,
"builtCount": 0,
"publishedCount": 0,
"generatedAt": "2026-05-21T17:31:56.761Z",
"generatedAt": "2026-05-21T18:14:22.229Z",
"services": [
{
"serviceId": "hwlab-cloud-api",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-cloud-api/main.mjs"
},
{
"serviceId": "hwlab-cloud-web",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "cloud-web",
"implementationState": "static-web-wrapper",
"sourceState": "source-present",
"entrypoint": "web/hwlab-cloud-web/index.html"
},
{
"serviceId": "hwlab-agent-mgr",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-mgr/main.mjs"
},
{
"serviceId": "hwlab-agent-worker",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-worker/main.mjs"
},
{
"serviceId": "hwlab-gateway",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway/main.mjs"
},
{
"serviceId": "hwlab-gateway-simu",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway-simu/main.mjs"
},
{
"serviceId": "hwlab-box-simu",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-box-simu/main.mjs"
},
{
"serviceId": "hwlab-patch-panel",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-patch-panel/main.mjs"
},
{
"serviceId": "hwlab-router",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-router:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "health-placeholder",
"implementationState": "missing-runtime-entrypoint",
"entrypoint": null
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-router/main.mjs"
},
{
"serviceId": "hwlab-tunnel-client",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "health-placeholder",
"implementationState": "missing-runtime-entrypoint",
"entrypoint": null
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-tunnel-client/main.mjs"
},
{
"serviceId": "hwlab-edge-proxy",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "health-placeholder",
"implementationState": "missing-runtime-entrypoint",
"entrypoint": null
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-edge-proxy/main.mjs"
},
{
"serviceId": "hwlab-cli",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "cli",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "tools/hwlab-cli/bin/hwlab-cli.mjs"
},
{
"serviceId": "hwlab-agent-skills",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:70bb916",
"imageTag": "70bb916",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e0ccb0",
"imageTag": "7e0ccb0",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "skills-bundle",
"implementationState": "repo-bundle",
"sourceState": "source-present",
"entrypoint": "skills/hwlab-agent-runtime/SKILL.md"
}
],
@@ -350,27 +345,6 @@
"status": "open",
"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",
"scope": "hwlab-router",
"status": "open",
"summary": "hwlab-router has no cmd/hwlab-router/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation.",
"next": "Add cmd/hwlab-router/main.mjs or a dedicated Dockerfile for hwlab-router."
},
{
"type": "runtime_blocker",
"scope": "hwlab-tunnel-client",
"status": "open",
"summary": "hwlab-tunnel-client has no cmd/hwlab-tunnel-client/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation.",
"next": "Add cmd/hwlab-tunnel-client/main.mjs or a dedicated Dockerfile for hwlab-tunnel-client."
},
{
"type": "runtime_blocker",
"scope": "hwlab-edge-proxy",
"status": "open",
"summary": "hwlab-edge-proxy has no cmd/hwlab-edge-proxy/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation.",
"next": "Add cmd/hwlab-edge-proxy/main.mjs or a dedicated Dockerfile for hwlab-edge-proxy."
}
]
},
+46 -11
View File
@@ -34,6 +34,10 @@ const buildableImplementationStates = new Set([
"static-web-wrapper",
"repo-bundle"
]);
const sourceStates = new Set([
"source-present",
"intentionally-disabled"
]);
const servicePorts = new Map([
["hwlab-cloud-api", 6667],
@@ -285,14 +289,20 @@ function repoLabelFromRemote(remoteUrl) {
}
}
async function resolveService(serviceId) {
function serviceSourceState(catalog, serviceId) {
return catalog?.services?.find((service) => service.serviceId === serviceId)?.sourceState ?? "source-present";
}
async function resolveService(serviceId, catalog) {
const sourceState = serviceSourceState(catalog, serviceId);
const cmdPath = `cmd/${serviceId}/main.mjs`;
if (await pathExists(cmdPath)) {
return {
serviceId,
runtimeKind: "node-command",
entrypoint: cmdPath,
implementationState: "repo-entrypoint"
implementationState: "repo-entrypoint",
sourceState
};
}
@@ -301,7 +311,8 @@ async function resolveService(serviceId) {
serviceId,
runtimeKind: "cloud-web",
entrypoint: "web/hwlab-cloud-web/index.html",
implementationState: "static-web-wrapper"
implementationState: "static-web-wrapper",
sourceState
};
}
@@ -311,7 +322,8 @@ async function resolveService(serviceId) {
serviceId,
runtimeKind: "cli",
entrypoint: hasBin ? "tools/hwlab-cli/bin/hwlab-cli.mjs" : "tools/hwlab-cli/lib/cli.mjs",
implementationState: hasBin ? "repo-entrypoint" : "library-only"
implementationState: hasBin ? "repo-entrypoint" : "library-only",
sourceState
};
}
@@ -320,7 +332,8 @@ async function resolveService(serviceId) {
serviceId,
runtimeKind: "skills-bundle",
entrypoint: "skills/hwlab-agent-runtime/SKILL.md",
implementationState: "repo-bundle"
implementationState: "repo-bundle",
sourceState
};
}
@@ -328,16 +341,17 @@ async function resolveService(serviceId) {
serviceId,
runtimeKind: "health-placeholder",
entrypoint: null,
implementationState: "missing-runtime-entrypoint"
implementationState: "missing-runtime-entrypoint",
sourceState
};
}
async function resolveServices(serviceIds) {
async function resolveServices(serviceIds, catalog) {
const unknown = serviceIds.filter((serviceId) => !SERVICE_IDS.includes(serviceId));
assert.deepEqual(unknown, [], `unknown service IDs: ${unknown.join(", ")}`);
const services = [];
for (const serviceId of serviceIds) {
services.push(await resolveService(serviceId));
services.push(await resolveService(serviceId, catalog));
}
return services;
}
@@ -504,6 +518,15 @@ function tailText(value, maxLength = 2500) {
return text.slice(text.length - maxLength);
}
function sourceStateBlocker(service) {
return blocker({
type: "contract_blocker",
scope: service.serviceId,
summary: `${service.serviceId} has invalid sourceState ${service.sourceState ?? "unset"}.`,
next: `Set ${service.serviceId} sourceState to source-present or intentionally-disabled.`
});
}
async function preflight({ args, services, catalog, deployManifest, baseImagePreflight }) {
const blockers = [];
let registryPrefix = args.registryPrefix;
@@ -571,7 +594,18 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
}
for (const service of services) {
if (service.implementationState === "missing-runtime-entrypoint") {
if (!sourceStates.has(service.sourceState)) {
blockers.push(sourceStateBlocker(service));
} else if (service.sourceState === "intentionally-disabled") {
blockers.push(
blocker({
type: "runtime_blocker",
scope: service.serviceId,
summary: `${service.serviceId} is intentionally disabled for MVP artifact publication.`,
next: `Keep ${service.serviceId} out of the MVP artifact catalog, or add a source entrypoint before publishing it.`
})
);
} else if (service.implementationState === "missing-runtime-entrypoint") {
blockers.push(
blocker({
type: "runtime_blocker",
@@ -809,7 +843,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
$schema: "https://hwlab.pikastech.local/schemas/dev-artifact-publish.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-artifacts.json",
reportVersion: "v1",
issue: "pikasTech/HWLAB#31",
issue: "pikasTech/HWLAB#35",
taskId: "dev-artifact-publish",
commitId: shortCommit,
acceptanceLevel: "dev_artifact_publish",
@@ -892,6 +926,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
repositoryDigest: artifact.repositoryDigest ?? null,
runtimeKind: artifact.runtimeKind,
implementationState: artifact.implementationState,
sourceState: artifact.sourceState,
entrypoint: artifact.entrypoint,
localImageId: artifact.localImageId
})),
@@ -929,7 +964,7 @@ async function main() {
]);
const shortCommit = commitId.slice(0, 7);
const repo = repoLabelFromRemote(remoteUrl);
const services = await resolveServices(args.services);
const services = await resolveServices(args.services, catalog);
let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight });
let artifacts = services.map((service) => ({
+10 -2
View File
@@ -473,6 +473,11 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
const artifactPublish = artifactReport.artifactPublish;
const services = artifactPublish?.services ?? [];
const sourcePresentCount = services.filter((service) => service.sourceState === "source-present").length;
const intentionallyDisabledCount = services.filter((service) => service.sourceState === "intentionally-disabled").length;
const sourcePresenceResolved =
services.length === SERVICE_IDS.length &&
services.every((service) => service.sourceState === "source-present" || service.sourceState === "intentionally-disabled");
const allServicesPublished = services.length === SERVICE_IDS.length &&
services.every((service) => service.status === "published" && /^sha256:[a-f0-9]{64}$/.test(service.digest));
const sourceMatchesTarget = [targetCommit, targetShortCommit].includes(artifactPublish?.sourceCommitId) ||
@@ -489,16 +494,19 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
const publishedCount = artifactPublish?.publishedCount ?? 0;
const serviceCount = artifactPublish?.serviceCount ?? SERVICE_IDS.length;
const sourceSummary = sourcePresenceResolved
? `source states resolved: ${sourcePresentCount} source-present, ${intentionallyDisabledCount} intentionally-disabled`
: "source states are not fully resolved";
reporter.check(
"dev-artifact-publish-report",
"registry",
"blocked",
`DEV artifact publish report status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${serviceCount} published for source ${artifactPublish?.sourceCommitId ?? artifactReport.commitId ?? "unknown"}.`
`DEV artifact publish report status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${serviceCount} published for source ${artifactPublish?.sourceCommitId ?? artifactReport.commitId ?? "unknown"}; ${sourceSummary}.`
);
reporter.block({
type: "runtime_blocker",
scope: "dev-artifact-publish",
summary: `reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${serviceCount} published.`,
summary: `reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${serviceCount} published and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests."
});
}
+5
View File
@@ -17,6 +17,7 @@ const digestPattern = /^sha256:[a-f0-9]{64}$/;
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
const catalogModes = new Set(["contract-skeleton", "published"]);
const skeletonProvenances = new Set(["not_available_in_mvp_skeleton", "not_available_until_publish"]);
const sourceStates = new Set(["source-present", "intentionally-disabled"]);
const requiredForbiddenItems = [
"prod-deploy",
"prod-profile-enabled",
@@ -156,6 +157,10 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) {
assert.equal(service.namespace, deployService.namespace, `${context} namespace must match deploy manifest`);
assert.equal(service.healthPath, catalog.healthContract.path, `${context} healthPath`);
assert.equal(service.healthPath, deployService.healthPath, `${context} healthPath must match deploy manifest`);
assert.ok(sourceStates.has(service.sourceState), `${context} sourceState must be source-present or intentionally-disabled`);
if (service.sourceState === "intentionally-disabled") {
assert.equal(deployService.replicas, 0, `${context} intentionally-disabled services must have zero deploy replicas`);
}
if (catalogMode === "contract-skeleton") {
assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
assert.equal(service.digest, "not_published", `${context} digest`);
+21
View File
@@ -196,6 +196,7 @@ const blockerTypes = new Set([
"safety_blocker"
]);
const blockerStates = new Set(["open", "acknowledged", "closed"]);
const artifactSourceStates = new Set(["source-present", "intentionally-disabled"]);
const preflightConclusions = new Set(["ready", "blocked"]);
const requiredPreflightSupports = [
"pikasTech/HWLAB#7",
@@ -407,6 +408,26 @@ async function validateReport(relativePath) {
assertStatus(report.gateStatus, `${label}.gateStatus`);
}
if (Object.hasOwn(report, "artifactPublish")) {
assertObject(report.artifactPublish, `${label}.artifactPublish`);
assertArray(report.artifactPublish.services ?? [], `${label}.artifactPublish.services`);
for (const [index, service] of report.artifactPublish.services.entries()) {
const serviceLabel = `${label}.artifactPublish.services[${index}]`;
assertObject(service, serviceLabel);
assertString(service.serviceId, `${serviceLabel}.serviceId`);
assertString(service.status, `${serviceLabel}.status`);
assertString(service.implementationState, `${serviceLabel}.implementationState`);
assertString(service.sourceState, `${serviceLabel}.sourceState`);
assert.ok(
artifactSourceStates.has(service.sourceState),
`${serviceLabel}.sourceState must be source-present or intentionally-disabled`
);
if (service.sourceState === "intentionally-disabled") {
assert.equal(service.entrypoint, null, `${serviceLabel}.entrypoint must be null when intentionally disabled`);
}
}
}
if (Object.hasOwn(report, "milestones")) {
assertArray(report.milestones, `${label}.milestones`);
assert.deepEqual(