fix: rebaseline dev base image evidence (#76)

Co-authored-by: HWLAB Code Queue <code-queue@pikastech.local>
This commit is contained in:
Lyon
2026-05-22 14:20:37 +08:00
committed by GitHub
parent 4f93bc07bf
commit 1af787f63f
10 changed files with 672 additions and 354 deletions
+3 -2
View File
@@ -64,8 +64,9 @@ rules:
The catalog `commitId` is the artifact source commit, not proof that a registry
artifact exists. It must match `deploy/deploy.json`, every service `commitId`,
and the image tag. If it does not match the selected source ref, the DEV gate
preflight reports `artifact-source-commit` as a blocker.
and the image tag. If a later target ref changes artifact build inputs after the
publish source, the DEV gate preflight may reopen `artifact-source-commit` even
when the base-image blocker is already closed.
## Refresh Workflow
+7
View File
@@ -141,6 +141,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.
## Known Implementation States
- `repo-entrypoint`: the repository has `cmd/<serviceId>/main.mjs`.
+9 -3
View File
@@ -13,15 +13,21 @@ PROD, read secrets, or use UniDesk runtime services as substitutes.
- `deploy/deploy.json` and `deploy/artifact-catalog.dev.json` pin service image
names to the DEV registry target, by default
`127.0.0.1:5000/hwlab/<hwlab-service-id>:<commit>`.
- The artifact catalog is `contract-skeleton` only. Service entries must keep
`publishState: "skeleton-only"` and `digest: "not_published"` until real
publish evidence exists.
- The artifact catalog may be either `contract-skeleton` or `published`. A
`published` catalog must be backed by `reports/dev-gate/dev-artifacts.json`
with real `sha256:<64 hex>` digests for every required service.
- The catalog and runtime boundary forbid PROD deployment, secret reads, force
push, real publish claims, and UniDesk runtime substitution.
- A builder base image is separate from HWLAB service artifact images. This
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.
## Command
```sh
+13 -8
View File
@@ -69,22 +69,27 @@ The report is blocked until real DEV-LIVE evidence is attached for the DEV
route and downstream HWLAB runtime services. Current source, local, and dry-run
contracts are useful readiness signals, but they are not live DEV proof.
The legacy base-image blocker is not current when the committed reports show an
approved Node 20 builder base. Artifact/current remains stricter: a target ref
can still keep `artifact-source-commit` or `dev-artifact-publish` open when it
changes artifact build inputs after the publish source commit, even if the
older DEV artifact report contains 13/13 published service digests. Registry
manifest reachability remains separate and belongs to the #66
process/Docker/k3s access dimensions.
## Next-Round Order
The JSON output groups recommended next actions in dependency order:
1. `base-image`: preload or tag an approved Node 20 DEV builder image, then
rerun the base-image and artifact publish preflights.
2. `artifact-publish`, `artifact-catalog`, `ghcr`: publish every frozen HWLAB
service artifact and record immutable registry digests for the selected
`origin/main` commit.
3. `d601-k3s`, `kubectl`, kubeconfig/maintenance bridge: restore read-only
1. `d601-k3s`, `kubectl`, kubeconfig/maintenance bridge: restore read-only
hwlab-dev cluster observability without printing Secrets.
4. `cloud-api-db`: prove DEV cloud-api DB env and health readiness with
2. `cloud-api-db`: prove DEV cloud-api DB env and health readiness with
redacted values only.
5. `dev-edge`, `dev-edge-health`, `dev-ingress-health`: repair
3. `dev-edge`, `dev-edge-health`, `dev-ingress-health`: repair
`:16667`/frp/edge/router reachability, then rerun read-only DEV edge health
before M3/M4/M5 live loops.
4. `registry-manifest-read`: resolve #66 registry reachability without
treating it as a missing publish digest or rerunning heavy publish.
## Boundaries
+27 -6
View File
@@ -42,13 +42,17 @@ The preflight checks:
- `deploy/deploy.json` and `deploy/artifact-catalog.dev.json` are internally
consistent and DEV-only.
- The deploy manifest, artifact catalog, catalog service commits, and image tags
target the selected `origin/main` source commit.
are covered by the selected `origin/main` target. If `origin/main` is a
report-only evidence commit whose changes do not touch artifact build inputs,
the preflight may accept the artifact source recorded in
`reports/dev-gate/dev-artifacts.json`.
- The artifact catalog clearly separates source commit, artifact commit,
`ciPublished`, `registryVerified`, and per-service digest state.
- The artifact catalog has real publish and registry digest evidence, or stays
blocked with `not_published` digests.
- `reports/dev-gate/dev-artifacts.json`, when present, proves all frozen DEV
service artifacts were published for the selected `origin/main` commit.
service artifacts were published for the selected target or for an artifact
source commit that still covers the target.
- Registry capability evidence is split into `process-http-access`,
`docker-daemon-push-access`, and `k3s-pull-access` instead of one ambiguous
"registry reachable" result.
@@ -60,14 +64,17 @@ The preflight checks:
`HWLAB_CLOUD_DB_SSL_MODE=require`.
- The local cloud-api health payload reports DB env presence/missing status with
redacted values only. This is a readiness gate, not live DB evidence.
- `deploy/frp` and `deploy/master-edge` describe the D601-to-master DEV routes
on public ports `16666`/`16667`.
- `deploy/frp` and `deploy/master-edge` describe the D601-to-master DEV route
on public frontend `:16666` and API/edge/live `:16667`. Legacy public
`:6666/:6667` observations are historical/deprecated only and must not be
used as current green evidence or current active blockers.
- The runner can perform read-only `kubectl` probes against `hwlab-dev`.
- `reports/dev-gate/dev-edge-health.json`, when present, records read-only
public edge, frps, tunnel-health, and k3s observability evidence.
- `http://74.48.78.17:16667/health/live` responds.
- Catalog image manifests are visible without reading credentials. This is
artifact catalog evidence and is separate from Docker daemon push access.
- Catalog image manifests are visible without reading credentials. A failed
manifest read is tracked as a #66 registry reachability dimension and must not
be conflated with missing publish digests.
- Deploy images do not point at UniDesk, provider-gateway, or microservice-proxy
substitutes.
@@ -117,3 +124,17 @@ proves every frozen service has a registry digest for that same source commit:
```sh
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json
```
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.
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
the current DEV endpoint.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+276 -194
View File
@@ -14,19 +14,20 @@
"pikasTech/HWLAB#30",
"pikasTech/HWLAB#31",
"pikasTech/HWLAB#33",
"pikasTech/HWLAB#39",
"pikasTech/HWLAB#49",
"pikasTech/HWLAB#66",
"pikasTech/HWLAB#33",
"pikasTech/HWLAB#35",
"pikasTech/HWLAB#48"
"pikasTech/HWLAB#39",
"pikasTech/HWLAB#43",
"pikasTech/HWLAB#48",
"pikasTech/HWLAB#49",
"pikasTech/HWLAB#57",
"pikasTech/HWLAB#66"
],
"target": {
"ref": "origin/main",
"commitId": "a1c3cb58da0067a12424277d47d9d50d0cbf7a9d",
"shortCommitId": "a1c3cb5"
"commitId": "82ad96c5c67c3906732a057d52395ec72851e4a9",
"shortCommitId": "82ad96c"
},
"generatedAt": "2026-05-22T04:31:08.449Z",
"generatedAt": "2026-05-22T05:35:39.805Z",
"mode": "read-only",
"devOnly": true,
"prodDisabled": true,
@@ -51,17 +52,43 @@
"artifactIdentity": {
"source": {
"ref": "origin/main",
"commitId": "a1c3cb58da0067a12424277d47d9d50d0cbf7a9d",
"shortCommitId": "a1c3cb5"
"commitId": "82ad96c5c67c3906732a057d52395ec72851e4a9",
"shortCommitId": "82ad96c"
},
"artifactSource": {
"source": "artifact-publish-report",
"ref": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"commitId": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"shortCommitId": "7e29522"
},
"targetCoverage": {
"sourceKind": "artifact-publish-report",
"sourceRef": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"sourceCommitId": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"sourceShortCommitId": "7e29522",
"targetRef": "origin/main",
"targetCommitId": "82ad96c5c67c3906732a057d52395ec72851e4a9",
"targetShortCommitId": "82ad96c",
"mode": "artifact-input-drift",
"covered": false,
"artifactBuildInputChanged": true,
"changedPathCount": 22,
"changedArtifactInputPaths": [
"internal/mvp-gate/summary.mjs",
"web/hwlab-cloud-web/gate-summary.mjs"
],
"changedNonArtifactPathCount": 20,
"reason": "target changes since the artifact source touch artifact build inputs"
},
"deployManifest": {
"path": "deploy/deploy.json",
"commitId": "73b379f",
"matchesSource": false
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false
},
"artifactCatalog": {
"path": "deploy/artifact-catalog.dev.json",
"commitId": "73b379f",
"commitId": "7e29522",
"artifactState": "published",
"ciPublished": true,
"registryVerified": true,
@@ -73,155 +100,169 @@
},
"requiredServiceCount": 13,
"disabledServiceCount": 0,
"matchesSource": false
"matchesSource": true,
"matchesTarget": false
},
"services": [
{
"serviceId": "hwlab-cloud-api",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f",
"imageTag": "73b379f",
"digest": "sha256:561e50494bf13bf61f83d28b371bc39b860b21333ef9991711853a095bd535bf",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
"imageTag": "7e29522",
"digest": "sha256:090b6f919be3b3c7594ca4073b193351689fc525f478b64d54d393725cb01ecf",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-cloud-web",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f",
"imageTag": "73b379f",
"digest": "sha256:c7939658af9f1109e5384619163dd0d11e1a1de14e7d15d4cbde7c4400b495c2",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e29522",
"imageTag": "7e29522",
"digest": "sha256:bc6e836b2bb796b30562ed3ae93d8534146a3b74a310906481c8641646d1b8f0",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-agent-mgr",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f",
"imageTag": "73b379f",
"digest": "sha256:ae6ca76cd09a7d21e46e0076e12dea1843417a0d59d57b3115915c831e88ebef",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e29522",
"imageTag": "7e29522",
"digest": "sha256:4d9b8944bc52012bb0a4a3a345f8939381d2804032e544d0c98f49fac0e46bfd",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-agent-worker",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:73b379f",
"imageTag": "73b379f",
"digest": "sha256:4d5f8d8d404fc2826256fb690fdea5570285a34d1bdffbaf1e0c7ca39603ffd8",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e29522",
"imageTag": "7e29522",
"digest": "sha256:2582176e2c35a2bfbce4edfd922749397d45f6f1f8c04d0394c501f7d87323ee",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-gateway",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:73b379f",
"imageTag": "73b379f",
"digest": "sha256:ff9f3f3dc6332c3d8e4e9825657c296c9664d84a8d40c3ffeab3c1e91a484008",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e29522",
"imageTag": "7e29522",
"digest": "sha256:290341fd6e6f036aa953943072ad58e0d0cfcc0e13950e6090f941b3310755a6",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-gateway-simu",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f",
"imageTag": "73b379f",
"digest": "sha256:f5ccff29da331e424a482f92394d071e1be90c0244e7137e07ac33d9a7cc4082",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e29522",
"imageTag": "7e29522",
"digest": "sha256:02627f880c7ddf2102e2255a43097d1f604cfb7a22215736e328287e27fc5f03",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-box-simu",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f",
"imageTag": "73b379f",
"digest": "sha256:2a12c1c462b2c01a9fa15efa75a6e1f17ecc8432dfa88bca53f0d8d4b6355daf",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e29522",
"imageTag": "7e29522",
"digest": "sha256:e02c270c71f83a2f4624722bb84616bea8374ef43439f27bdce310e4baa8dcca",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-patch-panel",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f",
"imageTag": "73b379f",
"digest": "sha256:5bc9a1c7ce65026cd7bf6d20a311226b35c796a4d52a914266244bf3c1a17286",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e29522",
"imageTag": "7e29522",
"digest": "sha256:f730323f83c62797f382932582a05df921045c86383dad7951f3d639b32456a9",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-router",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f",
"imageTag": "73b379f",
"digest": "sha256:356f37a7d78cc2f86230c2037f43215ad66979fbe4a949cb9241b5957692d9d8",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e29522",
"imageTag": "7e29522",
"digest": "sha256:7c40c573078a9c325402bd47b2eb78a46c0c80564a228cf7c9fcf0cfba5bc6d2",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-tunnel-client",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f",
"imageTag": "73b379f",
"digest": "sha256:8ca553408a633d7f287ca9440f822fb1458dd9a028eb4db178b466b0b5505c49",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e29522",
"imageTag": "7e29522",
"digest": "sha256:b141dc88557b8f6391aa082fdb37c6f0db5bc3a76c97bb39ba00354a9208abd8",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-edge-proxy",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f",
"imageTag": "73b379f",
"digest": "sha256:480dea831051d45b6d63a489db9c0fa314f88bae35dbf72be4186bf1de356f38",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e29522",
"imageTag": "7e29522",
"digest": "sha256:792505139793c3f3da7769f9dc9d1cb3203ff92797305b62e8f12207c69a9831",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-cli",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cli:73b379f",
"imageTag": "73b379f",
"digest": "sha256:125154cf59727ec230618c65b07a6235c18c1411f9ad51525ae411040169c9b5",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e29522",
"imageTag": "7e29522",
"digest": "sha256:43d1c175091bfb462eddd7dd22a780d6485d86bdb052c36b78201606f48fadc1",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-agent-skills",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f",
"imageTag": "73b379f",
"digest": "sha256:5bdf9a4d17f8f4c7f3ecead99dcbd4de0ac346a2c28fef020b8853cf494df573",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e29522",
"imageTag": "7e29522",
"digest": "sha256:c75510038dc33196dc254472e78101d545c0d777a7b33b531f02b39faf2588b5",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
}
],
"serviceCommitIds": [
"73b379f"
"7e29522"
],
"matchesSource": false,
"publishVerified": true,
@@ -233,7 +274,7 @@
"registryCapabilities": {
"version": "v1",
"registryPrefix": "127.0.0.1:5000/hwlab",
"generatedAt": "2026-05-22T04:31:07.663Z",
"generatedAt": "2026-05-22T05:35:38.863Z",
"classification": "degraded",
"dimensions": {
"processHttpAccess": {
@@ -281,7 +322,7 @@
"names": "unidesk-artifact-registry",
"ports": "127.0.0.1:5000->5000/tcp",
"state": "running",
"status": "Up 27 hours"
"status": "Up 28 hours"
}
],
"pushAttempted": false,
@@ -420,22 +461,48 @@
"id": "target-commit-pinning",
"category": "contract",
"status": "blocked",
"summary": "Source origin/main a1c3cb5 is not covered by deploy commit 73b379f, catalog commit 73b379f, service commits 73b379f.",
"summary": "Source origin/main 82ad96c is not covered by artifact source 7e29522: target changes since the artifact source touch artifact build inputs.",
"evidence": [
{
"source": {
"ref": "origin/main",
"commitId": "a1c3cb58da0067a12424277d47d9d50d0cbf7a9d",
"shortCommitId": "a1c3cb5"
"commitId": "82ad96c5c67c3906732a057d52395ec72851e4a9",
"shortCommitId": "82ad96c"
},
"artifactSource": {
"source": "artifact-publish-report",
"ref": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"commitId": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"shortCommitId": "7e29522"
},
"targetCoverage": {
"sourceKind": "artifact-publish-report",
"sourceRef": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"sourceCommitId": "7e29522b65c8a316b7c3fde12bebcb5f6baf8c33",
"sourceShortCommitId": "7e29522",
"targetRef": "origin/main",
"targetCommitId": "82ad96c5c67c3906732a057d52395ec72851e4a9",
"targetShortCommitId": "82ad96c",
"mode": "artifact-input-drift",
"covered": false,
"artifactBuildInputChanged": true,
"changedPathCount": 22,
"changedArtifactInputPaths": [
"internal/mvp-gate/summary.mjs",
"web/hwlab-cloud-web/gate-summary.mjs"
],
"changedNonArtifactPathCount": 20,
"reason": "target changes since the artifact source touch artifact build inputs"
},
"deployManifest": {
"path": "deploy/deploy.json",
"commitId": "73b379f",
"matchesSource": false
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false
},
"artifactCatalog": {
"path": "deploy/artifact-catalog.dev.json",
"commitId": "73b379f",
"commitId": "7e29522",
"artifactState": "published",
"ciPublished": true,
"registryVerified": true,
@@ -447,155 +514,169 @@
},
"requiredServiceCount": 13,
"disabledServiceCount": 0,
"matchesSource": false
"matchesSource": true,
"matchesTarget": false
},
"services": [
{
"serviceId": "hwlab-cloud-api",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f",
"imageTag": "73b379f",
"digest": "sha256:561e50494bf13bf61f83d28b371bc39b860b21333ef9991711853a095bd535bf",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
"imageTag": "7e29522",
"digest": "sha256:090b6f919be3b3c7594ca4073b193351689fc525f478b64d54d393725cb01ecf",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-cloud-web",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f",
"imageTag": "73b379f",
"digest": "sha256:c7939658af9f1109e5384619163dd0d11e1a1de14e7d15d4cbde7c4400b495c2",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e29522",
"imageTag": "7e29522",
"digest": "sha256:bc6e836b2bb796b30562ed3ae93d8534146a3b74a310906481c8641646d1b8f0",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-agent-mgr",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f",
"imageTag": "73b379f",
"digest": "sha256:ae6ca76cd09a7d21e46e0076e12dea1843417a0d59d57b3115915c831e88ebef",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e29522",
"imageTag": "7e29522",
"digest": "sha256:4d9b8944bc52012bb0a4a3a345f8939381d2804032e544d0c98f49fac0e46bfd",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-agent-worker",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:73b379f",
"imageTag": "73b379f",
"digest": "sha256:4d5f8d8d404fc2826256fb690fdea5570285a34d1bdffbaf1e0c7ca39603ffd8",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e29522",
"imageTag": "7e29522",
"digest": "sha256:2582176e2c35a2bfbce4edfd922749397d45f6f1f8c04d0394c501f7d87323ee",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-gateway",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:73b379f",
"imageTag": "73b379f",
"digest": "sha256:ff9f3f3dc6332c3d8e4e9825657c296c9664d84a8d40c3ffeab3c1e91a484008",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e29522",
"imageTag": "7e29522",
"digest": "sha256:290341fd6e6f036aa953943072ad58e0d0cfcc0e13950e6090f941b3310755a6",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-gateway-simu",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f",
"imageTag": "73b379f",
"digest": "sha256:f5ccff29da331e424a482f92394d071e1be90c0244e7137e07ac33d9a7cc4082",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e29522",
"imageTag": "7e29522",
"digest": "sha256:02627f880c7ddf2102e2255a43097d1f604cfb7a22215736e328287e27fc5f03",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-box-simu",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f",
"imageTag": "73b379f",
"digest": "sha256:2a12c1c462b2c01a9fa15efa75a6e1f17ecc8432dfa88bca53f0d8d4b6355daf",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e29522",
"imageTag": "7e29522",
"digest": "sha256:e02c270c71f83a2f4624722bb84616bea8374ef43439f27bdce310e4baa8dcca",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-patch-panel",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f",
"imageTag": "73b379f",
"digest": "sha256:5bc9a1c7ce65026cd7bf6d20a311226b35c796a4d52a914266244bf3c1a17286",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e29522",
"imageTag": "7e29522",
"digest": "sha256:f730323f83c62797f382932582a05df921045c86383dad7951f3d639b32456a9",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-router",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f",
"imageTag": "73b379f",
"digest": "sha256:356f37a7d78cc2f86230c2037f43215ad66979fbe4a949cb9241b5957692d9d8",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e29522",
"imageTag": "7e29522",
"digest": "sha256:7c40c573078a9c325402bd47b2eb78a46c0c80564a228cf7c9fcf0cfba5bc6d2",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-tunnel-client",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f",
"imageTag": "73b379f",
"digest": "sha256:8ca553408a633d7f287ca9440f822fb1458dd9a028eb4db178b466b0b5505c49",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e29522",
"imageTag": "7e29522",
"digest": "sha256:b141dc88557b8f6391aa082fdb37c6f0db5bc3a76c97bb39ba00354a9208abd8",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-edge-proxy",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f",
"imageTag": "73b379f",
"digest": "sha256:480dea831051d45b6d63a489db9c0fa314f88bae35dbf72be4186bf1de356f38",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e29522",
"imageTag": "7e29522",
"digest": "sha256:792505139793c3f3da7769f9dc9d1cb3203ff92797305b62e8f12207c69a9831",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-cli",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cli:73b379f",
"imageTag": "73b379f",
"digest": "sha256:125154cf59727ec230618c65b07a6235c18c1411f9ad51525ae411040169c9b5",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e29522",
"imageTag": "7e29522",
"digest": "sha256:43d1c175091bfb462eddd7dd22a780d6485d86bdb052c36b78201606f48fadc1",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
},
{
"serviceId": "hwlab-agent-skills",
"commitId": "73b379f",
"matchesSource": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f",
"imageTag": "73b379f",
"digest": "sha256:5bdf9a4d17f8f4c7f3ecead99dcbd4de0ac346a2c28fef020b8853cf494df573",
"commitId": "7e29522",
"matchesSource": true,
"matchesTarget": false,
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e29522",
"imageTag": "7e29522",
"digest": "sha256:c75510038dc33196dc254472e78101d545c0d777a7b33b531f02b39faf2588b5",
"publishState": "published",
"artifactRequired": true,
"notPublishedReason": null
}
],
"serviceCommitIds": [
"73b379f"
"7e29522"
],
"matchesSource": false,
"publishVerified": true,
@@ -614,7 +695,7 @@
"evidence": [
{
"path": "deploy/artifact-catalog.dev.json",
"commitId": "73b379f",
"commitId": "7e29522",
"artifactState": "published",
"ciPublished": true,
"registryVerified": true,
@@ -626,7 +707,8 @@
},
"requiredServiceCount": 13,
"disabledServiceCount": 0,
"matchesSource": false
"matchesSource": true,
"matchesTarget": false
}
]
},
@@ -696,7 +778,7 @@
"names": "unidesk-artifact-registry",
"ports": "127.0.0.1:5000->5000/tcp",
"state": "running",
"status": "Up 27 hours"
"status": "Up 28 hours"
}
],
"pushAttempted": false,
@@ -834,7 +916,7 @@
{
"version": "v1",
"registryPrefix": "127.0.0.1:5000/hwlab",
"generatedAt": "2026-05-22T04:31:07.663Z",
"generatedAt": "2026-05-22T05:35:38.863Z",
"classification": "degraded",
"dimensions": {
"processHttpAccess": {
@@ -882,7 +964,7 @@
"names": "unidesk-artifact-registry",
"ports": "127.0.0.1:5000->5000/tcp",
"state": "running",
"status": "Up 27 hours"
"status": "Up 28 hours"
}
],
"pushAttempted": false,
@@ -1218,7 +1300,7 @@
"id": "dev-artifact-publish-report",
"category": "registry",
"status": "blocked",
"summary": "DEV artifact publish report status is published with 13/13 required services published for source 73b379f4441d47a99dfd978a80fd43727410af51; source states resolved: 13 source-present, 0 intentionally-disabled.",
"summary": "DEV artifact publish report status is published with 13/13 required services published for source 7e29522b65c8a316b7c3fde12bebcb5f6baf8c33; source states resolved: 13 source-present, 0 intentionally-disabled.",
"evidence": []
},
{
@@ -1236,19 +1318,19 @@
"host": "74.48.78.17",
"port": 16667,
"status": "connected",
"durationMs": 212
"durationMs": 228
},
{
"host": "74.48.78.17",
"port": 7000,
"status": "connected",
"durationMs": 1205
"durationMs": 212
},
{
"host": "74.48.78.17",
"port": 7402,
"status": "connected",
"durationMs": 232
"durationMs": 216
}
],
"kubernetesObservable": true
@@ -1324,7 +1406,7 @@
"method": "GET",
"status": 200,
"statusText": "OK",
"body": "{\"serviceId\":\"hwlab-cloud-api\",\"environment\":\"dev\",\"status\":\"degraded\",\"service\":{\"id\":\"hwlab-cloud-api\",\"role\":\"cloud-api\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"cb35ada\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada\",\"tag\":\"cb35ada\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T04:31:08.245Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"c"
"body": "{\"serviceId\":\"hwlab-cloud-api\",\"environment\":\"dev\",\"status\":\"degraded\",\"service\":{\"id\":\"hwlab-cloud-api\",\"role\":\"cloud-api\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"cb35ada\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada\",\"tag\":\"cb35ada\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T05:35:39.607Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"c"
}
]
},
@@ -1336,105 +1418,105 @@
"evidence": [
{
"serviceId": "hwlab-cloud-api",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cloud-api/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cloud-api/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-cloud-web",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cloud-web/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cloud-web/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-agent-mgr",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-mgr/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-mgr/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-agent-worker",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-worker/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-worker/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-gateway",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-gateway/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-gateway/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-gateway-simu",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-gateway-simu/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-gateway-simu/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-box-simu",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-box-simu/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-box-simu/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-patch-panel",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-patch-panel/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-patch-panel/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-router",
"image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-router:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-router/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-router/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-tunnel-client",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-tunnel-client/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-tunnel-client/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-edge-proxy",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-edge-proxy/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-edge-proxy/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-cli",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-cli:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cli/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cli/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
},
{
"serviceId": "hwlab-agent-skills",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f",
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:7e29522",
"ok": false,
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-skills/manifests/73b379f",
"url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-skills/manifests/7e29522",
"method": "HEAD",
"error": "fetch failed ECONNREFUSED 127.0.0.1 5000"
}
@@ -1446,7 +1528,7 @@
"status": "open",
"type": "contract_blocker",
"scope": "artifact-source-commit",
"summary": "source commit origin/main a1c3cb5 does not match deploy/catalog artifact identity: deploy=73b379f, catalog=73b379f, services=73b379f.",
"summary": "source commit origin/main 82ad96c is not covered by artifact source 7e29522; target changes since the artifact source touch artifact build inputs.",
"nextTask": "Refresh without fake digests using `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked`, or after a successful publish run `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json`."
},
{
@@ -1460,8 +1542,8 @@
"status": "open",
"type": "runtime_blocker",
"scope": "dev-artifact-publish",
"summary": "reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for origin/main a1c3cb5; current status is published with 13/13 required services published and source states resolved: 13 source-present, 0 intentionally-disabled.",
"nextTask": "Complete DEV artifact publishing for every enabled required HWLAB service at the current origin/main commit and record immutable registry digests; keep disabled services marked not_published with reasons."
"summary": "reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for origin/main 82ad96c; current status is published with 13/13 required services published and source states resolved: 13 source-present, 0 intentionally-disabled.",
"nextTask": "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons."
},
{
"status": "open",
@@ -8,6 +8,8 @@ import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/proto
export const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const deprecatedLegacyPublicPort = 6000 + 667;
const deprecatedLegacyPublicEndpoint = `http://74.48.78.17:${deprecatedLegacyPublicPort}`;
const levels = Object.freeze(["SOURCE", "LOCAL", "DRY-RUN", "DEV-LIVE", "BLOCKED"]);
const statusRank = Object.freeze({
pass: 0,
@@ -263,9 +265,7 @@ function isEdgeOrFrpBlocker(blocker) {
blocker.scope.includes("ingress") ||
blocker.scope.includes("frp") ||
blocker.summary.includes(":16667") ||
blocker.summary.includes("16667") ||
blocker.summary.includes(":6667") ||
blocker.summary.includes("6667");
blocker.summary.includes("16667");
}
function sourceEntry(entry) {
@@ -519,7 +519,7 @@ function hasCurrentLiveEdgeEvidence(edgeReport) {
function isStaleLegacyIngressBlocker(blocker, sourceReport, reports) {
const text = `${blocker.scope ?? ""} ${blocker.summary ?? ""}`;
const staleLegacyPort = text.includes("74.48.78.17:6667");
const staleLegacyPort = text.includes(deprecatedLegacyPublicEndpoint);
const stalePreflightEdgeProbe = sourceReport.path === reportPaths.devPreflight &&
blocker.type === "network_blocker" &&
(blocker.scope === "dev-edge" || blocker.scope === "dev-edge-health") &&
+200 -24
View File
@@ -17,9 +17,9 @@ const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultReportPath = "reports/dev-gate/dev-preflight-report.json";
const issue = "pikasTech/HWLAB#34";
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49", "#66"].map(
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#35", "#39", "#43", "#48", "#49", "#57", "#66"].map(
(id) => `pikasTech/HWLAB${id}`
).concat(["pikasTech/HWLAB#33", "pikasTech/HWLAB#35", "pikasTech/HWLAB#48"]);
);
const forbiddenActions = [
"prod-deploy",
"secret-material-read",
@@ -39,6 +39,20 @@ const blockerTypes = new Set([
"safety_blocker"
]);
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const artifactBuildInputPrefixes = [
"cmd/",
"internal/",
"skills/",
"tools/",
"web/"
];
const artifactBuildInputFiles = new Set([
"package.json",
"package-lock.json",
"npm-shrinkwrap.json",
"scripts/dev-artifact-publish.mjs",
"scripts/src/dev-artifact-services.mjs"
]);
function parseArgs(argv) {
const args = {
@@ -210,6 +224,125 @@ function matchesTargetCommit(value, targetCommit, targetShortCommit) {
return value === targetCommit || value === targetShortCommit;
}
function isArtifactBuildInputPath(relativePath) {
return artifactBuildInputFiles.has(relativePath) ||
artifactBuildInputPrefixes.some((prefix) => relativePath.startsWith(prefix));
}
async function resolveCommit(ref) {
if (!ref) return null;
const result = await run("git", ["rev-parse", `${ref}^{commit}`]);
if (!result.ok || !/^[a-f0-9]{40}$/u.test(result.stdout)) return null;
return result.stdout;
}
async function sourceCoverageFor({ catalog, artifactReport, targetCommit, targetShortCommit, targetRef }) {
const reportSource = artifactReport?.artifactPublish?.sourceCommitId ?? artifactReport?.commitId ?? null;
const sourceRef = reportSource ?? catalog.commitId;
const sourceKind = reportSource ? "artifact-publish-report" : "artifact-catalog";
const sourceCommit = await resolveCommit(sourceRef);
if (!sourceCommit) {
return {
sourceKind,
sourceRef,
sourceCommitId: null,
sourceShortCommitId: null,
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "unresolved-source",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: `artifact source ${sourceRef ?? "unknown"} does not resolve to a local git commit`
};
}
if (matchesTargetCommit(sourceCommit, targetCommit, targetShortCommit)) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "exact-target",
covered: true,
artifactBuildInputChanged: false,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: "artifact source commit exactly matches the target ref"
};
}
const ancestor = await run("git", ["merge-base", "--is-ancestor", sourceCommit, targetCommit]);
if (!ancestor.ok) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "non-ancestor-source",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: `artifact source ${sourceCommit.slice(0, 12)} is not an ancestor of ${targetRef} ${targetShortCommit}`
};
}
const diff = await run("git", ["diff", "--name-only", `${sourceCommit}..${targetCommit}`]);
if (!diff.ok) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "diff-unavailable",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: diff.stderr || diff.error || "git diff failed"
};
}
const changedPaths = diff.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
const changedArtifactInputPaths = changedPaths.filter(isArtifactBuildInputPath);
const covered = changedArtifactInputPaths.length === 0;
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: covered ? "report-only-target" : "artifact-input-drift",
covered,
artifactBuildInputChanged: changedArtifactInputPaths.length > 0,
changedPathCount: changedPaths.length,
changedArtifactInputPaths,
changedNonArtifactPathCount: changedPaths.length - changedArtifactInputPaths.length,
reason: covered
? "target changes since the artifact source do not touch artifact build inputs"
: "target changes since the artifact source touch artifact build inputs"
};
}
function uniqueSorted(values) {
return [...new Set(values)].sort();
}
@@ -238,15 +371,24 @@ function disabledCatalogServices(catalog) {
return (catalog.services ?? []).filter((service) => service.artifactRequired === false);
}
function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef }) {
async function artifactIdentityFor({ deploy, catalog, artifactReport, targetCommit, targetShortCommit, targetRef }) {
const targetCoverage = await sourceCoverageFor({
catalog,
artifactReport,
targetCommit,
targetShortCommit,
targetRef
});
const artifactSourceCommit = targetCoverage.sourceCommitId ?? targetCommit;
const artifactSourceShortCommit = targetCoverage.sourceShortCommitId ?? targetShortCommit;
const serviceCommitIds = uniqueSorted((catalog.services ?? []).map((service) => service.commitId));
const digestCounts = catalogDigestCounts(catalog);
const requiredServices = requiredCatalogServices(catalog);
const disabledServices = disabledCatalogServices(catalog);
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, targetCommit, targetShortCommit);
const deployMatchesSource = matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit);
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, artifactSourceCommit, artifactSourceShortCommit);
const deployMatchesSource = matchesTargetCommit(deploy.commitId, artifactSourceCommit, artifactSourceShortCommit);
const servicesMatchSource = serviceCommitIds.length === 1 &&
matchesTargetCommit(serviceCommitIds[0], targetCommit, targetShortCommit);
matchesTargetCommit(serviceCommitIds[0], artifactSourceCommit, artifactSourceShortCommit);
const allRequiredDigestsPublished = requiredServices.length > 0 &&
requiredServices.every((service) => digestPattern.test(service.digest) && service.publishState === "published") &&
disabledServices.every((service) => service.digest === "not_published" && service.publishState === "skeleton-only") &&
@@ -262,10 +404,18 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
commitId: targetCommit,
shortCommitId: targetShortCommit
},
artifactSource: {
source: targetCoverage.sourceKind,
ref: targetCoverage.sourceRef,
commitId: targetCoverage.sourceCommitId,
shortCommitId: targetCoverage.sourceShortCommitId
},
targetCoverage,
deployManifest: {
path: "deploy/deploy.json",
commitId: deploy.commitId,
matchesSource: deployMatchesSource
matchesSource: deployMatchesSource,
matchesTarget: matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit)
},
artifactCatalog: {
path: "deploy/artifact-catalog.dev.json",
@@ -277,12 +427,14 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
digestCounts,
requiredServiceCount: requiredServices.length,
disabledServiceCount: disabledServices.length,
matchesSource: catalogMatchesSource
matchesSource: catalogMatchesSource,
matchesTarget: matchesTargetCommit(catalog.commitId, targetCommit, targetShortCommit)
},
services: (catalog.services ?? []).map((service) => ({
serviceId: service.serviceId,
commitId: service.commitId,
matchesSource: matchesTargetCommit(service.commitId, targetCommit, targetShortCommit),
matchesSource: matchesTargetCommit(service.commitId, artifactSourceCommit, artifactSourceShortCommit),
matchesTarget: matchesTargetCommit(service.commitId, targetCommit, targetShortCommit),
image: service.image,
imageTag: service.imageTag,
digest: service.digest,
@@ -291,7 +443,7 @@ function artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit,
notPublishedReason: service.notPublishedReason ?? null
})),
serviceCommitIds,
matchesSource: deployMatchesSource && catalogMatchesSource && servicesMatchSource,
matchesSource: targetCoverage.covered && deployMatchesSource && catalogMatchesSource && servicesMatchSource,
publishVerified,
refreshCommands: {
blocked: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --blocked`,
@@ -399,9 +551,16 @@ function makeReporter() {
};
}
function validateLocalContracts(reporter, contracts, targetShortCommit, targetCommit, targetRef) {
async function validateLocalContracts(reporter, contracts, optionalReports, targetShortCommit, targetCommit, targetRef) {
const [deploy, catalog, namespace, workloads, services, devKustomization, healthContract, masterEdge] = contracts;
const artifactIdentity = artifactIdentityFor({ deploy, catalog, targetCommit, targetShortCommit, targetRef });
const artifactIdentity = await artifactIdentityFor({
deploy,
catalog,
artifactReport: optionalReports.artifactPublish,
targetCommit,
targetShortCommit,
targetRef
});
try {
assertStaticContract(deploy, catalog);
@@ -417,25 +576,30 @@ function validateLocalContracts(reporter, contracts, targetShortCommit, targetCo
}
if (artifactIdentity.matchesSource) {
const coverage = artifactIdentity.targetCoverage;
const coverageSuffix = coverage.mode === "exact-target"
? "exact target commit"
: `${coverage.mode}; ${coverage.changedPathCount} changed path(s), ${coverage.changedArtifactInputPaths.length} artifact input change(s)`;
reporter.check(
"target-commit-pinning",
"contract",
"pass",
`Source ${targetRef} ${targetShortCommit} matches deploy/catalog artifact commit ${artifactIdentity.artifactCatalog.commitId}.`,
`Source ${targetRef} ${targetShortCommit} is covered by artifact source ${artifactIdentity.artifactSource.shortCommitId}; ${coverageSuffix}.`,
[artifactIdentity]
);
} else {
const coverage = artifactIdentity.targetCoverage;
reporter.check(
"target-commit-pinning",
"contract",
"blocked",
`Source ${targetRef} ${targetShortCommit} is not covered by deploy commit ${deploy.commitId}, catalog commit ${catalog.commitId}, service commits ${artifactIdentity.serviceCommitIds.join(", ")}.`,
`Source ${targetRef} ${targetShortCommit} is not covered by artifact source ${artifactIdentity.artifactSource.shortCommitId ?? "unknown"}: ${coverage.reason}.`,
[artifactIdentity]
);
reporter.block({
type: "contract_blocker",
scope: "artifact-source-commit",
summary: `source commit ${targetRef} ${targetShortCommit} does not match deploy/catalog artifact identity: deploy=${deploy.commitId}, catalog=${catalog.commitId}, services=${artifactIdentity.serviceCommitIds.join(", ")}.`,
summary: `source commit ${targetRef} ${targetShortCommit} is not covered by artifact source ${artifactIdentity.artifactSource.shortCommitId ?? "unknown"}; ${coverage.reason}.`,
nextTask: `Refresh without fake digests using \`${artifactIdentity.refreshCommands.blocked}\`, or after a successful publish run \`${artifactIdentity.refreshCommands.published}\`.`
});
}
@@ -480,7 +644,7 @@ function validateLocalContracts(reporter, contracts, targetShortCommit, targetCo
return { deploy, catalog, masterEdge, artifactIdentity };
}
function validateArtifactPublishReport(reporter, artifactReport, targetShortCommit, targetCommit, targetRef) {
function validateArtifactPublishReport(reporter, artifactReport, artifactIdentity, targetShortCommit, targetCommit, targetRef) {
if (!artifactReport) {
reporter.check("dev-artifact-publish-report", "registry", "blocked", "No DEV artifact publish report is present.");
reporter.block({
@@ -510,15 +674,26 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
);
const sourceMatchesTarget = [targetCommit, targetShortCommit].includes(artifactPublish?.sourceCommitId) ||
[targetCommit, targetShortCommit].includes(artifactReport.commitId);
const sourceCoversTarget = artifactIdentity?.targetCoverage?.covered === true &&
artifactIdentity?.artifactSource?.commitId &&
(
artifactPublish?.sourceCommitId === artifactIdentity.artifactSource.commitId ||
artifactPublish?.sourceCommitId === artifactIdentity.artifactSource.shortCommitId ||
artifactReport.commitId === artifactIdentity.artifactSource.commitId ||
artifactReport.commitId === artifactIdentity.artifactSource.shortCommitId
);
const publishReady = artifactPublish?.status === "published" &&
artifactPublish.publishedCount === requiredServices.length &&
allServicesAccountedFor &&
allRequiredServicesPublished &&
disabledServicesNotPublished &&
sourceMatchesTarget;
(sourceMatchesTarget || sourceCoversTarget);
if (publishReady) {
reporter.check("dev-artifact-publish-report", "registry", "pass", "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons.");
const summary = sourceMatchesTarget
? "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons."
: `DEV artifact publish report covers artifact source ${artifactIdentity.artifactSource.shortCommitId}; target ${targetRef} ${targetShortCommit} has no artifact build input changes since that source.`;
reporter.check("dev-artifact-publish-report", "registry", "pass", summary, [artifactIdentity?.targetCoverage].filter(Boolean));
return;
}
@@ -537,7 +712,7 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm
type: "runtime_blocker",
scope: "dev-artifact-publish",
summary: `reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the current origin/main commit and record immutable registry digests; keep disabled services marked not_published with reasons."
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons."
});
}
@@ -868,9 +1043,9 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) {
if (registryStatus === "blocked") {
reporter.block({
type: "runtime_blocker",
scope: "catalog-image-manifest-read",
summary: "This preflight could not verify registry manifests for the required DEV catalog images without reading credentials.",
nextTask: "Publish required DEV images or provide a non-secret registry evidence artifact with immutable digests for each required HWLAB service."
scope: "registry-manifest-read",
summary: "This preflight could not verify catalog image manifests through the runner process path; this is a #66 registry reachability dimension, not evidence that publish digests are missing.",
nextTask: "Resolve the #66 registry reachability path or provide a non-secret registry reachability artifact; do not rerun heavy publish solely for this manifest-read probe."
});
}
}
@@ -987,9 +1162,10 @@ export async function runPreflight(argv) {
registryPrefix,
timeoutMs: args.timeoutMs
});
const { deploy, catalog, masterEdge, artifactIdentity } = validateLocalContracts(
const { deploy, catalog, masterEdge, artifactIdentity } = await validateLocalContracts(
reporter,
contracts,
optionalReports,
targetShortCommit,
targetCommit,
args.targetRef
@@ -997,7 +1173,7 @@ export async function runPreflight(argv) {
validateRegistryCapabilities(reporter, registryCapabilities);
validateCloudApiDbContract(reporter, deploy, contracts[3]);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, targetShortCommit, targetCommit, args.targetRef);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, artifactIdentity, targetShortCommit, targetCommit, args.targetRef);
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
await validateEdgeContracts(reporter, masterEdge);
validateRuntimeBoundary(reporter, deploy);