fix: gate deploy desired-state promotion drift
fix: gate deploy desired-state promotion drift
This commit is contained in:
+12
-3
@@ -40,10 +40,11 @@ PROD. Later work can wire the same source fields into `hwlab edge apply` or
|
||||
Desired-state commit/image convergence review:
|
||||
|
||||
```sh
|
||||
node scripts/deploy-desired-state-plan.mjs --pretty
|
||||
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 --pretty
|
||||
node scripts/deploy-desired-state-plan.mjs --target-tag <tag> --pretty
|
||||
node scripts/deploy-desired-state-plan.mjs --promotion-commit <sha> --check
|
||||
```
|
||||
|
||||
The desired-state plan reads only `deploy/deploy.json`,
|
||||
@@ -52,8 +53,16 @@ optional artifact report snapshot. It checks that commit IDs, service image
|
||||
tags, workload images, and present env mirrors such as `HWLAB_COMMIT_ID`,
|
||||
`HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` converge. With
|
||||
`--target-ref` or `--target-tag`, a uniform older desired-state is reported as a
|
||||
read-only promotion plan; `--check` fails only for missing/invalid source files,
|
||||
internal mirror drift, invalid target tags, or partial target drift.
|
||||
read-only promotion plan. Use `--promotion-commit <sha>` 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.
|
||||
|
||||
`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.
|
||||
|
||||
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
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
"profile": "dev",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:16667",
|
||||
"commitId": "7e29522",
|
||||
"artifactState": "published",
|
||||
"commitId": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:090b6f919be3b3c7594ca4073b193351689fc525f478b64d54d393725cb01ecf",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:bc6e836b2bb796b30562ed3ae93d8534146a3b74a310906481c8641646d1b8f0",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:4d9b8944bc52012bb0a4a3a345f8939381d2804032e544d0c98f49fac0e46bfd",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:2582176e2c35a2bfbce4edfd922749397d45f6f1f8c04d0394c501f7d87323ee",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:290341fd6e6f036aa953943072ad58e0d0cfcc0e13950e6090f941b3310755a6",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:02627f880c7ddf2102e2255a43097d1f604cfb7a22215736e328287e27fc5f03",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:e02c270c71f83a2f4624722bb84616bea8374ef43439f27bdce310e4baa8dcca",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:f730323f83c62797f382932582a05df921045c86383dad7951f3d639b32456a9",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:7c40c573078a9c325402bd47b2eb78a46c0c80564a228cf7c9fcf0cfba5bc6d2",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:b141dc88557b8f6391aa082fdb37c6f0db5bc3a76c97bb39ba00354a9208abd8",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:792505139793c3f3da7769f9dc9d1cb3203ff92797305b62e8f12207c69a9831",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:43d1c175091bfb462eddd7dd22a780d6485d86bdb052c36b78201606f48fadc1",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": "7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e29522",
|
||||
"imageTag": "7e29522",
|
||||
"digest": "sha256:c75510038dc33196dc254472e78101d545c0d777a7b33b531f02b39faf2588b5",
|
||||
"publishState": "published",
|
||||
"commitId": "c7de474",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:c7de474",
|
||||
"imageTag": "c7de474",
|
||||
"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": {
|
||||
|
||||
+18
-18
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifestVersion": "v1",
|
||||
"environment": "dev",
|
||||
"commitId": "7e29522",
|
||||
"commitId": "c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:16667",
|
||||
"health": {
|
||||
@@ -167,7 +167,7 @@
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -175,9 +175,9 @@
|
||||
"env": {
|
||||
"HWLAB_ENVIRONMENT": "dev",
|
||||
"HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667",
|
||||
"HWLAB_COMMIT_ID": "7e29522",
|
||||
"HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
|
||||
"HWLAB_IMAGE_TAG": "7e29522",
|
||||
"HWLAB_COMMIT_ID": "c7de474",
|
||||
"HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:c7de474",
|
||||
"HWLAB_IMAGE_TAG": "c7de474",
|
||||
"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",
|
||||
@@ -189,7 +189,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -201,7 +201,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -213,7 +213,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -225,7 +225,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -237,7 +237,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -249,7 +249,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -261,7 +261,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -273,7 +273,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -285,7 +285,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -299,7 +299,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -311,7 +311,7 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -322,13 +322,13 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:c7de474",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {
|
||||
"HWLAB_SKILLS_COMMIT_ID": "7e29522"
|
||||
"HWLAB_SKILLS_COMMIT_ID": "c7de474"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cloud-api",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -49,15 +49,15 @@
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_COMMIT_ID",
|
||||
"value": "7e29522"
|
||||
"value": "c7de474"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_IMAGE",
|
||||
"value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522"
|
||||
"value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:c7de474"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_IMAGE_TAG",
|
||||
"value": "7e29522"
|
||||
"value": "c7de474"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN",
|
||||
@@ -147,7 +147,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cloud-web",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -207,7 +207,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-mgr",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -263,7 +263,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-worker",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:c7de474",
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_AGENT_SESSION_MODE",
|
||||
@@ -306,7 +306,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-gateway",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -366,7 +366,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-gateway-simu",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -426,7 +426,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-box-simu",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -486,7 +486,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-patch-panel",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -540,7 +540,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-router",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -600,7 +600,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-tunnel-client",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -672,7 +672,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-edge-proxy",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -733,7 +733,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cli",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:c7de474",
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_CLI_ENDPOINT",
|
||||
@@ -775,7 +775,7 @@
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-skills",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e29522",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:c7de474",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
@@ -785,7 +785,7 @@
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_SKILLS_COMMIT_ID",
|
||||
"value": "7e29522"
|
||||
"value": "c7de474"
|
||||
}
|
||||
],
|
||||
"readinessProbe": {
|
||||
|
||||
@@ -74,10 +74,11 @@ when the base-image blocker is already closed.
|
||||
Use the read-only planner before refreshing or reviewing deploy desired-state:
|
||||
|
||||
```sh
|
||||
node scripts/deploy-desired-state-plan.mjs --pretty
|
||||
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 --pretty
|
||||
node scripts/deploy-desired-state-plan.mjs --target-tag <tag> --pretty
|
||||
node scripts/deploy-desired-state-plan.mjs --promotion-commit <sha> --check
|
||||
```
|
||||
|
||||
The planner treats `deploy/deploy.json`,
|
||||
@@ -94,6 +95,14 @@ not. A uniform older state under `--target-ref` is a promotion review plan, not
|
||||
failure. 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 <sha>
|
||||
--check`. That mode is stricter than a `--target-ref` plan: every top-level
|
||||
commit, catalog service commit, deploy service image tag, workload image tag,
|
||||
and present mirror env (`HWLAB_COMMIT_ID`, `HWLAB_IMAGE`,
|
||||
`HWLAB_IMAGE_TAG`, `HWLAB_SKILLS_COMMIT_ID`, `HWLAB_IMAGE_DIGEST`) must align
|
||||
with the promotion commit/tag. A stale but internally consistent manifest is a
|
||||
blocker in promotion mode because applying it would roll DEV back.
|
||||
|
||||
The planner does not prove registry existence, publish success, DEV apply, or
|
||||
M3 DEV-LIVE. Only real DEV observation of
|
||||
`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1` with operation,
|
||||
@@ -106,6 +115,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
|
||||
```
|
||||
|
||||
This updates `deploy/deploy.json`, `deploy/artifact-catalog.dev.json`, and
|
||||
@@ -118,6 +128,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
|
||||
```
|
||||
|
||||
The publish-report mode refuses to update the catalog unless the report says
|
||||
|
||||
@@ -153,12 +153,13 @@ remain `status: "blocked"`. Do not turn a base-image or registry blocker into a
|
||||
fake digest or published state. Do not treat runner-process loopback HTTP
|
||||
failure as a publish failure by itself.
|
||||
|
||||
Current rebaseline: the committed DEV artifact report is `status: "published"`
|
||||
for source commit `73b379f`, with `baseImagePreflight.status: "ready"`,
|
||||
`baseImage: "node:20-bookworm-slim"`, and 13/13 required services carrying
|
||||
real registry digests. If a later read-only runner cannot fetch catalog
|
||||
manifests from `127.0.0.1:5000`, treat that as a `#66` registry reachability
|
||||
dimension, not as a missing base-image or missing-publish blocker.
|
||||
Committed DEV artifact reports are evidence snapshots and may lag the deploy
|
||||
desired-state files. Use `scripts/deploy-desired-state-plan.mjs` to see whether
|
||||
the report source matches the desired-state commit, then refresh the catalog
|
||||
from a publish report only when that report proves the same target source
|
||||
commit. If a later read-only runner cannot fetch catalog manifests from
|
||||
`127.0.0.1:5000`, treat that as a `#66` registry reachability dimension, not as
|
||||
a missing base-image or missing-publish blocker.
|
||||
|
||||
## Known Implementation States
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ PROD, read secrets, or use UniDesk runtime services as substitutes.
|
||||
preflight only decides whether #35 may proceed to build/publish; it does not
|
||||
prove that any service image was published.
|
||||
|
||||
Current rebaseline: `reports/dev-gate/dev-artifacts.json` records
|
||||
`baseImagePreflight.status: "ready"` for local `node:20-bookworm-slim` and
|
||||
13/13 published DEV artifacts from source commit `73b379f`. A failed catalog
|
||||
manifest read from a runner process is a registry reachability question for
|
||||
`#66`, not proof that the base image or publish digests are missing.
|
||||
Committed artifact reports may record successful base-image and publish evidence
|
||||
for an older source commit. Compare their source commit with the desired-state
|
||||
commit before using them for refresh or apply review. A failed catalog manifest
|
||||
read from a runner process is a registry reachability question for `#66`, not
|
||||
proof that the base image or publish digests are missing.
|
||||
|
||||
## Command
|
||||
|
||||
|
||||
@@ -133,15 +133,12 @@ node scripts/deploy-desired-state-plan.mjs --target-ref origin/main --pretty
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json
|
||||
```
|
||||
|
||||
Current rebaseline: the committed DEV artifact report proves 13/13 required
|
||||
DEV artifacts from source `73b379f` with `ciPublished=true`,
|
||||
`registryVerified=true`, and real `sha256` digests, and the base-image
|
||||
preflight is `ready` on local `node:20-bookworm-slim`. If the selected
|
||||
`origin/main` target later drifts onto files that change artifact build inputs,
|
||||
the preflight can still reopen `artifact-source-commit` or
|
||||
`dev-artifact-publish` even though the base-image blocker stays closed.
|
||||
Remaining registry manifest-read failures are #66 reachability evidence, not a
|
||||
reason to reopen the base-image blocker.
|
||||
Committed DEV artifact reports can prove required service digests only for the
|
||||
source commit they name. If the selected `origin/main` target drifts onto files
|
||||
that change artifact build inputs, the preflight can still reopen
|
||||
`artifact-source-commit` or `dev-artifact-publish` even though the base-image
|
||||
blocker stays closed. Remaining registry manifest-read failures are #66
|
||||
reachability evidence, not a reason to reopen the base-image blocker.
|
||||
|
||||
This is M3 virtual hardware trusted-loop support only. It does not claim M3
|
||||
DEV-LIVE unless a real `DO1 -> patch-panel -> DI1` chain is observed through
|
||||
|
||||
@@ -58,14 +58,26 @@ 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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
The desired-state files are a single deployment truth: `deploy/deploy.json`,
|
||||
`deploy/artifact-catalog.dev.json`, and `deploy/k8s/base/workloads.yaml` must
|
||||
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 <sha> --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.
|
||||
|
||||
## API, Edge, Health, And FRP
|
||||
|
||||
The public route is fixed:
|
||||
@@ -170,7 +182,7 @@ use the repo-owned scripts below instead of ad hoc shell fragments.
|
||||
| Action | Current controlled entrypoint | Gap to close |
|
||||
| --- | --- | --- |
|
||||
| Check deployment contract and FRP/endpoint drift | `node scripts/deploy-contract-plan.mjs --check` | Fold into `hwlab deploy check --env dev`. |
|
||||
| Plan desired image/workload state | `node scripts/deploy-desired-state-plan.mjs --check` | Fold into `hwlab deploy plan --env dev --file deploy/deploy.json`. |
|
||||
| Plan desired image/workload state | `node scripts/deploy-desired-state-plan.mjs --plan --pretty` and `node scripts/deploy-desired-state-plan.mjs --promotion-commit <sha> --check` | Fold into `hwlab deploy plan --env dev --file deploy/deploy.json` with a promotion commit gate. |
|
||||
| Preflight artifact publish | `node scripts/dev-artifact-publish.mjs --preflight` | Keep as backend for `hwlab artifact publish --preflight`. |
|
||||
| Publish DEV images | `node scripts/dev-artifact-publish.mjs --publish` | CI job should run this and upload digest reports. |
|
||||
| Refresh catalog/desired state | `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json` | Make refresh an explicit CLI/CD step after publish. |
|
||||
|
||||
@@ -6,11 +6,26 @@ import test from "node:test";
|
||||
|
||||
import { buildDesiredStatePlan } from "./src/deploy-desired-state-plan.mjs";
|
||||
|
||||
async function makeFixture({ commitId = "abc1234", workloadTag = commitId, workloadEnvTag = commitId } = {}) {
|
||||
async function makeFixture({
|
||||
commitId = "abc1234",
|
||||
catalogCommitId = commitId,
|
||||
catalogServiceCommitId = catalogCommitId,
|
||||
catalogImageTag = catalogServiceCommitId.slice(0, 7),
|
||||
deployEnvCommitId = commitId,
|
||||
deployEnvImageTag = commitId,
|
||||
deployEnvImage = null,
|
||||
deploySkillsCommitId = null,
|
||||
workloadTag = commitId,
|
||||
workloadEnvCommitId = commitId,
|
||||
workloadEnvImageTag = commitId,
|
||||
workloadEnvImage = null,
|
||||
workloadSkillsCommitId = null
|
||||
} = {}) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-desired-state-"));
|
||||
await mkdir(path.join(root, "deploy/k8s/base"), { recursive: true });
|
||||
await mkdir(path.join(root, "reports/dev-gate"), { recursive: true });
|
||||
const image = `127.0.0.1:5000/hwlab/hwlab-cloud-api:${commitId}`;
|
||||
const catalogImage = `127.0.0.1:5000/hwlab/hwlab-cloud-api:${catalogImageTag}`;
|
||||
const workloadImage = `127.0.0.1:5000/hwlab/hwlab-cloud-api:${workloadTag}`;
|
||||
const deploy = {
|
||||
manifestVersion: "v1",
|
||||
@@ -25,9 +40,10 @@ async function makeFixture({ commitId = "abc1234", workloadTag = commitId, workl
|
||||
profile: "dev",
|
||||
replicas: 1,
|
||||
env: {
|
||||
HWLAB_COMMIT_ID: commitId,
|
||||
HWLAB_IMAGE: image,
|
||||
HWLAB_IMAGE_TAG: commitId
|
||||
HWLAB_COMMIT_ID: deployEnvCommitId,
|
||||
HWLAB_IMAGE: deployEnvImage ?? image,
|
||||
HWLAB_IMAGE_TAG: deployEnvImageTag,
|
||||
...(deploySkillsCommitId ? { HWLAB_SKILLS_COMMIT_ID: deploySkillsCommitId } : {})
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -39,7 +55,7 @@ async function makeFixture({ commitId = "abc1234", workloadTag = commitId, workl
|
||||
profile: "dev",
|
||||
namespace: "hwlab-dev",
|
||||
endpoint: "http://74.48.78.17:16667",
|
||||
commitId,
|
||||
commitId: catalogCommitId,
|
||||
artifactState: "contract-skeleton",
|
||||
publish: {
|
||||
ciPublished: false,
|
||||
@@ -49,9 +65,9 @@ async function makeFixture({ commitId = "abc1234", workloadTag = commitId, workl
|
||||
services: [
|
||||
{
|
||||
serviceId: "hwlab-cloud-api",
|
||||
commitId,
|
||||
image,
|
||||
imageTag: commitId,
|
||||
commitId: catalogServiceCommitId,
|
||||
image: catalogImage,
|
||||
imageTag: catalogImageTag,
|
||||
digest: "not_published",
|
||||
publishState: "skeleton-only",
|
||||
artifactRequired: true
|
||||
@@ -80,9 +96,10 @@ async function makeFixture({ commitId = "abc1234", workloadTag = commitId, workl
|
||||
name: "hwlab-cloud-api",
|
||||
image: workloadImage,
|
||||
env: [
|
||||
{ name: "HWLAB_COMMIT_ID", value: commitId },
|
||||
{ name: "HWLAB_IMAGE", value: image },
|
||||
{ name: "HWLAB_IMAGE_TAG", value: workloadEnvTag }
|
||||
{ name: "HWLAB_COMMIT_ID", value: workloadEnvCommitId },
|
||||
{ name: "HWLAB_IMAGE", value: workloadEnvImage ?? image },
|
||||
{ name: "HWLAB_IMAGE_TAG", value: workloadEnvImageTag },
|
||||
...(workloadSkillsCommitId ? [{ name: "HWLAB_SKILLS_COMMIT_ID", value: workloadSkillsCommitId }] : [])
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -116,8 +133,35 @@ test("target tag review is read-only promotion_pending when current state is uni
|
||||
assert.match(plan.services[0].promotion.deployImage, /:def5678$/u);
|
||||
});
|
||||
|
||||
test("promotion commit check blocks when current desired-state is uniformly older", async () => {
|
||||
const repoRoot = await makeFixture();
|
||||
const plan = await buildDesiredStatePlan({ repoRoot, promotionCommit: "def5678" });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.target.convergence.state, "promotion_mismatch");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "promotion_commit_mismatch"));
|
||||
});
|
||||
|
||||
test("promotion commit check passes when every desired-state field is converged", async () => {
|
||||
const repoRoot = await makeFixture({ commitId: "def5678" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot, promotionCommit: "def5678" });
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.target.convergence.state, "already_promoted");
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("promotion commit check blocks an explicit non-matching target tag", async () => {
|
||||
const repoRoot = await makeFixture({ commitId: "def5678" });
|
||||
const plan = await buildDesiredStatePlan({
|
||||
repoRoot,
|
||||
promotionCommit: "def5678",
|
||||
targetTag: "abc1234"
|
||||
});
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "promotion_tag_mismatch"));
|
||||
});
|
||||
|
||||
test("blocks on mirror drift", async () => {
|
||||
const repoRoot = await makeFixture({ workloadEnvTag: "badcafe" });
|
||||
const repoRoot = await makeFixture({ workloadEnvImageTag: "badcafe" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.summary.blockers, 1);
|
||||
@@ -125,6 +169,16 @@ test("blocks on mirror drift", async () => {
|
||||
assert.match(plan.diagnostics[0].path, /HWLAB_IMAGE_TAG/u);
|
||||
});
|
||||
|
||||
test("blocks on skills commit mirror drift", async () => {
|
||||
const repoRoot = await makeFixture({ deploySkillsCommitId: "badcafe" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) =>
|
||||
diagnostic.code === "mirror_mismatch" &&
|
||||
diagnostic.path.endsWith("HWLAB_SKILLS_COMMIT_ID")
|
||||
));
|
||||
});
|
||||
|
||||
test("blocks on workload image drift", async () => {
|
||||
const repoRoot = await makeFixture({ workloadTag: "badcafe" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
@@ -133,8 +187,16 @@ test("blocks on workload image drift", async () => {
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "image_mismatch"));
|
||||
});
|
||||
|
||||
test("reports partial target drift as a blocker", async () => {
|
||||
const repoRoot = await makeFixture({ workloadTag: "def5678", workloadEnvTag: "def5678" });
|
||||
test("reports partial target drift when catalog top-level commit moves but services stay old", async () => {
|
||||
const repoRoot = await makeFixture({ catalogCommitId: "def5678" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot, targetTag: "def5678" });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.target.convergence.state, "partial_drift");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "partial_target_drift"));
|
||||
});
|
||||
|
||||
test("reports partial target drift as a blocker", async () => {
|
||||
const repoRoot = await makeFixture({ workloadTag: "def5678", workloadEnvImageTag: "def5678" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot, targetTag: "def5678" });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.target.convergence.state, "partial_drift");
|
||||
|
||||
@@ -27,11 +27,21 @@ const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = { check: false, pretty: false, targetRef: null, targetTag: null, help: false };
|
||||
const args = {
|
||||
check: false,
|
||||
plan: false,
|
||||
pretty: false,
|
||||
targetRef: null,
|
||||
targetTag: null,
|
||||
promotionCommit: null,
|
||||
help: false
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
if (arg === "--check") {
|
||||
args.check = true;
|
||||
} else if (arg === "--plan") {
|
||||
args.plan = true;
|
||||
} else if (arg === "--pretty") {
|
||||
args.pretty = true;
|
||||
} else if (arg === "--target-ref") {
|
||||
@@ -40,6 +50,9 @@ function parseArgs(argv) {
|
||||
} else if (arg === "--target-tag") {
|
||||
args.targetTag = requireValue(argv, index, arg);
|
||||
index += 1;
|
||||
} else if (arg === "--promotion-commit") {
|
||||
args.promotionCommit = requireValue(argv, index, arg);
|
||||
index += 1;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
args.help = true;
|
||||
} else {
|
||||
@@ -59,15 +72,18 @@ function requireValue(argv, index, arg) {
|
||||
|
||||
function usage() {
|
||||
return [
|
||||
"usage: node scripts/deploy-desired-state-plan.mjs [--check] [--pretty] [--target-ref REF] [--target-tag TAG]",
|
||||
"usage: node scripts/deploy-desired-state-plan.mjs [--plan] [--check] [--pretty] [--target-ref REF] [--target-tag TAG] [--promotion-commit SHA]",
|
||||
"",
|
||||
"Read-only DEV desired-state commit/image planner.",
|
||||
"",
|
||||
"Options:",
|
||||
" --check exit non-zero only when desired-state sources are missing, invalid, or partially drifted",
|
||||
" --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",
|
||||
" --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",
|
||||
" --promotion-commit SHA",
|
||||
" require every authoritative desired-state commit/image/tag field to match this promotion commit",
|
||||
"",
|
||||
"This command reads repository files only. It does not build, pull, push, kubectl apply, restart services, or touch PROD."
|
||||
].join("\n");
|
||||
@@ -285,8 +301,8 @@ function assertTargetTag(ctx, tag, pathName) {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveTarget(repoRoot, { targetRef, targetTag }, ctx) {
|
||||
if (!targetRef && !targetTag) return null;
|
||||
async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit }, ctx) {
|
||||
if (!targetRef && !targetTag && !promotionCommit) return null;
|
||||
let commitId = null;
|
||||
let shortCommitId = null;
|
||||
if (targetRef) {
|
||||
@@ -304,14 +320,31 @@ async function resolveTarget(repoRoot, { targetRef, targetTag }, ctx) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (promotionCommit) {
|
||||
if (!commitPattern.test(String(promotionCommit))) {
|
||||
addMismatch(ctx, "invalid_promotion_commit", "--promotion-commit", "promotion commit must be a short or full lowercase Git SHA", "7-40 lowercase hex", promotionCommit);
|
||||
} else {
|
||||
const promotionShortCommitId = promotionCommit.slice(0, 7);
|
||||
if (commitId && !commitEquivalent(promotionCommit, commitId)) {
|
||||
addMismatch(ctx, "promotion_commit_ref_mismatch", "--promotion-commit", "promotion commit must match the resolved target ref", commitId, promotionCommit);
|
||||
}
|
||||
commitId = commitId ?? promotionCommit;
|
||||
shortCommitId = shortCommitId ?? promotionShortCommitId;
|
||||
}
|
||||
}
|
||||
const tag = targetTag ?? shortCommitId;
|
||||
if (tag) assertTargetTag(ctx, tag, "--target-tag");
|
||||
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);
|
||||
}
|
||||
return {
|
||||
targetRef,
|
||||
promotionCommit,
|
||||
commitId,
|
||||
shortCommitId,
|
||||
tag,
|
||||
tagSource: targetTag ? "target-tag" : "target-ref-short-commit"
|
||||
tagSource: targetTag ? "target-tag" : promotionCommit ? "promotion-commit-short" : "target-ref-short-commit",
|
||||
requireConvergence: Boolean(promotionCommit)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -350,6 +383,7 @@ function buildTargetConvergence(ctx, target) {
|
||||
const matching = comparable.filter((observation) => observation.matchesTarget === true);
|
||||
const pending = comparable.filter((observation) => observation.matchesTarget === false);
|
||||
const partial = matching.length > 0 && pending.length > 0;
|
||||
const promotionRequired = target.requireConvergence === true;
|
||||
|
||||
if (partial) {
|
||||
addDiagnostic(ctx, {
|
||||
@@ -362,13 +396,28 @@ function buildTargetConvergence(ctx, target) {
|
||||
pending: pending.length
|
||||
}
|
||||
});
|
||||
} else if (promotionRequired && pending.length > 0) {
|
||||
addDiagnostic(ctx, {
|
||||
code: "promotion_commit_mismatch",
|
||||
path: "desired-state",
|
||||
message: "desired-state fields do not match the required promotion commit/tag",
|
||||
expected: {
|
||||
commitId: target.commitId,
|
||||
imageTag: target.tag
|
||||
},
|
||||
actual: {
|
||||
pending: pending.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const state = partial
|
||||
? "partial_drift"
|
||||
: pending.length === 0
|
||||
? "already_promoted"
|
||||
: "promotion_pending";
|
||||
: promotionRequired
|
||||
? "promotion_mismatch"
|
||||
: "promotion_pending";
|
||||
return {
|
||||
state,
|
||||
comparableFields: comparable.length,
|
||||
@@ -379,7 +428,7 @@ function buildTargetConvergence(ctx, target) {
|
||||
};
|
||||
}
|
||||
|
||||
function reportHints(report) {
|
||||
function reportHints(report, desiredCommitId) {
|
||||
if (!report) {
|
||||
return {
|
||||
path: artifactReportPath,
|
||||
@@ -389,15 +438,19 @@ function reportHints(report) {
|
||||
};
|
||||
}
|
||||
const artifactPublish = report.artifactPublish ?? {};
|
||||
const reportCommitId = report.commitId ?? null;
|
||||
const artifactSourceCommitId = artifactPublish.sourceCommitId ?? null;
|
||||
return {
|
||||
path: artifactReportPath,
|
||||
present: true,
|
||||
authoritative: false,
|
||||
note: "report snapshots are contextual evidence only; deploy desired-state remains authoritative in deploy/, not reports/",
|
||||
commitId: report.commitId ?? null,
|
||||
commitId: reportCommitId,
|
||||
matchesDesiredState: reportCommitId ? commitEquivalent(reportCommitId, desiredCommitId) : null,
|
||||
artifactPublish: {
|
||||
status: artifactPublish.status ?? null,
|
||||
sourceCommitId: artifactPublish.sourceCommitId ?? null,
|
||||
sourceCommitId: artifactSourceCommitId,
|
||||
sourceMatchesDesiredState: artifactSourceCommitId ? commitEquivalent(artifactSourceCommitId, desiredCommitId) : null,
|
||||
publishedCount: artifactPublish.publishedCount ?? null,
|
||||
serviceCount: artifactPublish.serviceCount ?? null,
|
||||
registryVerified: artifactPublish.registryVerified ?? null,
|
||||
@@ -422,12 +475,19 @@ function servicePromotion(service, target) {
|
||||
}
|
||||
|
||||
function targetCommands(target) {
|
||||
if (!target?.targetRef) return [];
|
||||
return [
|
||||
if (!target?.targetRef && !target?.promotionCommit) return [];
|
||||
const commands = [];
|
||||
if (target.promotionCommit) {
|
||||
commands.push(`node scripts/deploy-desired-state-plan.mjs --promotion-commit ${target.promotionCommit} --check --pretty`);
|
||||
}
|
||||
if (target.targetRef) {
|
||||
commands.push(
|
||||
`node scripts/deploy-desired-state-plan.mjs --target-ref ${target.targetRef} --pretty`,
|
||||
`node scripts/refresh-artifact-catalog.mjs --target-ref ${target.targetRef} --blocked`,
|
||||
`node scripts/refresh-artifact-catalog.mjs --target-ref ${target.targetRef} --publish-report ${artifactReportPath}`
|
||||
];
|
||||
);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
export async function buildDesiredStatePlan(options = {}) {
|
||||
@@ -441,7 +501,8 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
]);
|
||||
const target = await resolveTarget(repoRoot, {
|
||||
targetRef: options.targetRef ?? null,
|
||||
targetTag: options.targetTag ?? null
|
||||
targetTag: options.targetTag ?? null,
|
||||
promotionCommit: options.promotionCommit ?? null
|
||||
}, ctx);
|
||||
|
||||
const deployServices = deploy.services ?? [];
|
||||
@@ -674,7 +735,7 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
prod: false,
|
||||
devLiveClaim: false
|
||||
},
|
||||
checkSemantics: "--check exits non-zero only for missing/invalid desired-state sources, internal commit/image mirror drift, invalid target tags, or partial target drift. A uniform older desired-state under --target-ref 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 --promotion-commit. A uniform older desired-state under --target-ref alone is a read-only promotion plan, not a check failure.",
|
||||
summary: {
|
||||
desiredCommitId,
|
||||
desiredImageTag,
|
||||
@@ -699,7 +760,7 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
nonAuthoritativeEvidence: [artifactReportPath],
|
||||
note: "This planner reviews source desired-state only. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
|
||||
},
|
||||
reportHints: reportHints(artifactReport),
|
||||
reportHints: reportHints(artifactReport, desiredCommitId),
|
||||
services,
|
||||
diagnostics: ctx.diagnostics
|
||||
};
|
||||
@@ -714,7 +775,8 @@ export async function runDeployDesiredStatePlanCli(argv = process.argv.slice(2),
|
||||
const plan = await buildDesiredStatePlan({
|
||||
repoRoot: options.repoRoot,
|
||||
targetRef: args.targetRef,
|
||||
targetTag: args.targetTag
|
||||
targetTag: args.targetTag,
|
||||
promotionCommit: args.promotionCommit
|
||||
});
|
||||
process.stdout.write(`${JSON.stringify(plan, null, args.pretty ? 2 : 0)}\n`);
|
||||
if (args.check && plan.status === "blocked") {
|
||||
|
||||
Reference in New Issue
Block a user