diff --git a/deploy/README.md b/deploy/README.md index dd6488e5..3e246268 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -59,7 +59,7 @@ Desired-state commit/image convergence review: ```sh node scripts/deploy-desired-state-plan.mjs --plan --pretty -node scripts/deploy-desired-state-plan.mjs --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty node scripts/deploy-desired-state-plan.mjs --target-tag --pretty node scripts/deploy-desired-state-plan.mjs --promotion-commit --check @@ -72,18 +72,18 @@ tags, workload images, and present env mirrors such as `HWLAB_COMMIT_ID`, `HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` converge. It also blocks if `hwlab-cloud-api` loses the Code Agent provider Secret ref `hwlab-code-agent-provider/openai-api-key`, the `OPENAI_API_KEY` env name, or -the DEV egress proxy base-url env. With -`--target-ref` or `--target-tag`, a uniform older desired-state is reported as a -read-only promotion plan. Use `--promotion-commit ` when reviewing or -automating a concrete promotion; then `--check` also fails if any authoritative -desired-state commit, image tag, workload image, or env mirror still points to a -different commit/tag. +the DEV egress proxy base-url env. Without `--check`, `--target-ref` or +`--target-tag` reports a uniform older desired-state as a read-only promotion +plan. With `--check`, any explicit target is a hard current-main/commit-pinning +gate: stale deploy, catalog, workload image, or runtime mirror fields are +blocked before apply. `deploy/deploy.json` is the deploy truth, but artifact catalog and workload refs are part of the same desired-state contract. Promotion updates must move all three together by using the refresh command in blocked mode or -publish-report mode, then run the promotion commit check. Report files are -contextual evidence only and must not override these desired-state files. +publish-report mode, then run `--target-ref origin/main --check` or the +promotion commit check. Report files are contextual evidence only and must not +override these desired-state files. This is source/dry-run support only. It does not prove a registry image exists, does not build, pull, push, apply, restart, or touch PROD, and must not be used diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 8acdfb25..6a644a70 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,13 +5,13 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "7de6edd", - "artifactState": "published", + "commitId": "b9b625c", + "artifactState": "contract-skeleton", "publish": { - "ciPublished": true, - "registryVerified": true, - "provenance": "reports/dev-gate/dev-artifacts.json", - "note": "Digest fields were copied from a successful DEV artifact publish report for this source commit." + "ciPublished": false, + "registryVerified": false, + "provenance": "not_available_until_publish", + "note": "Artifact identity was refreshed to this source commit, but no publish report proved registry digests." }, "healthContract": { "method": "GET", @@ -66,11 +66,11 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:8a0120e976f8369c19887d33d004b763c7dbff3f43fd023fb186abffe199b3ea", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -78,15 +78,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-cloud-web", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:a302a8b4f3c6e7a91e5046398b803e9915dba873cefac97bb7069886b5a48d61", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -94,15 +94,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-agent-mgr", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:a16bba1762f58e4daed7ef94243e3f4f0522332704c9f40bcadab77f11acc7a2", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -110,15 +110,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-agent-worker", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:3c75cb9d6c2b7392749d972c78f9b7a89e4f099a64f902d2eb9810f16d551ceb", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -126,15 +126,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-gateway", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:b5374c763d54628c2e81b86c93c826a27629a8546ae789223e0553ca7c8ab80a", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -142,15 +142,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-gateway-simu", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:be6aff770319c6c2b45f7b5974d4fb7aae4aab79a6670b1d8b87e194387e30dd", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -158,15 +158,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-box-simu", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:54e77f4c2ea9e87fd16becde9a5fe05b935f2472dba443458257dfdda1e87c27", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -174,15 +174,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-patch-panel", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:02bec561d5f08f850e2884a2098da72978ad319d5bd799704ad9650778f11ae2", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -190,15 +190,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-router", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-router:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:bb3b10c486bb1305f4cae7ec2751947eeb0cc60c9993d54e93802b5df3b13fb6", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-router:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -206,15 +206,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-tunnel-client", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:ce9fa92362dd10911dcd66c59412690ccde401ce4ef82b548cfcea37499a725f", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -222,15 +222,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-edge-proxy", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:bf19f6812cbac3c54c938ad12a2b1ef9626a746017f85658b9960a1761afa32c", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -238,15 +238,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-cli", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:b169c015c4c06dcdf6d43131d69dd72be1996cb5970e25014c4cec0271b459c0", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -254,15 +254,15 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" }, { "serviceId": "hwlab-agent-skills", - "commitId": "7de6edd", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7de6edd", - "imageTag": "7de6edd", - "digest": "sha256:7d1b6871763613bf7c6e37f5b05b1b49e2c367b4b0b94abfbb9bceb7ab5de4ed", - "publishState": "published", + "commitId": "b9b625c", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b9b625c", + "imageTag": "b9b625c", + "digest": "not_published", + "publishState": "skeleton-only", "profile": "dev", "namespace": "hwlab-dev", "healthPath": "/health/live", @@ -270,7 +270,7 @@ "publishEnabled": true, "artifactRequired": true, "artifactScope": "required", - "notPublishedReason": null + "notPublishedReason": "publish_not_run" } ], "serviceInventory": { diff --git a/deploy/deploy.json b/deploy/deploy.json index c8372e3f..5ce550d3 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "7de6edd", + "commitId": "b9b625c", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -189,9 +189,9 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", - "HWLAB_COMMIT_ID": "7de6edd", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd", - "HWLAB_IMAGE_TAG": "7de6edd", + "HWLAB_COMMIT_ID": "b9b625c", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", + "HWLAB_IMAGE_TAG": "b9b625c", "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": "disable", @@ -210,7 +210,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -218,14 +218,14 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_COMMIT_ID": "7de6edd", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd", - "HWLAB_IMAGE_TAG": "7de6edd" + "HWLAB_COMMIT_ID": "b9b625c", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", + "HWLAB_IMAGE_TAG": "b9b625c" } }, { "serviceId": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -237,7 +237,7 @@ }, { "serviceId": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -249,7 +249,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -261,7 +261,7 @@ }, { "serviceId": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -281,7 +281,7 @@ }, { "serviceId": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -300,7 +300,7 @@ }, { "serviceId": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -321,7 +321,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-router:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -333,7 +333,7 @@ }, { "serviceId": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -347,7 +347,7 @@ }, { "serviceId": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -359,7 +359,7 @@ }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -370,13 +370,13 @@ }, { "serviceId": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b9b625c", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "7de6edd" + "HWLAB_SKILLS_COMMIT_ID": "b9b625c" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index b38234cd..39824804 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c", "ports": [ { "name": "http", @@ -49,15 +49,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "7de6edd" + "value": "b9b625c" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b9b625c" }, { "name": "HWLAB_IMAGE_TAG", - "value": "7de6edd" + "value": "b9b625c" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -175,7 +175,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c", "ports": [ { "name": "http", @@ -189,15 +189,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "7de6edd" + "value": "b9b625c" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7de6edd" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b9b625c" }, { "name": "HWLAB_IMAGE_TAG", - "value": "7de6edd" + "value": "b9b625c" } ], "readinessProbe": { @@ -247,7 +247,7 @@ "containers": [ { "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b9b625c", "ports": [ { "name": "http", @@ -303,7 +303,7 @@ "containers": [ { "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b9b625c", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -346,7 +346,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b9b625c", "ports": [ { "name": "http", @@ -407,7 +407,7 @@ "containers": [ { "name": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b9b625c", "ports": [ { "name": "http", @@ -480,7 +480,7 @@ "containers": [ { "name": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b9b625c", "ports": [ { "name": "http", @@ -548,7 +548,7 @@ "containers": [ { "name": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b9b625c", "ports": [ { "name": "http", @@ -612,7 +612,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-router:b9b625c", "ports": [ { "name": "http", @@ -672,7 +672,7 @@ "containers": [ { "name": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b9b625c", "ports": [ { "name": "http", @@ -744,7 +744,7 @@ "containers": [ { "name": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b9b625c", "ports": [ { "name": "http", @@ -805,7 +805,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:b9b625c", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -847,7 +847,7 @@ "containers": [ { "name": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7de6edd", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b9b625c", "ports": [ { "name": "http", @@ -857,7 +857,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "7de6edd" + "value": "b9b625c" } ], "readinessProbe": { diff --git a/docs/artifact-catalog.md b/docs/artifact-catalog.md index dd15b119..f4c68e06 100644 --- a/docs/artifact-catalog.md +++ b/docs/artifact-catalog.md @@ -75,7 +75,7 @@ Use the read-only planner before refreshing or reviewing deploy desired-state: ```sh node scripts/deploy-desired-state-plan.mjs --plan --pretty -node scripts/deploy-desired-state-plan.mjs --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty node scripts/deploy-desired-state-plan.mjs --target-tag --pretty node scripts/deploy-desired-state-plan.mjs --promotion-commit --check @@ -88,12 +88,13 @@ service `commitId`/`image`/`imageTag`, deploy service image tags, workload images, and present env mirrors including `HWLAB_COMMIT_ID`, `HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID`. -`--check` exits non-zero only when the source files are missing or invalid, the -desired-state has internal partial drift, an explicit target tag is invalid or -mutable, or some authoritative fields already point at a target while others do -not. A uniform older state under `--target-ref` is a promotion review plan, not a -failure. The optional report snapshot is contextual evidence only and is not an -authoritative desired-state source. +`--check` exits non-zero when the source files are missing or invalid, the +desired-state has internal drift, an explicit target tag is invalid or mutable, +or any authoritative field does not match the requested target. A uniform older +state under `--target-ref` is only allowed in read-only plan mode; with +`--target-ref origin/main --check` it is a blocker because applying it would keep +DEV pinned to stale artifacts. The optional report snapshot is contextual +evidence only and is not an authoritative desired-state source. For PR review or CI/CD promotion handoff, use `--promotion-commit --check`. That mode is stricter than a `--target-ref` plan: every top-level @@ -115,7 +116,7 @@ catalog and deploy manifests in blocked mode: ```sh node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked -node scripts/deploy-desired-state-plan.mjs --promotion-commit $(git rev-parse --short=7 origin/main) --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check ``` This updates `deploy/deploy.json`, `deploy/artifact-catalog.dev.json`, and @@ -128,7 +129,7 @@ report: ```sh node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json -node scripts/deploy-desired-state-plan.mjs --promotion-commit $(git rev-parse --short=7 origin/main) --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check ``` The publish-report mode refuses to update the catalog unless the report says @@ -177,7 +178,7 @@ Run the local static validator: node --check scripts/deploy-desired-state-plan.mjs node --check scripts/src/deploy-desired-state-plan.mjs node --test scripts/deploy-desired-state-plan.test.mjs -node scripts/deploy-desired-state-plan.mjs --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check node --check scripts/validate-artifact-catalog.mjs node --check scripts/refresh-artifact-catalog.mjs node --check scripts/preflight-dev-base-image.mjs diff --git a/docs/dev-deploy-apply.md b/docs/dev-deploy-apply.md index 2d32a72a..0523946b 100644 --- a/docs/dev-deploy-apply.md +++ b/docs/dev-deploy-apply.md @@ -38,7 +38,7 @@ node --check scripts/dev-deploy-apply.mjs node --check scripts/src/dev-deploy-apply.mjs node --check scripts/dev-runtime-migration.mjs node --check scripts/src/dev-runtime-migration.mjs -node scripts/deploy-desired-state-plan.mjs --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check node scripts/dev-runtime-migration.mjs --check node scripts/artifact-runtime-readiness-guard.mjs --target-ref origin/main --check --no-report --expect-blocked node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report diff --git a/docs/dev-gate-preflight.md b/docs/dev-gate-preflight.md index 81c56e1f..d3cff6e4 100644 --- a/docs/dev-gate-preflight.md +++ b/docs/dev-gate-preflight.md @@ -21,7 +21,7 @@ node --check scripts/src/deploy-desired-state-plan.mjs node --check scripts/dev-runtime-migration.mjs node --check scripts/src/dev-runtime-migration.mjs node --check scripts/refresh-artifact-catalog.mjs -node scripts/deploy-desired-state-plan.mjs --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check node scripts/dev-runtime-migration.mjs --check node scripts/dev-gate-preflight.mjs ``` @@ -47,9 +47,10 @@ The preflight checks: - `deploy/deploy.json` and `deploy/artifact-catalog.dev.json` are internally consistent and DEV-only. -- `scripts/deploy-desired-state-plan.mjs --check` can be run as a read-only - source support check for deploy commit/image/tag/workload/env mirror - convergence. It does not contact a registry or prove DEV runtime state. +- `scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check` can + be run as a read-only source support check for deploy commit/image/tag/workload + and env mirror convergence. It does not contact a registry or prove DEV + runtime state. - The deploy manifest, artifact catalog, catalog service commits, and image tags are covered by the selected `origin/main` target. If `origin/main` is a report-only evidence commit whose changes do not touch artifact build inputs, diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index 7cc81adf..b9d4be01 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -64,14 +64,14 @@ publish report: ```sh node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json -node scripts/deploy-desired-state-plan.mjs --promotion-commit $(git rev-parse --short=7 origin/main) --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check ``` If publish is blocked, keep the report blocked rather than inventing digests: ```sh node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked -node scripts/deploy-desired-state-plan.mjs --promotion-commit $(git rev-parse --short=7 origin/main) --check +node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check ``` The desired-state files are a single deployment truth: `deploy/deploy.json`, @@ -79,8 +79,9 @@ The desired-state files are a single deployment truth: `deploy/deploy.json`, carry the same promotion commit and immutable image tag before they are used as apply input. A `--target-ref` desired-state plan may report a uniform older state as `promotion_pending`; PR review and CI/CD promotion gates must use -`--promotion-commit --check` so stale or partially refreshed commit, -image, workload, and mirror-env fields are blocked before any DEV apply. +`--target-ref origin/main --check` or `--promotion-commit --check` so +stale or partially refreshed commit, image, workload, and mirror-env fields are +blocked before any DEV apply. Reports under `reports/` are evidence snapshots only and cannot override this desired-state contract. diff --git a/package.json b/package.json index 4c12df05..6ffdc0a4 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "scripts": { - "validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-migration.mjs --check && node --test scripts/artifact-runtime-readiness-guard.test.mjs", - "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-edge-proxy/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/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.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/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.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/src/registry-capabilities.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/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-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 --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-migration.mjs --check && 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 scripts/code-agent-chat-smoke.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-migration.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check && node scripts/dev-runtime-migration.mjs --check && node --test scripts/artifact-runtime-readiness-guard.test.mjs", + "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-edge-proxy/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/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.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/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.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/src/registry-capabilities.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/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-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 --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check && node scripts/dev-runtime-migration.mjs --check && 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 scripts/code-agent-chat-smoke.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-migration.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", "m1:smoke": "node scripts/m1-contract-smoke.mjs", @@ -17,7 +17,7 @@ "deploy:contract:plan": "node scripts/deploy-contract-plan.mjs --pretty", "deploy:contract:check": "node scripts/deploy-contract-plan.mjs --check", "deploy:desired-state:plan": "node scripts/deploy-desired-state-plan.mjs --pretty", - "deploy:desired-state:check": "node scripts/deploy-desired-state-plan.mjs --check", + "deploy:desired-state:check": "node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --check", "cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health", "cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp", "cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list", diff --git a/scripts/deploy-desired-state-plan.test.mjs b/scripts/deploy-desired-state-plan.test.mjs index 80a6b977..8b491415 100644 --- a/scripts/deploy-desired-state-plan.test.mjs +++ b/scripts/deploy-desired-state-plan.test.mjs @@ -277,6 +277,18 @@ test("target tag review is read-only promotion_pending when current state is uni assert.match(plan.services[0].promotion.deployImage, /:def5678$/u); }); +test("target check blocks when current desired-state is uniformly older", async () => { + const repoRoot = await makeFixture(); + const plan = await buildDesiredStatePlan({ repoRoot, targetTag: "def5678", requireTargetConvergence: true }); + assert.equal(plan.status, "blocked"); + assert.equal(plan.target.convergence.state, "target_mismatch"); + assert.equal(plan.target.convergenceRequirement, "target-check"); + assert.ok(plan.diagnostics.some((diagnostic) => + diagnostic.code === "target_desired_state_mismatch" && + diagnostic.message.includes("stale desired-state is not pinned") + )); +}); + test("promotion commit check blocks when current desired-state is uniformly older", async () => { const repoRoot = await makeFixture(); const plan = await buildDesiredStatePlan({ repoRoot, promotionCommit: "def5678" }); diff --git a/scripts/src/deploy-desired-state-plan.mjs b/scripts/src/deploy-desired-state-plan.mjs index a567b53f..8cb9ba25 100644 --- a/scripts/src/deploy-desired-state-plan.mjs +++ b/scripts/src/deploy-desired-state-plan.mjs @@ -87,7 +87,7 @@ function usage() { "", "Options:", " --plan explicitly request the default read-only JSON plan", - " --check exit non-zero when desired-state sources are invalid, drifted, or do not match --promotion-commit", + " --check exit non-zero when desired-state sources are invalid, drifted, or do not match an explicit target", " --pretty print indented JSON", " --target-ref REF resolve REF with git and review promotion to its short commit tag", " --target-tag TAG review promotion to an explicit image tag without proving registry existence", @@ -467,7 +467,7 @@ function assertTargetTag(ctx, tag, pathName) { } } -async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit }, ctx) { +async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit, requireTargetConvergence = false }, ctx) { if (!targetRef && !targetTag && !promotionCommit) return null; let commitId = null; let shortCommitId = null; @@ -503,6 +503,7 @@ async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit } if (promotionCommit && commitPattern.test(String(promotionCommit)) && targetTag && targetTag !== promotionCommit.slice(0, 7)) { addMismatch(ctx, "promotion_tag_mismatch", "--target-tag", "promotion target tag must be the short promotion commit", promotionCommit.slice(0, 7), targetTag); } + const requireConvergence = Boolean(promotionCommit || requireTargetConvergence); return { targetRef, promotionCommit, @@ -510,10 +511,18 @@ async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit } shortCommitId, tag, tagSource: targetTag ? "target-tag" : promotionCommit ? "promotion-commit-short" : "target-ref-short-commit", - requireConvergence: Boolean(promotionCommit) + requireConvergence, + convergenceRequirement: promotionCommit ? "promotion-commit" : requireTargetConvergence ? "target-check" : "plan-only" }; } +function targetLabel(target) { + if (target.targetRef && target.tag) return `${target.targetRef} (${target.tag})`; + if (target.promotionCommit) return target.promotionCommit; + if (target.tag) return `tag ${target.tag}`; + return "target"; +} + function observationMatchesTarget(observation, target) { if (!target) return null; if (observation.kind === "commit") { @@ -563,16 +572,20 @@ function buildTargetConvergence(ctx, target) { } }); } else if (promotionRequired && pending.length > 0) { + const promotionCommitRequired = Boolean(target.promotionCommit); addDiagnostic(ctx, { - code: "promotion_commit_mismatch", + code: promotionCommitRequired ? "promotion_commit_mismatch" : "target_desired_state_mismatch", path: "desired-state", - message: "desired-state fields do not match the required promotion commit/tag", + message: promotionCommitRequired + ? "desired-state fields do not match the required promotion commit/tag" + : `stale desired-state is not pinned to ${targetLabel(target)}; explicit target checks require current-main/commit-pinned deploy, catalog, workload, and runtime mirror fields`, expected: { commitId: target.commitId, imageTag: target.tag }, actual: { - pending: pending.length + pending: pending.length, + samplePendingPaths: pending.slice(0, 8).map((item) => item.path) } }); } @@ -582,7 +595,9 @@ function buildTargetConvergence(ctx, target) { : pending.length === 0 ? "already_promoted" : promotionRequired - ? "promotion_mismatch" + ? target.promotionCommit + ? "promotion_mismatch" + : "target_mismatch" : "promotion_pending"; return { state, @@ -668,7 +683,8 @@ export async function buildDesiredStatePlan(options = {}) { const target = await resolveTarget(repoRoot, { targetRef: options.targetRef ?? null, targetTag: options.targetTag ?? null, - promotionCommit: options.promotionCommit ?? null + promotionCommit: options.promotionCommit ?? null, + requireTargetConvergence: options.requireTargetConvergence === true }, ctx); const deployServices = deploy.services ?? []; @@ -910,7 +926,7 @@ export async function buildDesiredStatePlan(options = {}) { prod: false, devLiveClaim: false }, - checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, internal commit/image mirror drift, invalid target tags, partial target drift, or any mismatch with --promotion-commit. A uniform older desired-state under --target-ref alone is a read-only promotion plan, not a check failure.", + checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, internal commit/image mirror drift, invalid target tags, partial target drift, or any mismatch with an explicit --target-ref, --target-tag, or --promotion-commit. Without --check, a uniform older desired-state under --target-ref/--target-tag is a read-only promotion plan.", summary: { desiredCommitId, desiredImageTag, @@ -966,7 +982,8 @@ export async function runDeployDesiredStatePlanCli(argv = process.argv.slice(2), repoRoot: options.repoRoot, targetRef: args.targetRef, targetTag: args.targetTag, - promotionCommit: args.promotionCommit + promotionCommit: args.promotionCommit, + requireTargetConvergence: args.check && Boolean(args.targetRef || args.targetTag) }); process.stdout.write(`${JSON.stringify(plan, null, args.pretty ? 2 : 0)}\n`); if (args.check && plan.status === "blocked") {