fix: add dev artifact entrypoints
This commit is contained in:
@@ -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 });
|
||||
@@ -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 }
|
||||
);
|
||||
@@ -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 }
|
||||
);
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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."
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user