feat: add dev artifact publish v2 plan

This commit is contained in:
HWLAB Code Queue
2026-05-21 18:30:40 +00:00
parent 35ecb5e038
commit 95a6180439
16 changed files with 1233 additions and 285 deletions
+273 -54
View File
@@ -5,7 +5,7 @@
"profile": "dev",
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"commitId": "1a2efd4",
"commitId": "35ecb5e",
"artifactState": "contract-skeleton",
"publish": {
"ciPublished": false,
@@ -66,159 +66,378 @@
"services": [
{
"serviceId": "hwlab-cloud-api",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-cloud-web",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-agent-mgr",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-agent-worker",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-gateway",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-gateway-simu",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-box-simu",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-patch-panel",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-router",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-router:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-tunnel-client",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-edge-proxy",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-cli",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
},
{
"serviceId": "hwlab-agent-skills",
"commitId": "1a2efd4",
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
"imageTag": "1a2efd4",
"commitId": "35ecb5e",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"publishState": "skeleton-only",
"profile": "dev",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"sourceState": "source-present"
"sourceState": "source-present",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "publish_not_run"
}
]
],
"serviceInventory": {
"version": "v2",
"serviceCount": 13,
"requiredServiceCount": 13,
"disabledServiceCount": 0,
"requiredServiceIds": [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills"
],
"disabledServiceIds": [],
"services": [
{
"serviceId": "hwlab-cloud-api",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-cloud-api/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-cloud-web",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "cloud-web",
"implementationState": "static-web-wrapper",
"sourceState": "source-present",
"entrypoint": "web/hwlab-cloud-web/index.html",
"disabledReason": null
},
{
"serviceId": "hwlab-agent-mgr",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-mgr/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-agent-worker",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-worker/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-gateway",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-gateway-simu",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway-simu/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-box-simu",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-box-simu/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-patch-panel",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-patch-panel/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-router",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-router/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-tunnel-client",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-tunnel-client/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-edge-proxy",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-edge-proxy/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-cli",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "cli",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "tools/hwlab-cli/bin/hwlab-cli.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-agent-skills",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "skills-bundle",
"implementationState": "repo-bundle",
"sourceState": "source-present",
"entrypoint": "skills/hwlab-agent-runtime/SKILL.md",
"disabledReason": null
}
]
}
}
+18 -18
View File
@@ -1,7 +1,7 @@
{
"manifestVersion": "v1",
"environment": "dev",
"commitId": "1a2efd4",
"commitId": "35ecb5e",
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"profiles": {
@@ -22,7 +22,7 @@
"services": [
{
"serviceId": "hwlab-cloud-api",
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -30,9 +30,9 @@
"env": {
"HWLAB_ENVIRONMENT": "dev",
"HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:6667",
"HWLAB_COMMIT_ID": "1a2efd4",
"HWLAB_IMAGE": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
"HWLAB_IMAGE_TAG": "1a2efd4",
"HWLAB_COMMIT_ID": "35ecb5e",
"HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e",
"HWLAB_IMAGE_TAG": "35ecb5e",
"HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy",
"HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url",
"HWLAB_CLOUD_DB_SSL_MODE": "require",
@@ -41,7 +41,7 @@
},
{
"serviceId": "hwlab-cloud-web",
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -53,7 +53,7 @@
},
{
"serviceId": "hwlab-agent-mgr",
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -65,7 +65,7 @@
},
{
"serviceId": "hwlab-agent-worker",
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -77,7 +77,7 @@
},
{
"serviceId": "hwlab-gateway",
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -89,7 +89,7 @@
},
{
"serviceId": "hwlab-gateway-simu",
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -101,7 +101,7 @@
},
{
"serviceId": "hwlab-box-simu",
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -113,7 +113,7 @@
},
{
"serviceId": "hwlab-patch-panel",
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -125,7 +125,7 @@
},
{
"serviceId": "hwlab-router",
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-router:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -137,7 +137,7 @@
},
{
"serviceId": "hwlab-tunnel-client",
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -150,7 +150,7 @@
},
{
"serviceId": "hwlab-edge-proxy",
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -162,7 +162,7 @@
},
{
"serviceId": "hwlab-cli",
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
@@ -173,13 +173,13 @@
},
{
"serviceId": "hwlab-agent-skills",
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:35ecb5e",
"namespace": "hwlab-dev",
"healthPath": "/health/live",
"profile": "dev",
"replicas": 1,
"env": {
"HWLAB_SKILLS_COMMIT_ID": "1a2efd4"
"HWLAB_SKILLS_COMMIT_ID": "35ecb5e"
}
}
],
+13
View File
@@ -138,6 +138,19 @@
"const": "dev-redacted-presence-only"
}
}
},
"publishEnabled": {
"type": "boolean"
},
"artifactRequired": {
"type": "boolean"
},
"artifactScope": {
"type": "string",
"enum": ["required", "disabled"]
},
"notPublishedReason": {
"type": ["string", "null"]
}
},
"additionalProperties": false
+17 -17
View File
@@ -31,7 +31,7 @@
"containers": [
{
"name": "hwlab-cloud-api",
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e",
"ports": [
{
"name": "http",
@@ -49,15 +49,15 @@
},
{
"name": "HWLAB_COMMIT_ID",
"value": "1a2efd4"
"value": "35ecb5e"
},
{
"name": "HWLAB_IMAGE",
"value": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4"
"value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e"
},
{
"name": "HWLAB_IMAGE_TAG",
"value": "1a2efd4"
"value": "35ecb5e"
},
{
"name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN",
@@ -129,7 +129,7 @@
"containers": [
{
"name": "hwlab-cloud-web",
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:35ecb5e",
"ports": [
{
"name": "http",
@@ -189,7 +189,7 @@
"containers": [
{
"name": "hwlab-agent-mgr",
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:35ecb5e",
"ports": [
{
"name": "http",
@@ -245,7 +245,7 @@
"containers": [
{
"name": "hwlab-agent-worker",
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:35ecb5e",
"env": [
{
"name": "HWLAB_AGENT_SESSION_MODE",
@@ -288,7 +288,7 @@
"containers": [
{
"name": "hwlab-gateway",
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:35ecb5e",
"ports": [
{
"name": "http",
@@ -348,7 +348,7 @@
"containers": [
{
"name": "hwlab-gateway-simu",
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:35ecb5e",
"ports": [
{
"name": "http",
@@ -408,7 +408,7 @@
"containers": [
{
"name": "hwlab-box-simu",
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:35ecb5e",
"ports": [
{
"name": "http",
@@ -468,7 +468,7 @@
"containers": [
{
"name": "hwlab-patch-panel",
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:35ecb5e",
"ports": [
{
"name": "http",
@@ -522,7 +522,7 @@
"containers": [
{
"name": "hwlab-router",
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-router:35ecb5e",
"ports": [
{
"name": "http",
@@ -582,7 +582,7 @@
"containers": [
{
"name": "hwlab-tunnel-client",
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:35ecb5e",
"ports": [
{
"name": "http",
@@ -650,7 +650,7 @@
"containers": [
{
"name": "hwlab-edge-proxy",
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:35ecb5e",
"ports": [
{
"name": "http",
@@ -711,7 +711,7 @@
"containers": [
{
"name": "hwlab-cli",
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:35ecb5e",
"env": [
{
"name": "HWLAB_CLI_ENDPOINT",
@@ -753,7 +753,7 @@
"containers": [
{
"name": "hwlab-agent-skills",
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:35ecb5e",
"ports": [
{
"name": "http",
@@ -763,7 +763,7 @@
"env": [
{
"name": "HWLAB_SKILLS_COMMIT_ID",
"value": "1a2efd4"
"value": "35ecb5e"
}
],
"readinessProbe": {
+10 -5
View File
@@ -11,8 +11,9 @@ declares:
tags are aligned, but `ciPublished=false`, `registryVerified=false`, and every
digest must stay `not_published`.
- `artifactState: "published"` is allowed only after a DEV publish report proves
every frozen service was published for the same source commit with a real
`sha256:<64 hex>` registry digest.
every required enabled service was published for the same source commit with a
real `sha256:<64 hex>` registry digest. Services explicitly disabled in the
service inventory must stay `not_published` and carry a disabled reason.
Do not infer registry existence, digest values, or deployment from commit tags.
Real publish evidence must come from the publishing workflow and registry
@@ -50,13 +51,15 @@ rules:
| --- | --- |
| `serviceId` | Must be one of the frozen HWLAB service IDs and must appear exactly once. |
| `commitId` | Must match the catalog and `deploy/deploy.json` commit ID. Short or full lowercase Git SHA is allowed. |
| `image` | Must match `ghcr.io/pikastech/<serviceId>:<imageTag>` and the matching `deploy/deploy.json` service image. |
| `image` | Must match the DEV registry target `<registry>/<namespace>/<serviceId>:<imageTag>` and the matching `deploy/deploy.json` service image. |
| `imageTag` | Must be the first seven characters of `commitId`. Mutable tags such as `latest`, `dev`, or `main` are forbidden. |
| `profile` | Must be `dev`. |
| `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. |
| `artifactRequired` | Must match `serviceInventory.requiredServiceIds`; only required services need publish digests. |
| `artifactScope` | `required` for services that must be published, or `disabled` with a `notPublishedReason`. |
| `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
@@ -86,8 +89,10 @@ node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-rep
```
The publish-report mode refuses to update the catalog unless the report says
`artifactPublish.status="published"`, covers every frozen service, matches the
target source commit, and contains a valid `sha256` digest for each service.
`artifactPublish.status="published"`, covers every frozen service in the v2
publish plan, matches the target source commit, and contains a valid `sha256`
digest for each required service. Disabled services remain `skeleton-only` with
`digest: "not_published"`.
## Relationship To `deploy/deploy.json`
+15
View File
@@ -29,6 +29,7 @@ Static check:
```sh
node --check scripts/preflight-dev-base-image.mjs
node --check scripts/src/dev-base-image-preflight.mjs
node --check scripts/src/dev-artifact-services.mjs
node --check scripts/dev-artifact-publish.mjs
node --check scripts/refresh-artifact-catalog.mjs
```
@@ -75,11 +76,21 @@ Each service record contains:
- `image`
- `imageTag`
- `digest`
- `publishEnabled`
- `artifactRequired`
- `artifactScope`
- `notPublishedReason`
- `runtimeKind`
- `implementationState`
- `sourceState`
- `entrypoint`
The report also carries `artifactPublish.serviceInventory` and
`artifactPublish.publishPlan`. `serviceInventory` is the resolved frozen service
list with enabled/disabled state. `publishPlan` is the machine-readable v2 plan:
source commit, image tag, per-service registry target, image reference, digest
placeholder or real digest, and the reason a service was not published.
`digest` is only set to a registry digest after `docker push` succeeds and the
push output contains a `sha256:<64 hex>` digest. If the push succeeds but no
digest is observable, the service remains `published_unverified_digest`,
@@ -108,6 +119,10 @@ 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.
If the D601 builder has no approved Node 20 base image, or the selected DEV
registry is not reachable, the report must remain `status: "blocked"`. Do not
turn a base-image or registry blocker into a fake digest or published state.
## Known Implementation States
- `repo-entrypoint`: the repository has `cmd/<serviceId>/main.mjs`.
+4 -3
View File
@@ -11,7 +11,8 @@ PROD, read secrets, or use UniDesk runtime services as substitutes.
- No repository Dockerfile is present in the DEV skeleton.
- `deploy/deploy.json` and `deploy/artifact-catalog.dev.json` pin service image
names to `ghcr.io/pikastech/<hwlab-service-id>:<commit>`.
names to the DEV registry target, by default
`127.0.0.1:5000/hwlab/<hwlab-service-id>:<commit>`.
- The artifact catalog is `contract-skeleton` only. Service entries must keep
`publishState: "skeleton-only"` and `digest: "not_published"` until real
publish evidence exists.
@@ -54,8 +55,8 @@ Explicit base image values must match local `node:20-*`, `hwlab-dev-base:*`, or
`hwlab-node20-base:*` references. They must not reference UniDesk, Code Queue,
`backend-core`, `provider-gateway`, `microservice-proxy`, or an HWLAB runtime
service image. A service image such as
`ghcr.io/pikastech/hwlab-cloud-api:<sha>` is an artifact output, not a builder
base image.
`127.0.0.1:5000/hwlab/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:
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
"l2:smoke": "node scripts/l2-runtime-contract-smoke.mjs",
+469 -42
View File
@@ -4,7 +4,7 @@
"reportVersion": "v1",
"issue": "pikasTech/HWLAB#35",
"taskId": "dev-artifact-publish",
"commitId": "7e0ccb0",
"commitId": "35ecb5e",
"acceptanceLevel": "dev_artifact_publish",
"devOnly": true,
"prodDisabled": true,
@@ -21,6 +21,7 @@
},
"validationCommands": [
"node --check scripts/dev-artifact-publish.mjs",
"node --check scripts/src/dev-artifact-services.mjs",
"node --check scripts/preflight-dev-base-image.mjs",
"node --check scripts/src/dev-base-image-preflight.mjs",
"node scripts/preflight-dev-base-image.mjs",
@@ -72,7 +73,7 @@
"status": "blocked",
"mode": "preflight",
"repo": "pikasTech/HWLAB",
"sourceCommitId": "7e0ccb037f5680a21ee454276182da2012693e61",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryPrefix": "127.0.0.1:5000/hwlab",
"baseImage": null,
"baseImagePreflight": {
@@ -176,166 +177,592 @@
}
},
"environment": "dev",
"serviceInventory": {
"version": "v2",
"serviceCount": 13,
"requiredServiceCount": 13,
"disabledServiceCount": 0,
"requiredServiceIds": [
"hwlab-cloud-api",
"hwlab-cloud-web",
"hwlab-agent-mgr",
"hwlab-agent-worker",
"hwlab-gateway",
"hwlab-gateway-simu",
"hwlab-box-simu",
"hwlab-patch-panel",
"hwlab-router",
"hwlab-tunnel-client",
"hwlab-edge-proxy",
"hwlab-cli",
"hwlab-agent-skills"
],
"disabledServiceIds": [],
"services": [
{
"serviceId": "hwlab-cloud-api",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-cloud-api/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-cloud-web",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "cloud-web",
"implementationState": "static-web-wrapper",
"sourceState": "source-present",
"entrypoint": "web/hwlab-cloud-web/index.html",
"disabledReason": null
},
{
"serviceId": "hwlab-agent-mgr",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-mgr/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-agent-worker",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-worker/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-gateway",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-gateway-simu",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway-simu/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-box-simu",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-box-simu/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-patch-panel",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-patch-panel/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-router",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-router/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-tunnel-client",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-tunnel-client/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-edge-proxy",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-edge-proxy/main.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-cli",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "cli",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "tools/hwlab-cli/bin/hwlab-cli.mjs",
"disabledReason": null
},
{
"serviceId": "hwlab-agent-skills",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "skills-bundle",
"implementationState": "repo-bundle",
"sourceState": "source-present",
"entrypoint": "skills/hwlab-agent-runtime/SKILL.md",
"disabledReason": null
}
]
},
"publishPlan": {
"version": "v2",
"mode": "preflight",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"imageTag": "35ecb5e",
"registryTarget": "127.0.0.1:5000/hwlab",
"digestPlaceholder": "not_published",
"services": [
{
"serviceId": "hwlab-cloud-api",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-cloud-api",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-cloud-web",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-cloud-web",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-agent-mgr",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-agent-mgr",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-agent-worker",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-agent-worker",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-gateway",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-gateway",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-gateway-simu",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-gateway-simu",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-box-simu",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-box-simu",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-patch-panel",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-patch-panel",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-router",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-router",
"image": "127.0.0.1:5000/hwlab/hwlab-router:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-tunnel-client",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-tunnel-client",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-edge-proxy",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-edge-proxy",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-cli",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-cli",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-agent-skills",
"enabled": true,
"required": true,
"artifactScope": "required",
"sourceCommitId": "35ecb5e038b0b06962151f3f1751b72e1d9f6c73",
"registryTarget": "127.0.0.1:5000/hwlab/hwlab-agent-skills",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"digestStatus": "placeholder",
"status": "preflight_only",
"notPublishedReason": "blocked_by_preflight"
}
]
},
"serviceCount": 13,
"requiredServiceCount": 13,
"disabledServiceCount": 0,
"builtCount": 0,
"publishedCount": 0,
"generatedAt": "2026-05-21T18:14:22.229Z",
"generatedAt": "2026-05-21T18:29:58.488Z",
"services": [
{
"serviceId": "hwlab-cloud-api",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-cloud-api/main.mjs"
"entrypoint": "cmd/hwlab-cloud-api/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-cloud-web",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "cloud-web",
"implementationState": "static-web-wrapper",
"sourceState": "source-present",
"entrypoint": "web/hwlab-cloud-web/index.html"
"entrypoint": "web/hwlab-cloud-web/index.html",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-agent-mgr",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-mgr/main.mjs"
"entrypoint": "cmd/hwlab-agent-mgr/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-agent-worker",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-agent-worker/main.mjs"
"entrypoint": "cmd/hwlab-agent-worker/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-gateway",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway/main.mjs"
"entrypoint": "cmd/hwlab-gateway/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-gateway-simu",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-gateway-simu/main.mjs"
"entrypoint": "cmd/hwlab-gateway-simu/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-box-simu",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-box-simu/main.mjs"
"entrypoint": "cmd/hwlab-box-simu/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-patch-panel",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-patch-panel/main.mjs"
"entrypoint": "cmd/hwlab-patch-panel/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-router",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-router:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-router/main.mjs"
"entrypoint": "cmd/hwlab-router/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-tunnel-client",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-tunnel-client/main.mjs"
"entrypoint": "cmd/hwlab-tunnel-client/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-edge-proxy",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "node-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-edge-proxy/main.mjs"
"entrypoint": "cmd/hwlab-edge-proxy/main.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-cli",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "cli",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "tools/hwlab-cli/bin/hwlab-cli.mjs"
"entrypoint": "tools/hwlab-cli/bin/hwlab-cli.mjs",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
},
{
"serviceId": "hwlab-agent-skills",
"status": "preflight_only",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e0ccb0",
"imageTag": "7e0ccb0",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:35ecb5e",
"imageTag": "35ecb5e",
"digest": "not_published",
"repositoryDigest": null,
"runtimeKind": "skills-bundle",
"implementationState": "repo-bundle",
"sourceState": "source-present",
"entrypoint": "skills/hwlab-agent-runtime/SKILL.md"
"entrypoint": "skills/hwlab-agent-runtime/SKILL.md",
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"notPublishedReason": "blocked_by_preflight"
}
],
"blockers": [
+94 -100
View File
@@ -1,9 +1,7 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { constants as fsConstants } from "node:fs";
import {
access,
mkdir,
readFile,
writeFile
@@ -12,6 +10,11 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
import {
BUILDABLE_IMPLEMENTATION_STATES,
resolveDevArtifactServices,
serviceInventoryFromServices
} from "./src/dev-artifact-services.mjs";
import { runDevBaseImagePreflight } from "./src/dev-base-image-preflight.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -29,11 +32,7 @@ const fatalBlockerTypes = new Set([
"network_blocker",
"safety_blocker"
]);
const buildableImplementationStates = new Set([
"repo-entrypoint",
"static-web-wrapper",
"repo-bundle"
]);
const buildableImplementationStates = new Set(BUILDABLE_IMPLEMENTATION_STATES);
const sourceStates = new Set([
"source-present",
"intentionally-disabled"
@@ -109,15 +108,6 @@ async function readJson(relativePath) {
return JSON.parse(await readFile(path.join(repoRoot, relativePath), "utf8"));
}
async function pathExists(relativePath) {
try {
await access(path.join(repoRoot, relativePath), fsConstants.R_OK);
return true;
} catch {
return false;
}
}
function emit(event, payload = {}) {
process.stderr.write(`${JSON.stringify({ event, ...payload })}\n`);
}
@@ -289,71 +279,8 @@ function repoLabelFromRemote(remoteUrl) {
}
}
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",
sourceState
};
}
if (serviceId === "hwlab-cloud-web") {
return {
serviceId,
runtimeKind: "cloud-web",
entrypoint: "web/hwlab-cloud-web/index.html",
implementationState: "static-web-wrapper",
sourceState
};
}
if (serviceId === "hwlab-cli") {
const hasBin = await pathExists("tools/hwlab-cli/bin/hwlab-cli.mjs");
return {
serviceId,
runtimeKind: "cli",
entrypoint: hasBin ? "tools/hwlab-cli/bin/hwlab-cli.mjs" : "tools/hwlab-cli/lib/cli.mjs",
implementationState: hasBin ? "repo-entrypoint" : "library-only",
sourceState
};
}
if (serviceId === "hwlab-agent-skills") {
return {
serviceId,
runtimeKind: "skills-bundle",
entrypoint: "skills/hwlab-agent-runtime/SKILL.md",
implementationState: "repo-bundle",
sourceState
};
}
return {
serviceId,
runtimeKind: "health-placeholder",
entrypoint: null,
implementationState: "missing-runtime-entrypoint",
sourceState
};
}
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, catalog));
}
return services;
return resolveDevArtifactServices(repoRoot, serviceIds, SERVICE_IDS, catalog);
}
function runtimeScriptBase64() {
@@ -605,7 +532,7 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
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") {
} else if (service.artifactRequired && service.implementationState === "missing-runtime-entrypoint") {
blockers.push(
blocker({
type: "runtime_blocker",
@@ -808,11 +735,12 @@ function repositoryFromImageRef(image) {
}
function reportStatus(mode, artifacts, blockers) {
const requiredArtifacts = artifacts.filter((artifact) => artifact.artifactRequired);
if (mode === "preflight") return blockers.length ? "blocked" : "pass";
if (artifacts.some((artifact) => artifact.status === "publish_failed" || artifact.status === "build_failed")) return "failed";
if (artifacts.some((artifact) => artifact.status.startsWith("blocked_"))) return "blocked";
if (mode === "publish" && artifacts.every((artifact) => artifact.status === "published")) return "published";
if (mode === "build" && artifacts.every((artifact) => artifact.status === "built")) return "built";
if (requiredArtifacts.some((artifact) => artifact.status === "publish_failed" || artifact.status === "build_failed")) return "failed";
if (requiredArtifacts.some((artifact) => artifact.status.startsWith("blocked_"))) return "blocked";
if (mode === "publish" && requiredArtifacts.length > 0 && requiredArtifacts.every((artifact) => artifact.status === "published")) return "published";
if (mode === "build" && requiredArtifacts.length > 0 && requiredArtifacts.every((artifact) => artifact.status === "built")) return "built";
return blockers.length ? "blocked" : "pass";
}
@@ -833,11 +761,66 @@ function devGateBlockers(blockers) {
return result;
}
function createReport({ args, repo, commitId, shortCommit, mode, artifacts, blockers, baseImagePreflight }) {
function artifactNotPublishedReason(artifact, mode, fatalBlocked) {
if (/^sha256:[a-f0-9]{64}$/u.test(artifact.digest ?? "")) return null;
if (!artifact.artifactRequired) return artifact.notPublishedReason;
if (artifact.status === "preflight_only") return fatalBlocked ? "blocked_by_preflight" : "preflight_only";
if (artifact.status === "built") return "build_only_not_published";
if (artifact.status === "build_failed") return "build_failed";
if (artifact.status === "publish_failed") return "publish_failed";
if (artifact.status === "published_unverified_digest") return "digest_unverified";
return artifact.notPublishedReason ?? "publish_not_run";
}
function artifactRecord({ args, service, shortCommit, status, digest = "not_published" }) {
return {
...service,
status,
image: imageRef(args.registryPrefix, service.serviceId, shortCommit),
imageTag: shortCommit,
digest,
repositoryDigest: null
};
}
function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked }) {
return {
version: "v2",
mode,
sourceCommitId: commitId,
imageTag: shortCommit,
registryTarget: args.registryPrefix,
digestPlaceholder: "not_published",
services: artifacts.map((artifact) => {
const image = artifact.image ?? imageRef(args.registryPrefix, artifact.serviceId, shortCommit);
const digest = artifact.digest ?? "not_published";
return {
serviceId: artifact.serviceId,
enabled: artifact.publishEnabled,
required: artifact.artifactRequired,
artifactScope: artifact.artifactScope,
sourceCommitId: commitId,
registryTarget: imageRepository(args.registryPrefix, artifact.serviceId),
image,
imageTag: artifact.imageTag ?? shortCommit,
digest,
repositoryDigest: artifact.repositoryDigest ?? null,
digestStatus: /^sha256:[a-f0-9]{64}$/u.test(digest) ? "real" : "placeholder",
status: artifact.status,
notPublishedReason: artifactNotPublishedReason(artifact, mode, fatalBlocked)
};
})
};
}
function createReport({ args, repo, commitId, shortCommit, mode, services, artifacts, blockers, baseImagePreflight }) {
const status = reportStatus(mode, artifacts, blockers);
const fatalBlocked = hasFatalBlocker(blockers);
const publishedCount = artifacts.filter((artifact) => artifact.status === "published").length;
const builtCount = artifacts.filter((artifact) => ["built", "published", "published_unverified_digest"].includes(artifact.status)).length;
const requiredArtifacts = artifacts.filter((artifact) => artifact.artifactRequired);
const serviceInventory = serviceInventoryFromServices(services);
const publishPlan = publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked });
const publishedCount = requiredArtifacts.filter((artifact) => artifact.status === "published").length;
const builtCount = requiredArtifacts.filter((artifact) => ["built", "published", "published_unverified_digest"].includes(artifact.status)).length;
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-artifact-publish.schema.json",
@@ -862,6 +845,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
},
validationCommands: [
"node --check scripts/dev-artifact-publish.mjs",
"node --check scripts/src/dev-artifact-services.mjs",
"node --check scripts/preflight-dev-base-image.mjs",
"node --check scripts/src/dev-base-image-preflight.mjs",
"node scripts/preflight-dev-base-image.mjs",
@@ -913,7 +897,11 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
baseImage: args.baseImage ?? null,
baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight),
environment: ENVIRONMENT_DEV,
serviceInventory,
publishPlan,
serviceCount: artifacts.length,
requiredServiceCount: serviceInventory.requiredServiceCount,
disabledServiceCount: serviceInventory.disabledServiceCount,
builtCount,
publishedCount,
generatedAt: new Date().toISOString(),
@@ -928,6 +916,10 @@ function createReport({ args, repo, commitId, shortCommit, mode, artifacts, bloc
implementationState: artifact.implementationState,
sourceState: artifact.sourceState,
entrypoint: artifact.entrypoint,
publishEnabled: artifact.publishEnabled,
artifactRequired: artifact.artifactRequired,
artifactScope: artifact.artifactScope,
notPublishedReason: artifactNotPublishedReason(artifact, mode, fatalBlocked),
localImageId: artifact.localImageId
})),
blockers
@@ -967,12 +959,11 @@ async function main() {
const services = await resolveServices(args.services, catalog);
let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight });
let artifacts = services.map((service) => ({
...service,
status: "preflight_only",
image: imageRef(args.registryPrefix, service.serviceId, shortCommit),
imageTag: shortCommit,
digest: "not_published"
let artifacts = services.map((service) => artifactRecord({
args,
service,
shortCommit,
status: service.artifactRequired ? "preflight_only" : "not_required"
}));
if ((args.mode === "build" || args.mode === "publish") && hasFatalBlocker(blockers)) {
@@ -980,18 +971,18 @@ async function main() {
fatalBlockers: blockers.filter((item) => fatalBlockerTypes.has(item.type)).map((item) => item.scope)
});
} else if (args.mode === "build" || args.mode === "publish") {
artifacts = [];
for (const service of services) {
const artifactByServiceId = new Map(artifacts.map((artifact) => [artifact.serviceId, artifact]));
for (const service of services.filter((item) => item.artifactRequired)) {
emit("build_start", { serviceId: service.serviceId, image: imageRef(args.registryPrefix, service.serviceId, shortCommit) });
const artifact = await buildService({ args, repo, commitId, shortCommit, service });
artifacts.push(artifact);
artifactByServiceId.set(service.serviceId, artifact);
if (artifact.blocker) blockers.push(artifact.blocker);
emit("build_done", { serviceId: service.serviceId, status: artifact.status, image: artifact.image });
if (args.mode === "publish" && artifact.status === "built") {
emit("publish_start", { serviceId: service.serviceId, image: artifact.image });
const published = await publishService(artifact);
artifacts[artifacts.length - 1] = published;
artifactByServiceId.set(service.serviceId, published);
if (published.blocker) blockers.push(published.blocker);
emit("publish_done", {
serviceId: service.serviceId,
@@ -1001,9 +992,10 @@ async function main() {
});
}
}
artifacts = services.map((service) => artifactByServiceId.get(service.serviceId));
}
const report = createReport({ args, repo, commitId, shortCommit, mode: args.mode, artifacts, blockers, baseImagePreflight });
const report = createReport({ args, repo, commitId, shortCommit, mode: args.mode, services, artifacts, blockers, baseImagePreflight });
if (args.emitReport) {
await writeReport(args.reportPath, report);
}
@@ -1015,11 +1007,13 @@ async function main() {
sourceCommitId: commitId,
registryPrefix: args.registryPrefix,
baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight),
publishPlan: report.artifactPublish.publishPlan,
services: report.artifactPublish.services.map((service) => ({
serviceId: service.serviceId,
status: service.status,
image: service.image,
digest: service.digest
digest: service.digest,
notPublishedReason: service.notPublishedReason
})),
blockers
}, null, 2));
+68 -16
View File
@@ -7,6 +7,10 @@ import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
import {
resolveDevArtifactServices,
serviceInventoryFromServices
} from "./src/dev-artifact-services.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -14,6 +18,7 @@ const catalogPath = "deploy/artifact-catalog.dev.json";
const deployPath = "deploy/deploy.json";
const workloadsPath = "deploy/k8s/base/workloads.yaml";
const defaultPublishReportPath = "reports/dev-gate/dev-artifacts.json";
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const commitPattern = /^[a-f0-9]{7,40}$/;
@@ -107,8 +112,8 @@ function parseTaggedImage(image, context) {
};
}
function targetImage(serviceId, shortCommitId) {
return `ghcr.io/pikastech/${serviceId}:${shortCommitId}`;
function targetImage(serviceId, shortCommitId, registryPrefix = defaultRegistryPrefix) {
return `${registryPrefix.replace(/\/+$/u, "")}/${serviceId}:${shortCommitId}`;
}
function commitMatchesTarget(value, target) {
@@ -137,16 +142,42 @@ function publishRecordsFromReport(report, target) {
assert.ok(artifactPublish && typeof artifactPublish === "object", "publish report missing artifactPublish");
assert.equal(artifactPublish.status, "published", "publish report artifactPublish.status must be published");
assert.ok(commitMatchesTarget(artifactPublish.sourceCommitId, target), "publish report sourceCommitId must match target ref");
assert.equal(artifactPublish.publishedCount, SERVICE_IDS.length, "publish report must publish every frozen service");
assert.equal(artifactPublish.serviceCount, SERVICE_IDS.length, "publish report must cover every frozen service");
assert.ok(artifactPublish.publishPlan && typeof artifactPublish.publishPlan === "object", "publish report missing artifactPublish.publishPlan");
assert.equal(artifactPublish.publishPlan.version, "v2", "publish report plan version must be v2");
const requiredServiceIds = artifactPublish.publishPlan.services
.filter((service) => service.required)
.map((service) => service.serviceId);
const disabledServiceIds = artifactPublish.publishPlan.services
.filter((service) => !service.required)
.map((service) => service.serviceId);
assert.deepEqual(
[...requiredServiceIds, ...disabledServiceIds].sort(),
[...SERVICE_IDS].sort(),
"publish plan must cover every frozen service"
);
assert.equal(
artifactPublish.publishedCount,
requiredServiceIds.length,
"publish report must publish every required enabled service"
);
const records = new Map();
for (const service of artifactPublish.services ?? []) {
assert.ok(SERVICE_IDS.includes(service.serviceId), `unknown service ${service.serviceId} in publish report`);
assert.equal(service.status, "published", `${service.serviceId} status must be published`);
assert.match(service.digest, digestPattern, `${service.serviceId} digest must be a sha256 registry digest`);
const image = parseTaggedImage(service.image, service.serviceId);
assert.equal(image.tag, target.shortCommitId, `${service.serviceId} image tag must match target short commit`);
if (!requiredServiceIds.includes(service.serviceId)) {
assert.equal(service.artifactRequired, false, `${service.serviceId} disabled service must not be required`);
assert.equal(service.digest, "not_published", `${service.serviceId} disabled digest must stay not_published`);
assert.equal(service.notPublishedReason?.startsWith("disabled_"), true, `${service.serviceId} disabled service must state a disabled reason`);
continue;
}
assert.equal(service.status, "published", `${service.serviceId} status must be published`);
assert.equal(service.artifactRequired, true, `${service.serviceId} required service must state artifactRequired=true`);
assert.match(service.digest, digestPattern, `${service.serviceId} digest must be a sha256 registry digest`);
records.set(service.serviceId, {
serviceId: service.serviceId,
image: service.image,
@@ -156,8 +187,8 @@ function publishRecordsFromReport(report, target) {
});
}
assert.deepEqual([...records.keys()], SERVICE_IDS, "publish report services must cover frozen service IDs in order");
return records;
assert.deepEqual([...records.keys()], requiredServiceIds, "publish report services must publish required service IDs in order");
return { records, requiredServiceIds, disabledServiceIds };
}
function updateEnvObject(env, service, target, publishRecord) {
@@ -192,15 +223,19 @@ function workloadContainers(item) {
return item?.spec?.template?.spec?.containers ?? [];
}
function refreshDocuments({ deploy, catalog, workloads, target, publishRecords, provenancePath }) {
function refreshDocuments({ deploy, catalog, workloads, target, publishRecords, serviceInventory, provenancePath, registryPrefix }) {
const published = Boolean(publishRecords);
const records = publishRecords?.records ?? null;
const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds);
const catalogByService = new Map(catalog.services.map((service) => [service.serviceId, service]));
const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service]));
const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service]));
const refreshedServices = [];
deploy.commitId = target.shortCommitId;
catalog.commitId = target.shortCommitId;
catalog.artifactState = published ? "published" : "contract-skeleton";
catalog.serviceInventory = serviceInventory;
catalog.publish = {
...catalog.publish,
ciPublished: published,
@@ -217,8 +252,10 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
assert.ok(deployService, `${serviceId} missing from deploy manifest`);
assert.ok(catalogService, `${serviceId} missing from catalog`);
const publishRecord = publishRecords?.get(serviceId) ?? null;
const image = publishRecord?.image ?? targetImage(serviceId, target.shortCommitId);
const inventory = inventoryByService.get(serviceId);
const required = requiredIds.has(serviceId);
const publishRecord = records?.get(serviceId) ?? null;
const image = publishRecord?.image ?? targetImage(serviceId, target.shortCommitId, registryPrefix);
const imageTag = publishRecord?.imageTag ?? target.shortCommitId;
deployService.image = image;
@@ -228,13 +265,21 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
catalogService.image = image;
catalogService.imageTag = imageTag;
catalogService.digest = publishRecord?.digest ?? "not_published";
catalogService.publishState = published ? "published" : "skeleton-only";
catalogService.publishState = publishRecord ? "published" : "skeleton-only";
catalogService.publishEnabled = inventory?.publishEnabled ?? required;
catalogService.artifactRequired = required;
catalogService.artifactScope = required ? "required" : "disabled";
catalogService.notPublishedReason = publishRecord
? null
: inventory?.disabledReason ?? (published ? "not_required" : "publish_not_run");
refreshedServices.push({
serviceId,
image,
imageTag,
digest: catalogService.digest,
publishState: catalogService.publishState
publishState: catalogService.publishState,
artifactRequired: catalogService.artifactRequired,
notPublishedReason: catalogService.notPublishedReason
});
}
@@ -268,13 +313,18 @@ async function main() {
assertDevOnlyDeployAndCatalog(deploy, catalog);
const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target) : null;
const serviceInventory = publishReport?.artifactPublish?.serviceInventory ?? serviceInventoryFromServices(
await resolveDevArtifactServices(repoRoot, SERVICE_IDS, SERVICE_IDS)
);
const services = refreshDocuments({
deploy,
catalog,
workloads,
target,
publishRecords,
provenancePath: args.publishReportPath ?? defaultPublishReportPath
serviceInventory,
provenancePath: args.publishReportPath ?? defaultPublishReportPath,
registryPrefix: publishReport?.artifactPublish?.registryPrefix ?? defaultRegistryPrefix
});
if (args.write) {
@@ -293,10 +343,12 @@ async function main() {
wrote: args.write ? [deployPath, catalogPath, workloadsPath] : [],
ciPublished: Boolean(publishRecords),
registryVerified: Boolean(publishRecords),
publishedCount: publishRecords?.size ?? 0,
notPublishedCount: publishRecords ? 0 : services.length,
publishedCount: publishRecords?.records.size ?? 0,
requiredServiceCount: serviceInventory.requiredServiceCount,
disabledServiceCount: serviceInventory.disabledServiceCount,
notPublishedCount: services.filter((service) => service.digest === "not_published").length,
digestPolicy: publishRecords
? "catalog digests copied only from a published DEV artifact report"
? "catalog digests copied only for required services from a published DEV artifact report; disabled services stay not_published"
: "all catalog digests remain not_published",
services
}, null, 2));
+157
View File
@@ -0,0 +1,157 @@
import { constants as fsConstants } from "node:fs";
import { access } from "node:fs/promises";
import path from "node:path";
export const BUILDABLE_IMPLEMENTATION_STATES = Object.freeze([
"repo-entrypoint",
"static-web-wrapper",
"repo-bundle"
]);
const buildableImplementationStates = new Set(BUILDABLE_IMPLEMENTATION_STATES);
async function pathExists(repoRoot, relativePath) {
try {
await access(path.join(repoRoot, relativePath), fsConstants.R_OK);
return true;
} catch {
return false;
}
}
export function publishEnabledForService(service) {
return service.sourceState !== "intentionally-disabled" &&
buildableImplementationStates.has(service.implementationState);
}
export function notPublishedReasonForService(service) {
if (publishEnabledForService(service)) {
return "publish_not_run";
}
if (service.sourceState === "intentionally-disabled") {
return "disabled_intentionally";
}
if (service.implementationState === "missing-runtime-entrypoint") {
return "disabled_missing_runtime_entrypoint";
}
if (service.implementationState === "library-only") {
return "disabled_missing_executable_entrypoint";
}
return `disabled_${service.implementationState}`;
}
export function servicePublishPolicy(service) {
const publishEnabled = publishEnabledForService(service);
return {
publishEnabled,
artifactRequired: publishEnabled,
artifactScope: publishEnabled ? "required" : "disabled",
notPublishedReason: publishEnabled ? "publish_not_run" : notPublishedReasonForService(service)
};
}
export function withServicePublishPolicy(service) {
return {
...service,
...servicePublishPolicy(service)
};
}
function serviceSourceState(catalog, serviceId) {
return catalog?.services?.find((service) => service.serviceId === serviceId)?.sourceState ?? "source-present";
}
export async function resolveDevArtifactService(repoRoot, serviceId, catalog = null) {
const sourceState = serviceSourceState(catalog, serviceId);
const cmdPath = `cmd/${serviceId}/main.mjs`;
if (await pathExists(repoRoot, cmdPath)) {
return withServicePublishPolicy({
serviceId,
runtimeKind: "node-command",
entrypoint: cmdPath,
implementationState: "repo-entrypoint",
sourceState
});
}
if (serviceId === "hwlab-cloud-web") {
return withServicePublishPolicy({
serviceId,
runtimeKind: "cloud-web",
entrypoint: "web/hwlab-cloud-web/index.html",
implementationState: "static-web-wrapper",
sourceState
});
}
if (serviceId === "hwlab-cli") {
const hasBin = await pathExists(repoRoot, "tools/hwlab-cli/bin/hwlab-cli.mjs");
return withServicePublishPolicy({
serviceId,
runtimeKind: "cli",
entrypoint: hasBin ? "tools/hwlab-cli/bin/hwlab-cli.mjs" : "tools/hwlab-cli/lib/cli.mjs",
implementationState: hasBin ? "repo-entrypoint" : "library-only",
sourceState
});
}
if (serviceId === "hwlab-agent-skills") {
return withServicePublishPolicy({
serviceId,
runtimeKind: "skills-bundle",
entrypoint: "skills/hwlab-agent-runtime/SKILL.md",
implementationState: "repo-bundle",
sourceState
});
}
return withServicePublishPolicy({
serviceId,
runtimeKind: "health-placeholder",
entrypoint: null,
implementationState: "missing-runtime-entrypoint",
sourceState
});
}
export async function resolveDevArtifactServices(repoRoot, serviceIds, frozenServiceIds, catalog = null) {
const unknown = serviceIds.filter((serviceId) => !frozenServiceIds.includes(serviceId));
if (unknown.length > 0) {
throw new Error(`unknown service IDs: ${unknown.join(", ")}`);
}
const services = [];
for (const serviceId of serviceIds) {
services.push(await resolveDevArtifactService(repoRoot, serviceId, catalog));
}
return services;
}
export function serviceInventoryFromServices(services) {
const requiredServiceIds = services
.filter((service) => service.artifactRequired)
.map((service) => service.serviceId);
const disabledServiceIds = services
.filter((service) => !service.artifactRequired)
.map((service) => service.serviceId);
return {
version: "v2",
serviceCount: services.length,
requiredServiceCount: requiredServiceIds.length,
disabledServiceCount: disabledServiceIds.length,
requiredServiceIds,
disabledServiceIds,
services: services.map((service) => ({
serviceId: service.serviceId,
publishEnabled: service.publishEnabled,
artifactRequired: service.artifactRequired,
artifactScope: service.artifactScope,
runtimeKind: service.runtimeKind,
implementationState: service.implementationState,
sourceState: service.sourceState,
entrypoint: service.entrypoint,
disabledReason: service.artifactRequired ? null : service.notPublishedReason
}))
};
}
+10 -6
View File
@@ -170,6 +170,7 @@ function buildServicePlan(services) {
}
function buildArtifactPlan(deploy, catalog, sourceCommitId, artifactEvidence) {
const requiredServices = (catalog?.services ?? []).filter((service) => service.artifactRequired !== false);
const serviceCommitIds = uniqueStrings([
...(catalog?.services ?? []).map((service) => service.commitId),
...(artifactEvidence ?? []).map((service) => service.catalogCommitId)
@@ -188,8 +189,10 @@ function buildArtifactPlan(deploy, catalog, sourceCommitId, artifactEvidence) {
published: catalog?.publish?.ciPublished === true,
registryVerified: catalog?.publish?.registryVerified === true,
imageCount: artifactEvidence.length,
unpublishedServices: (catalog?.services ?? [])
.filter((service) => service.publishState === "skeleton-only" || !service.digest?.startsWith("sha256:"))
requiredServiceCount: requiredServices.length,
disabledServiceCount: (catalog?.services ?? []).filter((service) => service.artifactRequired === false).length,
unpublishedServices: requiredServices
.filter((service) => service.publishState !== "published" || !service.digest?.startsWith("sha256:"))
.map((service) => service.serviceId)
};
}
@@ -359,18 +362,19 @@ function validateDeployAndCatalog(deploy, catalog, sourceCommitId, blockers) {
});
}
const requiredServices = (catalog.services ?? []).filter((service) => service.artifactRequired !== false);
const disabledServices = (catalog.services ?? []).filter((service) => service.artifactRequired === false);
const hasPublishedCatalog =
catalog.publish?.ciPublished === true &&
catalog.publish?.registryVerified === true &&
catalog.services?.every(
(service) => service.digest?.startsWith("sha256:") && service.publishState !== "skeleton-only"
);
requiredServices.every((service) => service.digest?.startsWith("sha256:") && service.publishState === "published") &&
disabledServices.every((service) => service.digest === "not_published" && service.publishState === "skeleton-only");
if (!hasPublishedCatalog) {
addBlocker(
blockers,
"observability_blocker",
"artifact-publish",
"DEV artifact catalog has no CI publish, registry verification, or registry digests"
"DEV artifact catalog has no CI publish, registry verification, or required-service registry digests"
);
}
return artifactEvidence;
+48 -20
View File
@@ -198,7 +198,11 @@ function addBlocker(blockers, blocker) {
function imageToManifestUrl(image) {
const match = image.match(/^ghcr\.io\/pikastech\/([^:@]+):([^:@]+)$/);
return match ? `https://ghcr.io/v2/pikastech/${match[1]}/manifests/${match[2]}` : null;
if (match) return `https://ghcr.io/v2/pikastech/${match[1]}/manifests/${match[2]}`;
const localMatch = image.match(/^(127\.0\.0\.1:5000|localhost:5000)\/hwlab\/([^:@]+):([^:@]+)$/);
if (localMatch) return `http://${localMatch[1]}/v2/hwlab/${localMatch[2]}/manifests/${localMatch[3]}`;
return null;
}
function matchesTargetCommit(value, targetCommit, targetShortCommit) {
@@ -225,21 +229,31 @@ function catalogDigestCounts(catalog) {
return counts;
}
function requiredCatalogServices(catalog) {
return (catalog.services ?? []).filter((service) => service.artifactRequired !== false);
}
function disabledCatalogServices(catalog) {
return (catalog.services ?? []).filter((service) => service.artifactRequired === false);
}
function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef }) {
const serviceCommitIds = uniqueSorted((catalog.services ?? []).map((service) => service.commitId));
const digestCounts = catalogDigestCounts(catalog);
const requiredServices = requiredCatalogServices(catalog);
const disabledServices = disabledCatalogServices(catalog);
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, targetCommit, targetShortCommit);
const deployMatchesSource = matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit);
const servicesMatchSource = serviceCommitIds.length === 1 &&
matchesTargetCommit(serviceCommitIds[0], targetCommit, targetShortCommit);
const allDigestsPublished = digestCounts.sha256 === SERVICE_IDS.length &&
digestCounts.notPublished === 0 &&
const allRequiredDigestsPublished = requiredServices.length > 0 &&
requiredServices.every((service) => digestPattern.test(service.digest) && service.publishState === "published") &&
disabledServices.every((service) => service.digest === "not_published" && service.publishState === "skeleton-only") &&
digestCounts.invalid === 0;
const publishVerified = catalog.publish?.ciPublished === true &&
catalog.publish?.registryVerified === true &&
catalog.artifactState === "published" &&
allDigestsPublished &&
(catalog.services ?? []).every((service) => service.publishState === "published");
allRequiredDigestsPublished;
return {
source: {
@@ -260,6 +274,8 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
registryVerified: catalog.publish?.registryVerified === true,
provenance: catalog.publish?.provenance ?? "unknown",
digestCounts,
requiredServiceCount: requiredServices.length,
disabledServiceCount: disabledServices.length,
matchesSource: catalogMatchesSource
},
services: (catalog.services ?? []).map((service) => ({
@@ -269,7 +285,9 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
image: service.image,
imageTag: service.imageTag,
digest: service.digest,
publishState: service.publishState
publishState: service.publishState,
artifactRequired: service.artifactRequired !== false,
notPublishedReason: service.notPublishedReason ?? null
})),
serviceCommitIds,
matchesSource: deployMatchesSource && catalogMatchesSource && servicesMatchSource,
@@ -478,17 +496,26 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
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 requiredServices = services.filter((service) => service.artifactRequired !== false);
const disabledServices = services.filter((service) => service.artifactRequired === false);
const allServicesAccountedFor = services.length === SERVICE_IDS.length &&
[...services].map((service) => service.serviceId).sort().join("\n") === [...SERVICE_IDS].sort().join("\n");
const allRequiredServicesPublished = requiredServices.length > 0 &&
requiredServices.every((service) => service.status === "published" && /^sha256:[a-f0-9]{64}$/.test(service.digest));
const disabledServicesNotPublished = disabledServices.every(
(service) => service.digest === "not_published" && service.notPublishedReason?.startsWith("disabled_")
);
const sourceMatchesTarget = [targetCommit, targetShortCommit].includes(artifactPublish?.sourceCommitId) ||
[targetCommit, targetShortCommit].includes(artifactReport.commitId);
const publishReady = artifactPublish?.status === "published" &&
artifactPublish.publishedCount === SERVICE_IDS.length &&
allServicesPublished &&
artifactPublish.publishedCount === requiredServices.length &&
allServicesAccountedFor &&
allRequiredServicesPublished &&
disabledServicesNotPublished &&
sourceMatchesTarget;
if (publishReady) {
reporter.check("dev-artifact-publish-report", "registry", "pass", "DEV artifact publish report covers all frozen service IDs with immutable digests for the target commit.");
reporter.check("dev-artifact-publish-report", "registry", "pass", "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons.");
return;
}
@@ -501,13 +528,13 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
"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"}; ${sourceSummary}.`
`DEV artifact publish report status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services 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 and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests."
summary: `reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the current origin/main commit and record immutable registry digests; keep disabled services marked not_published with reasons."
});
}
@@ -762,7 +789,8 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) {
});
}
const registryEvidence = await Promise.all(catalog.services.map(async (service) => {
const registryServices = requiredCatalogServices(catalog);
const registryEvidence = await Promise.all(registryServices.map(async (service) => {
const url = imageToManifestUrl(service.image);
if (!url) {
return { serviceId: service.serviceId, ok: false, image: service.image, error: "unsupported image format" };
@@ -778,18 +806,18 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) {
}));
const registryOk = registryEvidence.every((probe) => probe.ok);
reporter.check(
"ghcr-anonymous-manifest-read",
"registry-manifest-read",
"registry",
registryOk ? "pass" : "blocked",
registryOk ? "All catalog images were visible through anonymous GHCR manifest HEAD probes." : "One or more catalog images could not be verified through anonymous GHCR manifest HEAD probes.",
registryOk ? "All required catalog images were visible through registry manifest HEAD probes." : "One or more required catalog images could not be verified through registry manifest HEAD probes.",
registryEvidence
);
if (!registryOk) {
reporter.block({
type: "runtime_blocker",
scope: "ghcr",
summary: "This preflight could not verify GHCR manifests for the DEV catalog images without reading credentials.",
nextTask: "Publish public DEV images or provide a non-secret registry evidence artifact with immutable digests for each HWLAB service."
scope: "registry-manifest-read",
summary: "This preflight could not verify registry manifests for the required DEV catalog images without reading credentials.",
nextTask: "Publish required DEV images or provide a non-secret registry evidence artifact with immutable digests for each required HWLAB service."
});
}
}
+35 -2
View File
@@ -18,6 +18,7 @@ const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "product
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 artifactScopes = new Set(["required", "disabled"]);
const requiredForbiddenItems = [
"prod-deploy",
"prod-profile-enabled",
@@ -111,6 +112,16 @@ function assertDevOnlyCatalog(catalog) {
return catalog.artifactState;
}
function assertServiceInventory(catalog) {
assert.ok(catalog.serviceInventory && typeof catalog.serviceInventory === "object", "catalog serviceInventory must be an object");
assert.equal(catalog.serviceInventory.version, "v2", "catalog serviceInventory.version");
assert.deepEqual(catalog.serviceInventory.requiredServiceIds, SERVICE_IDS, "catalog required service IDs");
assert.deepEqual(catalog.serviceInventory.disabledServiceIds, [], "catalog disabled service IDs");
assert.equal(catalog.serviceInventory.serviceCount, SERVICE_IDS.length, "catalog serviceInventory.serviceCount");
assert.equal(catalog.serviceInventory.requiredServiceCount, catalog.serviceInventory.requiredServiceIds.length, "catalog requiredServiceCount");
assert.equal(catalog.serviceInventory.disabledServiceCount, catalog.serviceInventory.disabledServiceIds.length, "catalog disabledServiceCount");
}
function assertDeployManifest(deployManifest, catalog) {
assert.equal(deployManifest.manifestVersion, "v1", "deploy manifestVersion");
assert.equal(deployManifest.environment, ENVIRONMENT_DEV, "deploy environment");
@@ -136,6 +147,8 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) {
assertUnique("deploy service ids", deployServiceIds);
const deployByServiceId = new Map(deployManifest.services.map((service) => [service.serviceId, service]));
const requiredServiceIds = new Set(catalog.serviceInventory.requiredServiceIds);
const disabledServiceIds = new Set(catalog.serviceInventory.disabledServiceIds);
for (const service of catalog.services) {
const context = `service ${service.serviceId}`;
const deployService = deployByServiceId.get(service.serviceId);
@@ -161,12 +174,31 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) {
if (service.sourceState === "intentionally-disabled") {
assert.equal(deployService.replicas, 0, `${context} intentionally-disabled services must have zero deploy replicas`);
}
assert.ok(artifactScopes.has(service.artifactScope), `${context} artifactScope`);
assert.equal(typeof service.publishEnabled, "boolean", `${context} publishEnabled`);
assert.equal(typeof service.artifactRequired, "boolean", `${context} artifactRequired`);
assert.equal(service.artifactRequired, requiredServiceIds.has(service.serviceId), `${context} artifactRequired must match serviceInventory`);
if (requiredServiceIds.has(service.serviceId)) {
assert.equal(service.artifactScope, "required", `${context} artifactScope`);
} else {
assert.ok(disabledServiceIds.has(service.serviceId), `${context} disabled service must be in serviceInventory`);
assert.equal(service.artifactScope, "disabled", `${context} artifactScope`);
assert.equal(service.publishEnabled, false, `${context} publishEnabled`);
assert.equal(service.notPublishedReason.startsWith("disabled_"), true, `${context} notPublishedReason must explain disabled service`);
}
if (catalogMode === "contract-skeleton") {
assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
assert.equal(service.digest, "not_published", `${context} digest`);
} else {
assert.equal(service.publishState, "published", `${context} publishState`);
assert.match(service.digest, digestPattern, `${context} digest`);
if (service.artifactRequired) {
assert.equal(service.publishState, "published", `${context} publishState`);
assert.match(service.digest, digestPattern, `${context} digest`);
} else {
assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
assert.equal(service.digest, "not_published", `${context} digest`);
}
}
}
}
@@ -199,6 +231,7 @@ const ci = await readJSON(ciPath);
const healthContract = await readJSON(healthContractPath);
const catalogMode = assertDevOnlyCatalog(catalog);
assertServiceInventory(catalog);
assertDeployManifest(deployManifest, catalog);
assertCatalogServices(catalog, deployManifest, catalogMode);
assertCIForbidden(ci, catalog);
+1 -1
View File
@@ -145,7 +145,7 @@ function assertHwlabImage(image, serviceId, label) {
);
assert.match(
image,
new RegExp(`^ghcr\\.io/pikastech/${serviceId}:[a-f0-9]{7,40}$`),
new RegExp(`^(ghcr\\.io/pikastech|127\\.0\\.0\\.1:5000/hwlab|localhost:5000/hwlab)/${serviceId}:[a-f0-9]{7,40}$`),
`${label}.image must be the HWLAB image for ${serviceId}`
);
}