From 95a618043930c541812d81eebc618066e9f0f913 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Thu, 21 May 2026 18:30:40 +0000 Subject: [PATCH] feat: add dev artifact publish v2 plan --- deploy/artifact-catalog.dev.json | 327 +++++++++++++--- deploy/deploy.json | 36 +- deploy/deploy.schema.json | 13 + deploy/k8s/base/workloads.yaml | 34 +- docs/artifact-catalog.md | 15 +- docs/dev-artifact-publish.md | 15 + docs/dev-base-image-preflight.md | 7 +- package.json | 2 +- reports/dev-gate/dev-artifacts.json | 511 +++++++++++++++++++++++--- scripts/dev-artifact-publish.mjs | 194 +++++----- scripts/refresh-artifact-catalog.mjs | 84 ++++- scripts/src/dev-artifact-services.mjs | 157 ++++++++ scripts/src/dev-deploy-apply.mjs | 16 +- scripts/src/dev-gate-preflight.mjs | 68 +++- scripts/validate-artifact-catalog.mjs | 37 +- scripts/validate-runtime-boundary.mjs | 2 +- 16 files changed, 1233 insertions(+), 285 deletions(-) create mode 100644 scripts/src/dev-artifact-services.mjs diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index dcc9bd7c..bcbb440b 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -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 + } + ] + } } diff --git a/deploy/deploy.json b/deploy/deploy.json index 115292df..deb434cc 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -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" } } ], diff --git a/deploy/deploy.schema.json b/deploy/deploy.schema.json index 43d62250..fc171f40 100644 --- a/deploy/deploy.schema.json +++ b/deploy/deploy.schema.json @@ -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 diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index d928a0ae..fe668bc3 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -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": { diff --git a/docs/artifact-catalog.md b/docs/artifact-catalog.md index 5e547f58..4cc4cb30 100644 --- a/docs/artifact-catalog.md +++ b/docs/artifact-catalog.md @@ -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/:` and the matching `deploy/deploy.json` service image. | +| `image` | Must match the DEV registry target `//:` 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` diff --git a/docs/dev-artifact-publish.md b/docs/dev-artifact-publish.md index 2c74440f..a057678c 100644 --- a/docs/dev-artifact-publish.md +++ b/docs/dev-artifact-publish.md @@ -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//main.mjs`. diff --git a/docs/dev-base-image-preflight.md b/docs/dev-base-image-preflight.md index 26a20cc3..12b4af56 100644 --- a/docs/dev-base-image-preflight.md +++ b/docs/dev-base-image-preflight.md @@ -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/:`. + names to the DEV registry target, by default + `127.0.0.1:5000/hwlab/:`. - 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:` is an artifact output, not a builder -base image. +`127.0.0.1:5000/hwlab/hwlab-cloud-api:` is an artifact output, not a +builder base image. The recommended D601 preload source is `node:20-bookworm-slim`. The recommended HWLAB-specific local tag is: diff --git a/package.json b/package.json index 12658531..441cdbe4 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/reports/dev-gate/dev-artifacts.json b/reports/dev-gate/dev-artifacts.json index 0843a938..c0035f99 100644 --- a/reports/dev-gate/dev-artifacts.json +++ b/reports/dev-gate/dev-artifacts.json @@ -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": [ diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index a6d6c7d0..ad6165f8 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -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)); diff --git a/scripts/refresh-artifact-catalog.mjs b/scripts/refresh-artifact-catalog.mjs index 90e6da7a..ae263a58 100644 --- a/scripts/refresh-artifact-catalog.mjs +++ b/scripts/refresh-artifact-catalog.mjs @@ -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)); diff --git a/scripts/src/dev-artifact-services.mjs b/scripts/src/dev-artifact-services.mjs new file mode 100644 index 00000000..0ec39039 --- /dev/null +++ b/scripts/src/dev-artifact-services.mjs @@ -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 + })) + }; +} diff --git a/scripts/src/dev-deploy-apply.mjs b/scripts/src/dev-deploy-apply.mjs index 9c419ba1..f3c0fa26 100755 --- a/scripts/src/dev-deploy-apply.mjs +++ b/scripts/src/dev-deploy-apply.mjs @@ -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; diff --git a/scripts/src/dev-gate-preflight.mjs b/scripts/src/dev-gate-preflight.mjs index ee5c768a..2359d639 100644 --- a/scripts/src/dev-gate-preflight.mjs +++ b/scripts/src/dev-gate-preflight.mjs @@ -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." }); } } diff --git a/scripts/validate-artifact-catalog.mjs b/scripts/validate-artifact-catalog.mjs index 8ed121c9..8867593c 100644 --- a/scripts/validate-artifact-catalog.mjs +++ b/scripts/validate-artifact-catalog.mjs @@ -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); diff --git a/scripts/validate-runtime-boundary.mjs b/scripts/validate-runtime-boundary.mjs index 20ad0298..f588cc1b 100644 --- a/scripts/validate-runtime-boundary.mjs +++ b/scripts/validate-runtime-boundary.mjs @@ -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}` ); }