feat: add dev db health contract

This commit is contained in:
HWLAB Code Queue
2026-05-21 17:53:36 +00:00
parent 36bf2c42ed
commit 1a2efd4915
27 changed files with 928 additions and 265 deletions
+5
View File
@@ -12,6 +12,11 @@ future compatibility, but PROD deployment is not part of MVP acceptance.
- `deploy/k8s/base` contains parseable k3s resource skeletons for the frozen
HWLAB service IDs.
- `deploy/k8s/dev` contains the DEV health contract and endpoint metadata.
- `hwlab-cloud-api` declares the DEV DB env contract with
`HWLAB_CLOUD_DB_URL` from Secret reference
`hwlab-cloud-api-dev-db/database-url` and non-secret
`HWLAB_CLOUD_DB_SSL_MODE=require`. The repository records names only, never
secret values or a live DB connection string.
- `deploy/k8s/prod` is a disabled placeholder gate only.
- `deploy/frp` describes the D601-to-master reverse link without secrets.
- `deploy/master-edge` describes public edge ownership and health boundaries.
+4 -1
View File
@@ -33,7 +33,10 @@
"HWLAB_COMMIT_ID": "24eb3bf",
"HWLAB_IMAGE": "ghcr.io/pikastech/hwlab-cloud-api:24eb3bf",
"HWLAB_IMAGE_TAG": "24eb3bf",
"HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy"
"HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy",
"HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url",
"HWLAB_CLOUD_DB_SSL_MODE": "require",
"HWLAB_CLOUD_DB_CONTRACT": "dev-redacted-presence-only"
}
},
{
+14
View File
@@ -123,6 +123,20 @@
"type": "object",
"additionalProperties": {
"type": "string"
},
"properties": {
"HWLAB_CLOUD_DB_URL": {
"type": "string",
"const": "secretRef:hwlab-cloud-api-dev-db/database-url"
},
"HWLAB_CLOUD_DB_SSL_MODE": {
"type": "string",
"const": "require"
},
"HWLAB_CLOUD_DB_CONTRACT": {
"type": "string",
"const": "dev-redacted-presence-only"
}
}
}
},
+18
View File
@@ -62,6 +62,24 @@
{
"name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN",
"value": "unidesk-backend,provider-gateway,microservice-proxy"
},
{
"name": "HWLAB_CLOUD_DB_URL",
"valueFrom": {
"secretKeyRef": {
"name": "hwlab-cloud-api-dev-db",
"key": "database-url",
"optional": true
}
}
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"value": "require"
},
{
"name": "HWLAB_CLOUD_DB_CONTRACT",
"value": "dev-redacted-presence-only"
}
],
"readinessProbe": {
+1
View File
@@ -12,6 +12,7 @@
"data": {
"endpoint": "http://74.48.78.17:6667",
"cloud-api": "GET /health/live via hwlab-edge-proxy then hwlab-cloud-api:6667",
"cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url plus HWLAB_CLOUD_DB_SSL_MODE=require; health reports only env presence and redacted Secret reference names, not live DB evidence",
"cloud-web": "GET /health/live on hwlab-cloud-web:8080; consumes cloud-api only",
"agent": "hwlab-agent-mgr readiness gates worker template creation; hwlab-agent-worker is suspended until a session is scheduled",
"sim": "hwlab-gateway-simu:7101 and hwlab-box-simu:7201 expose /health/live for DEV smoke",
+1 -1
View File
@@ -46,7 +46,7 @@ provide a current health timestamp. Existing skeleton services may expose
| master edge proxy | `hwlab-edge-proxy` | Edge proxy health and route table observation | Route for `http://74.48.78.17:6667` forwards to `frp` and identifies commit/image | No DEV route, wrong port, missing route observability |
| frp tunnel | `hwlab-tunnel-client` | Tunnel session/status observation | Tunnel for DEV route is established to D601 router and identifies commit/image | Tunnel down, wrong target, ambiguous service identity |
| D601 router | `hwlab-router` | `hwlab-dev/hwlab-router` health/status | Router is live, environment is `dev`, route target is cloud API/web or runtime service | Namespace mismatch, route missing, stale health timestamp |
| Cloud API | `hwlab-cloud-api` | `GET /health`, `GET /live`, optional JSON-RPC `/rpc` probe | Returns service ID, `dev`, healthy/live status, and accepts a harmless protocol probe | HTTP error, unknown service ID, wrong environment, invalid JSON-RPC envelope |
| Cloud API | `hwlab-cloud-api` | `GET /health`, `GET /live`, optional JSON-RPC `/rpc` probe | Returns service ID, `dev`, healthy/live status, redacted DB env readiness, and accepts a harmless protocol probe | HTTP error, unknown service ID, wrong environment, invalid JSON-RPC envelope, missing DB env readiness |
| Cloud Web | `hwlab-cloud-web` | Static asset check and configured DEV endpoint | Web bundle references `http://74.48.78.17:6667` and does not call hardware directly | Endpoint drift, direct hardware control path, missing asset |
| Agent manager | `hwlab-agent-mgr` | Agent scheduling health/status | Reports live manager, commit/image, and DEV environment; no worker leak after smoke | Scheduler unavailable, wrong environment, unbounded worker session |
| Agent worker | `hwlab-agent-worker` | Worker health/status during dry-run or scoped smoke | Worker session is scoped to DEV project and emits trace/audit identifiers | Missing session identity, unsafe mutation, cleanup failure |
+6 -2
View File
@@ -100,8 +100,12 @@ The current `origin/main` deploy contract is blocked for real DEV apply:
`hwlab-dev` namespace.
- The public DEV health probe at `http://74.48.78.17:6667/health/live` is not
reachable from this runner.
- `hwlab-cloud-api` has no DEV DB connection configuration and its local health
payload reports `db.connected: false`.
- `hwlab-cloud-api` now has a DEV DB env contract placeholder:
`HWLAB_CLOUD_DB_URL` must come from Secret reference
`hwlab-cloud-api-dev-db/database-url`, and
`HWLAB_CLOUD_DB_SSL_MODE=require`. The local health gate still reports DB
runtime config blocked when those env values are absent, and it never prints
the connection string.
No PROD resource, secret value, UniDesk runtime substitute, browser e2e,
heavyweight e2e, or force push is part of this flow.
+15
View File
@@ -23,6 +23,21 @@ HWLAB runtime.
`hwlab-cloud-api` also serves `GET /health/live` for k3s probes and edge
forwarding checks.
`db` in the cloud-api health payload is a redacted configuration gate. It
reports required env names, presence, missing names, and Secret reference names
only. It must not expose a connection string, token, password, or claim fixture
output as live DB evidence.
Required DEV DB config names:
- `HWLAB_CLOUD_DB_URL` from Secret reference
`hwlab-cloud-api-dev-db/database-url`.
- `HWLAB_CLOUD_DB_SSL_MODE=require`.
When the Secret/env is absent, the health JSON should keep
`db.connected: false`, set `db.status: "blocked"`, and list the missing env
names so gate reports can identify `cloud-api-db-health-gate`.
## Route Classification
The smoke classifies failures into these blockers:
+13
View File
@@ -45,6 +45,12 @@ The preflight checks:
service artifacts were published for the selected `origin/main` commit.
- `deploy/k8s/base` and `deploy/k8s/dev` parse and remain scoped to
`hwlab-dev`.
- `hwlab-cloud-api` declares the DEV DB env contract:
`HWLAB_CLOUD_DB_URL` from Secret reference
`hwlab-cloud-api-dev-db/database-url` and
`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 route
on port `6667`.
- The runner can perform read-only `kubectl` probes against `hwlab-dev`.
@@ -62,3 +68,10 @@ The preflight checks:
`blocked` is expected until the real DEV runtime path can be proven. Each
blocker includes a `type`, `scope`, `summary`, and `nextTask` so the next task is
the smallest repair needed before rerunning the preflight.
The DB gate has two common blocked scopes:
- `cloud-api-db-env-contract`: manifest/Kubernetes placeholders are incomplete.
- `cloud-api-db-health-gate`: runtime env presence is missing. The next task is
to configure the DEV Secret/env names, not to print or commit any secret
value.
+4
View File
@@ -51,6 +51,10 @@ Every report must include:
own state with a status, command list, evidence list, and short summary.
- `blockers` should list the remaining open blockers as machine-readable
objects.
- DB gate extensions may add redacted `db`, `cloudApiDb`, or check evidence
objects. These must include only env names, presence/missing status, and
Secret reference names. They must not include connection strings, passwords,
tokens, or fixture output presented as live DB evidence.
## Validation
+2
View File
@@ -24,6 +24,8 @@ node scripts/m2-dev-deploy-smoke.mjs --dry-run
for every recorded service contract.
- Confirms the cloud surface bundles `hwlab-cloud-api` and `hwlab-cloud-web`
artifacts together under the same route phase.
- Marks the fixture as dry-run evidence only. Any DB fields in fixture health
records are contract placeholders, not live DB evidence.
- Keeps the default mode in dry-run and refuses real requests unless an
explicit live gate is supplied.
@@ -95,6 +95,27 @@
"status": "ok",
"live": true,
"route": "cloud-api/web",
"db": {
"fixtureOnly": true,
"liveDbEvidence": false,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"present": false,
"redacted": true
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"present": false,
"redacted": false
}
],
"missingEnv": [
"HWLAB_CLOUD_DB_URL",
"HWLAB_CLOUD_DB_SSL_MODE"
],
"summary": "Fixture records the DB env contract shape only; it is not live DB evidence."
},
"artifacts": [
{
"serviceId": "hwlab-cloud-api",
+111
View File
@@ -0,0 +1,111 @@
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
export const DEV_DB_ENV_CONTRACT = Object.freeze({
contractVersion: "v1",
environment: ENVIRONMENT_DEV,
requiredEnv: Object.freeze([
"HWLAB_CLOUD_DB_URL",
"HWLAB_CLOUD_DB_SSL_MODE"
]),
secretRefs: Object.freeze([
Object.freeze({
env: "HWLAB_CLOUD_DB_URL",
secretName: "hwlab-cloud-api-dev-db",
secretKey: "database-url"
})
]),
nonSecretDefaults: Object.freeze({
HWLAB_CLOUD_DB_SSL_MODE: "require"
}),
forbidden: Object.freeze({
prodAllowed: false,
secretPlaintextAllowed: false,
liveDbEvidenceFromFixtures: false
})
});
const secretEnvNames = new Set(DEV_DB_ENV_CONTRACT.secretRefs.map((item) => item.env));
export function buildDbHealthContract(env = process.env) {
const fields = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
const present = hasEnvValue(env, name);
return {
name,
present,
redacted: secretEnvNames.has(name),
source: secretEnvNames.has(name) ? "k8s-secret-ref" : "runtime-env",
required: true
};
});
const missingEnv = fields.filter((field) => !field.present).map((field) => field.name);
const ready = missingEnv.length === 0;
return {
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
environment: DEV_DB_ENV_CONTRACT.environment,
connected: false,
connectionChecked: false,
configReady: ready,
ready,
status: ready ? "ready" : "blocked",
mode: ready ? "configured_pending_live_probe" : "not_configured",
fields,
missingEnv,
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map(({ env: envName, secretName, secretKey }) => ({
env: envName,
secretName,
secretKey,
present: hasEnvValue(env, envName),
redacted: true
})),
safety: {
devOnly: true,
prodAllowed: DEV_DB_ENV_CONTRACT.forbidden.prodAllowed,
secretsRead: false,
valuesRedacted: true,
liveDbEvidence: false
},
evidence: ready ? "env_presence_only" : "env_contract_blocked"
};
}
export function buildDbEnvManifestPlaceholder() {
return {
requiredEnv: [...DEV_DB_ENV_CONTRACT.requiredEnv],
secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map((item) => ({ ...item })),
nonSecretDefaults: { ...DEV_DB_ENV_CONTRACT.nonSecretDefaults },
fixtureEvidence: {
liveDbEvidence: false,
summary: "This manifest contract records only env and Secret reference names. It is not live DB evidence."
}
};
}
export function summarizeDbContract(db = buildDbHealthContract()) {
return {
status: db.status,
ready: db.ready,
configReady: db.configReady,
connected: db.connected,
connectionChecked: db.connectionChecked,
requiredEnv: db.fields.map((field) => ({
name: field.name,
present: field.present,
redacted: field.redacted
})),
missingEnv: [...db.missingEnv],
secretRefs: db.secretRefs.map(({ env: envName, secretName, secretKey, present, redacted }) => ({
env: envName,
secretName,
secretKey,
present,
redacted
})),
liveDbEvidence: db.safety.liveDbEvidence,
fixtureEvidence: false
};
}
function hasEnvValue(env, name) {
return typeof env?.[name] === "string" && env[name].trim().length > 0;
}
+3 -8
View File
@@ -13,6 +13,7 @@ import {
createAuditRecord,
deriveActorFromMeta
} from "../audit/index.mjs";
import { buildDbHealthContract } from "./db-contract.mjs";
export const SUPPORTED_RPC_METHODS = Object.freeze([
"system.health",
@@ -181,10 +182,7 @@ function handleSystemHealth() {
status: "ok",
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
db: {
connected: false,
mode: "not_configured"
}
db: buildDbHealthContract()
};
}
@@ -195,10 +193,7 @@ function handleAdapterDescribe() {
restEndpoint: "POST /v1/rpc/{method}",
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db: {
connected: false,
mode: "migration_skeleton_only"
}
db: buildDbHealthContract()
};
}
+40
View File
@@ -62,3 +62,43 @@ test("hardware.operation.request is a non-dispatching skeleton with visible audi
assert.equal(response.result.audit.operation, "hardware.operation.request");
assert.equal(response.result.audit.target.id, "prj_01J00000000000000000000000");
});
test("system.health includes the redacted DEV DB env contract", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
delete process.env.HWLAB_CLOUD_DB_URL;
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
try {
const response = await handleJsonRpcRequest({
jsonrpc: "2.0",
id: "req_01J00000000000000000000002",
method: "system.health",
params: {},
meta: {
traceId: "trc_01J00000000000000000000002",
serviceId: "hwlab-cloud-web",
environment: "dev"
}
});
validateResponse(response);
assert.equal(response.result.db.status, "blocked");
assert.equal(response.result.db.connected, false);
assert.ok(response.result.db.missingEnv.includes("HWLAB_CLOUD_DB_URL"));
assert.equal(response.result.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
assert.equal(response.result.db.secretRefs[0].secretKey, "database-url");
assert.equal(response.result.db.secretRefs[0].redacted, true);
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
}
});
+4 -8
View File
@@ -12,6 +12,7 @@ import {
createErrorEnvelope,
handleJsonRpcRequest
} from "./json-rpc.mjs";
import { buildDbHealthContract } from "./db-contract.mjs";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
@@ -36,6 +37,7 @@ export function buildHealthPayload() {
const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA || "unknown";
const imageReference = process.env.HWLAB_IMAGE || "unknown";
const imageTag = process.env.HWLAB_IMAGE_TAG || commitId.slice(0, 7) || "unknown";
const db = buildDbHealthContract();
return {
serviceId,
@@ -58,10 +60,7 @@ export function buildHealthPayload() {
},
endpoint: process.env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
db: {
connected: false,
mode: "not_configured"
}
db
};
}
@@ -145,10 +144,7 @@ async function handleRestAdapter(request, response, url, options) {
rpcBridge: "POST /v1/rpc/{method}",
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db: {
connected: false,
mode: "not_configured"
}
db: buildDbHealthContract()
});
return;
}
+60
View File
@@ -4,6 +4,11 @@ import test from "node:test";
import { createCloudApiServer } from "./server.mjs";
test("cloud api exposes /health, /health/live, and /live probes", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
delete process.env.HWLAB_CLOUD_DB_URL;
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
@@ -18,6 +23,11 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
assert.equal(healthPayload.commit.id.length > 0, true);
assert.equal(healthPayload.image.reference.length > 0, true);
assert.equal(healthPayload.service.id, "hwlab-cloud-api");
assert.equal(healthPayload.db.status, "blocked");
assert.equal(healthPayload.db.connected, false);
assert.deepEqual(healthPayload.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
assert.equal(healthPayload.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
assert.equal(healthPayload.db.secretRefs[0].redacted, true);
const healthLive = await fetch(`http://127.0.0.1:${port}/health/live`);
assert.equal(healthLive.status, 200);
@@ -25,6 +35,7 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
assert.equal(healthLivePayload.serviceId, "hwlab-cloud-api");
assert.equal(healthLivePayload.status, "ok");
assert.equal(healthLivePayload.commit.id.length > 0, true);
assert.equal(healthLivePayload.db.ready, false);
const readiness = await fetch(`http://127.0.0.1:${port}/health/live`);
assert.equal(readiness.status, 200);
@@ -37,5 +48,54 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
}
});
test("cloud api health reports DB env presence without leaking values", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
process.env.HWLAB_CLOUD_DB_URL = "postgres://user:password@db.example.invalid/hwlab";
process.env.HWLAB_CLOUD_DB_SSL_MODE = "require";
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health`);
const payload = await response.json();
assert.equal(payload.db.status, "ready");
assert.equal(payload.db.connected, false);
assert.equal(payload.db.connectionChecked, false);
assert.equal(payload.db.configReady, true);
assert.equal(payload.db.evidence, "env_presence_only");
assert.deepEqual(payload.db.missingEnv, []);
assert.equal(JSON.stringify(payload.db).includes("password"), false);
assert.equal(JSON.stringify(payload.db).includes("db.example.invalid"), false);
assert.equal(payload.db.fields.find((field) => field.name === "HWLAB_CLOUD_DB_URL").redacted, true);
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node --test internal/agent/index.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node --test internal/agent/index.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh",
"dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs",
"m1:smoke": "node scripts/m1-contract-smoke.mjs",
"dev:evidence": "node scripts/dev-evidence-blocker-aggregator.mjs --pretty",
+82 -41
View File
@@ -12,12 +12,12 @@
"pikasTech/HWLAB#30"
],
"taskId": "dev-deploy-apply",
"commitId": "845133ae149a",
"commitId": "9b47ebe49f70",
"acceptanceLevel": "dev_deploy_apply",
"devOnly": true,
"prodDisabled": true,
"status": "blocked",
"generatedAt": "2026-05-21T17:47:32.687Z",
"generatedAt": "2026-05-21T17:57:43.720Z",
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"sourceContract": {
@@ -60,7 +60,7 @@
"namespace: hwlab-dev",
"workloads planned: 13",
"kubectl executor: missing",
"live health: blocked"
"live health: not_run"
],
"summary": "DEV apply is blocked before mutation."
},
@@ -69,7 +69,7 @@
"requirements": [
"DEV artifact catalog must contain CI publish evidence and registry digests",
"kubectl must be available for D601 hwlab-dev and must not target PROD",
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB connectivity",
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB env readiness without exposing secret values",
"pods, services, configmaps, and image commit tags must be observable in hwlab-dev"
],
"summary": "One or more required DEV deploy preconditions are blocked."
@@ -78,8 +78,8 @@
"mode": "dry-run",
"conclusion": {
"status": "blocked",
"reason": "6 blocker(s) must be cleared before any live DEV apply.",
"blockerCount": 6
"reason": "4 blocker(s) must be cleared before any live DEV apply.",
"blockerCount": 4
},
"applyBoundary": {
"currentMode": "dry-run",
@@ -115,7 +115,7 @@
"expectedArtifactCommit": "24eb3bf",
"deployCommitId": "24eb3bf",
"catalogCommitId": "24eb3bf",
"sourceCommitId": "845133ae149a",
"sourceCommitId": "9b47ebe49f70",
"serviceCommitIds": [
"24eb3bf"
],
@@ -566,7 +566,7 @@
"type": "observability_blocker",
"scope": "artifact-source-commit",
"status": "open",
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 845133ae149a",
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 9b47ebe49f70",
"unblockHint": "Regenerate deploy/deploy.json and deploy/artifact-catalog.dev.json for the source commit that will be promoted."
},
{
@@ -576,19 +576,12 @@
"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."
"summary": "cloud-api health reports DB config blocked; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE",
"unblockHint": "Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values."
},
{
"type": "environment_blocker",
@@ -596,13 +589,6 @@
"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": [
@@ -711,6 +697,74 @@
"publishState": "skeleton-only"
}
],
"cloudApiDb": {
"status": "contract-ready",
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"manifestPresent": true,
"k8sPresent": true,
"redacted": true,
"source": "k8s-secret-ref",
"secretRef": {
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": true,
"redacted": true
}
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"manifestPresent": true,
"k8sPresent": true,
"redacted": false,
"source": "runtime-env",
"secretRef": null
}
],
"missingManifest": [],
"missingK8s": [],
"missingSecretRefs": [],
"runtimeHealth": {
"status": "blocked",
"ready": false,
"configReady": false,
"connected": false,
"connectionChecked": false,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"present": false,
"redacted": true
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"present": false,
"redacted": false
}
],
"missingEnv": [
"HWLAB_CLOUD_DB_URL",
"HWLAB_CLOUD_DB_SSL_MODE"
],
"secretRefs": [
{
"env": "HWLAB_CLOUD_DB_URL",
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": false,
"redacted": true
}
],
"liveDbEvidence": false,
"fixtureEvidence": false
},
"secretMaterialRead": false,
"valuesRedacted": true,
"liveDbEvidence": false,
"fixtureEvidence": false,
"note": "This check records DB env/Secret reference presence only. It is not live DB evidence."
},
"k8sManifest": {
"resourceCounts": {
"workloads": 13,
@@ -804,9 +858,8 @@
"resources": []
},
"liveProbe": {
"status": "blocked",
"url": "http://74.48.78.17:6667/health/live",
"error": "connect ECONNREFUSED 74.48.78.17:6667"
"status": "not_run",
"reason": "--skip-live-probe"
}
},
"blockers": [
@@ -814,7 +867,7 @@
"type": "observability_blocker",
"scope": "artifact-source-commit",
"status": "open",
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 845133ae149a"
"summary": "deploy/catalog artifact commit 24eb3bf/24eb3bf does not match source 9b47ebe49f70"
},
{
"type": "observability_blocker",
@@ -822,29 +875,17 @@
"status": "open",
"summary": "DEV artifact catalog has no CI publish, registry verification, or registry digests"
},
{
"type": "runtime_blocker",
"scope": "cloud-api-db-config",
"status": "open",
"summary": "cloud-api has no DEV DB connection env configured"
},
{
"type": "runtime_blocker",
"scope": "cloud-api-db",
"status": "open",
"summary": "cloud-api health reports DB not connected in this artifact"
"summary": "cloud-api health reports DB config blocked; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE"
},
{
"type": "environment_blocker",
"scope": "kubectl",
"status": "open",
"summary": "kubectl is not installed in the runner"
},
{
"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"
}
],
"notes": "DEV-only result. No PROD resources, secret values, force push, heavy e2e, browser e2e, or UniDesk runtime substitute were used."
+45 -146
View File
@@ -4,12 +4,12 @@
"reportVersion": "v1",
"issue": "pikasTech/HWLAB#36",
"taskId": "dev-edge-health",
"commitId": "cef2befdd156",
"commitId": "9b47ebe49f70",
"acceptanceLevel": "dev_edge_health",
"devOnly": true,
"prodDisabled": true,
"status": "blocked",
"generatedAt": "2026-05-21T17:35:52.483Z",
"status": "not_run",
"generatedAt": "2026-05-21T17:57:41.742Z",
"namespace": "hwlab-dev",
"endpoint": "http://74.48.78.17:6667",
"sourceContract": {
@@ -51,34 +51,23 @@
"summary": "MVP dry-run remains the generic gate command; edge health uses a narrower route smoke."
},
"devPreconditions": {
"status": "blocked",
"status": "not_run",
"requirements": [
"http://74.48.78.17:6667/health returns HWLAB DEV JSON with service, commit, and image evidence",
"frps control on 74.48.78.17:7000 accepts the D601 frp client",
"hwlab-dev hwlab-edge-proxy, hwlab-tunnel-client, hwlab-router, and hwlab-cloud-api are observable",
"No PROD endpoint, secret read, restart, or UniDesk runtime substitute is used"
],
"summary": "public 6667, frps control 7000, and tunnel health 7402 all refuse TCP connections; the master-edge frps listener or its ports are most likely down or closed"
"summary": "live network probes require --live"
},
"blockers": [
{
"type": "network_blocker",
"scope": "master-edge/frps",
"status": "open",
"summary": "public 6667, frps control 7000, and tunnel health 7402 all refuse TCP connections; the master-edge frps listener or its ports are most likely down or closed"
}
],
"blockers": [],
"edgeHealth": {
"generatedAt": "2026-05-21T17:35:52.483Z",
"mode": "live-read-only",
"generatedAt": "2026-05-21T17:57:41.742Z",
"mode": "contract-only",
"endpoint": "http://74.48.78.17:6667",
"safety": {
"environment": "dev",
"prodTouched": false,
"secretsRead": false,
"restarts": false,
"unideskRuntimeSubstitute": false
},
"status": "not_run",
"classification": "not_run",
"blocker": "live network probes require --live",
"contracts": {
"devEndpoint": "http://74.48.78.17:6667",
"frp": {
@@ -121,132 +110,42 @@
"HWLAB_FRP_SERVER_ADDR": "74.48.78.17",
"HWLAB_FRP_PUBLIC_PORT": "6667"
}
},
"cloudApiDb": {
"status": "ready",
"ready": true,
"configReady": true,
"connected": false,
"connectionChecked": false,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"present": true,
"redacted": true
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"present": true,
"redacted": false
}
],
"missingEnv": [],
"secretRefs": [
{
"env": "HWLAB_CLOUD_DB_URL",
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": true,
"redacted": true
}
],
"liveDbEvidence": false,
"fixtureEvidence": false,
"secretMaterialRead": false,
"valuesRedacted": true,
"note": "Manifest-only DB env placeholder; this is not live DB evidence."
}
}
},
"publicTcp": [
{
"host": "74.48.78.17",
"port": 6667,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:6667",
"durationMs": 223
},
{
"host": "74.48.78.17",
"port": 7000,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:7000",
"durationMs": 209
},
{
"host": "74.48.78.17",
"port": 7402,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:7402",
"durationMs": 1222
}
],
"publicHttp": [
{
"url": "http://74.48.78.17:6667/health",
"ok": false,
"error": {
"name": "TypeError",
"message": "fetch failed"
},
"durationMs": 40
},
{
"url": "http://74.48.78.17:6667/health/live",
"ok": false,
"error": {
"name": "TypeError",
"message": "fetch failed"
},
"durationMs": 5
}
],
"kubernetes": {
"observable": false,
"namespace": "hwlab-dev",
"context": null,
"services": [],
"endpoints": [],
"pods": [],
"notes": [
"kubectl is not installed in this runner"
]
},
"clusterDns": [
{
"serviceId": "hwlab-cloud-api",
"host": "hwlab-cloud-api.hwlab-dev.svc.cluster.local",
"port": 6667,
"resolved": false,
"output": ""
},
{
"serviceId": "hwlab-router",
"host": "hwlab-router.hwlab-dev.svc.cluster.local",
"port": 7401,
"resolved": false,
"output": ""
},
{
"serviceId": "hwlab-tunnel-client",
"host": "hwlab-tunnel-client.hwlab-dev.svc.cluster.local",
"port": 7402,
"resolved": false,
"output": ""
},
{
"serviceId": "hwlab-edge-proxy",
"host": "hwlab-edge-proxy.hwlab-dev.svc.cluster.local",
"port": 6667,
"resolved": false,
"output": ""
}
],
"status": "blocker",
"classification": "frp_blocker",
"blocker": "public 6667, frps control 7000, and tunnel health 7402 all refuse TCP connections; the master-edge frps listener or its ports are most likely down or closed",
"diagnosis": {
"likelyLayer": "master-edge/frps",
"confidence": "high",
"likelyCause": "public 6667, frps control 7000, and tunnel health 7402 all refuse TCP connections; the master-edge frps listener or its ports are most likely down or closed",
"evidence": [
{
"host": "74.48.78.17",
"port": 6667,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:6667"
},
{
"host": "74.48.78.17",
"port": 7000,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:7000"
},
{
"host": "74.48.78.17",
"port": 7402,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:7402"
}
],
"notProven": [
"k3s service readiness",
"frpc connectivity",
"edge proxy deployment"
],
"nextTask": "Restore or reopen frps on 74.48.78.17, then rerun the read-only edge smoke."
}
}
}
+185 -37
View File
@@ -7,18 +7,22 @@
"supports": [
"pikasTech/HWLAB#7",
"pikasTech/HWLAB#12",
"pikasTech/HWLAB#14",
"pikasTech/HWLAB#22",
"pikasTech/HWLAB#23",
"pikasTech/HWLAB#29",
"pikasTech/HWLAB#30",
"pikasTech/HWLAB#31"
"pikasTech/HWLAB#31",
"pikasTech/HWLAB#33",
"pikasTech/HWLAB#39",
"pikasTech/HWLAB#49"
],
"target": {
"ref": "origin/main",
"commitId": "cef2befdd156d3d4028f087a96e9c118b9174bf1",
"shortCommitId": "cef2bef"
"commitId": "36bf2c42edd3011deb0889a25aba077a2a69f872",
"shortCommitId": "36bf2c4"
},
"generatedAt": "2026-05-21T17:35:52.507Z",
"generatedAt": "2026-05-21T17:57:44.613Z",
"mode": "read-only",
"devOnly": true,
"prodDisabled": true,
@@ -51,7 +55,7 @@
"id": "target-commit-pinning",
"category": "contract",
"status": "blocked",
"summary": "deploy/catalog commitId 24eb3bf does not match origin/main cef2bef.",
"summary": "deploy/catalog commitId 24eb3bf does not match origin/main 36bf2c4.",
"evidence": []
},
{
@@ -68,6 +72,168 @@
"summary": "DEV k3s manifest files parse and stay scoped to hwlab-dev.",
"evidence": []
},
{
"id": "cloud-api-db-env-contract",
"category": "db",
"status": "pass",
"summary": "cloud-api DEV DB manifest declares the required env names and redacted Secret reference.",
"evidence": [
{
"staticContract": {
"contractVersion": "v1",
"environment": "dev",
"ready": true,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"manifestPresent": true,
"k8sPresent": true,
"redacted": true,
"source": "k8s-secret-ref",
"secretRef": {
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": true,
"redacted": true
}
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"manifestPresent": true,
"k8sPresent": true,
"redacted": false,
"source": "runtime-env",
"secretRef": null
}
],
"missingDeployEnv": [],
"missingK8sEnv": [],
"missingSecretRefs": [],
"secretMaterialRead": false,
"valuesRedacted": true,
"liveDbEvidence": false,
"fixtureEvidence": false
},
"healthContract": {
"status": "blocked",
"ready": false,
"configReady": false,
"connected": false,
"connectionChecked": false,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"present": false,
"redacted": true
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"present": false,
"redacted": false
}
],
"missingEnv": [
"HWLAB_CLOUD_DB_URL",
"HWLAB_CLOUD_DB_SSL_MODE"
],
"secretRefs": [
{
"env": "HWLAB_CLOUD_DB_URL",
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": false,
"redacted": true
}
],
"liveDbEvidence": false,
"fixtureEvidence": false
},
"fixtureEvidence": false,
"liveDbEvidence": false
}
]
},
{
"id": "cloud-api-db-health-gate",
"category": "db",
"status": "blocked",
"summary": "cloud-api DB health gate is missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE.",
"evidence": [
{
"staticContract": {
"contractVersion": "v1",
"environment": "dev",
"ready": true,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"manifestPresent": true,
"k8sPresent": true,
"redacted": true,
"source": "k8s-secret-ref",
"secretRef": {
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": true,
"redacted": true
}
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"manifestPresent": true,
"k8sPresent": true,
"redacted": false,
"source": "runtime-env",
"secretRef": null
}
],
"missingDeployEnv": [],
"missingK8sEnv": [],
"missingSecretRefs": [],
"secretMaterialRead": false,
"valuesRedacted": true,
"liveDbEvidence": false,
"fixtureEvidence": false
},
"healthContract": {
"status": "blocked",
"ready": false,
"configReady": false,
"connected": false,
"connectionChecked": false,
"requiredEnv": [
{
"name": "HWLAB_CLOUD_DB_URL",
"present": false,
"redacted": true
},
{
"name": "HWLAB_CLOUD_DB_SSL_MODE",
"present": false,
"redacted": false
}
],
"missingEnv": [
"HWLAB_CLOUD_DB_URL",
"HWLAB_CLOUD_DB_SSL_MODE"
],
"secretRefs": [
{
"env": "HWLAB_CLOUD_DB_URL",
"secretName": "hwlab-cloud-api-dev-db",
"secretKey": "database-url",
"present": false,
"redacted": true
}
],
"liveDbEvidence": false,
"fixtureEvidence": false
},
"fixtureEvidence": false,
"liveDbEvidence": false
}
]
},
{
"id": "dev-artifact-publish-report",
"category": "registry",
@@ -79,38 +245,13 @@
"id": "dev-edge-health-report",
"category": "edge",
"status": "blocked",
"summary": "Committed DEV edge health report is blocked with classification frp_blocker.",
"summary": "Committed DEV edge health report is not_run with classification not_run.",
"evidence": [
{
"report": "reports/dev-gate/dev-edge-health.json",
"status": "blocked",
"classification": "frp_blocker",
"publicTcp": [
{
"host": "74.48.78.17",
"port": 6667,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:6667",
"durationMs": 221
},
{
"host": "74.48.78.17",
"port": 7000,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:7000",
"durationMs": 218
},
{
"host": "74.48.78.17",
"port": 7402,
"status": "error",
"code": "ECONNREFUSED",
"message": "connect ECONNREFUSED 74.48.78.17:7402",
"durationMs": 207
}
],
"status": "not_run",
"classification": "not_run",
"publicTcp": [],
"kubernetesObservable": false
}
]
@@ -294,7 +435,7 @@
"status": "open",
"type": "contract_blocker",
"scope": "deploy-target",
"summary": "deploy/deploy.json and deploy/artifact-catalog.dev.json still target 24eb3bf, not origin/main cef2bef.",
"summary": "deploy/deploy.json and deploy/artifact-catalog.dev.json still target 24eb3bf, not origin/main 36bf2c4.",
"nextTask": "Publish or select a DEV artifact set for the current origin/main commit and update deploy/deploy.json plus deploy/artifact-catalog.dev.json to that immutable commit/tag."
},
{
@@ -304,18 +445,25 @@
"summary": "deploy/artifact-catalog.dev.json has ciPublished=false, registryVerified=false, and not_published digests.",
"nextTask": "Run the DEV image publishing workflow and record immutable GHCR digests before real deployment."
},
{
"status": "open",
"type": "runtime_blocker",
"scope": "cloud-api-db-health-gate",
"summary": "cloud-api DB runtime env is not ready; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE.",
"nextTask": "Configure DEV hwlab-cloud-api with Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require, then rerun health/preflight without printing the secret value."
},
{
"status": "open",
"type": "runtime_blocker",
"scope": "dev-artifact-publish",
"summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main cef2bef; current status is blocked with 0/13 published.",
"summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 36bf2c4; current status is blocked with 0/13 published.",
"nextTask": "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests."
},
{
"status": "open",
"type": "network_blocker",
"scope": "dev-edge-health",
"summary": "public 6667 and frps control 7000 both refuse TCP connections; frps is not reachable on the master edge",
"summary": "live network probes require --live",
"nextTask": "Repair the master frps/public DEV edge route and rerun the read-only DEV edge health smoke before real deployment."
},
{
+9 -5
View File
@@ -139,7 +139,9 @@ async function smokeCloudApi() {
const port = await freePort();
const service = startNode("cmd/hwlab-cloud-api/main.mjs", {
HWLAB_CLOUD_API_HOST: "127.0.0.1",
HWLAB_CLOUD_API_PORT: String(port)
HWLAB_CLOUD_API_PORT: String(port),
HWLAB_CLOUD_DB_URL: "",
HWLAB_CLOUD_DB_SSL_MODE: ""
});
const baseUrl = `http://127.0.0.1:${port}`;
@@ -150,10 +152,11 @@ async function smokeCloudApi() {
assert.equal(health.body.serviceId, "hwlab-cloud-api");
assert.equal(health.body.environment, ENVIRONMENT_DEV);
assert.equal(health.body.status, "ok");
assert.deepEqual(health.body.db, {
connected: false,
mode: "not_configured"
});
assert.equal(health.body.db.connected, false);
assert.equal(health.body.db.status, "blocked");
assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
assert.equal(health.body.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
assert.equal(health.body.db.secretRefs[0].redacted, true);
const live = await requestJson(`${baseUrl}/live`);
assert.equal(live.response.status, 200);
@@ -173,6 +176,7 @@ async function smokeCloudApi() {
assert.equal(rpcHealth.body.jsonrpc, "2.0");
assert.equal(rpcHealth.body.result.status, "ok");
assert.equal(rpcHealth.body.result.serviceId, "hwlab-cloud-api");
assert.equal(rpcHealth.body.result.db.status, "blocked");
assert.equal(rpcHealth.body.meta.serviceId, "hwlab-cloud-api");
const rpcHardware = await requestJson(`${baseUrl}/rpc`, {
+7
View File
@@ -98,6 +98,13 @@ function assertAllServiceIds(entries) {
function assertCloudSurface(entry) {
const healthArtifacts = entry.health.artifacts ?? [];
assert.equal(entry.serviceId, "hwlab-cloud-api", "cloud surface entry must start at cloud-api");
assert.equal(entry.health.db?.fixtureOnly, true, "cloud surface DB note must be fixture-only");
assert.equal(entry.health.db?.liveDbEvidence, false, "cloud surface fixture must not claim live DB evidence");
assert.deepEqual(
entry.health.db?.missingEnv,
["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"],
"cloud surface fixture must name missing DB env without values"
);
assert.equal(healthArtifacts.length, 2, "cloud surface must include cloud-api and cloud-web artifacts");
assert.deepEqual(
healthArtifacts.map((artifact) => artifact.serviceId),
+76 -13
View File
@@ -6,6 +6,10 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { spawn } from "node:child_process";
import {
DEV_DB_ENV_CONTRACT,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -207,7 +211,7 @@ function blockerHint(blocker) {
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 "Configure and verify the DEV cloud-api DB env readiness through health output, without reading secret values.";
}
return blocker.summary;
}
@@ -411,29 +415,87 @@ function validateK8s(devKustomization, namespaceDoc, workloads, services, health
}
async function checkCloudApiDb(deploy, workloads, blockers) {
const envNames = new Set(Object.keys(deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {}));
const deployEnv = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {};
const envNames = new Set(Object.keys(deployEnv));
const workloadEnv = new Map();
for (const item of listItems(workloads)) {
for (const container of containersFor(item)) {
if (serviceIdFor(item, container) === "hwlab-cloud-api") {
for (const env of container.env ?? []) envNames.add(env.name);
for (const env of container.env ?? []) {
envNames.add(env.name);
workloadEnv.set(env.name, env);
}
}
}
}
const dbEnvNames = [...envNames].filter((name) => /DATABASE|POSTGRES|MYSQL|SQLITE|MONGO|_DB(_|$)/u.test(name));
if (dbEnvNames.length === 0) {
addBlocker(blockers, "runtime_blocker", "cloud-api-db-config", "cloud-api has no DEV DB connection env configured");
const requiredEnv = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
const secretRef = DEV_DB_ENV_CONTRACT.secretRefs.find((ref) => ref.env === name) ?? null;
const workloadEntry = workloadEnv.get(name) ?? null;
const secretRefPresent = secretRef
? deployEnv[name] === `secretRef:${secretRef.secretName}/${secretRef.secretKey}` &&
workloadEntry?.valueFrom?.secretKeyRef?.name === secretRef.secretName &&
workloadEntry?.valueFrom?.secretKeyRef?.key === secretRef.secretKey
: true;
return {
name,
manifestPresent: Object.hasOwn(deployEnv, name),
k8sPresent: workloadEnv.has(name),
redacted: Boolean(secretRef),
source: secretRef ? "k8s-secret-ref" : "runtime-env",
secretRef: secretRef
? {
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretRefPresent,
redacted: true
}
: null
};
});
const missingManifest = requiredEnv.filter((env) => !env.manifestPresent).map((env) => env.name);
const missingK8s = requiredEnv.filter((env) => !env.k8sPresent).map((env) => env.name);
const missingSecretRefs = requiredEnv
.filter((env) => env.secretRef && !env.secretRef.present)
.map((env) => `${env.secretRef.secretName}/${env.secretRef.secretKey}`);
if (missingManifest.length > 0 || missingK8s.length > 0 || missingSecretRefs.length > 0) {
addBlocker(
blockers,
"runtime_blocker",
"cloud-api-db-config",
`cloud-api DEV DB env contract is incomplete; missing ${[...missingManifest, ...missingK8s, ...missingSecretRefs].join(", ")}`
);
}
let health = null;
try {
const modulePath = pathToFileURL(path.join(repoRoot, "internal/cloud/server.mjs")).href;
const { buildHealthPayload } = await import(modulePath);
const health = buildHealthPayload();
if (health.db?.connected !== true) {
addBlocker(blockers, "runtime_blocker", "cloud-api-db", "cloud-api health reports DB not connected in this artifact");
health = buildHealthPayload();
if (health.db?.ready !== true) {
addBlocker(
blockers,
"runtime_blocker",
"cloud-api-db",
`cloud-api health reports DB config blocked; missing ${health.db?.missingEnv?.join(", ") ?? "unknown DB env"}`
);
}
} catch (error) {
addBlocker(blockers, "observability_blocker", "cloud-api-health", `Cannot inspect cloud-api health payload: ${error.message}`);
}
return {
status: missingManifest.length === 0 && missingK8s.length === 0 && missingSecretRefs.length === 0 ? "contract-ready" : "contract-blocked",
requiredEnv,
missingManifest,
missingK8s,
missingSecretRefs,
runtimeHealth: health?.db ? summarizeDbContract(health.db) : null,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false,
note: "This check records DB env/Secret reference presence only. It is not live DB evidence."
};
}
async function probeDevEndpoint(blockers) {
@@ -451,8 +513,8 @@ async function probeDevEndpoint(blockers) {
if (json?.serviceId !== "hwlab-cloud-api" || json?.environment !== ENVIRONMENT_DEV) {
addBlocker(blockers, "runtime_blocker", "dev-health-identity", "DEV health did not identify hwlab-cloud-api in dev");
}
if (json?.db?.connected !== true) {
addBlocker(blockers, "runtime_blocker", "dev-health-db", "DEV health did not confirm cloud-api DB connectivity");
if (json?.db?.ready !== true && json?.db?.configReady !== true) {
addBlocker(blockers, "runtime_blocker", "dev-health-db", "DEV health did not confirm cloud-api DB config readiness");
}
return {
status: response.statusCode >= 200 && response.statusCode < 300 ? "pass" : "blocked",
@@ -551,7 +613,7 @@ export async function runDevDeployApply(argv, io = {}) {
const artifactEvidence = validateDeployAndCatalog(deploy, catalog, commitId, blockers);
const k8sManifest = validateK8s(devKustomization, namespaceDoc, workloads, services, healthContract, deploy, blockers);
await checkCloudApiDb(deploy, workloads, blockers);
const cloudApiDb = await checkCloudApiDb(deploy, workloads, blockers);
if (!(await readText("internal/cloud/server.mjs")).includes('url.pathname === "/health/live"')) {
addBlocker(blockers, "contract_blocker", "cloud-api-health-path", "cloud-api does not implement the k8s /health/live path");
}
@@ -622,7 +684,7 @@ export async function runDevDeployApply(argv, io = {}) {
requirements: [
"DEV artifact catalog must contain CI publish evidence and registry digests",
"kubectl must be available for D601 hwlab-dev and must not target PROD",
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB connectivity",
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB env readiness without exposing secret values",
"pods, services, configmaps, and image commit tags must be observable in hwlab-dev"
],
summary: status === "blocked" ? "One or more required DEV deploy preconditions are blocked." : "DEV deploy preconditions are satisfied."
@@ -646,6 +708,7 @@ export async function runDevDeployApply(argv, io = {}) {
rollbackHint: buildRollbackHint(workloads),
remainingBlockers,
artifactEvidence,
cloudApiDb,
k8sManifest,
clusterObservation,
liveProbe
+45 -1
View File
@@ -5,6 +5,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { summarizeDbContract } from "../../internal/cloud/db-contract.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -235,11 +236,54 @@ async function inspectContracts() {
namespace: deploy.namespace,
serviceCount: deploy.services?.length ?? 0,
edgeProxy: deploy.services?.find((service) => service.serviceId === "hwlab-edge-proxy") ?? null,
tunnelClient: deploy.services?.find((service) => service.serviceId === "hwlab-tunnel-client") ?? null
tunnelClient: deploy.services?.find((service) => service.serviceId === "hwlab-tunnel-client") ?? null,
cloudApiDb: inspectCloudApiDbContract(deploy)
}
};
}
function inspectCloudApiDbContract(deploy) {
const env = deploy.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {};
const db = {
status: env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE ? "ready" : "blocked",
ready: Boolean(env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE),
configReady: Boolean(env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE),
connected: false,
connectionChecked: false,
fields: [
{
name: "HWLAB_CLOUD_DB_URL",
present: Boolean(env.HWLAB_CLOUD_DB_URL),
redacted: true
},
{
name: "HWLAB_CLOUD_DB_SSL_MODE",
present: Boolean(env.HWLAB_CLOUD_DB_SSL_MODE),
redacted: false
}
],
missingEnv: ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"].filter((name) => !env[name]),
secretRefs: [
{
env: "HWLAB_CLOUD_DB_URL",
secretName: "hwlab-cloud-api-dev-db",
secretKey: "database-url",
present: env.HWLAB_CLOUD_DB_URL === "secretRef:hwlab-cloud-api-dev-db/database-url",
redacted: true
}
],
safety: {
liveDbEvidence: false
}
};
return {
...summarizeDbContract(db),
secretMaterialRead: false,
valuesRedacted: true,
note: "Manifest-only DB env placeholder; this is not live DB evidence."
};
}
async function inspectKubernetes() {
const hasKubectl = await commandExists("kubectl");
const result = {
+153 -1
View File
@@ -5,13 +5,18 @@ import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
DEV_DB_ENV_CONTRACT,
buildDbHealthContract,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultReportPath = "reports/dev-gate/dev-preflight-report.json";
const issue = "pikasTech/HWLAB#34";
const supports = ["#7", "#12", "#22", "#23", "#29", "#30", "#31"].map(
const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49"].map(
(id) => `pikasTech/HWLAB${id}`
);
const forbiddenActions = [
@@ -437,6 +442,152 @@ function validateEdgeHealthReport(reporter, edgeReport) {
});
}
function validateCloudApiDbContract(reporter, deploy, workloads) {
const staticContract = inspectCloudApiDbStaticContract(deploy, workloads);
const healthContract = summarizeDbContract(buildDbHealthContract());
const evidence = [
{
staticContract,
healthContract,
fixtureEvidence: false,
liveDbEvidence: false
}
];
if (staticContract.ready) {
reporter.check(
"cloud-api-db-env-contract",
"db",
"pass",
"cloud-api DEV DB manifest declares the required env names and redacted Secret reference.",
evidence
);
} else {
const missing = [
...staticContract.missingDeployEnv,
...staticContract.missingK8sEnv,
...staticContract.missingSecretRefs
];
reporter.check(
"cloud-api-db-env-contract",
"db",
"blocked",
`cloud-api DEV DB manifest is missing ${missing.join(", ")}.`,
evidence
);
reporter.block({
type: "contract_blocker",
scope: "cloud-api-db-env-contract",
summary: `cloud-api DEV DB contract is incomplete; missing ${missing.join(", ")}.`,
nextTask: "Repair deploy/deploy.json and deploy/k8s/base/workloads.yaml so they expose only the required DB env names and redacted Secret reference."
});
}
if (healthContract.ready) {
reporter.check(
"cloud-api-db-health-gate",
"db",
"pass",
"cloud-api DB health gate reports required env presence with redacted values.",
evidence
);
return;
}
reporter.check(
"cloud-api-db-health-gate",
"db",
"blocked",
`cloud-api DB health gate is missing ${healthContract.missingEnv.join(", ")}.`,
evidence
);
reporter.block({
type: "runtime_blocker",
scope: "cloud-api-db-health-gate",
summary: `cloud-api DB runtime env is not ready; missing ${healthContract.missingEnv.join(", ")}.`,
nextTask: "Configure DEV hwlab-cloud-api with Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require, then rerun health/preflight without printing the secret value."
});
}
function inspectCloudApiDbStaticContract(deploy, workloads) {
const cloudApi = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api") ?? {};
const deployEnv = cloudApi.env ?? {};
const workloadEnv = getWorkloadEnvByServiceId(workloads, "hwlab-cloud-api");
const secretEnvNames = new Set(DEV_DB_ENV_CONTRACT.secretRefs.map((ref) => ref.env));
const fields = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
const secretRef = DEV_DB_ENV_CONTRACT.secretRefs.find((ref) => ref.env === name) ?? null;
const workloadEntry = workloadEnv.get(name) ?? null;
const manifestPresent = Object.hasOwn(deployEnv, name);
const k8sPresent = workloadEnv.has(name);
const secretRefMatches = secretRef
? deployEnv[name] === `secretRef:${secretRef.secretName}/${secretRef.secretKey}` &&
workloadEntry?.valueFrom?.secretKeyRef?.name === secretRef.secretName &&
workloadEntry?.valueFrom?.secretKeyRef?.key === secretRef.secretKey
: true;
return {
name,
manifestPresent,
k8sPresent,
redacted: secretEnvNames.has(name),
source: secretEnvNames.has(name) ? "k8s-secret-ref" : "runtime-env",
secretRef: secretRef
? {
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretRefMatches,
redacted: true
}
: null
};
});
const missingDeployEnv = fields.filter((field) => !field.manifestPresent).map((field) => field.name);
const missingK8sEnv = fields.filter((field) => !field.k8sPresent).map((field) => field.name);
const missingSecretRefs = fields
.filter((field) => field.secretRef && !field.secretRef.present)
.map((field) => `${field.secretRef.secretName}/${field.secretRef.secretKey}`);
return {
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
environment: DEV_DB_ENV_CONTRACT.environment,
ready: missingDeployEnv.length === 0 && missingK8sEnv.length === 0 && missingSecretRefs.length === 0,
requiredEnv: fields,
missingDeployEnv,
missingK8sEnv,
missingSecretRefs,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false
};
}
function getWorkloadEnvByServiceId(workloads, serviceId) {
const envByName = new Map();
for (const item of listManifestItems(workloads)) {
for (const container of item?.spec?.template?.spec?.containers ?? []) {
const itemServiceId =
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
container?.name;
if (itemServiceId !== serviceId) {
continue;
}
for (const env of container.env ?? []) {
envByName.set(env.name, env);
}
}
}
return envByName;
}
function listManifestItems(document) {
if (!document) {
return [];
}
return document.kind === "List" ? document.items ?? [] : [document];
}
async function validateEdgeContracts(reporter, masterEdge) {
try {
const [frpc, frps] = await Promise.all([
@@ -634,6 +785,7 @@ export async function runPreflight(argv) {
args.targetRef
);
validateCloudApiDbContract(reporter, deploy, contracts[3]);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, targetShortCommit, targetCommit, args.targetRef);
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
await validateEdgeContracts(reporter, masterEdge);
+3
View File
@@ -136,5 +136,8 @@ assert.equal(cloudApi.healthPath, "/health/live", "cloud-api health path");
assert.equal(cloudApi.env.HWLAB_COMMIT_ID, deployManifest.commitId, "cloud-api health commit evidence");
assert.equal(cloudApi.env.HWLAB_IMAGE, cloudApi.image, "cloud-api health image evidence");
assert.equal(cloudApi.env.HWLAB_IMAGE_TAG, deployManifest.commitId.slice(0, 7), "cloud-api health image tag evidence");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_URL, "secretRef:hwlab-cloud-api-dev-db/database-url", "cloud-api DB URL must be a Secret reference placeholder");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_SSL_MODE, "require", "cloud-api DB SSL mode");
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_CONTRACT, "dev-redacted-presence-only", "cloud-api DB contract marker");
console.log(`validated ${schemas.length + deploySchemas.length} JSON files and ${SERVICE_IDS.length} service ids`);