fix: align dev artifact catalog identity
This commit is contained in:
@@ -5,22 +5,30 @@
|
||||
"profile": "dev",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"commitId": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"artifactState": "contract-skeleton",
|
||||
"publish": {
|
||||
"ciPublished": false,
|
||||
"registryVerified": false,
|
||||
"provenance": "not_available_in_mvp_skeleton",
|
||||
"note": "This catalog is a static DEV contract only. It is not evidence that images were built, pushed, or deployed."
|
||||
"provenance": "not_available_until_publish",
|
||||
"note": "Artifact identity was refreshed to this source commit, but no publish report proved registry digests."
|
||||
},
|
||||
"healthContract": {
|
||||
"method": "GET",
|
||||
"path": "/health/live",
|
||||
"responseFormat": "json",
|
||||
"requiredFields": ["serviceId", "environment", "status"]
|
||||
"requiredFields": [
|
||||
"serviceId",
|
||||
"environment",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
"allowedProfiles": ["dev"],
|
||||
"forbiddenProfiles": ["prod"],
|
||||
"allowedProfiles": [
|
||||
"dev"
|
||||
],
|
||||
"forbiddenProfiles": [
|
||||
"prod"
|
||||
],
|
||||
"forbiddenItems": [
|
||||
{
|
||||
"id": "prod-deploy",
|
||||
@@ -58,9 +66,9 @@
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -69,9 +77,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -80,9 +88,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -91,9 +99,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -102,9 +110,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -113,9 +121,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -124,9 +132,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -135,9 +143,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -146,9 +154,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -157,9 +165,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -168,9 +176,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -179,9 +187,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
@@ -190,9 +198,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"commitId": "24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:24eb3bf",
|
||||
"imageTag": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only",
|
||||
"profile": "dev",
|
||||
|
||||
+18
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifestVersion": "v1",
|
||||
"environment": "dev",
|
||||
"commitId": "24eb3bf",
|
||||
"commitId": "1a2efd4",
|
||||
"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:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"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": "24eb3bf",
|
||||
"HWLAB_IMAGE": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf",
|
||||
"HWLAB_IMAGE_TAG": "24eb3bf",
|
||||
"HWLAB_COMMIT_ID": "1a2efd4",
|
||||
"HWLAB_IMAGE": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"HWLAB_IMAGE_TAG": "1a2efd4",
|
||||
"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:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -53,7 +53,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -65,7 +65,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -77,7 +77,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -89,7 +89,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -113,7 +113,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -137,7 +137,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -150,7 +150,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -162,7 +162,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -173,13 +173,13 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {
|
||||
"HWLAB_SKILLS_COMMIT_ID": "24eb3bf"
|
||||
"HWLAB_SKILLS_COMMIT_ID": "1a2efd4"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cloud-api",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -49,15 +49,15 @@
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_COMMIT_ID",
|
||||
"value": "24eb3bf"
|
||||
"value": "1a2efd4"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_IMAGE",
|
||||
"value": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf"
|
||||
"value": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_IMAGE_TAG",
|
||||
"value": "24eb3bf"
|
||||
"value": "1a2efd4"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN",
|
||||
@@ -129,7 +129,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cloud-web",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -189,7 +189,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-mgr",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -245,7 +245,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-worker",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_AGENT_SESSION_MODE",
|
||||
@@ -288,7 +288,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-gateway",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -348,7 +348,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-gateway-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -408,7 +408,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-box-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -468,7 +468,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-patch-panel",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -522,7 +522,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-router",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -582,7 +582,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-tunnel-client",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -650,7 +650,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-edge-proxy",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -711,7 +711,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cli",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_CLI_ENDPOINT",
|
||||
@@ -753,7 +753,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-skills",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -763,7 +763,7 @@
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_SKILLS_COMMIT_ID",
|
||||
"value": "24eb3bf"
|
||||
"value": "1a2efd4"
|
||||
}
|
||||
],
|
||||
"readinessProbe": {
|
||||
|
||||
+49
-12
@@ -4,11 +4,19 @@ The artifact catalog is the DEV-only MVP contract for interpreting deployment
|
||||
artifacts. It pins which HWLAB service image, commit, profile, namespace, and
|
||||
health path a static deploy manifest is claiming for acceptance review.
|
||||
|
||||
This file is not a CI publish record. It does not prove that an image exists in a
|
||||
registry, that a digest was produced, or that a deployment happened. Real CI
|
||||
publish evidence must come from the future publishing workflow and registry
|
||||
metadata. Until that exists, the catalog can only validate that the local
|
||||
skeleton files agree with each other and do not drift into PROD.
|
||||
This file is not automatically a CI publish record. It proves only the state it
|
||||
declares:
|
||||
|
||||
- `artifactState: "contract-skeleton"` means the source/artifact commit and image
|
||||
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.
|
||||
|
||||
Do not infer registry existence, digest values, or deployment from commit tags.
|
||||
Real publish evidence must come from the publishing workflow and registry
|
||||
metadata, then be copied into the catalog by the refresh command below.
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -28,6 +36,7 @@ skeleton files agree with each other and do not drift into PROD.
|
||||
| `deploy/artifact-catalog.dev.json` | Machine-readable DEV artifact catalog sample and static source of truth for the skeleton. |
|
||||
| `deploy/deploy.json` | DEV deploy manifest skeleton that the catalog explains. |
|
||||
| `CI.json` | Lightweight command and forbidden-action skeleton. |
|
||||
| `scripts/refresh-artifact-catalog.mjs` | Refreshes source/artifact commit tags and records either blocked `not_published` state or proven publish digests. |
|
||||
| `scripts/validate-artifact-catalog.mjs` | Local static validator for the catalog, deploy manifest, and CI forbidden list. |
|
||||
| `scripts/preflight-dev-base-image.mjs` | Local DEV builder base-image preflight for future artifact publish. |
|
||||
| `docs/dev-base-image-preflight.md` | Operator contract for interpreting preflight readiness and blockers. |
|
||||
@@ -46,12 +55,38 @@ rules:
|
||||
| `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` until real CI publish evidence exists. |
|
||||
| `digest` | Must be `not_published` in the skeleton catalog; CI must replace this with a registry digest only after publishing. |
|
||||
| `publishState` | Must be `skeleton-only` in a blocked skeleton catalog, or `published` only when the catalog was refreshed from a successful publish report. |
|
||||
| `digest` | Must be `not_published` in the skeleton catalog; it may be a registry `sha256:<64 hex>` digest only after publish evidence exists. |
|
||||
|
||||
The current skeleton commit and tag values intentionally mirror
|
||||
`deploy/deploy.json`. They are traceability placeholders for static acceptance
|
||||
work, not a statement that GHCR already contains those images.
|
||||
The catalog `commitId` is the artifact source commit, not proof that a registry
|
||||
artifact exists. It must match `deploy/deploy.json`, every service `commitId`,
|
||||
and the image tag. If it does not match the selected source ref, the DEV gate
|
||||
preflight reports `artifact-source-commit` as a blocker.
|
||||
|
||||
## Refresh Workflow
|
||||
|
||||
When the source commit changes and no publish report proves digests, refresh the
|
||||
catalog and deploy manifests in blocked mode:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked
|
||||
```
|
||||
|
||||
This updates `deploy/deploy.json`, `deploy/artifact-catalog.dev.json`, and
|
||||
`deploy/k8s/base/workloads.yaml` to the selected commit tag. It keeps
|
||||
`artifactState: "contract-skeleton"`, `ciPublished=false`,
|
||||
`registryVerified=false`, and every service digest as `not_published`.
|
||||
|
||||
After a successful DEV artifact publish, record digests only from the publish
|
||||
report:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Relationship To `deploy/deploy.json`
|
||||
|
||||
@@ -91,10 +126,12 @@ Run the local static validator:
|
||||
|
||||
```sh
|
||||
node --check scripts/validate-artifact-catalog.mjs
|
||||
node --check scripts/refresh-artifact-catalog.mjs
|
||||
node --check scripts/preflight-dev-base-image.mjs
|
||||
node --check scripts/src/dev-base-image-preflight.mjs
|
||||
node scripts/validate-artifact-catalog.mjs
|
||||
```
|
||||
|
||||
The validator reads only local repository files. It does not contact a registry,
|
||||
read secrets, build images, push images, deploy to DEV, or deploy to PROD.
|
||||
The validator and refresh command read only local repository files and optional
|
||||
publish reports. They do not contact a registry, read secrets, build images,
|
||||
push images, deploy to DEV, or deploy to PROD.
|
||||
|
||||
@@ -30,6 +30,7 @@ Static check:
|
||||
node --check scripts/preflight-dev-base-image.mjs
|
||||
node --check scripts/src/dev-base-image-preflight.mjs
|
||||
node --check scripts/dev-artifact-publish.mjs
|
||||
node --check scripts/refresh-artifact-catalog.mjs
|
||||
```
|
||||
|
||||
Base image preflight:
|
||||
@@ -79,8 +80,24 @@ Each service record contains:
|
||||
- `entrypoint`
|
||||
|
||||
`digest` is only set to a registry digest after `docker push` succeeds and the
|
||||
push output contains a digest. The script records blockers instead of claiming a
|
||||
publish when build, push, base image, registry, contract, or safety checks fail.
|
||||
push output contains a `sha256:<64 hex>` digest. If the push succeeds but no
|
||||
digest is observable, the service remains `published_unverified_digest`,
|
||||
`digest` stays `not_published`, and the report carries a blocker. The script
|
||||
records blockers instead of claiming a publish when build, push, digest
|
||||
observation, base image, registry, contract, or safety checks fail.
|
||||
|
||||
After a fully successful publish, update the catalog from the report:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json
|
||||
```
|
||||
|
||||
If publish remains blocked, refresh only commit/tag identity and keep digests
|
||||
blocked:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked
|
||||
```
|
||||
|
||||
The report also includes `artifactPublish.baseImagePreflight` with the image
|
||||
source, local tag, local image ID, `publishUsable` gate, blockers, and next
|
||||
|
||||
@@ -15,6 +15,7 @@ Run from the repository root:
|
||||
```sh
|
||||
node --check scripts/dev-gate-preflight.mjs
|
||||
node --check scripts/src/dev-gate-preflight.mjs
|
||||
node --check scripts/refresh-artifact-catalog.mjs
|
||||
node scripts/dev-gate-preflight.mjs
|
||||
```
|
||||
|
||||
@@ -39,8 +40,12 @@ The preflight checks:
|
||||
|
||||
- `deploy/deploy.json` and `deploy/artifact-catalog.dev.json` are internally
|
||||
consistent and DEV-only.
|
||||
- The deploy manifest and catalog target the selected `origin/main` commit.
|
||||
- The artifact catalog has real publish and registry digest evidence.
|
||||
- The deploy manifest, artifact catalog, catalog service commits, and image tags
|
||||
target the selected `origin/main` source commit.
|
||||
- The artifact catalog clearly separates source commit, artifact commit,
|
||||
`ciPublished`, `registryVerified`, and per-service digest state.
|
||||
- The artifact catalog has real publish and registry digest evidence, or stays
|
||||
blocked with `not_published` digests.
|
||||
- `reports/dev-gate/dev-artifacts.json`, when present, proves all frozen DEV
|
||||
service artifacts were published for the selected `origin/main` commit.
|
||||
- `deploy/k8s/base` and `deploy/k8s/dev` parse and remain scoped to
|
||||
@@ -75,3 +80,18 @@ The DB gate has two common blocked scopes:
|
||||
- `cloud-api-db-health-gate`: runtime env presence is missing. The next task is
|
||||
to configure the DEV Secret/env names, not to print or commit any secret
|
||||
value.
|
||||
|
||||
For catalog commit mismatch, the preflight emits an
|
||||
`artifactIdentity.refreshCommands.blocked` command. Use it when the source
|
||||
commit changed but publish is still blocked:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked
|
||||
```
|
||||
|
||||
Only use the published refresh path after `reports/dev-gate/dev-artifacts.json`
|
||||
proves every frozen service has a registry digest for that same source commit:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json
|
||||
```
|
||||
|
||||
+2
-1
@@ -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/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/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-m3-cardinality.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/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && 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/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/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-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/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && 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",
|
||||
"dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty",
|
||||
@@ -16,6 +16,7 @@
|
||||
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
|
||||
"web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
|
||||
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs",
|
||||
"artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
|
||||
"dev-artifact:preflight": "node scripts/dev-artifact-publish.mjs --preflight",
|
||||
"dev-artifact:publish": "node scripts/dev-artifact-publish.mjs --publish",
|
||||
"d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs",
|
||||
|
||||
@@ -15,14 +15,17 @@
|
||||
"pikasTech/HWLAB#31",
|
||||
"pikasTech/HWLAB#33",
|
||||
"pikasTech/HWLAB#39",
|
||||
"pikasTech/HWLAB#49"
|
||||
"pikasTech/HWLAB#49",
|
||||
"pikasTech/HWLAB#33",
|
||||
"pikasTech/HWLAB#35",
|
||||
"pikasTech/HWLAB#48"
|
||||
],
|
||||
"target": {
|
||||
"ref": "origin/main",
|
||||
"commitId": "36bf2c42edd3011deb0889a25aba077a2a69f872",
|
||||
"shortCommitId": "36bf2c4"
|
||||
"commitId": "1a2efd4915b2d7a6f640a22cc22087e7cae4c654",
|
||||
"shortCommitId": "1a2efd4"
|
||||
},
|
||||
"generatedAt": "2026-05-21T17:57:44.613Z",
|
||||
"generatedAt": "2026-05-21T18:00:08.924Z",
|
||||
"mode": "read-only",
|
||||
"devOnly": true,
|
||||
"prodDisabled": true,
|
||||
@@ -38,10 +41,165 @@
|
||||
"validationCommands": [
|
||||
"node --check scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/src/dev-gate-preflight.mjs",
|
||||
"node --check scripts/refresh-artifact-catalog.mjs",
|
||||
"node scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
"artifactIdentity": {
|
||||
"source": {
|
||||
"ref": "origin/main",
|
||||
"commitId": "1a2efd4915b2d7a6f640a22cc22087e7cae4c654",
|
||||
"shortCommitId": "1a2efd4"
|
||||
},
|
||||
"deployManifest": {
|
||||
"path": "deploy/deploy.json",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true
|
||||
},
|
||||
"artifactCatalog": {
|
||||
"path": "deploy/artifact-catalog.dev.json",
|
||||
"commitId": "1a2efd4",
|
||||
"artifactState": "contract-skeleton",
|
||||
"ciPublished": false,
|
||||
"registryVerified": false,
|
||||
"provenance": "not_available_until_publish",
|
||||
"digestCounts": {
|
||||
"sha256": 0,
|
||||
"notPublished": 13,
|
||||
"invalid": 0
|
||||
},
|
||||
"matchesSource": true
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
}
|
||||
],
|
||||
"serviceCommitIds": [
|
||||
"1a2efd4"
|
||||
],
|
||||
"matchesSource": true,
|
||||
"publishVerified": false,
|
||||
"refreshCommands": {
|
||||
"blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
|
||||
"published": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json"
|
||||
}
|
||||
},
|
||||
"conclusion": "blocked",
|
||||
"checks": [
|
||||
{
|
||||
@@ -54,16 +212,186 @@
|
||||
{
|
||||
"id": "target-commit-pinning",
|
||||
"category": "contract",
|
||||
"status": "blocked",
|
||||
"summary": "deploy/catalog commitId 24eb3bf does not match origin/main 36bf2c4.",
|
||||
"evidence": []
|
||||
"status": "pass",
|
||||
"summary": "Source origin/main 1a2efd4 matches deploy/catalog artifact commit 1a2efd4.",
|
||||
"evidence": [
|
||||
{
|
||||
"source": {
|
||||
"ref": "origin/main",
|
||||
"commitId": "1a2efd4915b2d7a6f640a22cc22087e7cae4c654",
|
||||
"shortCommitId": "1a2efd4"
|
||||
},
|
||||
"deployManifest": {
|
||||
"path": "deploy/deploy.json",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true
|
||||
},
|
||||
"artifactCatalog": {
|
||||
"path": "deploy/artifact-catalog.dev.json",
|
||||
"commitId": "1a2efd4",
|
||||
"artifactState": "contract-skeleton",
|
||||
"ciPublished": false,
|
||||
"registryVerified": false,
|
||||
"provenance": "not_available_until_publish",
|
||||
"digestCounts": {
|
||||
"sha256": 0,
|
||||
"notPublished": 13,
|
||||
"invalid": 0
|
||||
},
|
||||
"matchesSource": true
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"commitId": "1a2efd4",
|
||||
"matchesSource": true,
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
|
||||
"imageTag": "1a2efd4",
|
||||
"digest": "not_published",
|
||||
"publishState": "skeleton-only"
|
||||
}
|
||||
],
|
||||
"serviceCommitIds": [
|
||||
"1a2efd4"
|
||||
],
|
||||
"matchesSource": true,
|
||||
"publishVerified": false,
|
||||
"refreshCommands": {
|
||||
"blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
|
||||
"published": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact-catalog-publish-state",
|
||||
"category": "registry",
|
||||
"status": "blocked",
|
||||
"summary": "Artifact catalog is still a skeleton and does not carry registry digests.",
|
||||
"evidence": []
|
||||
"summary": "Artifact catalog publish state is contract-skeleton; ciPublished=false, registryVerified=false, digests sha256=0, not_published=13, invalid=0.",
|
||||
"evidence": [
|
||||
{
|
||||
"path": "deploy/artifact-catalog.dev.json",
|
||||
"commitId": "1a2efd4",
|
||||
"artifactState": "contract-skeleton",
|
||||
"ciPublished": false,
|
||||
"registryVerified": false,
|
||||
"provenance": "not_available_until_publish",
|
||||
"digestCounts": {
|
||||
"sha256": 0,
|
||||
"notPublished": 13,
|
||||
"invalid": 0
|
||||
},
|
||||
"matchesSource": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "k8s-manifest-static",
|
||||
@@ -299,19 +627,17 @@
|
||||
"evidence": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-cloud-api/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-cloud-api/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
"body": ""
|
||||
"error": "This operation was aborted"
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-cloud-web/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-cloud-web/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -319,9 +645,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-agent-mgr/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-agent-mgr/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -329,9 +655,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-agent-worker/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-agent-worker/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -339,9 +665,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-gateway/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-gateway/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -349,9 +675,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-gateway-simu/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-gateway-simu/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -359,9 +685,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-box-simu/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-box-simu/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -369,9 +695,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-patch-panel/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-patch-panel/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -379,9 +705,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-router/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-router/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -389,9 +715,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-tunnel-client/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-tunnel-client/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -399,9 +725,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-edge-proxy/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-edge-proxy/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -409,9 +735,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-cli/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-cli/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -419,9 +745,9 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:24eb3bf",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4",
|
||||
"ok": false,
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-agent-skills/manifests/24eb3bf",
|
||||
"url": "https://ghcr.io/v2/pikastech/hwlab-agent-skills/manifests/1a2efd4",
|
||||
"method": "HEAD",
|
||||
"status": 401,
|
||||
"statusText": "Unauthorized",
|
||||
@@ -431,19 +757,12 @@
|
||||
}
|
||||
],
|
||||
"blockers": [
|
||||
{
|
||||
"status": "open",
|
||||
"type": "contract_blocker",
|
||||
"scope": "deploy-target",
|
||||
"summary": "deploy/deploy.json and deploy/artifact-catalog.dev.json still target 24eb3bf, not origin/main 36bf2c4.",
|
||||
"nextTask": "Publish or select a DEV artifact set for the current origin/main commit and update deploy/deploy.json plus deploy/artifact-catalog.dev.json to that immutable commit/tag."
|
||||
},
|
||||
{
|
||||
"status": "open",
|
||||
"type": "runtime_blocker",
|
||||
"scope": "artifact-catalog",
|
||||
"summary": "deploy/artifact-catalog.dev.json has ciPublished=false, registryVerified=false, and not_published digests.",
|
||||
"nextTask": "Run the DEV image publishing workflow and record immutable GHCR digests before real deployment."
|
||||
"summary": "deploy/artifact-catalog.dev.json does not prove published artifacts for origin/main 1a2efd4; ciPublished=false, registryVerified=false, not_published=13.",
|
||||
"nextTask": "Run the DEV artifact publish workflow, then record only real sha256 digests with `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json`; if publish is still blocked, keep not_published via `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked`."
|
||||
},
|
||||
{
|
||||
"status": "open",
|
||||
@@ -456,7 +775,7 @@
|
||||
"status": "open",
|
||||
"type": "runtime_blocker",
|
||||
"scope": "dev-artifact-publish",
|
||||
"summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 36bf2c4; current status is blocked with 0/13 published.",
|
||||
"summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 1a2efd4; current status is blocked with 0/13 published.",
|
||||
"nextTask": "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests."
|
||||
},
|
||||
{
|
||||
|
||||
@@ -744,11 +744,26 @@ async function publishService(artifact) {
|
||||
}
|
||||
|
||||
const digest = inspectDigest(`${result.stdout}\n${result.stderr}`);
|
||||
if (!digest) {
|
||||
return {
|
||||
...artifact,
|
||||
status: "published_unverified_digest",
|
||||
digest: "not_published",
|
||||
blocker: blocker({
|
||||
type: "environment_blocker",
|
||||
scope: artifact.serviceId,
|
||||
summary: `docker push completed for ${artifact.image} but did not return an immutable digest`,
|
||||
next: "Verify registry digest reporting, then rerun --publish; do not update the catalog with a digest until a sha256 registry digest is observed."
|
||||
}),
|
||||
pushLogTail: tailText(`${result.stdout}\n${result.stderr}`, 1200)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...artifact,
|
||||
status: digest ? "published" : "published_unverified_digest",
|
||||
digest: digest ?? "digest_not_found",
|
||||
repositoryDigest: digest ? `${repositoryFromImageRef(artifact.image)}@${digest}` : null,
|
||||
status: "published",
|
||||
digest,
|
||||
repositoryDigest: `${repositoryFromImageRef(artifact.image)}@${digest}`,
|
||||
pushLogTail: tailText(`${result.stdout}\n${result.stderr}`, 1200)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
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 digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
targetRef: "HEAD",
|
||||
publishReportPath: null,
|
||||
blocked: false,
|
||||
write: true
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--target-ref") {
|
||||
args.targetRef = readOption(argv, ++index, arg);
|
||||
} else if (arg === "--publish-report") {
|
||||
args.publishReportPath = readOption(argv, ++index, arg);
|
||||
} else if (arg === "--blocked") {
|
||||
args.blocked = true;
|
||||
} else if (arg === "--no-write") {
|
||||
args.write = false;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
args.help = true;
|
||||
} else {
|
||||
throw new Error(`unknown argument ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!args.help) {
|
||||
assert.notEqual(args.blocked && Boolean(args.publishReportPath), true, "--blocked and --publish-report are mutually exclusive");
|
||||
assert.ok(args.blocked || args.publishReportPath, "choose --blocked or --publish-report reports/dev-gate/dev-artifacts.json");
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function readOption(argv, index, name) {
|
||||
const value = argv[index];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error(`${name} requires a value`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
"usage: node scripts/refresh-artifact-catalog.mjs --target-ref REF (--blocked|--publish-report PATH) [--no-write]",
|
||||
"",
|
||||
"Refresh DEV deploy/catalog artifact identity without faking digest evidence.",
|
||||
"",
|
||||
"examples:",
|
||||
" node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked",
|
||||
` node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report ${defaultPublishReportPath}`
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function readJson(relativePath) {
|
||||
return JSON.parse(await readFile(path.join(repoRoot, relativePath), "utf8"));
|
||||
}
|
||||
|
||||
async function writeJson(relativePath, value) {
|
||||
await writeFile(path.join(repoRoot, relativePath), `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
async function gitValue(args) {
|
||||
const result = await execFileAsync("git", args, {
|
||||
cwd: repoRoot,
|
||||
timeout: 5000,
|
||||
maxBuffer: 1024 * 1024
|
||||
});
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
async function resolveTarget(ref) {
|
||||
const commitId = await gitValue(["rev-parse", ref]);
|
||||
const shortCommitId = await gitValue(["rev-parse", "--short=7", ref]);
|
||||
assert.match(commitId, /^[a-f0-9]{40}$/, `target ref ${ref} must resolve to a full SHA`);
|
||||
return { ref, commitId, shortCommitId };
|
||||
}
|
||||
|
||||
function parseTaggedImage(image, context) {
|
||||
assert.equal(typeof image, "string", `${context} image must be a string`);
|
||||
assert.equal(image.includes("@"), false, `${context} image must not include a digest suffix`);
|
||||
assert.ok(!/prod|production/iu.test(image), `${context} image must not target prod`);
|
||||
const slashIndex = image.lastIndexOf("/");
|
||||
const colonIndex = image.lastIndexOf(":");
|
||||
assert.ok(colonIndex > slashIndex, `${context} image must be a tagged image reference`);
|
||||
return {
|
||||
repository: image.slice(0, colonIndex),
|
||||
tag: image.slice(colonIndex + 1)
|
||||
};
|
||||
}
|
||||
|
||||
function targetImage(serviceId, shortCommitId) {
|
||||
return `ghcr.io/pikastech/${serviceId}:${shortCommitId}`;
|
||||
}
|
||||
|
||||
function commitMatchesTarget(value, target) {
|
||||
return value === target.commitId || value === target.shortCommitId;
|
||||
}
|
||||
|
||||
function assertDevOnlyDeployAndCatalog(deploy, catalog) {
|
||||
assert.equal(deploy.environment, ENVIRONMENT_DEV, "deploy environment must be dev");
|
||||
assert.equal(deploy.namespace, "hwlab-dev", "deploy namespace must be hwlab-dev");
|
||||
assert.equal(deploy.endpoint, DEV_ENDPOINT, "deploy endpoint must stay frozen");
|
||||
assert.equal(deploy.profiles?.dev?.enabled, true, "deploy dev profile must be enabled");
|
||||
assert.equal(deploy.profiles?.prod?.enabled, false, "deploy prod profile must stay disabled");
|
||||
assert.deepEqual((deploy.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "deploy services must cover frozen service IDs in order");
|
||||
|
||||
assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment must be dev");
|
||||
assert.equal(catalog.profile, ENVIRONMENT_DEV, "catalog profile must be dev");
|
||||
assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace must be hwlab-dev");
|
||||
assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint must stay frozen");
|
||||
assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV], "catalog allowedProfiles must only allow dev");
|
||||
assert.deepEqual(catalog.forbiddenProfiles, ["prod"], "catalog forbiddenProfiles must forbid prod");
|
||||
assert.deepEqual((catalog.services ?? []).map((service) => service.serviceId), SERVICE_IDS, "catalog services must cover frozen service IDs in order");
|
||||
}
|
||||
|
||||
function publishRecordsFromReport(report, target) {
|
||||
const artifactPublish = report.artifactPublish;
|
||||
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");
|
||||
|
||||
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`);
|
||||
records.set(service.serviceId, {
|
||||
serviceId: service.serviceId,
|
||||
image: service.image,
|
||||
imageTag: image.tag,
|
||||
digest: service.digest,
|
||||
repositoryDigest: service.repositoryDigest ?? `${image.repository}@${service.digest}`
|
||||
});
|
||||
}
|
||||
|
||||
assert.deepEqual([...records.keys()], SERVICE_IDS, "publish report services must cover frozen service IDs in order");
|
||||
return records;
|
||||
}
|
||||
|
||||
function updateEnvObject(env, service, target, publishRecord) {
|
||||
if (!env || typeof env !== "object" || Array.isArray(env)) return;
|
||||
if (Object.hasOwn(env, "HWLAB_COMMIT_ID")) env.HWLAB_COMMIT_ID = target.shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE")) env.HWLAB_IMAGE = service.image;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE_TAG")) env.HWLAB_IMAGE_TAG = service.imageTag;
|
||||
if (Object.hasOwn(env, "HWLAB_SKILLS_COMMIT_ID")) env.HWLAB_SKILLS_COMMIT_ID = target.shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE_DIGEST")) env.HWLAB_IMAGE_DIGEST = publishRecord?.digest ?? "not_published";
|
||||
}
|
||||
|
||||
function serviceIdForWorkload(item, container) {
|
||||
return (
|
||||
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
|
||||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
|
||||
container?.name
|
||||
);
|
||||
}
|
||||
|
||||
function updateEnvList(envList, service, target, publishRecord) {
|
||||
if (!Array.isArray(envList)) return;
|
||||
for (const entry of envList) {
|
||||
if (entry.name === "HWLAB_COMMIT_ID") entry.value = target.shortCommitId;
|
||||
if (entry.name === "HWLAB_IMAGE") entry.value = service.image;
|
||||
if (entry.name === "HWLAB_IMAGE_TAG") entry.value = service.imageTag;
|
||||
if (entry.name === "HWLAB_SKILLS_COMMIT_ID") entry.value = target.shortCommitId;
|
||||
if (entry.name === "HWLAB_IMAGE_DIGEST") entry.value = publishRecord?.digest ?? "not_published";
|
||||
}
|
||||
}
|
||||
|
||||
function workloadContainers(item) {
|
||||
return item?.spec?.template?.spec?.containers ?? [];
|
||||
}
|
||||
|
||||
function refreshDocuments({ deploy, catalog, workloads, target, publishRecords, provenancePath }) {
|
||||
const published = Boolean(publishRecords);
|
||||
const catalogByService = new Map(catalog.services.map((service) => [service.serviceId, service]));
|
||||
const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service]));
|
||||
const refreshedServices = [];
|
||||
|
||||
deploy.commitId = target.shortCommitId;
|
||||
catalog.commitId = target.shortCommitId;
|
||||
catalog.artifactState = published ? "published" : "contract-skeleton";
|
||||
catalog.publish = {
|
||||
...catalog.publish,
|
||||
ciPublished: published,
|
||||
registryVerified: published,
|
||||
provenance: published ? provenancePath : "not_available_until_publish",
|
||||
note: published
|
||||
? "Digest fields were copied from a successful DEV artifact publish report for this source commit."
|
||||
: "Artifact identity was refreshed to this source commit, but no publish report proved registry digests."
|
||||
};
|
||||
|
||||
for (const serviceId of SERVICE_IDS) {
|
||||
const deployService = deployByService.get(serviceId);
|
||||
const catalogService = catalogByService.get(serviceId);
|
||||
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 imageTag = publishRecord?.imageTag ?? target.shortCommitId;
|
||||
|
||||
deployService.image = image;
|
||||
updateEnvObject(deployService.env, { image, imageTag }, target, publishRecord);
|
||||
|
||||
catalogService.commitId = target.shortCommitId;
|
||||
catalogService.image = image;
|
||||
catalogService.imageTag = imageTag;
|
||||
catalogService.digest = publishRecord?.digest ?? "not_published";
|
||||
catalogService.publishState = published ? "published" : "skeleton-only";
|
||||
refreshedServices.push({
|
||||
serviceId,
|
||||
image,
|
||||
imageTag,
|
||||
digest: catalogService.digest,
|
||||
publishState: catalogService.publishState
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of workloads.items ?? []) {
|
||||
for (const container of workloadContainers(item)) {
|
||||
const serviceId = serviceIdForWorkload(item, container);
|
||||
const service = catalogByService.get(serviceId);
|
||||
if (!service) continue;
|
||||
container.image = service.image;
|
||||
updateEnvList(container.env, service, target, publishRecords?.get(serviceId) ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
return refreshedServices;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
console.log(usage());
|
||||
return;
|
||||
}
|
||||
|
||||
const target = await resolveTarget(args.targetRef);
|
||||
const [deploy, catalog, workloads, publishReport] = await Promise.all([
|
||||
readJson(deployPath),
|
||||
readJson(catalogPath),
|
||||
readJson(workloadsPath),
|
||||
args.publishReportPath ? readJson(args.publishReportPath) : Promise.resolve(null)
|
||||
]);
|
||||
assertDevOnlyDeployAndCatalog(deploy, catalog);
|
||||
|
||||
const publishRecords = publishReport ? publishRecordsFromReport(publishReport, target) : null;
|
||||
const services = refreshDocuments({
|
||||
deploy,
|
||||
catalog,
|
||||
workloads,
|
||||
target,
|
||||
publishRecords,
|
||||
provenancePath: args.publishReportPath ?? defaultPublishReportPath
|
||||
});
|
||||
|
||||
if (args.write) {
|
||||
await Promise.all([
|
||||
writeJson(deployPath, deploy),
|
||||
writeJson(catalogPath, catalog),
|
||||
writeJson(workloadsPath, workloads)
|
||||
]);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: publishRecords ? "published" : "blocked",
|
||||
targetRef: target.ref,
|
||||
sourceCommitId: target.commitId,
|
||||
artifactCommitId: target.shortCommitId,
|
||||
wrote: args.write ? [deployPath, catalogPath, workloadsPath] : [],
|
||||
ciPublished: Boolean(publishRecords),
|
||||
registryVerified: Boolean(publishRecords),
|
||||
publishedCount: publishRecords?.size ?? 0,
|
||||
notPublishedCount: publishRecords ? 0 : services.length,
|
||||
digestPolicy: publishRecords
|
||||
? "catalog digests copied only from a published DEV artifact report"
|
||||
: "all catalog digests remain not_published",
|
||||
services
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(JSON.stringify({
|
||||
status: "failed",
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}, null, 2));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -18,7 +18,7 @@ const defaultReportPath = "reports/dev-gate/dev-preflight-report.json";
|
||||
const issue = "pikasTech/HWLAB#34";
|
||||
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49"].map(
|
||||
(id) => `pikasTech/HWLAB${id}`
|
||||
);
|
||||
).concat(["pikasTech/HWLAB#33", "pikasTech/HWLAB#35", "pikasTech/HWLAB#48"]);
|
||||
const forbiddenActions = [
|
||||
"prod-deploy",
|
||||
"secret-material-read",
|
||||
@@ -37,6 +37,7 @@ const blockerTypes = new Set([
|
||||
"observability_blocker",
|
||||
"safety_blocker"
|
||||
]);
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
@@ -200,6 +201,86 @@ function imageToManifestUrl(image) {
|
||||
return match ? `https://ghcr.io/v2/pikastech/${match[1]}/manifests/${match[2]}` : null;
|
||||
}
|
||||
|
||||
function matchesTargetCommit(value, targetCommit, targetShortCommit) {
|
||||
return value === targetCommit || value === targetShortCommit;
|
||||
}
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return [...new Set(values)].sort();
|
||||
}
|
||||
|
||||
function catalogDigestCounts(catalog) {
|
||||
const counts = {
|
||||
sha256: 0,
|
||||
notPublished: 0,
|
||||
invalid: 0
|
||||
};
|
||||
|
||||
for (const service of catalog.services ?? []) {
|
||||
if (digestPattern.test(service.digest)) counts.sha256 += 1;
|
||||
else if (service.digest === "not_published") counts.notPublished += 1;
|
||||
else counts.invalid += 1;
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef }) {
|
||||
const serviceCommitIds = uniqueSorted((catalog.services ?? []).map((service) => service.commitId));
|
||||
const digestCounts = catalogDigestCounts(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 &&
|
||||
digestCounts.invalid === 0;
|
||||
const publishVerified = catalog.publish?.ciPublished === true &&
|
||||
catalog.publish?.registryVerified === true &&
|
||||
catalog.artifactState === "published" &&
|
||||
allDigestsPublished &&
|
||||
(catalog.services ?? []).every((service) => service.publishState === "published");
|
||||
|
||||
return {
|
||||
source: {
|
||||
ref: targetRef,
|
||||
commitId: targetCommit,
|
||||
shortCommitId: targetShortCommit
|
||||
},
|
||||
deployManifest: {
|
||||
path: "deploy/deploy.json",
|
||||
commitId: deploy.commitId,
|
||||
matchesSource: deployMatchesSource
|
||||
},
|
||||
artifactCatalog: {
|
||||
path: "deploy/artifact-catalog.dev.json",
|
||||
commitId: catalog.commitId,
|
||||
artifactState: catalog.artifactState,
|
||||
ciPublished: catalog.publish?.ciPublished === true,
|
||||
registryVerified: catalog.publish?.registryVerified === true,
|
||||
provenance: catalog.publish?.provenance ?? "unknown",
|
||||
digestCounts,
|
||||
matchesSource: catalogMatchesSource
|
||||
},
|
||||
services: (catalog.services ?? []).map((service) => ({
|
||||
serviceId: service.serviceId,
|
||||
commitId: service.commitId,
|
||||
matchesSource: matchesTargetCommit(service.commitId, targetCommit, targetShortCommit),
|
||||
image: service.image,
|
||||
imageTag: service.imageTag,
|
||||
digest: service.digest,
|
||||
publishState: service.publishState
|
||||
})),
|
||||
serviceCommitIds,
|
||||
matchesSource: deployMatchesSource && catalogMatchesSource && servicesMatchSource,
|
||||
publishVerified,
|
||||
refreshCommands: {
|
||||
blocked: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --blocked`,
|
||||
published: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --publish-report reports/dev-gate/dev-artifacts.json`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function assertStaticContract(deploy, catalog) {
|
||||
assert.equal(deploy.environment, ENVIRONMENT_DEV, "deploy environment must be dev");
|
||||
assert.equal(deploy.namespace, "hwlab-dev", "deploy namespace must be hwlab-dev");
|
||||
@@ -299,6 +380,7 @@ function makeReporter() {
|
||||
|
||||
function validateLocalContracts(reporter, contracts, targetShortCommit, targetCommit, targetRef) {
|
||||
const [deploy, catalog, namespace, workloads, services, devKustomization, healthContract, masterEdge] = contracts;
|
||||
const artifactIdentity = artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef });
|
||||
|
||||
try {
|
||||
assertStaticContract(deploy, catalog);
|
||||
@@ -313,32 +395,51 @@ function validateLocalContracts(reporter, contracts, targetShortCommit, targetCo
|
||||
});
|
||||
}
|
||||
|
||||
const pinnedToTarget = [targetCommit, targetShortCommit].includes(deploy.commitId) &&
|
||||
[targetCommit, targetShortCommit].includes(catalog.commitId);
|
||||
if (pinnedToTarget) {
|
||||
reporter.check("target-commit-pinning", "contract", "pass", `deploy/deploy.json and artifact catalog target ${targetShortCommit}.`);
|
||||
if (artifactIdentity.matchesSource) {
|
||||
reporter.check(
|
||||
"target-commit-pinning",
|
||||
"contract",
|
||||
"pass",
|
||||
`Source ${targetRef} ${targetShortCommit} matches deploy/catalog artifact commit ${artifactIdentity.artifactCatalog.commitId}.`,
|
||||
[artifactIdentity]
|
||||
);
|
||||
} else {
|
||||
reporter.check("target-commit-pinning", "contract", "blocked", `deploy/catalog commitId ${deploy.commitId} does not match ${targetRef} ${targetShortCommit}.`);
|
||||
reporter.check(
|
||||
"target-commit-pinning",
|
||||
"contract",
|
||||
"blocked",
|
||||
`Source ${targetRef} ${targetShortCommit} is not covered by deploy commit ${deploy.commitId}, catalog commit ${catalog.commitId}, service commits ${artifactIdentity.serviceCommitIds.join(", ")}.`,
|
||||
[artifactIdentity]
|
||||
);
|
||||
reporter.block({
|
||||
type: "contract_blocker",
|
||||
scope: "deploy-target",
|
||||
summary: `deploy/deploy.json and deploy/artifact-catalog.dev.json still target ${deploy.commitId}, not ${targetRef} ${targetShortCommit}.`,
|
||||
nextTask: "Publish or select a DEV artifact set for the current origin/main commit and update deploy/deploy.json plus deploy/artifact-catalog.dev.json to that immutable commit/tag."
|
||||
scope: "artifact-source-commit",
|
||||
summary: `source commit ${targetRef} ${targetShortCommit} does not match deploy/catalog artifact identity: deploy=${deploy.commitId}, catalog=${catalog.commitId}, services=${artifactIdentity.serviceCommitIds.join(", ")}.`,
|
||||
nextTask: `Refresh without fake digests using \`${artifactIdentity.refreshCommands.blocked}\`, or after a successful publish run \`${artifactIdentity.refreshCommands.published}\`.`
|
||||
});
|
||||
}
|
||||
|
||||
const catalogClaimsPublished = catalog.publish?.ciPublished === true &&
|
||||
catalog.publish?.registryVerified === true &&
|
||||
catalog.services.every((service) => /^sha256:[a-f0-9]{64}$/.test(service.digest));
|
||||
if (catalogClaimsPublished) {
|
||||
reporter.check("artifact-catalog-publish-state", "registry", "pass", "Artifact catalog claims published images with registry digests.");
|
||||
if (artifactIdentity.publishVerified) {
|
||||
reporter.check(
|
||||
"artifact-catalog-publish-state",
|
||||
"registry",
|
||||
"pass",
|
||||
"Artifact catalog records CI publish, registry verification, and immutable service digests.",
|
||||
[artifactIdentity.artifactCatalog]
|
||||
);
|
||||
} else {
|
||||
reporter.check("artifact-catalog-publish-state", "registry", "blocked", "Artifact catalog is still a skeleton and does not carry registry digests.");
|
||||
reporter.check(
|
||||
"artifact-catalog-publish-state",
|
||||
"registry",
|
||||
"blocked",
|
||||
`Artifact catalog publish state is ${catalog.artifactState}; ciPublished=${artifactIdentity.artifactCatalog.ciPublished}, registryVerified=${artifactIdentity.artifactCatalog.registryVerified}, digests sha256=${artifactIdentity.artifactCatalog.digestCounts.sha256}, not_published=${artifactIdentity.artifactCatalog.digestCounts.notPublished}, invalid=${artifactIdentity.artifactCatalog.digestCounts.invalid}.`,
|
||||
[artifactIdentity.artifactCatalog]
|
||||
);
|
||||
reporter.block({
|
||||
type: "runtime_blocker",
|
||||
scope: "artifact-catalog",
|
||||
summary: "deploy/artifact-catalog.dev.json has ciPublished=false, registryVerified=false, and not_published digests.",
|
||||
nextTask: "Run the DEV image publishing workflow and record immutable GHCR digests before real deployment."
|
||||
summary: `deploy/artifact-catalog.dev.json does not prove published artifacts for ${targetRef} ${targetShortCommit}; ciPublished=${artifactIdentity.artifactCatalog.ciPublished}, registryVerified=${artifactIdentity.artifactCatalog.registryVerified}, not_published=${artifactIdentity.artifactCatalog.digestCounts.notPublished}.`,
|
||||
nextTask: `Run the DEV artifact publish workflow, then record only real sha256 digests with \`${artifactIdentity.refreshCommands.published}\`; if publish is still blocked, keep not_published via \`${artifactIdentity.refreshCommands.blocked}\`.`
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,7 +456,7 @@ function validateLocalContracts(reporter, contracts, targetShortCommit, targetCo
|
||||
});
|
||||
}
|
||||
|
||||
return { deploy, catalog, masterEdge };
|
||||
return { deploy, catalog, masterEdge, artifactIdentity };
|
||||
}
|
||||
|
||||
function validateArtifactPublishReport(reporter, artifactReport, targetShortCommit, targetCommit, targetRef) {
|
||||
@@ -709,7 +810,7 @@ async function writeReport(report, reportPath) {
|
||||
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function makeReport(args, targetCommit, targetShortCommit, reporter) {
|
||||
function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity) {
|
||||
const conclusion = reporter.blockers.length === 0 &&
|
||||
reporter.checks.every((item) => item.status === "pass") ? "ready" : "blocked";
|
||||
|
||||
@@ -733,10 +834,12 @@ function makeReport(args, targetCommit, targetShortCommit, reporter) {
|
||||
validationCommands: [
|
||||
"node --check scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/src/dev-gate-preflight.mjs",
|
||||
"node --check scripts/refresh-artifact-catalog.mjs",
|
||||
"node scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
artifactIdentity,
|
||||
conclusion,
|
||||
checks: reporter.checks,
|
||||
blockers: reporter.blockers,
|
||||
@@ -748,7 +851,10 @@ function printSummary(args, report) {
|
||||
console.log(JSON.stringify({
|
||||
issue,
|
||||
targetRef: args.targetRef,
|
||||
targetCommit: report.target.shortCommitId,
|
||||
targetCommit: report.target.commitId,
|
||||
targetShortCommit: report.target.shortCommitId,
|
||||
artifactCatalogCommit: report.artifactIdentity?.artifactCatalog?.commitId,
|
||||
catalogDigests: report.artifactIdentity?.artifactCatalog?.digestCounts,
|
||||
conclusion: report.conclusion,
|
||||
report: args.writeReport ? args.reportPath : null,
|
||||
checks: report.checks.reduce((counts, item) => {
|
||||
@@ -777,7 +883,7 @@ export async function runPreflight(argv) {
|
||||
const reporter = makeReporter();
|
||||
const contracts = await loadContracts();
|
||||
const optionalReports = await loadOptionalReports();
|
||||
const { deploy, catalog, masterEdge } = validateLocalContracts(
|
||||
const { deploy, catalog, masterEdge, artifactIdentity } = validateLocalContracts(
|
||||
reporter,
|
||||
contracts,
|
||||
targetShortCommit,
|
||||
@@ -792,7 +898,7 @@ export async function runPreflight(argv) {
|
||||
validateRuntimeBoundary(reporter, deploy);
|
||||
await validateLiveProbes(reporter, catalog, args.timeoutMs);
|
||||
|
||||
const report = makeReport(args, targetCommit, targetShortCommit, reporter);
|
||||
const report = makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity);
|
||||
if (args.writeReport) {
|
||||
await writeReport(report, args.reportPath);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@ const deployPath = "deploy/deploy.json";
|
||||
const ciPath = "CI.json";
|
||||
const healthContractPath = "deploy/k8s/dev/health-contract.yaml";
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
|
||||
const catalogModes = new Set(["contract-skeleton", "published"]);
|
||||
const skeletonProvenances = new Set(["not_available_in_mvp_skeleton", "not_available_until_publish"]);
|
||||
const requiredForbiddenItems = [
|
||||
"prod-deploy",
|
||||
"prod-profile-enabled",
|
||||
@@ -46,11 +49,19 @@ function assertCommitId(value, context) {
|
||||
|
||||
function imageParts(image, context) {
|
||||
assertString(image, context);
|
||||
const match = image.match(/^(ghcr\.io\/pikastech\/[^:@]+):([^:@]+)$/);
|
||||
assert.ok(match, `${context} must be a tagged ghcr.io/pikastech image`);
|
||||
assert.equal(image.includes("@"), false, `${context} must keep digest in the digest field, not the image reference`);
|
||||
assert.ok(!/prod|production/iu.test(image), `${context} must not target prod`);
|
||||
const slashIndex = image.lastIndexOf("/");
|
||||
const colonIndex = image.lastIndexOf(":");
|
||||
assert.ok(colonIndex > slashIndex, `${context} must be a tagged image reference`);
|
||||
const repository = image.slice(0, colonIndex);
|
||||
const tag = image.slice(colonIndex + 1);
|
||||
assertString(repository, `${context} repository`);
|
||||
assertString(tag, `${context} tag`);
|
||||
return {
|
||||
repository: match[1],
|
||||
tag: match[2]
|
||||
repository,
|
||||
repositoryName: repository.slice(repository.lastIndexOf("/") + 1),
|
||||
tag
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,13 +77,22 @@ function assertDevOnlyCatalog(catalog) {
|
||||
assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace");
|
||||
assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint");
|
||||
assertCommitId(catalog.commitId, "catalog commitId");
|
||||
assert.equal(catalog.artifactState, "contract-skeleton", "catalog artifactState");
|
||||
assert.ok(catalogModes.has(catalog.artifactState), "catalog artifactState must be contract-skeleton or published");
|
||||
|
||||
assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV], "only dev profile is allowed");
|
||||
assert.deepEqual(catalog.forbiddenProfiles, ["prod"], "prod profile must be forbidden");
|
||||
assert.equal(catalog.publish.ciPublished, false, "catalog must not claim CI publish");
|
||||
assert.equal(catalog.publish.registryVerified, false, "catalog must not claim registry verification");
|
||||
assert.equal(catalog.publish.provenance, "not_available_in_mvp_skeleton", "catalog provenance");
|
||||
assert.ok(catalog.publish && typeof catalog.publish === "object", "catalog publish must be an object");
|
||||
|
||||
if (catalog.artifactState === "contract-skeleton") {
|
||||
assert.equal(catalog.publish.ciPublished, false, "skeleton catalog must not claim CI publish");
|
||||
assert.equal(catalog.publish.registryVerified, false, "skeleton catalog must not claim registry verification");
|
||||
assert.ok(skeletonProvenances.has(catalog.publish.provenance), "skeleton catalog provenance must say publish evidence is unavailable");
|
||||
} else {
|
||||
assert.equal(catalog.publish.ciPublished, true, "published catalog must claim CI publish");
|
||||
assert.equal(catalog.publish.registryVerified, true, "published catalog must claim registry verification");
|
||||
assertString(catalog.publish.provenance, "published catalog provenance");
|
||||
assert.equal(skeletonProvenances.has(catalog.publish.provenance), false, "published catalog provenance must name real publish evidence");
|
||||
}
|
||||
|
||||
assert.equal(catalog.healthContract.method, "GET", "health method");
|
||||
assert.equal(catalog.healthContract.path, "/health/live", "health path");
|
||||
@@ -86,6 +106,8 @@ function assertDevOnlyCatalog(catalog) {
|
||||
for (const required of requiredForbiddenItems) {
|
||||
assert.ok(forbiddenIds.includes(required), `catalog forbiddenItems missing ${required}`);
|
||||
}
|
||||
|
||||
return catalog.artifactState;
|
||||
}
|
||||
|
||||
function assertDeployManifest(deployManifest, catalog) {
|
||||
@@ -101,7 +123,7 @@ function assertDeployManifest(deployManifest, catalog) {
|
||||
assert.equal(deployManifest.profiles.prod.enabled, false, "deploy prod profile must stay disabled");
|
||||
}
|
||||
|
||||
function assertCatalogServices(catalog, deployManifest) {
|
||||
function assertCatalogServices(catalog, deployManifest, catalogMode) {
|
||||
assert.ok(Array.isArray(catalog.services), "catalog services must be an array");
|
||||
assert.ok(Array.isArray(deployManifest.services), "deploy services must be an array");
|
||||
|
||||
@@ -124,7 +146,7 @@ function assertCatalogServices(catalog, deployManifest) {
|
||||
assertNoMutableTag(service.imageTag, `${context} imageTag`);
|
||||
|
||||
const image = imageParts(service.image, `${context} image`);
|
||||
assert.equal(image.repository, `ghcr.io/pikastech/${service.serviceId}`, `${context} image repository`);
|
||||
assert.equal(image.repositoryName, service.serviceId, `${context} image repository must end with serviceId`);
|
||||
assert.equal(image.tag, service.imageTag, `${context} image tag`);
|
||||
assert.equal(service.image, deployService.image, `${context} image must match deploy manifest`);
|
||||
|
||||
@@ -134,8 +156,13 @@ function assertCatalogServices(catalog, deployManifest) {
|
||||
assert.equal(service.namespace, deployService.namespace, `${context} namespace must match deploy manifest`);
|
||||
assert.equal(service.healthPath, catalog.healthContract.path, `${context} healthPath`);
|
||||
assert.equal(service.healthPath, deployService.healthPath, `${context} healthPath must match deploy manifest`);
|
||||
assert.equal(service.publishState, "skeleton-only", `${context} publishState`);
|
||||
assert.equal(service.digest, "not_published", `${context} digest`);
|
||||
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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,10 +193,10 @@ const deployManifest = await readJSON(deployPath);
|
||||
const ci = await readJSON(ciPath);
|
||||
const healthContract = await readJSON(healthContractPath);
|
||||
|
||||
assertDevOnlyCatalog(catalog);
|
||||
const catalogMode = assertDevOnlyCatalog(catalog);
|
||||
assertDeployManifest(deployManifest, catalog);
|
||||
assertCatalogServices(catalog, deployManifest);
|
||||
assertCatalogServices(catalog, deployManifest, catalogMode);
|
||||
assertCIForbidden(ci, catalog);
|
||||
assertHealthContract(healthContract, catalog);
|
||||
|
||||
console.log(`validated ${catalog.services.length} DEV artifact catalog services at ${catalog.commitId}`);
|
||||
console.log(`validated ${catalog.services.length} DEV artifact catalog services at ${catalog.commitId} (${catalogMode})`);
|
||||
|
||||
@@ -48,6 +48,7 @@ const requiredDevDeployApplyValidationCommands = [
|
||||
const requiredPreflightValidationCommands = [
|
||||
"node --check scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/src/dev-gate-preflight.mjs",
|
||||
"node --check scripts/refresh-artifact-catalog.mjs",
|
||||
"node scripts/dev-gate-preflight.mjs"
|
||||
];
|
||||
const requiredSmokeCommand = "node scripts/m1-contract-smoke.mjs";
|
||||
@@ -627,6 +628,94 @@ function assertDevDeployApplyReport(report, label) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function assertShaOrNotPublished(value, label) {
|
||||
assertString(value, label);
|
||||
assert.ok(
|
||||
value === "not_published" || /^sha256:[a-f0-9]{64}$/.test(value),
|
||||
`${label} must be not_published or a sha256 digest`
|
||||
);
|
||||
}
|
||||
|
||||
function assertArtifactIdentity(identity, label, target) {
|
||||
assertObject(identity, label);
|
||||
for (const field of [
|
||||
"source",
|
||||
"deployManifest",
|
||||
"artifactCatalog",
|
||||
"services",
|
||||
"serviceCommitIds",
|
||||
"matchesSource",
|
||||
"publishVerified",
|
||||
"refreshCommands"
|
||||
]) {
|
||||
assert.ok(Object.hasOwn(identity, field), `${label} missing ${field}`);
|
||||
}
|
||||
|
||||
assertObject(identity.source, `${label}.source`);
|
||||
assert.equal(identity.source.ref, target.ref, `${label}.source.ref`);
|
||||
assert.equal(identity.source.commitId, target.commitId, `${label}.source.commitId`);
|
||||
assert.equal(identity.source.shortCommitId, target.shortCommitId, `${label}.source.shortCommitId`);
|
||||
|
||||
assertObject(identity.deployManifest, `${label}.deployManifest`);
|
||||
assert.equal(identity.deployManifest.path, "deploy/deploy.json", `${label}.deployManifest.path`);
|
||||
assert.match(identity.deployManifest.commitId, /^[a-f0-9]{7,40}$/, `${label}.deployManifest.commitId`);
|
||||
assert.equal(typeof identity.deployManifest.matchesSource, "boolean", `${label}.deployManifest.matchesSource`);
|
||||
|
||||
assertObject(identity.artifactCatalog, `${label}.artifactCatalog`);
|
||||
assert.equal(identity.artifactCatalog.path, "deploy/artifact-catalog.dev.json", `${label}.artifactCatalog.path`);
|
||||
assert.match(identity.artifactCatalog.commitId, /^[a-f0-9]{7,40}$/, `${label}.artifactCatalog.commitId`);
|
||||
assert.ok(["contract-skeleton", "published"].includes(identity.artifactCatalog.artifactState), `${label}.artifactCatalog.artifactState`);
|
||||
assert.equal(typeof identity.artifactCatalog.ciPublished, "boolean", `${label}.artifactCatalog.ciPublished`);
|
||||
assert.equal(typeof identity.artifactCatalog.registryVerified, "boolean", `${label}.artifactCatalog.registryVerified`);
|
||||
assertString(identity.artifactCatalog.provenance, `${label}.artifactCatalog.provenance`);
|
||||
assert.equal(typeof identity.artifactCatalog.matchesSource, "boolean", `${label}.artifactCatalog.matchesSource`);
|
||||
assertObject(identity.artifactCatalog.digestCounts, `${label}.artifactCatalog.digestCounts`);
|
||||
for (const field of ["sha256", "notPublished", "invalid"]) {
|
||||
assert.equal(typeof identity.artifactCatalog.digestCounts[field], "number", `${label}.artifactCatalog.digestCounts.${field}`);
|
||||
}
|
||||
|
||||
assertArray(identity.services, `${label}.services`);
|
||||
assert.ok(identity.services.length >= 1, `${label}.services must not be empty`);
|
||||
assertUnique(identity.services.map((service) => service.serviceId), `${label}.services`);
|
||||
for (const [index, service] of identity.services.entries()) {
|
||||
const serviceLabel = `${label}.services[${index}]`;
|
||||
assertObject(service, serviceLabel);
|
||||
for (const field of ["serviceId", "commitId", "matchesSource", "image", "imageTag", "digest", "publishState"]) {
|
||||
assert.ok(Object.hasOwn(service, field), `${serviceLabel} missing ${field}`);
|
||||
}
|
||||
assertString(service.serviceId, `${serviceLabel}.serviceId`);
|
||||
assert.match(service.commitId, /^[a-f0-9]{7,40}$/, `${serviceLabel}.commitId`);
|
||||
assert.equal(typeof service.matchesSource, "boolean", `${serviceLabel}.matchesSource`);
|
||||
assertString(service.image, `${serviceLabel}.image`);
|
||||
assertString(service.imageTag, `${serviceLabel}.imageTag`);
|
||||
assertShaOrNotPublished(service.digest, `${serviceLabel}.digest`);
|
||||
assert.ok(["skeleton-only", "published"].includes(service.publishState), `${serviceLabel}.publishState`);
|
||||
}
|
||||
|
||||
assertStringArray(identity.serviceCommitIds, `${label}.serviceCommitIds`, { minLength: 1 });
|
||||
assert.equal(typeof identity.matchesSource, "boolean", `${label}.matchesSource`);
|
||||
assert.equal(typeof identity.publishVerified, "boolean", `${label}.publishVerified`);
|
||||
assertObject(identity.refreshCommands, `${label}.refreshCommands`);
|
||||
assert.equal(
|
||||
identity.refreshCommands.blocked,
|
||||
`node scripts/refresh-artifact-catalog.mjs --target-ref ${target.ref} --blocked`,
|
||||
`${label}.refreshCommands.blocked`
|
||||
);
|
||||
assert.equal(
|
||||
identity.refreshCommands.published,
|
||||
`node scripts/refresh-artifact-catalog.mjs --target-ref ${target.ref} --publish-report reports/dev-gate/dev-artifacts.json`,
|
||||
`${label}.refreshCommands.published`
|
||||
);
|
||||
|
||||
if (identity.publishVerified) {
|
||||
assert.equal(identity.artifactCatalog.ciPublished, true, `${label}.artifactCatalog.ciPublished`);
|
||||
assert.equal(identity.artifactCatalog.registryVerified, true, `${label}.artifactCatalog.registryVerified`);
|
||||
assert.equal(identity.artifactCatalog.digestCounts.sha256, identity.services.length, `${label}.artifactCatalog.digestCounts.sha256`);
|
||||
assert.equal(identity.artifactCatalog.digestCounts.notPublished, 0, `${label}.artifactCatalog.digestCounts.notPublished`);
|
||||
assert.equal(identity.artifactCatalog.digestCounts.invalid, 0, `${label}.artifactCatalog.digestCounts.invalid`);
|
||||
}
|
||||
}
|
||||
async function validateDevEdgeReport(report, label) {
|
||||
for (const field of [
|
||||
"$schema",
|
||||
@@ -892,6 +981,7 @@ async function validatePreflightReport(relativePath, report) {
|
||||
"prodDisabled",
|
||||
"forbiddenActions",
|
||||
"validationCommands",
|
||||
"artifactIdentity",
|
||||
"conclusion",
|
||||
"checks",
|
||||
"blockers"
|
||||
@@ -933,6 +1023,8 @@ async function validatePreflightReport(relativePath, report) {
|
||||
);
|
||||
}
|
||||
|
||||
assertArtifactIdentity(report.artifactIdentity, `${label}.artifactIdentity`, report.target);
|
||||
|
||||
assertArray(report.checks, `${label}.checks`);
|
||||
assert.ok(report.checks.length >= 1, `${label}.checks must not be empty`);
|
||||
assertUnique(report.checks.map((check) => check.id), `${label}.checks`);
|
||||
|
||||
Reference in New Issue
Block a user