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
+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