feat: add dev deploy apply plan report
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# HWLAB DEV Deploy Apply
|
||||
|
||||
This document records the DEV-only apply/preflight path for
|
||||
`pikasTech/HWLAB#33`. It targets only the D601 `hwlab-dev` namespace and must
|
||||
`pikasTech/HWLAB#33` and the apply-plan/report path for
|
||||
`pikasTech/HWLAB#50`. It targets only the D601 `hwlab-dev` namespace and must
|
||||
not be used for PROD.
|
||||
|
||||
## Scope
|
||||
@@ -11,6 +12,9 @@ not be used for PROD.
|
||||
`deploy/artifact-catalog.dev.json`, and `deploy/k8s/dev`.
|
||||
- Validate artifact identity, image commit tags, Kubernetes workload/service
|
||||
shape, DEV health, and cloud-api DB connectivity.
|
||||
- Emit an operator-readable plan in `devDeployApply`: plan/apply boundary,
|
||||
expected artifact commit, target namespace, workloads, services, remaining
|
||||
blockers, minimal manual commands, and rollback hints.
|
||||
- Stop before mutation when artifact publish evidence, registry digests,
|
||||
`kubectl`, DEV health, or cloud-api DB connectivity are missing.
|
||||
|
||||
@@ -20,6 +24,7 @@ Run a preflight that is expected to report blockers on the current skeleton:
|
||||
|
||||
```sh
|
||||
node --check scripts/dev-deploy-apply.mjs
|
||||
node --check scripts/src/dev-deploy-apply.mjs
|
||||
node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report
|
||||
node scripts/validate-dev-gate-report.mjs
|
||||
```
|
||||
@@ -41,6 +46,47 @@ The script refuses PROD flags, secret-read flags, heavyweight e2e flags, and
|
||||
force-push flags. It also refuses to apply if the DEV artifact catalog still
|
||||
contains skeleton-only unpublished images.
|
||||
|
||||
## Report Contract
|
||||
|
||||
The machine-readable result is written to
|
||||
`reports/dev-gate/dev-deploy-report.json` when `--write-report` is used. The
|
||||
report remains compatible with the #33 DEV gate validator and adds these
|
||||
operator fields:
|
||||
|
||||
- `devDeployApply.conclusion`: `blocked` while blockers are open, otherwise
|
||||
`ready` for human approval.
|
||||
- `devDeployApply.applyBoundary`: default no-write behavior, required apply
|
||||
flags, DEV-only write scope, no-write scope, and forbidden actions.
|
||||
- `devDeployApply.artifactPlan`: expected artifact commit, source commit,
|
||||
deploy/catalog commit IDs, publish/registry status, and unpublished services.
|
||||
- `devDeployApply.target`: DEV endpoint and `hwlab-dev` namespace.
|
||||
- `devDeployApply.workloadPlan` and `servicePlan`: planned Kubernetes
|
||||
workloads, containers, images, service IDs, ports, and replica intent.
|
||||
- `devDeployApply.manualCommands`: dry-run/report commands before human
|
||||
approval; the apply command is present only when the plan is ready.
|
||||
- `devDeployApply.rollbackHint`: pre-apply capture commands, deployment
|
||||
rollbacks, suspended job cleanup, and post-rollback health checks.
|
||||
- `devDeployApply.remainingBlockers`: open blockers with concrete unblock
|
||||
hints. These do not claim live apply happened.
|
||||
|
||||
When `conclusion.status` is `blocked`, operators should clear the listed
|
||||
`remainingBlockers` and rerun:
|
||||
|
||||
```sh
|
||||
node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report
|
||||
node scripts/validate-dev-gate-report.mjs
|
||||
```
|
||||
|
||||
When `conclusion.status` is `ready`, run the dry-run plan and validator
|
||||
immediately before human approval:
|
||||
|
||||
```sh
|
||||
node scripts/dev-deploy-apply.mjs --dry-run --write-report
|
||||
node scripts/validate-dev-gate-report.mjs
|
||||
```
|
||||
|
||||
Only after that approval may the DEV-only apply command be used.
|
||||
|
||||
## Current Result
|
||||
|
||||
The current `origin/main` deploy contract is blocked for real DEV apply:
|
||||
@@ -57,7 +103,5 @@ The current `origin/main` deploy contract is blocked for real DEV apply:
|
||||
- `hwlab-cloud-api` has no DEV DB connection configuration and its local health
|
||||
payload reports `db.connected: false`.
|
||||
|
||||
The machine-readable result is written to
|
||||
`reports/dev-gate/dev-deploy-report.json`. No PROD resource, secret value,
|
||||
UniDesk runtime substitute, browser e2e, heavyweight e2e, or force push is part
|
||||
of this flow.
|
||||
No PROD resource, secret value, UniDesk runtime substitute, browser e2e,
|
||||
heavyweight e2e, or force push is part of this flow.
|
||||
|
||||
@@ -3,13 +3,21 @@
|
||||
"$id": "https://hwlab.pikastech.local/reports/dev-gate/dev-deploy-report.json",
|
||||
"reportVersion": "v1",
|
||||
"issue": "pikasTech/HWLAB#33",
|
||||
"supports": [
|
||||
"pikasTech/HWLAB#50",
|
||||
"pikasTech/HWLAB#7",
|
||||
"pikasTech/HWLAB#33",
|
||||
"pikasTech/HWLAB#17",
|
||||
"pikasTech/HWLAB#32",
|
||||
"pikasTech/HWLAB#30"
|
||||
],
|
||||
"taskId": "dev-deploy-apply",
|
||||
"commitId": "d312cd0f3f2e",
|
||||
"commitId": "845133ae149a",
|
||||
"acceptanceLevel": "dev_deploy_apply",
|
||||
"devOnly": true,
|
||||
"prodDisabled": true,
|
||||
"status": "blocked",
|
||||
"generatedAt": "2026-05-21T16:43:19.846Z",
|
||||
"generatedAt": "2026-05-21T17:47:32.687Z",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"sourceContract": {
|
||||
@@ -27,6 +35,7 @@
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-deploy-apply.mjs",
|
||||
"node --check scripts/src/dev-deploy-apply.mjs",
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked"
|
||||
],
|
||||
"localSmoke": {
|
||||
@@ -47,6 +56,9 @@
|
||||
],
|
||||
"evidence": [
|
||||
"artifact services checked: 13",
|
||||
"expected artifact commit: 24eb3bf",
|
||||
"namespace: hwlab-dev",
|
||||
"workloads planned: 13",
|
||||
"kubectl executor: missing",
|
||||
"live health: blocked"
|
||||
],
|
||||
@@ -64,11 +76,535 @@
|
||||
},
|
||||
"devDeployApply": {
|
||||
"mode": "dry-run",
|
||||
"conclusion": {
|
||||
"status": "blocked",
|
||||
"reason": "6 blocker(s) must be cleared before any live DEV apply.",
|
||||
"blockerCount": 6
|
||||
},
|
||||
"applyBoundary": {
|
||||
"currentMode": "dry-run",
|
||||
"defaultNoWrite": true,
|
||||
"mutationAttempted": false,
|
||||
"mutationAllowed": false,
|
||||
"applyRequiresFlags": [
|
||||
"--apply",
|
||||
"--confirm-dev",
|
||||
"--confirmed-non-production"
|
||||
],
|
||||
"writeScope": "kubectl apply -k deploy/k8s/dev, namespace hwlab-dev only",
|
||||
"noWriteScope": "source/catalog/k8s validation, optional DEV health probe, kubectl read, and server-side dry-run only",
|
||||
"forbiddenActions": [
|
||||
"prod-deploy",
|
||||
"secret-read",
|
||||
"runtime-substitute",
|
||||
"unidesk-restart",
|
||||
"code-queue-restart",
|
||||
"backend-core-restart",
|
||||
"heavy-master-e2e",
|
||||
"force-push"
|
||||
]
|
||||
},
|
||||
"mutationAttempted": false,
|
||||
"target": {
|
||||
"environment": "dev",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:6667",
|
||||
"prodDisabled": true
|
||||
},
|
||||
"artifactPlan": {
|
||||
"expectedArtifactCommit": "24eb3bf",
|
||||
"deployCommitId": "24eb3bf",
|
||||
"catalogCommitId": "24eb3bf",
|
||||
"sourceCommitId": "845133ae149a",
|
||||
"serviceCommitIds": [
|
||||
"24eb3bf"
|
||||
],
|
||||
"matchesSourceCommit": false,
|
||||
"published": false,
|
||||
"registryVerified": false,
|
||||
"imageCount": 13,
|
||||
"unpublishedServices": [
|
||||
"hwlab-cloud-api",
|
||||
"hwlab-cloud-web",
|
||||
"hwlab-agent-mgr",
|
||||
"hwlab-agent-worker",
|
||||
"hwlab-gateway",
|
||||
"hwlab-gateway-simu",
|
||||
"hwlab-box-simu",
|
||||
"hwlab-patch-panel",
|
||||
"hwlab-router",
|
||||
"hwlab-tunnel-client",
|
||||
"hwlab-edge-proxy",
|
||||
"hwlab-cli",
|
||||
"hwlab-agent-skills"
|
||||
]
|
||||
},
|
||||
"workloadPlan": [
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-cloud-api",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-cloud-api"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cloud-api",
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-cloud-web",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-cloud-web"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cloud-web",
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"image": "ghcr.io/pikastech/hwlab-cloud-web:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-agent-mgr",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-agent-mgr"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-mgr",
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-mgr:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Job",
|
||||
"name": "hwlab-agent-worker-template",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 0,
|
||||
"serviceIds": [
|
||||
"hwlab-agent-worker"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-worker",
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-worker:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-gateway",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 0,
|
||||
"serviceIds": [
|
||||
"hwlab-gateway"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-gateway",
|
||||
"serviceId": "hwlab-gateway",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-gateway-simu",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 2,
|
||||
"serviceIds": [
|
||||
"hwlab-gateway-simu"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-gateway-simu",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-gateway-simu:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-box-simu",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 2,
|
||||
"serviceIds": [
|
||||
"hwlab-box-simu"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-box-simu",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"image": "ghcr.io/pikastech/hwlab-box-simu:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-patch-panel",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-patch-panel"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-patch-panel",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"image": "ghcr.io/pikastech/hwlab-patch-panel:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-router",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-router"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-router",
|
||||
"serviceId": "hwlab-router",
|
||||
"image": "ghcr.io/pikastech/hwlab-router:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-tunnel-client",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-tunnel-client"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-tunnel-client",
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"image": "ghcr.io/pikastech/hwlab-tunnel-client:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-edge-proxy",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-edge-proxy"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-edge-proxy",
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"image": "ghcr.io/pikastech/hwlab-edge-proxy:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Job",
|
||||
"name": "hwlab-cli-template",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 0,
|
||||
"serviceIds": [
|
||||
"hwlab-cli"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-cli",
|
||||
"serviceId": "hwlab-cli",
|
||||
"image": "ghcr.io/pikastech/hwlab-cli:24eb3bf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Deployment",
|
||||
"name": "hwlab-agent-skills",
|
||||
"namespace": "hwlab-dev",
|
||||
"replicas": 1,
|
||||
"serviceIds": [
|
||||
"hwlab-agent-skills"
|
||||
],
|
||||
"containers": [
|
||||
{
|
||||
"name": "hwlab-agent-skills",
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"image": "ghcr.io/pikastech/hwlab-agent-skills:24eb3bf"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"servicePlan": [
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-cloud-api",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 6667,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-cloud-web",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 8080,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-gateway",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-gateway",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7001,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-cli",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-cli",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7501,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-agent-skills",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7502,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-agent-mgr",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7410,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-gateway-simu",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7101,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-box-simu",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7201,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-patch-panel",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7301,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-router",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-router",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7401,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-tunnel-client",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 7402,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "Service",
|
||||
"name": "hwlab-edge-proxy",
|
||||
"namespace": "hwlab-dev",
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"type": "ClusterIP",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 6667,
|
||||
"targetPort": "http"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"applyStep": {
|
||||
"status": "not_run",
|
||||
"summary": "Skipped because preflight blockers are open"
|
||||
},
|
||||
"manualCommands": {
|
||||
"status": "blocked",
|
||||
"beforeHumanApproval": [
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
],
|
||||
"afterHumanApproval": [],
|
||||
"summary": "No apply command is offered while blockers remain open."
|
||||
},
|
||||
"rollbackHint": {
|
||||
"namespace": "hwlab-dev",
|
||||
"strategy": "DEV-only Kubernetes rollback using deployment revision history; do not touch PROD or UniDesk services.",
|
||||
"captureBeforeApply": [
|
||||
"kubectl -n hwlab-dev get pods,services,configmaps,deployments,jobs -o wide",
|
||||
"kubectl -n hwlab-dev get deployments -o json"
|
||||
],
|
||||
"deploymentRollbackCommands": [
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-cloud-api",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-cloud-web",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-agent-mgr",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-gateway",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-gateway-simu",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-box-simu",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-patch-panel",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-router",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-tunnel-client",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-edge-proxy",
|
||||
"kubectl -n hwlab-dev rollout undo deployment/hwlab-agent-skills"
|
||||
],
|
||||
"jobCleanupCommands": [
|
||||
"kubectl -n hwlab-dev delete job hwlab-agent-worker-template --ignore-not-found",
|
||||
"kubectl -n hwlab-dev delete job hwlab-cli-template --ignore-not-found"
|
||||
],
|
||||
"postRollbackChecks": [
|
||||
"kubectl -n hwlab-dev rollout status deployment/hwlab-cloud-api --timeout=120s",
|
||||
"curl -fsS http://74.48.78.17:6667/health/live"
|
||||
]
|
||||
},
|
||||
"remainingBlockers": [
|
||||
{
|
||||
"type": "observability_blocker",
|
||||
"scope": "artifact-source-commit",
|
||||
"status": "open",
|
||||
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 845133ae149a",
|
||||
"unblockHint": "Regenerate deploy/deploy.json and deploy/artifact-catalog.dev.json for the source commit that will be promoted."
|
||||
},
|
||||
{
|
||||
"type": "observability_blocker",
|
||||
"scope": "artifact-publish",
|
||||
"status": "open",
|
||||
"summary": "DEV artifact catalog has no CI publish, registry verification, or registry digests",
|
||||
"unblockHint": "Publish DEV images from the intended commit and update the catalog with registry digests and verification evidence."
|
||||
},
|
||||
{
|
||||
"type": "runtime_blocker",
|
||||
"scope": "cloud-api-db-config",
|
||||
"status": "open",
|
||||
"summary": "cloud-api has no DEV DB connection env configured",
|
||||
"unblockHint": "Configure and verify the DEV cloud-api DB connection through health output, without reading secret values."
|
||||
},
|
||||
{
|
||||
"type": "runtime_blocker",
|
||||
"scope": "cloud-api-db",
|
||||
"status": "open",
|
||||
"summary": "cloud-api health reports DB not connected in this artifact",
|
||||
"unblockHint": "Configure and verify the DEV cloud-api DB connection through health output, without reading secret values."
|
||||
},
|
||||
{
|
||||
"type": "environment_blocker",
|
||||
"scope": "kubectl",
|
||||
"status": "open",
|
||||
"summary": "kubectl is not installed in the runner",
|
||||
"unblockHint": "Provide a kubectl client configured for the D601 DEV k3s context without exposing token or secret values."
|
||||
},
|
||||
{
|
||||
"type": "network_blocker",
|
||||
"scope": "dev-health",
|
||||
"status": "open",
|
||||
"summary": "Cannot reach http://74.48.78.17:6667/health/live: connect ECONNREFUSED 74.48.78.17:6667",
|
||||
"unblockHint": "Restore the public DEV health path and confirm it identifies HWLAB dev, not a substitute runtime."
|
||||
}
|
||||
],
|
||||
"artifactEvidence": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
@@ -278,7 +814,7 @@
|
||||
"type": "observability_blocker",
|
||||
"scope": "artifact-source-commit",
|
||||
"status": "open",
|
||||
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source d312cd0f3f2e"
|
||||
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 845133ae149a"
|
||||
},
|
||||
{
|
||||
"type": "observability_blocker",
|
||||
|
||||
@@ -16,8 +16,22 @@ const requiredValidationCommands = [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-deploy-apply.mjs",
|
||||
"node --check scripts/src/dev-deploy-apply.mjs",
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked"
|
||||
];
|
||||
const applyCommand = "node scripts/dev-deploy-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report";
|
||||
const dryRunPlanCommand = "node scripts/dev-deploy-apply.mjs --dry-run --write-report";
|
||||
const blockedDryRunCommand = "node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked --write-report";
|
||||
const forbiddenActions = [
|
||||
"prod-deploy",
|
||||
"secret-read",
|
||||
"runtime-substitute",
|
||||
"unidesk-restart",
|
||||
"code-queue-restart",
|
||||
"backend-core-restart",
|
||||
"heavy-master-e2e",
|
||||
"force-push"
|
||||
];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const flags = new Set(argv.filter((arg) => arg.startsWith("--")));
|
||||
@@ -107,6 +121,173 @@ function serviceIdFor(item, container) {
|
||||
);
|
||||
}
|
||||
|
||||
function uniqueStrings(values) {
|
||||
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
||||
}
|
||||
|
||||
function replicaPlan(item) {
|
||||
if (item?.kind === "Job") {
|
||||
return item?.spec?.suspend === true ? 0 : 1;
|
||||
}
|
||||
return item?.spec?.replicas ?? null;
|
||||
}
|
||||
|
||||
function buildWorkloadPlan(workloads) {
|
||||
return listItems(workloads).map((item) => {
|
||||
const containers = containersFor(item).map((container) => ({
|
||||
name: container.name,
|
||||
serviceId: serviceIdFor(item, container),
|
||||
image: container.image
|
||||
}));
|
||||
return {
|
||||
kind: item?.kind ?? "unknown",
|
||||
name: item?.metadata?.name ?? "unknown",
|
||||
namespace: item?.metadata?.namespace ?? namespace,
|
||||
replicas: replicaPlan(item),
|
||||
serviceIds: uniqueStrings(containers.map((container) => container.serviceId)),
|
||||
containers
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildServicePlan(services) {
|
||||
return listItems(services).map((item) => ({
|
||||
kind: item?.kind ?? "unknown",
|
||||
name: item?.metadata?.name ?? "unknown",
|
||||
namespace: item?.metadata?.namespace ?? namespace,
|
||||
serviceId: item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ?? item?.metadata?.name ?? "unknown",
|
||||
type: item?.spec?.type ?? "unknown",
|
||||
ports: (item?.spec?.ports ?? []).map((port) => ({
|
||||
name: port.name,
|
||||
port: port.port,
|
||||
targetPort: port.targetPort
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
function buildArtifactPlan(deploy, catalog, sourceCommitId, artifactEvidence) {
|
||||
const serviceCommitIds = uniqueStrings([
|
||||
...(catalog?.services ?? []).map((service) => service.commitId),
|
||||
...(artifactEvidence ?? []).map((service) => service.catalogCommitId)
|
||||
]);
|
||||
const expectedArtifactCommit = deploy?.commitId ?? catalog?.commitId ?? "unknown";
|
||||
return {
|
||||
expectedArtifactCommit,
|
||||
deployCommitId: deploy?.commitId ?? "unknown",
|
||||
catalogCommitId: catalog?.commitId ?? "unknown",
|
||||
sourceCommitId,
|
||||
serviceCommitIds,
|
||||
matchesSourceCommit:
|
||||
deploy?.commitId && catalog?.commitId
|
||||
? commitMatchesSource(deploy.commitId, sourceCommitId) && commitMatchesSource(catalog.commitId, sourceCommitId)
|
||||
: false,
|
||||
published: catalog?.publish?.ciPublished === true,
|
||||
registryVerified: catalog?.publish?.registryVerified === true,
|
||||
imageCount: artifactEvidence.length,
|
||||
unpublishedServices: (catalog?.services ?? [])
|
||||
.filter((service) => service.publishState === "skeleton-only" || !service.digest?.startsWith("sha256:"))
|
||||
.map((service) => service.serviceId)
|
||||
};
|
||||
}
|
||||
|
||||
function blockerHint(blocker) {
|
||||
if (blocker.scope === "artifact-publish") {
|
||||
return "Publish DEV images from the intended commit and update the catalog with registry digests and verification evidence.";
|
||||
}
|
||||
if (blocker.scope === "artifact-source-commit") {
|
||||
return "Regenerate deploy/deploy.json and deploy/artifact-catalog.dev.json for the source commit that will be promoted.";
|
||||
}
|
||||
if (blocker.scope === "kubectl" || blocker.scope === "kubectl-version") {
|
||||
return "Provide a kubectl client configured for the D601 DEV k3s context without exposing token or secret values.";
|
||||
}
|
||||
if (blocker.scope === "cluster-read") {
|
||||
return "Restore read-only visibility for pods, services, configmaps, deployments, and jobs in hwlab-dev.";
|
||||
}
|
||||
if (blocker.scope === "dev-health" || blocker.scope === "dev-health-identity") {
|
||||
return "Restore the public DEV health path and confirm it identifies HWLAB dev, not a substitute runtime.";
|
||||
}
|
||||
if (blocker.scope === "dev-health-db" || blocker.scope === "cloud-api-db" || blocker.scope === "cloud-api-db-config") {
|
||||
return "Configure and verify the DEV cloud-api DB connection through health output, without reading secret values.";
|
||||
}
|
||||
return blocker.summary;
|
||||
}
|
||||
|
||||
function buildRemainingBlockers(blockers) {
|
||||
return blockers.map((blocker) => ({
|
||||
type: blocker.type,
|
||||
scope: blocker.scope,
|
||||
status: blocker.status,
|
||||
summary: blocker.summary,
|
||||
unblockHint: oneLine(blockerHint(blocker))
|
||||
}));
|
||||
}
|
||||
|
||||
function buildRollbackHint(workloads) {
|
||||
const deploymentNames = buildWorkloadPlan(workloads)
|
||||
.filter((workload) => workload.kind === "Deployment")
|
||||
.map((workload) => workload.name);
|
||||
const jobNames = buildWorkloadPlan(workloads)
|
||||
.filter((workload) => workload.kind === "Job")
|
||||
.map((workload) => workload.name);
|
||||
|
||||
return {
|
||||
namespace,
|
||||
strategy: "DEV-only Kubernetes rollback using deployment revision history; do not touch PROD or UniDesk services.",
|
||||
captureBeforeApply: [
|
||||
"kubectl -n hwlab-dev get pods,services,configmaps,deployments,jobs -o wide",
|
||||
"kubectl -n hwlab-dev get deployments -o json"
|
||||
],
|
||||
deploymentRollbackCommands: deploymentNames.map((name) => `kubectl -n hwlab-dev rollout undo deployment/${name}`),
|
||||
jobCleanupCommands: jobNames.map((name) => `kubectl -n hwlab-dev delete job ${name} --ignore-not-found`),
|
||||
postRollbackChecks: [
|
||||
"kubectl -n hwlab-dev rollout status deployment/hwlab-cloud-api --timeout=120s",
|
||||
`curl -fsS ${new URL(healthPath, DEV_ENDPOINT).href}`
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function buildManualCommands(status) {
|
||||
if (status === "blocked") {
|
||||
return {
|
||||
status: "blocked",
|
||||
beforeHumanApproval: [blockedDryRunCommand, "node scripts/validate-dev-gate-report.mjs"],
|
||||
afterHumanApproval: [],
|
||||
summary: "No apply command is offered while blockers remain open."
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "ready",
|
||||
beforeHumanApproval: [dryRunPlanCommand, "node scripts/validate-dev-gate-report.mjs"],
|
||||
afterHumanApproval: [applyCommand],
|
||||
summary: "Run the dry-run plan and validator immediately before requesting human approval; live apply still requires the explicit DEV confirmation flags."
|
||||
};
|
||||
}
|
||||
|
||||
function buildApplyBoundary(args, status, applyStep) {
|
||||
return {
|
||||
currentMode: args.apply ? "apply-requested" : "dry-run",
|
||||
defaultNoWrite: !args.apply,
|
||||
mutationAttempted: applyStep.status === "pass" && args.apply,
|
||||
mutationAllowed: status === "pass" && args.apply,
|
||||
applyRequiresFlags: ["--apply", "--confirm-dev", "--confirmed-non-production"],
|
||||
writeScope: "kubectl apply -k deploy/k8s/dev, namespace hwlab-dev only",
|
||||
noWriteScope: "source/catalog/k8s validation, optional DEV health probe, kubectl read, and server-side dry-run only",
|
||||
forbiddenActions
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlanConclusion(status, blockers) {
|
||||
return {
|
||||
status: status === "blocked" ? "blocked" : "ready",
|
||||
reason:
|
||||
status === "blocked"
|
||||
? `${blockers.length} blocker(s) must be cleared before any live DEV apply.`
|
||||
: "All DEV-only preconditions passed; human approval is still required before apply.",
|
||||
blockerCount: blockers.length
|
||||
};
|
||||
}
|
||||
|
||||
function validateArgs(args, blockers) {
|
||||
for (const forbidden of ["--prod", "--production", "--read-secret", "--force-push", "--heavyweight-e2e"]) {
|
||||
if (args.flags.has(forbidden)) {
|
||||
@@ -380,12 +561,19 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
const liveProbe = args.skipLiveProbe ? { status: "not_run", reason: "--skip-live-probe" } : await probeDevEndpoint(blockers);
|
||||
const applyStep = await runApplyStep(args, kubectlPath, blockers);
|
||||
const status = blockers.length > 0 ? "blocked" : "pass";
|
||||
const artifactPlan = buildArtifactPlan(deploy, catalog, commitId, artifactEvidence);
|
||||
const workloadPlan = buildWorkloadPlan(workloads);
|
||||
const servicePlan = buildServicePlan(services);
|
||||
const planConclusion = buildPlanConclusion(status, blockers);
|
||||
const manualCommands = buildManualCommands(status);
|
||||
const remainingBlockers = buildRemainingBlockers(blockers);
|
||||
|
||||
const report = {
|
||||
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-deploy-report.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#33",
|
||||
supports: ["pikasTech/HWLAB#50", "pikasTech/HWLAB#7", "pikasTech/HWLAB#33", "pikasTech/HWLAB#17", "pikasTech/HWLAB#32", "pikasTech/HWLAB#30"],
|
||||
taskId: "dev-deploy-apply",
|
||||
commitId,
|
||||
acceptanceLevel: "dev_deploy_apply",
|
||||
@@ -421,6 +609,9 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
],
|
||||
evidence: [
|
||||
`artifact services checked: ${artifactEvidence.length}`,
|
||||
`expected artifact commit: ${artifactPlan.expectedArtifactCommit}`,
|
||||
`namespace: ${namespace}`,
|
||||
`workloads planned: ${workloadPlan.length}`,
|
||||
`kubectl executor: ${kubectlPath ?? "missing"}`,
|
||||
`live health: ${liveProbe.status}`
|
||||
],
|
||||
@@ -438,8 +629,22 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
},
|
||||
devDeployApply: {
|
||||
mode: args.apply ? "apply" : "dry-run",
|
||||
conclusion: planConclusion,
|
||||
applyBoundary: buildApplyBoundary(args, status, applyStep),
|
||||
mutationAttempted: applyStep.status === "pass" && args.apply,
|
||||
target: {
|
||||
environment: ENVIRONMENT_DEV,
|
||||
namespace,
|
||||
endpoint: DEV_ENDPOINT,
|
||||
prodDisabled: true
|
||||
},
|
||||
artifactPlan,
|
||||
workloadPlan,
|
||||
servicePlan,
|
||||
applyStep,
|
||||
manualCommands,
|
||||
rollbackHint: buildRollbackHint(workloads),
|
||||
remainingBlockers,
|
||||
artifactEvidence,
|
||||
k8sManifest,
|
||||
clusterObservation,
|
||||
|
||||
@@ -30,6 +30,21 @@ const requiredValidationCommands = [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs"
|
||||
];
|
||||
const requiredDevDeployApplySupports = [
|
||||
"pikasTech/HWLAB#50",
|
||||
"pikasTech/HWLAB#7",
|
||||
"pikasTech/HWLAB#33",
|
||||
"pikasTech/HWLAB#17",
|
||||
"pikasTech/HWLAB#32",
|
||||
"pikasTech/HWLAB#30"
|
||||
];
|
||||
const requiredDevDeployApplyValidationCommands = [
|
||||
"node --check scripts/validate-dev-gate-report.mjs",
|
||||
"node scripts/validate-dev-gate-report.mjs",
|
||||
"node --check scripts/dev-deploy-apply.mjs",
|
||||
"node --check scripts/src/dev-deploy-apply.mjs",
|
||||
"node scripts/dev-deploy-apply.mjs --dry-run --expect-blocked"
|
||||
];
|
||||
const requiredPreflightValidationCommands = [
|
||||
"node --check scripts/dev-gate-preflight.mjs",
|
||||
"node --check scripts/src/dev-gate-preflight.mjs",
|
||||
@@ -435,6 +450,10 @@ async function validateReport(relativePath) {
|
||||
});
|
||||
assertUnique(report.devPreconditions.evidence, `${label}.devPreconditions.evidence`);
|
||||
}
|
||||
|
||||
if (report.taskId === "dev-deploy-apply") {
|
||||
assertDevDeployApplyReport(report, label);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertDocumentSet(documentsValue, label, requiredDocs) {
|
||||
@@ -469,6 +488,145 @@ function assertBlockers(blockers, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertCommandList(value, label, { minLength = 1 } = {}) {
|
||||
assertStringArray(value, label, { minLength });
|
||||
assertUnique(value, label);
|
||||
for (const command of value) {
|
||||
assert.ok(!command.includes("--prod"), `${label} must not include PROD command`);
|
||||
assert.ok(!command.includes("--production"), `${label} must not include production command`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertDevDeployApplyReport(report, label) {
|
||||
assert.equal(report.issue, issueFamily.DEV_DEPLOY_APPLY, `${label}.issue`);
|
||||
|
||||
assertStringArray(report.supports, `${label}.supports`, {
|
||||
minLength: requiredDevDeployApplySupports.length
|
||||
});
|
||||
for (const supportedIssue of requiredDevDeployApplySupports) {
|
||||
assert.ok(report.supports.includes(supportedIssue), `${label}.supports missing ${supportedIssue}`);
|
||||
}
|
||||
|
||||
for (const requiredCommand of requiredDevDeployApplyValidationCommands) {
|
||||
assert.ok(
|
||||
report.validationCommands.includes(requiredCommand),
|
||||
`${label}.validationCommands missing ${requiredCommand}`
|
||||
);
|
||||
}
|
||||
|
||||
assertObject(report.devDeployApply, `${label}.devDeployApply`);
|
||||
const plan = report.devDeployApply;
|
||||
for (const field of [
|
||||
"conclusion",
|
||||
"applyBoundary",
|
||||
"target",
|
||||
"artifactPlan",
|
||||
"workloadPlan",
|
||||
"servicePlan",
|
||||
"manualCommands",
|
||||
"rollbackHint",
|
||||
"remainingBlockers"
|
||||
]) {
|
||||
assert.ok(Object.hasOwn(plan, field), `${label}.devDeployApply missing ${field}`);
|
||||
}
|
||||
|
||||
assertObject(plan.conclusion, `${label}.devDeployApply.conclusion`);
|
||||
assert.ok(["blocked", "ready"].includes(plan.conclusion.status), `${label}.devDeployApply.conclusion.status`);
|
||||
assertString(plan.conclusion.reason, `${label}.devDeployApply.conclusion.reason`);
|
||||
assert.equal(plan.conclusion.blockerCount, report.blockers.length, `${label}.devDeployApply.conclusion.blockerCount`);
|
||||
|
||||
assertObject(plan.applyBoundary, `${label}.devDeployApply.applyBoundary`);
|
||||
assert.equal(plan.applyBoundary.defaultNoWrite, plan.mode !== "apply", `${label}.devDeployApply.applyBoundary.defaultNoWrite`);
|
||||
assert.equal(plan.applyBoundary.mutationAttempted, report.devDeployApply.mutationAttempted, `${label}.devDeployApply.applyBoundary.mutationAttempted`);
|
||||
assertStringArray(plan.applyBoundary.applyRequiresFlags, `${label}.devDeployApply.applyBoundary.applyRequiresFlags`, {
|
||||
minLength: 3
|
||||
});
|
||||
for (const flag of ["--apply", "--confirm-dev", "--confirmed-non-production"]) {
|
||||
assert.ok(plan.applyBoundary.applyRequiresFlags.includes(flag), `${label}.devDeployApply.applyBoundary.applyRequiresFlags missing ${flag}`);
|
||||
}
|
||||
assertStringArray(plan.applyBoundary.forbiddenActions, `${label}.devDeployApply.applyBoundary.forbiddenActions`, {
|
||||
minLength: 6
|
||||
});
|
||||
for (const forbiddenAction of ["prod-deploy", "secret-read", "runtime-substitute", "heavy-master-e2e"]) {
|
||||
assert.ok(plan.applyBoundary.forbiddenActions.includes(forbiddenAction), `${label}.devDeployApply.applyBoundary.forbiddenActions missing ${forbiddenAction}`);
|
||||
}
|
||||
assertString(plan.applyBoundary.writeScope, `${label}.devDeployApply.applyBoundary.writeScope`);
|
||||
assertString(plan.applyBoundary.noWriteScope, `${label}.devDeployApply.applyBoundary.noWriteScope`);
|
||||
|
||||
assertObject(plan.target, `${label}.devDeployApply.target`);
|
||||
assert.equal(plan.target.environment, "dev", `${label}.devDeployApply.target.environment`);
|
||||
assert.equal(plan.target.namespace, "hwlab-dev", `${label}.devDeployApply.target.namespace`);
|
||||
assert.equal(plan.target.endpoint, "http://74.48.78.17:6667", `${label}.devDeployApply.target.endpoint`);
|
||||
assert.equal(plan.target.prodDisabled, true, `${label}.devDeployApply.target.prodDisabled`);
|
||||
|
||||
assertObject(plan.artifactPlan, `${label}.devDeployApply.artifactPlan`);
|
||||
for (const field of ["expectedArtifactCommit", "deployCommitId", "catalogCommitId", "sourceCommitId"]) {
|
||||
assertString(plan.artifactPlan[field], `${label}.devDeployApply.artifactPlan.${field}`);
|
||||
}
|
||||
assertStringArray(plan.artifactPlan.serviceCommitIds, `${label}.devDeployApply.artifactPlan.serviceCommitIds`, {
|
||||
minLength: 1
|
||||
});
|
||||
assert.equal(typeof plan.artifactPlan.matchesSourceCommit, "boolean", `${label}.devDeployApply.artifactPlan.matchesSourceCommit`);
|
||||
assert.equal(typeof plan.artifactPlan.published, "boolean", `${label}.devDeployApply.artifactPlan.published`);
|
||||
assert.equal(typeof plan.artifactPlan.registryVerified, "boolean", `${label}.devDeployApply.artifactPlan.registryVerified`);
|
||||
assert.ok(Number.isInteger(plan.artifactPlan.imageCount), `${label}.devDeployApply.artifactPlan.imageCount`);
|
||||
assertStringArray(plan.artifactPlan.unpublishedServices, `${label}.devDeployApply.artifactPlan.unpublishedServices`);
|
||||
|
||||
assertArray(plan.workloadPlan, `${label}.devDeployApply.workloadPlan`);
|
||||
assert.ok(plan.workloadPlan.length >= 1, `${label}.devDeployApply.workloadPlan must not be empty`);
|
||||
assertArray(plan.servicePlan, `${label}.devDeployApply.servicePlan`);
|
||||
assert.ok(plan.servicePlan.length >= 1, `${label}.devDeployApply.servicePlan must not be empty`);
|
||||
for (const [index, workload] of plan.workloadPlan.entries()) {
|
||||
const workloadLabel = `${label}.devDeployApply.workloadPlan[${index}]`;
|
||||
assertObject(workload, workloadLabel);
|
||||
for (const field of ["kind", "name", "namespace", "serviceIds", "containers"]) {
|
||||
assert.ok(Object.hasOwn(workload, field), `${workloadLabel} missing ${field}`);
|
||||
}
|
||||
assert.equal(workload.namespace, "hwlab-dev", `${workloadLabel}.namespace`);
|
||||
assertStringArray(workload.serviceIds, `${workloadLabel}.serviceIds`, { minLength: 1 });
|
||||
assertArray(workload.containers, `${workloadLabel}.containers`);
|
||||
}
|
||||
|
||||
assertObject(plan.manualCommands, `${label}.devDeployApply.manualCommands`);
|
||||
assert.ok(["blocked", "ready"].includes(plan.manualCommands.status), `${label}.devDeployApply.manualCommands.status`);
|
||||
assertCommandList(plan.manualCommands.beforeHumanApproval, `${label}.devDeployApply.manualCommands.beforeHumanApproval`);
|
||||
assertCommandList(plan.manualCommands.afterHumanApproval, `${label}.devDeployApply.manualCommands.afterHumanApproval`, {
|
||||
minLength: 0
|
||||
});
|
||||
assertString(plan.manualCommands.summary, `${label}.devDeployApply.manualCommands.summary`);
|
||||
if (plan.conclusion.status === "blocked") {
|
||||
assert.equal(plan.manualCommands.status, "blocked", `${label}.devDeployApply.manualCommands.status`);
|
||||
assert.equal(plan.manualCommands.afterHumanApproval.length, 0, `${label}.devDeployApply.manualCommands.afterHumanApproval`);
|
||||
}
|
||||
if (plan.conclusion.status === "ready") {
|
||||
assert.equal(plan.manualCommands.status, "ready", `${label}.devDeployApply.manualCommands.status`);
|
||||
assert.ok(
|
||||
plan.manualCommands.afterHumanApproval.includes("node scripts/dev-deploy-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report"),
|
||||
`${label}.devDeployApply.manualCommands.afterHumanApproval missing apply command`
|
||||
);
|
||||
}
|
||||
|
||||
assertObject(plan.rollbackHint, `${label}.devDeployApply.rollbackHint`);
|
||||
assert.equal(plan.rollbackHint.namespace, "hwlab-dev", `${label}.devDeployApply.rollbackHint.namespace`);
|
||||
assertString(plan.rollbackHint.strategy, `${label}.devDeployApply.rollbackHint.strategy`);
|
||||
assertCommandList(plan.rollbackHint.captureBeforeApply, `${label}.devDeployApply.rollbackHint.captureBeforeApply`);
|
||||
assertCommandList(plan.rollbackHint.deploymentRollbackCommands, `${label}.devDeployApply.rollbackHint.deploymentRollbackCommands`);
|
||||
assertCommandList(plan.rollbackHint.jobCleanupCommands, `${label}.devDeployApply.rollbackHint.jobCleanupCommands`);
|
||||
assertCommandList(plan.rollbackHint.postRollbackChecks, `${label}.devDeployApply.rollbackHint.postRollbackChecks`);
|
||||
|
||||
assertArray(plan.remainingBlockers, `${label}.devDeployApply.remainingBlockers`);
|
||||
assert.equal(plan.remainingBlockers.length, report.blockers.length, `${label}.devDeployApply.remainingBlockers.length`);
|
||||
for (const [index, blocker] of plan.remainingBlockers.entries()) {
|
||||
const blockerLabel = `${label}.devDeployApply.remainingBlockers[${index}]`;
|
||||
assertObject(blocker, blockerLabel);
|
||||
for (const field of ["type", "scope", "status", "summary", "unblockHint"]) {
|
||||
assert.ok(Object.hasOwn(blocker, field), `${blockerLabel} missing ${field}`);
|
||||
}
|
||||
assert.ok(blockerTypes.has(blocker.type), `${blockerLabel}.type must be known`);
|
||||
assertString(blocker.unblockHint, `${blockerLabel}.unblockHint`);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateDevEdgeReport(report, label) {
|
||||
for (const field of [
|
||||
"$schema",
|
||||
|
||||
Reference in New Issue
Block a user