feat: add dev durable runtime provisioning gates

This commit is contained in:
Code Queue Review
2026-05-23 10:16:59 +00:00
parent d7e84f359b
commit 14529d7531
13 changed files with 2340 additions and 23 deletions
+14
View File
@@ -191,6 +191,20 @@ transaction reproduces. New DEV side effects should use
`scripts/dev-cd-apply.mjs`; do not bypass the Lease with ad hoc publish,
`kubectl set image`, or restart commands.
The DEV CD transaction owns the durable runtime support sequence before final
live postflight:
```sh
node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --write-report
node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --write-report
node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --write-report
```
These commands are called by `scripts/dev-cd-apply.mjs` under the transaction
Lease. Runners without explicit rollout authorization may run the source-only
`--check` variants and prepare PRs/artifacts, but must leave DEV apply,
rollout, live verification, and lock ownership to the host commander.
The previous verified manual path for `hwlab-cloud-web` was:
```sh
+56
View File
@@ -196,6 +196,39 @@ runtime durability blocker, not a DB connectivity blocker, and must keep M3,
M4, and M5 full acceptance blocked until the durable runtime postconditions
below are true.
## DEV DB Provisioning Automation
The repo-owned DEV DB provisioning entrypoint is:
```sh
node scripts/dev-runtime-provisioning.mjs --check
node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --write-report
```
The default `--check` path validates the redacted target DB URL contract only:
the application role name, target database name, password presence, SecretRef
names, and endpoint authority are represented as booleans or redacted endpoint
class. It never connects to Postgres, never reads Kubernetes Secret data, and
never prints the target role, database name, host, password, token, DSN, or
kubeconfig material.
An authorized DEV provisioning apply is:
```sh
node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --write-report
```
The apply path uses `HWLAB_CLOUD_DB_URL` from
`hwlab-cloud-api-dev-db/database-url` as the redacted target contract and
`HWLAB_CLOUD_DB_ADMIN_URL` from
`hwlab-cloud-api-dev-db-admin/admin-url` as the admin SecretRef env. It may
create or update only the DEV target role, target database, database `CONNECT`
grant, and `public` schema `USAGE, CREATE` grants needed by the cloud-api
durable runtime. It must report `created` or `existed` booleans and structured
blockers such as role missing, database missing, SSL, auth, schema, migration,
or durability readiness. It must not read Secret objects, print Secret values,
run PROD changes, use manual `psql`, or perform schema migration.
## Runtime Migration Automation
The repo-owned runtime migration entrypoint is:
@@ -240,6 +273,24 @@ Runtime migration reports separate the blockers as:
| Migration | The required source migration is recorded in the ledger. |
| Readiness | Durable runtime read readiness passed after schema and ledger checks. |
## Durable Runtime Postflight
The repo-owned postflight entrypoint is:
```sh
node scripts/dev-runtime-postflight.mjs --check
node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --write-report
```
Default `--check` is source-only. Live mode first reads
`http://74.48.78.17:16667/health/live` and
`http://74.48.78.17:16667/v1`. It only performs the M3
`DO1=true -> DI1=true -> DO1=false -> DI1=false` postflight when both
endpoints prove `runtime.adapter="postgres"`, `runtime.durable=true`,
`runtime.ready=true`, and `runtime.liveRuntimeEvidence=true`. The M3 portion
must then prove operation, trace, audit, and evidence identifiers with durable
green evidence; otherwise the report remains blocked.
## Durable Runtime Unblock Runbook
This runbook is a reusable checklist for a future authorized live
@@ -252,8 +303,10 @@ authorizes the DEV apply command and the preconditions below are already true.
| Source migration contract | Yes | `node scripts/dev-runtime-migration.mjs --check` and `node scripts/dev-runtime-migration.mjs --dry-run --write-report` | DB connection, Secret read, DEV/PROD write |
| Static runtime boundary contract | Yes | `node scripts/validate-runtime-boundary.mjs`, source docs, manifests, and redacted Secret refs | Live mutation, service restart, Secret value output |
| Public health observation | Yes, when command scope is read-only | `/health/live` fields such as `db.liveDbEvidence`, `runtime.adapter`, `runtime.durable`, `runtime.blocker`, `readiness.durability.blockedLayer`, `requiredEvidence`, and redaction/safety flags | Printing DB URLs, passwords, tokens, kubeconfig material, or Secret data |
| Live provisioning apply | No, unless explicitly authorized by commander/operator | `node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --write-report` with role/database created/existed booleans and no secret output | PROD apply, Secret value output, manual DB shell, schema migration, service restart, M3 acceptance promotion |
| Live DB read verification | Only with explicit DEV read authorization | `--dry-run --allow-live-db-read --confirm-dev --write-report` output with redacted endpoint and `secretValuesPrinted=false` | Kubernetes Secret reads, writes, migration apply, PROD target |
| Live migration/repair apply | No, unless explicitly authorized by commander/operator | `--apply --confirm-dev --confirmed-non-production --write-report` plus post-apply health evidence | PROD apply, Secret value output, service restart, Code Agent POST, hardware write, or acceptance promotion |
| Durable runtime postflight | Only after durable runtime readiness is already green | `node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --write-report` with `/health/live`, `/v1`, and M3 true/false durable green evidence | Running M3 writes while runtime durable readiness is blocked, Secret output, PROD, or manual rollout |
Secret handling rules are the same for every row: verify env names,
`secretKeyRef` names/keys, configured provider/model/base URL, redacted endpoint
@@ -271,6 +324,9 @@ be true:
- Source validation passes for `internal/db/migrations/0001_cloud_core_skeleton.sql`,
the durable runtime tables/columns, and the `hwlab_schema_migrations` ledger
row.
- DEV DB provisioning is ready: target role and database are either already
present or created by `scripts/dev-runtime-provisioning.mjs`, and the report
carries only created/existed booleans plus redacted endpoint class.
- The runtime is requesting the Postgres durable adapter, not silently using
memory: `runtime.adapter="postgres"` or equivalent selected-adapter evidence,
with `runtime.durableRequested=true`.
+4 -2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -569,6 +569,7 @@ function dockerfile(baseImage, port) {
"RUN if [ -f package-lock.json ]; then npm ci --omit=dev --ignore-scripts; else npm install --omit=dev --ignore-scripts; fi",
"COPY internal ./internal",
"COPY cmd ./cmd",
"COPY scripts ./scripts",
"COPY web ./web",
"COPY tools ./tools",
"COPY skills ./skills",
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import {
formatDevRuntimePostflightFailure,
runDevRuntimePostflightCli
} from "./src/dev-runtime-postflight.mjs";
try {
process.exitCode = await runDevRuntimePostflightCli(process.argv.slice(2));
} catch (error) {
process.stdout.write(`${JSON.stringify(formatDevRuntimePostflightFailure(error), null, 2)}\n`);
process.exitCode = 1;
}
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import {
formatDevRuntimeProvisioningFailure,
runDevRuntimeProvisioningCli
} from "./src/dev-runtime-provisioning.mjs";
try {
process.exitCode = await runDevRuntimeProvisioningCli(process.argv.slice(2));
} catch (error) {
process.stdout.write(`${JSON.stringify(formatDevRuntimeProvisioningFailure(error), null, 2)}\n`);
process.exitCode = 1;
}
+290 -20
View File
@@ -19,6 +19,10 @@ const defaultLockName = "hwlab-dev-cd-lock";
const defaultTtlSeconds = 3600;
const defaultReportPath = "reports/dev-gate/dev-cd-apply.json";
const artifactReportPath = "reports/dev-gate/dev-artifacts.json";
const runtimeProvisioningReportPath = "reports/dev-gate/dev-runtime-provisioning-report.json";
const runtimeMigrationReportPath = "reports/dev-gate/dev-runtime-migration-report.json";
const runtimePostflightReportPath = "reports/dev-gate/dev-runtime-postflight-report.json";
const runtimeJobReportPrefix = "/tmp/hwlab-dev-runtime-report";
const browserLiveUrl = "http://74.48.78.17:16666/health/live";
const apiLiveUrl = `${DEV_ENDPOINT}/health/live`;
const lockAnnotationPrefix = "hwlab.pikastech.local";
@@ -1042,6 +1046,213 @@ async function runStep(ctx, transaction, step) {
};
}
async function runRuntimeK8sJobStep(ctx, transaction, step) {
const startedAt = ctx.now().toISOString();
const result = await runRuntimeK8sJob(ctx, transaction, step);
const finishedAt = ctx.now().toISOString();
return {
id: step.id,
phase: step.phase,
status: result.status,
command: result.command,
code: result.status === "pass" ? 0 : 1,
startedAt,
finishedAt,
stdoutJson: result.report,
stdoutTail: null,
stderrTail: result.error ?? "",
reportPath: step.reportPath ?? null,
k8sJob: result.k8sJob
};
}
async function runRuntimeK8sJob(ctx, transaction, step) {
const jobName = `${step.jobNamePrefix}-${transaction.transactionId.slice(0, 8)}`;
const reportFile = `${runtimeJobReportPrefix}-${step.id}.json`;
const image = step.image ?? await resolveRuntimeJobImage(ctx);
const manifest = buildRuntimeK8sJobManifest({
jobName,
namespace: transaction.targetNamespace,
image,
commandArgs: [...step.commandArgs, "--report", reportFile],
transaction
});
const kubectl = transaction.kubectlContext;
const apply = await kubectlCommandResult(ctx, kubectl, ["-n", transaction.targetNamespace, "apply", "-f", "-"], {
input: JSON.stringify(manifest),
timeoutMs: 30000
});
if (apply.code !== 0) {
return runtimeJobFailure(step, "apply", apply, jobName, image);
}
const wait = await kubectlCommandResult(ctx, kubectl, [
"-n",
transaction.targetNamespace,
"wait",
"--for=condition=complete",
`job/${jobName}`,
"--timeout=300s"
], { timeoutMs: step.timeoutMs ?? 6 * 60 * 1000 });
if (wait.code !== 0) {
return runtimeJobFailure(step, "wait", wait, jobName, image);
}
const logs = await kubectlCommandResult(ctx, kubectl, ["-n", transaction.targetNamespace, "logs", `job/${jobName}`], {
timeoutMs: 30000
});
const logText = redactSensitiveText(`${logs.stdout}\n${logs.stderr}`);
const report = parseLastJsonObject(logText);
if (logs.code !== 0 || !report) {
return {
status: "blocked",
command: `${shellCommand("kubectl", ["-n", transaction.targetNamespace, "logs", `job/${jobName}`])}`,
error: logText.trim().slice(-2000) || "runtime job did not emit a JSON report",
report: null,
k8sJob: runtimeJobSummary(jobName, step, "logs", image)
};
}
const reportStatus = report.conclusion?.status ?? report.summary?.status ?? report.status;
return {
status: ["ready", "pass"].includes(reportStatus) ? "pass" : "blocked",
command: shellCommand("kubectl", ["-n", transaction.targetNamespace, "apply/wait/logs", `job/${jobName}`]),
error: null,
report,
k8sJob: runtimeJobSummary(jobName, step, "complete", image)
};
}
function runtimeJobFailure(step, phase, result, jobName, image) {
return {
status: "blocked",
command: result.command ?? shellCommand("kubectl", [phase, `job/${jobName}`]),
error: redactSensitiveText(`${result.stderr}\n${result.stdout}`).trim().slice(-2000),
report: null,
k8sJob: runtimeJobSummary(jobName, step, phase, image)
};
}
function runtimeJobSummary(jobName, step, phase, image = step.image ?? "deploy-current:hwlab-cloud-api") {
return {
jobName,
phase,
serviceId: "hwlab-cloud-api",
image,
secretRefsOnly: true,
secretValuesRead: false,
secretValuesPrinted: false
};
}
async function resolveRuntimeJobImage(ctx) {
const deploy = await readDeployJson(ctx.repoRoot);
const image = deploy.manifest?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.image;
if (typeof image === "string" && image.trim().length > 0) {
return image;
}
if (typeof deploy.commitId === "string" && deploy.commitId !== "unknown") {
return `127.0.0.1:5000/hwlab/hwlab-cloud-api:${shortCommit(deploy.commitId)}`;
}
throw new DevCdApplyError("could not resolve hwlab-cloud-api image for runtime maintenance Job", {
code: "runtime-job-image-unresolved"
});
}
function buildRuntimeK8sJobManifest({ jobName, namespace, image, commandArgs, transaction }) {
return {
apiVersion: "batch/v1",
kind: "Job",
metadata: {
name: jobName,
namespace,
labels: {
"app.kubernetes.io/part-of": "hwlab",
"app.kubernetes.io/name": "hwlab-runtime-maintenance",
"hwlab.pikastech.local/profile": "dev",
"hwlab.pikastech.local/service-id": "hwlab-cloud-api",
"hwlab.pikastech.local/cd-transaction": transaction.transactionId
}
},
spec: {
backoffLimit: 0,
ttlSecondsAfterFinished: 600,
template: {
metadata: {
labels: {
"app.kubernetes.io/name": "hwlab-runtime-maintenance",
"hwlab.pikastech.local/service-id": "hwlab-cloud-api",
"hwlab.pikastech.local/cd-transaction": transaction.transactionId
}
},
spec: {
restartPolicy: "Never",
containers: [
{
name: "runtime-maintenance",
image,
command: ["node"],
args: commandArgs,
env: [
{ name: "HWLAB_ENVIRONMENT", value: ENVIRONMENT_DEV },
{ name: "HWLAB_CLOUD_RUNTIME_ADAPTER", value: "postgres" },
{ name: "HWLAB_CLOUD_RUNTIME_DURABLE", value: "true" },
{
name: "HWLAB_CLOUD_DB_URL",
valueFrom: {
secretKeyRef: {
name: "hwlab-cloud-api-dev-db",
key: "database-url",
optional: false
}
}
},
{
name: "HWLAB_CLOUD_DB_ADMIN_URL",
valueFrom: {
secretKeyRef: {
name: "hwlab-cloud-api-dev-db-admin",
key: "admin-url",
optional: false
}
}
},
{ name: "HWLAB_CLOUD_DB_SSL_MODE", value: "disable" },
{ name: "HWLAB_CD_TRANSACTION_ID", value: transaction.transactionId },
{ name: "HWLAB_CD_TRANSACTION_OWNER", value: transaction.ownerTaskId },
{ name: "HWLAB_CD_LOCK_NAME", value: transaction.lockName }
]
}
]
}
}
}
};
}
async function kubectlCommandResult(ctx, kubectlContext, args, options = {}) {
const result = await ctx.runCommand(kubectlContext.executor, args, {
cwd: ctx.repoRoot,
env: kubectlContext.env,
input: options.input,
timeoutMs: options.timeoutMs ?? 30000
});
return {
...result,
command: shellCommand("kubectl", args),
stdout: result.stdout ?? "",
stderr: result.stderr ?? ""
};
}
function parseLastJsonObject(text) {
const lines = String(text ?? "").trim().split(/\r?\n/u).reverse();
for (const line of lines) {
const parsed = parseJsonMaybe(line.trim());
if (parsed && typeof parsed === "object") return parsed;
}
return parseJsonMaybe(text);
}
function stepBlocker(stepResult) {
return {
type: "runtime_blocker",
@@ -1478,8 +1689,9 @@ function buildReport({
"HEAD must match the requested target ref before publish.",
"Lease/hwlab-dev-cd-lock must be acquired before publish/apply.",
"Legacy publish/apply side-effect scripts must run with HWLAB_CD_TRANSACTION_ID.",
"DEV DB role/database provisioning and runtime migration must run through repo-owned commands before workload apply.",
"deploy/deploy.json, artifact catalog, and workloads must converge before apply.",
"Public 16666 and 16667 live health must be recorded in this report."
"Public 16666 and 16667 live health plus M3 durable postflight must be recorded in this report."
],
summary: blockers.length ? "One or more DEV CD transaction preconditions failed." : "DEV CD transaction preconditions passed."
},
@@ -1520,11 +1732,14 @@ function buildReport({
steps,
liveBefore,
liveVerify,
reportPaths: {
transaction: args.writeReport ? args.reportPath : null,
artifacts: artifactReportPath,
deployApply: "reports/dev-gate/dev-deploy-report.json"
},
reportPaths: {
transaction: args.writeReport ? args.reportPath : null,
artifacts: artifactReportPath,
deployApply: "reports/dev-gate/dev-deploy-report.json",
runtimeProvisioning: runtimeProvisioningReportPath,
runtimeMigration: runtimeMigrationReportPath,
runtimePostflight: runtimePostflightReportPath
},
safety: {
prodTouched: false,
secretValuesRead: false,
@@ -1607,7 +1822,8 @@ export async function runDevCdApply(argv, io = {}) {
ownerTaskId: args.ownerTaskId ?? defaultOwnerTaskId(env),
targetNamespace: args.targetNamespace,
lockName: args.lockName,
phases: []
phases: [],
kubectlContext: null
};
const steps = [];
const blockers = [];
@@ -1624,6 +1840,7 @@ export async function runDevCdApply(argv, io = {}) {
target = await resolveTargetRef(ctx, args.targetRef);
deployBefore = await readDeployJson(ctx.repoRoot);
const kubectlContext = await resolveKubectlContext(args, env, ctx.runCommand);
transaction.kubectlContext = kubectlContext;
liveBefore = {
status: "pending",
reason: "liveBefore is captured immediately after Lease acquisition so publish/apply cannot race before the transaction lock."
@@ -1708,6 +1925,38 @@ export async function runDevCdApply(argv, io = {}) {
],
timeoutMs: 120000
},
{
id: "runtime-db-provisioning",
phase: "applying",
kind: "runtime-k8s-job",
jobNamePrefix: "hwlab-runtime-provision",
commandArgs: [
"scripts/dev-runtime-provisioning.mjs",
"--apply",
"--confirm-dev",
"--confirmed-non-production",
"--write-report",
"--fail-on-blocked"
],
timeoutMs: 5 * 60 * 1000,
reportPath: runtimeProvisioningReportPath
},
{
id: "runtime-db-migration",
phase: "applying",
kind: "runtime-k8s-job",
jobNamePrefix: "hwlab-runtime-migrate",
commandArgs: [
"scripts/dev-runtime-migration.mjs",
"--apply",
"--confirm-dev",
"--confirmed-non-production",
"--write-report",
"--fail-on-blocked"
],
timeoutMs: 5 * 60 * 1000,
reportPath: runtimeMigrationReportPath
},
{
id: "dev-deploy-apply",
phase: "applying",
@@ -1722,23 +1971,42 @@ export async function runDevCdApply(argv, io = {}) {
],
timeoutMs: 20 * 60 * 1000,
reportPath: "reports/dev-gate/dev-deploy-report.json"
},
{
id: "runtime-durable-postflight",
phase: "verifying",
command: process.execPath,
args: [
"scripts/dev-runtime-postflight.mjs",
"--live",
"--confirm-dev",
"--confirmed-non-production",
"--target",
"api",
"--write-report",
"--fail-on-blocked"
],
timeoutMs: 2 * 60 * 1000,
reportPath: runtimePostflightReportPath
}
];
if (blockers.length === 0) {
for (const step of plannedSteps) {
if (step.phase === "applying" && lockState.lock?.phase !== "applying") {
if (step.phase !== lockState.lock?.phase) {
await updateDeployLockPhase({
ctx,
args,
kubectlContext,
transactionId: transaction.transactionId,
lockState,
phase: "applying"
phase: step.phase
});
transaction.phases.push({ phase: "applying", status: "entered", at: ctx.now().toISOString() });
transaction.phases.push({ phase: step.phase, status: "entered", at: ctx.now().toISOString() });
}
const result = await runStep(ctx, transaction, step);
const result = step.kind === "runtime-k8s-job"
? await runRuntimeK8sJobStep(ctx, transaction, step)
: await runStep(ctx, transaction, step);
steps.push(result);
if (result.status !== "pass") {
blockers.push(stepBlocker(result));
@@ -1748,15 +2016,17 @@ export async function runDevCdApply(argv, io = {}) {
}
if (blockers.length === 0) {
await updateDeployLockPhase({
ctx,
args,
kubectlContext,
transactionId: transaction.transactionId,
lockState,
phase: "verifying"
});
transaction.phases.push({ phase: "verifying", status: "entered", at: ctx.now().toISOString() });
if (lockState.lock?.phase !== "verifying") {
await updateDeployLockPhase({
ctx,
args,
kubectlContext,
transactionId: transaction.transactionId,
lockState,
phase: "verifying"
});
transaction.phases.push({ phase: "verifying", status: "entered", at: ctx.now().toISOString() });
}
deployAfter = await readDeployJson(ctx.repoRoot);
liveVerify = args.skipLiveVerify
? { status: "not_run", reason: "--skip-live-verify", endpoints: [], summary: { checked: 0 } }
+50
View File
@@ -142,6 +142,34 @@ function makeRunCommand({ heldLock = null, commandLog = [], psOutput = "", killL
lease.metadata.resourceVersion = String(Number(lease.metadata.resourceVersion ?? "2") + 1);
return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" };
}
if (command.includes("kubectl") && args.includes("apply") && args.includes("-f") && args.includes("-")) {
const manifest = JSON.parse(options.input);
assert.equal(manifest.kind, "Job");
assert.equal(manifest.metadata.namespace, "hwlab-dev");
assert.ok(manifest.metadata.name.startsWith("hwlab-runtime-"));
assert.equal(manifest.spec.template.spec.containers[0].env.some((entry) => entry.name === "HWLAB_CLOUD_DB_URL" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-api-dev-db"), true);
assert.equal(manifest.spec.template.spec.containers[0].env.some((entry) => entry.name === "HWLAB_CLOUD_DB_ADMIN_URL" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-api-dev-db-admin"), true);
assert.equal(JSON.stringify(manifest).includes("database-url"), true);
assert.equal(JSON.stringify(manifest).includes("postgresql://"), false);
return { code: 0, stdout: JSON.stringify(manifest), stderr: "" };
}
if (command.includes("kubectl") && args.includes("wait") && args.some((arg) => String(arg).startsWith("job/hwlab-runtime-"))) {
return { code: 0, stdout: "job.batch/hwlab-runtime complete\n", stderr: "" };
}
if (command.includes("kubectl") && args.includes("logs") && args.some((arg) => String(arg).startsWith("job/hwlab-runtime-provision"))) {
return {
code: 0,
stdout: `${JSON.stringify({ conclusion: { status: "ready" }, safety: { secretValuesPrinted: false } }, null, 2)}\n`,
stderr: ""
};
}
if (command.includes("kubectl") && args.includes("logs") && args.some((arg) => String(arg).startsWith("job/hwlab-runtime-migrate"))) {
return {
code: 0,
stdout: `${JSON.stringify({ conclusion: { status: "ready" }, safety: { secretValuesPrinted: false } }, null, 2)}\n`,
stderr: ""
};
}
if (command.includes("kubectl") && args.includes("patch")) {
assert.ok(lease, "lease must exist before patch");
const patch = parsePatch(args);
@@ -394,12 +422,34 @@ test("transaction runs phases, allows internal side-effect env, releases lock, a
assert.equal(report.devCdApply.liveVerify.endpoints.some((endpoint) => endpoint.id === "cloud-web-16666"), true);
assert.equal(report.devCdApply.liveVerify.endpoints.some((endpoint) => endpoint.id === "cloud-api-16667"), true);
assert.equal(report.devCdApply.reportPaths.transaction, "reports/dev-gate/dev-cd-apply.json");
assert.equal(report.devCdApply.reportPaths.runtimeProvisioning, "reports/dev-gate/dev-runtime-provisioning-report.json");
assert.equal(report.devCdApply.reportPaths.runtimeMigration, "reports/dev-gate/dev-runtime-migration-report.json");
assert.equal(report.devCdApply.reportPaths.runtimePostflight, "reports/dev-gate/dev-runtime-postflight-report.json");
const publishCall = commandLog.find((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs"));
const applyCall = commandLog.find((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"));
const provisioningCall = commandLog.find((entry) => entry.args.includes("scripts/dev-runtime-provisioning.mjs"));
const migrationCall = commandLog.find((entry) => entry.args.includes("scripts/dev-runtime-migration.mjs"));
const postflightCall = commandLog.find((entry) => entry.args.includes("scripts/dev-runtime-postflight.mjs"));
const refreshCall = commandLog.find((entry) => entry.args.includes("scripts/refresh-artifact-catalog.mjs"));
assert.ok(publishCall?.env.HWLAB_CD_TRANSACTION_ID);
assert.equal(applyCall?.env.HWLAB_CD_TRANSACTION_ID, publishCall.env.HWLAB_CD_TRANSACTION_ID);
const provisioningJobApply = commandLog.find((entry) =>
entry.command.includes("kubectl") &&
entry.args.includes("apply") &&
entry.input.includes("hwlab-runtime-provision")
);
const migrationJobApply = commandLog.find((entry) =>
entry.command.includes("kubectl") &&
entry.args.includes("apply") &&
entry.input.includes("hwlab-runtime-migrate")
);
assert.ok(provisioningJobApply);
assert.ok(migrationJobApply);
assert.equal(JSON.parse(provisioningJobApply.input).spec.template.spec.containers[0].args[0], "scripts/dev-runtime-provisioning.mjs");
assert.equal(JSON.parse(migrationJobApply.input).spec.template.spec.containers[0].args[0], "scripts/dev-runtime-migration.mjs");
assert.equal(postflightCall?.env.HWLAB_CD_TRANSACTION_ID, publishCall.env.HWLAB_CD_TRANSACTION_ID);
assert.deepEqual(postflightCall.args.slice(0, 4), ["scripts/dev-runtime-postflight.mjs", "--live", "--confirm-dev", "--confirmed-non-production"]);
assert.equal(refreshCall.args[refreshCall.args.indexOf("--target-ref") + 1], "abc1234abc1234abc1234abc1234abc1234abc1");
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
+492
View File
@@ -0,0 +1,492 @@
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
import {
buildM3IoControlSourceReport,
runM3IoControlLiveReport
} from "./m3-io-control-e2e.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultReportPath = "reports/dev-gate/dev-runtime-postflight-report.json";
const issue = "pikasTech/HWLAB#311";
const defaultHealthUrl = `${DEV_ENDPOINT}/health/live`;
const defaultV1Url = `${DEV_ENDPOINT}/v1`;
export async function runDevRuntimePostflightCli(argv = process.argv.slice(2), options = {}) {
const args = parseArgs(argv);
if (args.help) {
process.stdout.write(`${usage()}\n`);
return 0;
}
const report = await buildDevRuntimePostflightReport(args, options);
if (args.writeReport) {
await writeReport(report, args.reportPath, options.repoRoot ?? repoRoot);
}
const printable = args.pretty || args.writeReport ? report : summarizeReport(report);
process.stdout.write(`${JSON.stringify(printable, null, 2)}\n`);
return report.conclusion.status === "pass" || !args.failOnBlocked ? 0 : 1;
}
export async function buildDevRuntimePostflightReport(args = {}, options = {}) {
const parsed = normalizeArgs(args);
const now = options.now ?? (() => new Date().toISOString());
const report = {
issue,
mode: parsed.live ? "dev-live" : "check",
generatedAt: now(),
target: {
environment: ENVIRONMENT_DEV,
namespace: "hwlab-dev",
prodAllowed: false,
apiHealthUrl: defaultHealthUrl,
apiV1Url: defaultV1Url,
m3Target: parsed.target
},
actions: {
healthLiveReadAttempted: false,
v1ReadAttempted: false,
m3MutationAttempted: false,
m3MutationAllowed: false
},
apiHealth: null,
apiV1: null,
m3: null,
blockers: [],
safety: buildSafety(parsed),
safetyRefusal: false
};
if (!parsed.live) {
const sourceReport = await (options.m3SourceBuilder ?? buildM3IoControlSourceReport)({
repoRoot: options.repoRoot ?? repoRoot
});
report.m3 = summarizeM3Report(sourceReport);
report.conclusion = {
status: sourceReport.summary?.status === "pass" ? "pass" : "blocked",
summary: "DEV runtime postflight source contract checked only; no live HTTP or M3 write was attempted.",
blockerCount: sourceReport.summary?.status === "pass" ? 0 : 1
};
if (sourceReport.summary?.status !== "pass") {
addBlocker(report, "contract_blocker", "m3-source-contract", "M3 postflight source contract is blocked.", {
classification: sourceReport.summary?.classification ?? "unknown"
});
}
return report;
}
if (!parsed.confirmDev || !parsed.confirmedNonProduction) {
addSafetyRefusal(report, "DEV runtime postflight live mode requires --live --confirm-dev --confirmed-non-production.");
return finalizeReport(report);
}
const httpGetJson = options.httpGetJson ?? defaultHttpGetJson;
report.actions.healthLiveReadAttempted = true;
report.actions.v1ReadAttempted = true;
const [health, v1] = await Promise.all([
httpGetJson(defaultHealthUrl, parsed.timeoutMs),
httpGetJson(defaultV1Url, parsed.timeoutMs)
]);
report.apiHealth = summarizeApiPayload("GET /health/live", health);
report.apiV1 = summarizeApiPayload("GET /v1", v1);
addApiReadinessBlockers(report);
if (report.blockers.length === 0) {
report.actions.m3MutationAllowed = true;
report.actions.m3MutationAttempted = true;
const m3Runner = options.m3Runner ?? defaultM3Runner;
report.m3 = summarizeM3Report(await m3Runner(parsed));
addM3Blockers(report);
} else {
report.m3 = {
status: "not_run",
trustedGreen: false,
classification: "runtime_durable_postflight_precondition_blocked",
operationCount: 0,
reason: "M3 true/false write/read postflight is skipped until /health/live and /v1 prove durable runtime readiness."
};
}
return finalizeReport(report);
}
export function parseArgs(argv = []) {
const args = {
live: false,
confirmDev: false,
confirmedNonProduction: false,
writeReport: false,
reportPath: defaultReportPath,
pretty: false,
failOnBlocked: false,
target: "api",
timeoutMs: 8000,
help: false
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--check") args.live = false;
else if (arg === "--live") args.live = true;
else if (arg === "--confirm-dev") args.confirmDev = true;
else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true;
else if (arg === "--write-report") args.writeReport = true;
else if (arg === "--pretty") args.pretty = true;
else if (arg === "--fail-on-blocked") args.failOnBlocked = true;
else if (arg === "--target") {
index += 1;
args.target = requireArgValue(argv[index], "--target");
} else if (arg === "--timeout-ms") {
index += 1;
args.timeoutMs = parseTimeout(requireArgValue(argv[index], "--timeout-ms"));
} else if (arg === "--report") {
index += 1;
args.reportPath = requireArgValue(argv[index], "--report");
} else if (arg.startsWith("--report=")) {
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
} else if (arg === "--help" || arg === "-h") {
args.help = true;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
if (!["api", "frontend"].includes(args.target)) {
throw new Error("--target must be api or frontend");
}
return args;
}
function normalizeArgs(args) {
return {
...parseArgs([]),
...args
};
}
async function defaultM3Runner(args) {
const sourceReport = await buildM3IoControlSourceReport({ repoRoot });
return runM3IoControlLiveReport({
flags: new Set(["--live", "--confirm-dev", "--confirmed-non-production"]),
live: true,
confirmDev: true,
confirmedNonProduction: true,
target: args.target,
frontendUrl: "http://74.48.78.17:16666/",
apiUrl: `${DEV_ENDPOINT}/v1/m3/io`,
timeoutMs: args.timeoutMs
}, sourceReport);
}
async function defaultHttpGetJson(url, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method: "GET",
headers: {
accept: "application/json"
},
signal: controller.signal
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
return {
ok: response.ok,
status: response.status,
json,
error: response.ok ? null : json?.error?.message ?? response.statusText
};
} catch (error) {
return {
ok: false,
status: 0,
json: null,
error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message
};
} finally {
clearTimeout(timer);
}
}
function summarizeApiPayload(check, response) {
const json = response.json ?? {};
const runtime = summarizeRuntime(json.runtime);
return {
check,
httpStatus: response.status,
reachable: response.ok === true && Boolean(response.json),
serviceId: json.serviceId ?? json.service?.id ?? null,
environment: json.environment ?? null,
status: json.status ?? null,
ready: json.ready === true,
runtime,
db: summarizeDb(json.db),
readiness: {
ready: json.readiness?.ready === true,
status: json.readiness?.status ?? null,
durabilityReady: json.readiness?.durability?.ready === true,
durabilityBlockedLayer: json.readiness?.durability?.blockedLayer ?? json.runtime?.durabilityContract?.blockedLayer ?? null
},
blockerCodes: Array.isArray(json.blockerCodes) ? json.blockerCodes : [],
m3IoControl: json.m3IoControl
? {
route: json.m3IoControl.route ?? null,
enabled: json.m3IoControl.enabled === true,
contractVersion: json.m3IoControl.contractVersion ?? null
}
: null,
error: response.error ?? null
};
}
function summarizeRuntime(runtime = {}) {
return {
adapter: runtime?.adapter ?? "unknown",
durable: runtime?.durable === true,
durableRequested: runtime?.durableRequested === true,
ready: runtime?.ready === true,
status: runtime?.status ?? null,
blocker: runtime?.blocker ?? null,
liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true,
requiredEvidence: runtime?.durabilityContract?.requiredEvidence ?? null,
blockedLayer: runtime?.durabilityContract?.blockedLayer ?? null,
gates: summarizeRuntimeGates(runtime?.gates)
};
}
function summarizeRuntimeGates(gates = {}) {
return Object.fromEntries(["ssl", "auth", "schema", "migration", "durability"].map((name) => [
name,
{
checked: gates?.[name]?.checked === true,
ready: gates?.[name]?.ready === true,
status: gates?.[name]?.status ?? "unknown",
blocker: gates?.[name]?.blocker ?? null
}
]));
}
function summarizeDb(db = {}) {
return {
ready: db?.ready === true,
connected: db?.connected === true,
liveDbEvidence: db?.liveDbEvidence === true,
endpointSource: db?.endpointSource ?? null,
connectionResult: db?.connectionResult ?? db?.connection?.result ?? null,
endpointRedacted: true,
valueRedacted: true,
runtimeReadiness: {
ready: db?.runtimeReadiness?.ready === true,
status: db?.runtimeReadiness?.status ?? null,
blocker: db?.runtimeReadiness?.blocker ?? null,
queryResult: db?.runtimeReadiness?.queryResult ?? null,
requiredEvidence: db?.runtimeReadiness?.requiredEvidence ?? null
}
};
}
function summarizeM3Report(report = {}) {
const operations = Array.isArray(report.liveOperations) ? report.liveOperations : [];
return {
mode: report.mode ?? "unknown",
status: report.summary?.status ?? report.status ?? "unknown",
classification: report.summary?.classification ?? null,
trustedGreen: report.summary?.trustedGreen === true,
result: report.summary?.result ?? null,
operationCount: operations.length,
operations: operations.map((operation) => ({
id: operation.id,
action: operation.action,
status: operation.status,
operationId: operation.operationId ?? null,
traceId: operation.traceId ?? null,
auditId: operation.auditId ?? null,
evidenceId: operation.evidenceId ?? null,
resultValue: operation.resultValue,
evidenceState: {
status: operation.evidenceState?.status ?? null,
durable: operation.evidenceState?.durable === true,
sourceKind: operation.evidenceState?.sourceKind ?? null
},
blocker: operation.blocker ?? null
}))
};
}
function addApiReadinessBlockers(report) {
for (const [scope, payload] of [["api-health-live", report.apiHealth], ["api-v1", report.apiV1]]) {
if (!payload.reachable) {
addBlocker(report, "runtime_blocker", scope, `${payload.check} was not reachable.`, {
httpStatus: payload.httpStatus,
error: payload.error
});
continue;
}
if (!runtimeDurableReady(payload.runtime)) {
addBlocker(report, "runtime_blocker", scope, `${payload.check} did not prove durable runtime readiness.`, {
runtime: payload.runtime,
blockerCodes: payload.blockerCodes
});
}
}
}
function addM3Blockers(report) {
if (report.m3?.trustedGreen !== true || report.m3?.status !== "pass") {
addBlocker(report, "runtime_blocker", "m3-durable-postflight", "M3 true/false durable postflight did not produce trusted green persisted evidence.", {
classification: report.m3?.classification ?? "unknown",
trustedGreen: report.m3?.trustedGreen === true,
operationCount: report.m3?.operationCount ?? 0
});
return;
}
for (const operation of report.m3.operations ?? []) {
if (!operation.operationId || !operation.auditId || !operation.evidenceId || operation.evidenceState?.status !== "green" || operation.evidenceState?.durable !== true) {
addBlocker(report, "runtime_blocker", "m3-durable-evidence", "M3 operation/audit/evidence durable fields were incomplete.", {
operationIdPresent: Boolean(operation.operationId),
auditIdPresent: Boolean(operation.auditId),
evidenceIdPresent: Boolean(operation.evidenceId),
evidenceState: operation.evidenceState
});
return;
}
}
}
function runtimeDurableReady(runtime) {
return runtime.adapter === "postgres" &&
runtime.durable === true &&
runtime.ready === true &&
runtime.liveRuntimeEvidence === true;
}
function addSafetyRefusal(report, summary) {
report.safetyRefusal = true;
addBlocker(report, "safety_refusal", "runtime-postflight-live-boundary", summary, {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
});
}
function addBlocker(report, type, scope, summary, evidence = {}) {
report.blockers.push({
type,
scope,
status: "open",
summary,
sourceIssue: issue,
evidence
});
}
function finalizeReport(report) {
const openBlockers = report.blockers.filter((blocker) => blocker.status === "open");
report.conclusion = {
status: openBlockers.length === 0 ? "pass" : "blocked",
summary: openBlockers.length === 0
? "DEV runtime postflight passed: /health/live, /v1, and M3 true/false durable evidence are green."
: "DEV runtime postflight is blocked; M3 is skipped unless durable runtime readiness is proven first.",
blockerCount: openBlockers.length
};
return report;
}
function buildSafety(args) {
return {
devOnly: true,
environment: ENVIRONMENT_DEV,
prodAllowed: false,
liveHttpReads: args.live === true,
liveM3Writes: args.live === true,
liveM3WritesRequireDurableRuntimeReady: true,
readsKubernetesSecrets: false,
writesKubernetesSecrets: false,
secretValuesPrinted: false,
dbUrlValueRedacted: true,
endpointRedacted: true
};
}
function summarizeReport(report) {
return {
issue: report.issue,
mode: report.mode,
conclusion: report.conclusion,
actions: report.actions,
apiHealth: report.apiHealth,
apiV1: report.apiV1,
m3: report.m3,
blockers: report.blockers,
safety: report.safety
};
}
async function writeReport(report, reportPath, root) {
const absolute = path.resolve(root, reportPath);
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
}
function parseTimeout(value) {
const parsed = Number.parseInt(value, 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
throw new Error("--timeout-ms must be a positive integer");
}
return Math.min(parsed, 30000);
}
function requireArgValue(value, flag) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`${flag} requires a value`);
}
return value;
}
function usage() {
return [
"Usage: node scripts/dev-runtime-postflight.mjs [--check|--live]",
"",
"Default --check validates source M3 postflight contracts only.",
"--live requires --confirm-dev --confirmed-non-production and reads /health/live plus /v1 before any M3 write.",
"M3 true/false write/read is skipped unless durable runtime readiness is already green.",
"Reports never print DB URLs, passwords, tokens, Secret values, or kubeconfig material."
].join("\n");
}
export function formatDevRuntimePostflightFailure(error) {
const message = redactFailureText(error instanceof Error ? error.message : String(error));
return {
issue,
conclusion: {
status: "blocked",
summary: "DEV runtime postflight command failed before producing a report."
},
error: message,
safety: {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
},
trace: createHash("sha256").update(message).digest("hex").slice(0, 12)
};
}
function redactFailureText(value) {
return String(value ?? "")
.replace(/postgres(?:ql)?:\/\/[^\s"'<>]+/giu, "[redacted-postgres-url]")
.replace(/(password\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]")
.replace(/(token\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]")
.replace(/(kubeconfig\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]");
}
+202
View File
@@ -0,0 +1,202 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildDevRuntimePostflightReport,
parseArgs
} from "./dev-runtime-postflight.mjs";
test("check mode is source-only and does not attempt live reads or M3 writes", async () => {
const report = await buildDevRuntimePostflightReport(parseArgs(["--check"]), {
m3SourceBuilder: async () => ({
mode: "source-static",
summary: {
status: "pass",
classification: "source_static_contract_checked",
trustedGreen: false,
result: "source checked"
}
}),
now: () => "2026-05-23T00:00:00.000Z"
});
assert.equal(report.conclusion.status, "pass");
assert.equal(report.actions.healthLiveReadAttempted, false);
assert.equal(report.actions.v1ReadAttempted, false);
assert.equal(report.actions.m3MutationAttempted, false);
assert.equal(report.safety.secretValuesPrinted, false);
});
test("live mode refuses without explicit DEV confirmations", async () => {
const report = await buildDevRuntimePostflightReport(parseArgs(["--live"]), {
now: () => "2026-05-23T00:00:00.000Z"
});
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.safetyRefusal, true);
assert.equal(report.actions.healthLiveReadAttempted, false);
assert.equal(report.actions.m3MutationAttempted, false);
});
test("live mode skips M3 writes when /health/live durable runtime is blocked", async () => {
const report = await buildDevRuntimePostflightReport(
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
{
httpGetJson: async (url) => ({ ok: true, status: 200, json: apiPayload({ ready: url.endsWith("/v1") }) }),
m3Runner: async () => {
throw new Error("M3 runner should not run before durable readiness is green");
},
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.actions.healthLiveReadAttempted, true);
assert.equal(report.actions.v1ReadAttempted, true);
assert.equal(report.actions.m3MutationAttempted, false);
assert.equal(report.m3.status, "not_run");
assert.equal(report.blockers.some((blocker) => blocker.scope === "api-health-live"), true);
assert.equal(JSON.stringify(report).includes("postgresql://"), false);
});
test("live mode requires /v1 durable readiness before M3 writes", async () => {
const report = await buildDevRuntimePostflightReport(
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
{
httpGetJson: async (url) => ({ ok: true, status: 200, json: apiPayload({ ready: !url.endsWith("/v1") }) }),
m3Runner: async () => {
throw new Error("M3 runner should not run when /v1 is blocked");
},
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.actions.m3MutationAttempted, false);
assert.equal(report.blockers.some((blocker) => blocker.scope === "api-v1"), true);
});
test("live mode records M3 true/false durable green evidence", async () => {
const report = await buildDevRuntimePostflightReport(
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
{
httpGetJson: async () => ({ ok: true, status: 200, json: apiPayload({ ready: true }) }),
m3Runner: async () => m3GreenReport(),
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "pass");
assert.equal(report.actions.m3MutationAttempted, true);
assert.equal(report.actions.m3MutationAllowed, true);
assert.equal(report.m3.trustedGreen, true);
assert.equal(report.m3.operationCount, 4);
assert.deepEqual(report.m3.operations.map((operation) => operation.resultValue), [true, true, false, false]);
assert.equal(report.blockers.length, 0);
});
test("live mode blocks when M3 operation evidence is not durable green", async () => {
const m3 = m3GreenReport();
m3.liveOperations[1].evidenceState.durable = false;
const report = await buildDevRuntimePostflightReport(
parseArgs(["--live", "--confirm-dev", "--confirmed-non-production"]),
{
httpGetJson: async () => ({ ok: true, status: 200, json: apiPayload({ ready: true }) }),
m3Runner: async () => m3,
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.blockers.some((blocker) => blocker.scope === "m3-durable-evidence"), true);
});
function apiPayload({ ready }) {
return {
serviceId: "hwlab-cloud-api",
environment: "dev",
status: ready ? "ready" : "blocked",
ready,
db: {
ready,
connected: ready,
liveDbEvidence: ready,
endpointSource: "secret-url-host",
connectionResult: ready ? "connected" : "connected",
runtimeReadiness: {
ready,
status: ready ? "ready" : "blocked",
blocker: ready ? null : "runtime_durable_adapter_auth_blocked",
queryResult: ready ? "durable_readiness_ready" : "auth_blocked",
requiredEvidence: "runtime_adapter_schema_migration_read_query"
}
},
runtime: {
adapter: "postgres",
durable: ready,
durableRequested: true,
ready,
status: ready ? "ready" : "blocked",
blocker: ready ? null : "runtime_durable_adapter_auth_blocked",
liveRuntimeEvidence: ready,
durabilityContract: {
requiredEvidence: "runtime_adapter_schema_migration_read_query",
blockedLayer: ready ? null : "auth"
},
gates: {
ssl: gate(true),
auth: gate(ready, "runtime_durable_adapter_auth_blocked"),
schema: gate(ready),
migration: gate(ready),
durability: gate(ready)
}
},
readiness: {
ready,
status: ready ? "ready" : "blocked",
durability: {
ready,
blockedLayer: ready ? null : "auth"
}
},
blockerCodes: ready ? []
: ["runtime_durable_adapter_auth_blocked"]
};
}
function gate(ready, blocker = null) {
return {
checked: true,
ready,
status: ready ? "ready" : "blocked",
blocker: ready ? null : blocker
};
}
function m3GreenReport() {
const values = [true, true, false, false];
return {
mode: "dev-live",
summary: {
status: "pass",
classification: "trusted_green",
trustedGreen: true,
result: "DEV-LIVE M3 IO control path and durable trusted records are green."
},
liveOperations: values.map((value, index) => ({
id: ["write-do1-true", "read-di1-true", "write-do1-false", "read-di1-false"][index],
action: index % 2 === 0 ? "do.write" : "di.read",
status: "succeeded",
operationId: `op_${index}`,
traceId: `trc_${index}`,
auditId: `aud_${index}`,
evidenceId: `ev_${index}`,
resultValue: value,
evidenceState: {
status: "green",
durable: true,
sourceKind: "DEV-LIVE"
}
}))
};
}
+928
View File
@@ -0,0 +1,928 @@
import { createHash } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
import {
PostgresCloudRuntimeStore,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
buildPostgresPoolConfig
} from "../../internal/db/runtime-store.mjs";
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultReportPath = "reports/dev-gate/dev-runtime-provisioning-report.json";
const issue = "pikasTech/HWLAB#311";
export const DEV_DB_ADMIN_SECRET_REF = Object.freeze({
env: "HWLAB_CLOUD_DB_ADMIN_URL",
secretName: "hwlab-cloud-api-dev-db-admin",
secretKey: "admin-url"
});
const appSecretRef = DEV_DB_ENV_CONTRACT.secretRefs[0];
const systemDatabases = new Set(["postgres", "template0", "template1"]);
export async function runDevRuntimeProvisioningCli(argv = process.argv.slice(2), options = {}) {
const args = parseArgs(argv);
if (args.help) {
process.stdout.write(`${usage()}\n`);
return 0;
}
const report = await buildDevRuntimeProvisioningReport(args, options);
if (args.writeReport) {
await writeReport(report, args.reportPath, options.repoRoot ?? repoRoot);
}
const printable = args.pretty || args.writeReport ? report : summarizeReport(report);
process.stdout.write(`${JSON.stringify(printable, null, 2)}\n`);
if (report.safetyRefusal || (args.failOnBlocked && report.conclusion.status !== "ready")) {
return 1;
}
return 0;
}
export async function buildDevRuntimeProvisioningReport(args = {}, options = {}) {
const parsed = normalizeArgs(args);
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
const target = parseProvisioningTarget(env.HWLAB_CLOUD_DB_URL);
const admin = summarizeAdminEnv(env);
const report = {
issue,
mode: parsed.mode,
generatedAt: now(),
target: {
environment: ENVIRONMENT_DEV,
namespace: DEV_DB_ENV_CONTRACT.dns.namespace,
prodAllowed: false,
dbUrlEnv: appSecretRef.env,
dbUrlSecretRef: appSecretRef,
adminUrlEnv: DEV_DB_ADMIN_SECRET_REF.env,
adminUrlSecretRef: DEV_DB_ADMIN_SECRET_REF,
dbEndpointAuthority: DEV_DB_ENV_CONTRACT.endpointAuthority.source
},
db: summarizeDbEnv(env, target, admin),
applyBoundary: buildApplyBoundary(parsed),
safety: buildSafety(parsed),
actions: {
liveDbReadAttempted: false,
liveDbWriteAttempted: false,
adminInspectionAttempted: false,
adminWriteAttempted: false,
appRuntimeReadinessAttempted: false,
roleCreated: false,
roleExisted: false,
rolePasswordSynchronized: false,
databaseCreated: false,
databaseExisted: false,
databaseConnectGranted: false,
schemaPrivilegesGranted: false,
readinessVerified: false
},
provisioning: {
targetRole: emptyObjectState(target.roleNamePresent),
targetDatabase: emptyObjectState(target.databaseNamePresent),
privileges: {
checked: false,
ready: false,
databaseConnectGranted: false,
schemaPrivilegesGranted: false
},
ready: false
},
runtime: null,
gates: sourceOnlyGates(target),
downstreamRuntimeGates: null,
blockers: [],
safetyRefusal: false
};
validateSourceContract(report, target, parsed);
if (parsed.mode === "check" || (parsed.mode === "dry-run" && !parsed.allowLiveDbRead)) {
return finalizeReport(report);
}
if (parsed.mode === "dry-run") {
if (!parsed.confirmDev) {
addSafetyRefusal(report, "DEV DB provisioning live dry-run requires --confirm-dev.");
return finalizeReport(report);
}
if (!target.ok) {
return finalizeReport(report);
}
await verifyRuntimeReadiness(report, env, options);
if (admin.present || options.adminClient) {
await inspectAdminProvisioning(report, env, target, options);
} else {
inferProvisioningFromRuntime(report);
if (!report.provisioning.ready) {
addBlocker(report, "runtime_blocker", "runtime-provisioning-admin", "Role/database inspection requires the DEV admin SecretRef env when runtime readiness is not already green.", {
adminUrlEnv: DEV_DB_ADMIN_SECRET_REF.env,
adminUrlSecretRef: DEV_DB_ADMIN_SECRET_REF,
valueRedacted: true,
secretValuesPrinted: false
});
}
}
addRuntimeProvisioningBlocker(report);
return finalizeReport(report);
}
if (parsed.mode === "apply") {
if (!parsed.confirmDev || !parsed.confirmedNonProduction) {
addSafetyRefusal(report, "DEV DB provisioning apply requires --confirm-dev and --confirmed-non-production.");
return finalizeReport(report);
}
if (!target.ok) {
return finalizeReport(report);
}
if (!admin.present && !options.adminClient) {
addBlocker(report, "runtime_blocker", "runtime-provisioning-admin", "DEV DB provisioning apply requires the admin DB URL from its SecretRef env.", {
adminUrlEnv: DEV_DB_ADMIN_SECRET_REF.env,
adminUrlSecretRef: DEV_DB_ADMIN_SECRET_REF,
valueRedacted: true,
secretValuesPrinted: false
});
return finalizeReport(report);
}
await applyProvisioning(report, env, target, options);
if (report.actions.adminWriteAttempted && report.blockers.length === 0) {
await verifyRuntimeReadiness(report, env, options);
inferProvisioningFromRuntime(report);
addRuntimeProvisioningBlocker(report);
}
return finalizeReport(report);
}
addSafetyRefusal(report, `Unsupported DEV DB provisioning mode: ${parsed.mode}`);
return finalizeReport(report);
}
export function parseArgs(argv = []) {
const args = {
mode: "check",
allowLiveDbRead: false,
confirmDev: false,
confirmedNonProduction: false,
writeReport: false,
reportPath: defaultReportPath,
pretty: false,
failOnBlocked: false,
help: false
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--check") args.mode = "check";
else if (arg === "--dry-run") args.mode = "dry-run";
else if (arg === "--apply") args.mode = "apply";
else if (arg === "--allow-live-db-read") args.allowLiveDbRead = true;
else if (arg === "--confirm-dev") args.confirmDev = true;
else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true;
else if (arg === "--write-report") args.writeReport = true;
else if (arg === "--pretty") args.pretty = true;
else if (arg === "--fail-on-blocked") args.failOnBlocked = true;
else if (arg === "--help" || arg === "-h") args.help = true;
else if (arg === "--report") {
index += 1;
args.reportPath = requireArgValue(argv[index], "--report");
} else if (arg.startsWith("--report=")) {
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
return args;
}
export function parseProvisioningTarget(rawUrl) {
const present = typeof rawUrl === "string" && rawUrl.trim().length > 0;
if (!present) {
return {
ok: false,
present: false,
parseStatus: "missing",
errorCode: "TARGET_DB_URL_MISSING",
roleNamePresent: false,
databaseNamePresent: false,
passwordPresent: false
};
}
let url;
try {
url = new URL(rawUrl);
} catch {
return {
ok: false,
present: true,
parseStatus: "invalid_url",
errorCode: "TARGET_DB_URL_INVALID",
roleNamePresent: false,
databaseNamePresent: false,
passwordPresent: false
};
}
const protocolReady = ["postgres:", "postgresql:"].includes(url.protocol);
const roleName = decodeURIComponent(url.username || "");
const password = decodeURIComponent(url.password || "");
const databaseName = decodeURIComponent(url.pathname.replace(/^\/+/u, ""));
const databaseAllowed = Boolean(databaseName) && !systemDatabases.has(databaseName.toLowerCase());
const ok = protocolReady && Boolean(roleName) && Boolean(password) && databaseAllowed;
return {
ok,
present: true,
parseStatus: ok ? "ready" : "blocked",
errorCode: ok ? null : targetParseErrorCode({ protocolReady, roleName, password, databaseName, databaseAllowed }),
roleNamePresent: Boolean(roleName),
databaseNamePresent: Boolean(databaseName),
databaseAllowed,
passwordPresent: Boolean(password),
endpointClass: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
roleName,
databaseName,
password
};
}
function normalizeArgs(args) {
return {
...parseArgs([]),
...args
};
}
function targetParseErrorCode({ protocolReady, roleName, password, databaseName, databaseAllowed }) {
if (!protocolReady) return "TARGET_DB_URL_PROTOCOL_BLOCKED";
if (!roleName) return "TARGET_DB_URL_ROLE_MISSING";
if (!password) return "TARGET_DB_URL_PASSWORD_MISSING";
if (!databaseName) return "TARGET_DB_URL_DATABASE_MISSING";
if (!databaseAllowed) return "TARGET_DB_URL_DATABASE_FORBIDDEN";
return "TARGET_DB_URL_BLOCKED";
}
function summarizeAdminEnv(env) {
return {
present: typeof env?.[DEV_DB_ADMIN_SECRET_REF.env] === "string" && env[DEV_DB_ADMIN_SECRET_REF.env].trim().length > 0,
env: DEV_DB_ADMIN_SECRET_REF.env,
secretRef: DEV_DB_ADMIN_SECRET_REF,
valueRedacted: true
};
}
function summarizeDbEnv(env, target, admin) {
return {
targetUrlPresent: target.present,
targetUrlValueRedacted: true,
targetEndpointRedacted: true,
targetEndpointClass: target.endpointClass ?? DEV_DB_ENV_CONTRACT.endpointAuthority.source,
targetParseStatus: target.parseStatus,
targetErrorCode: target.errorCode,
targetRoleNamePresent: target.roleNamePresent,
targetDatabaseNamePresent: target.databaseNamePresent,
targetDatabaseAllowed: target.databaseAllowed === true,
targetPasswordPresent: target.passwordPresent,
sslMode: env?.HWLAB_CLOUD_DB_SSL_MODE || DEV_DB_ENV_CONTRACT.nonSecretDefaults.HWLAB_CLOUD_DB_SSL_MODE,
sslModeSecret: false,
adminUrlPresent: admin.present,
adminUrlValueRedacted: true,
adminEndpointRedacted: true,
dbUrlSecretRef: appSecretRef,
adminUrlSecretRef: DEV_DB_ADMIN_SECRET_REF,
endpointAuthority: DEV_DB_ENV_CONTRACT.endpointAuthority.source
};
}
function validateSourceContract(report, target, args) {
if (target.ok) return;
if (args.mode === "check" && target.present === false) return;
addBlocker(report, "runtime_blocker", "runtime-provisioning-target", "DEV DB provisioning requires a valid target DB URL from the cloud-api SecretRef env.", {
env: appSecretRef.env,
secretRef: appSecretRef,
errorCode: target.errorCode,
valueRedacted: true,
endpointRedacted: true,
secretValuesPrinted: false
});
}
async function verifyRuntimeReadiness(report, env, options) {
report.actions.liveDbReadAttempted = true;
report.actions.appRuntimeReadinessAttempted = true;
const runtimeStore = new PostgresCloudRuntimeStore({
env,
dbUrl: env?.HWLAB_CLOUD_DB_URL,
sslMode: env?.HWLAB_CLOUD_DB_SSL_MODE,
queryClient: options.queryClient,
pgModuleLoader: options.pgModuleLoader
});
let runtime;
try {
runtime = await runtimeStore.readiness();
} finally {
if (!options.queryClient && typeof runtimeStore.pool?.end === "function") {
await runtimeStore.pool.end();
}
}
report.runtime = summarizeRuntime(runtime);
report.actions.readinessVerified = runtime.ready === true;
report.downstreamRuntimeGates = report.runtime.gates;
report.gates = {
...report.gates,
ssl: normalizeGate(runtime.gates?.ssl),
auth: normalizeGate(runtime.gates?.auth),
schema: normalizeGate(runtime.gates?.schema),
migration: normalizeGate(runtime.gates?.migration),
durability: normalizeGate(runtime.gates?.durability)
};
}
async function inspectAdminProvisioning(report, env, target, options) {
report.actions.liveDbReadAttempted = true;
report.actions.adminInspectionAttempted = true;
const { client, close } = await openAdminClient(env, options);
try {
const [role, database] = await Promise.all([
objectExists(client, "SELECT 1 AS present FROM pg_catalog.pg_roles WHERE rolname = $1 LIMIT 1", [target.roleName]),
objectExists(client, "SELECT 1 AS present FROM pg_catalog.pg_database WHERE datname = $1 LIMIT 1", [target.databaseName])
]);
report.provisioning.targetRole = {
checked: true,
exists: role,
created: false,
ready: role,
targetNamePresent: target.roleNamePresent
};
report.provisioning.targetDatabase = {
checked: true,
exists: database,
created: false,
ready: database,
targetNamePresent: target.databaseNamePresent
};
report.actions.roleExisted = role;
report.actions.databaseExisted = database;
report.provisioning.ready = role && database && runtimeAuthSatisfied(report.runtime);
if (!role) addMissingObjectBlocker(report, "role");
if (!database) addMissingObjectBlocker(report, "database");
} catch (error) {
addClassifiedAdminBlocker(report, error, "DEV DB provisioning inspection could not verify the target role/database.");
} finally {
await close();
}
}
async function applyProvisioning(report, env, target, options) {
report.actions.liveDbWriteAttempted = true;
report.actions.adminWriteAttempted = true;
let admin;
try {
admin = await openAdminClient(env, options);
const client = admin.client;
const roleExists = await objectExists(client, "SELECT 1 AS present FROM pg_catalog.pg_roles WHERE rolname = $1 LIMIT 1", [target.roleName]);
const databaseExists = await objectExists(client, "SELECT 1 AS present FROM pg_catalog.pg_database WHERE datname = $1 LIMIT 1", [target.databaseName]);
report.actions.roleExisted = roleExists;
report.actions.databaseExisted = databaseExists;
if (!roleExists) {
await client.query(`CREATE ROLE ${quotePgIdent(target.roleName)} LOGIN PASSWORD ${quotePgLiteral(target.password)}`);
report.actions.roleCreated = true;
} else {
await client.query(`ALTER ROLE ${quotePgIdent(target.roleName)} WITH LOGIN PASSWORD ${quotePgLiteral(target.password)}`);
}
report.actions.rolePasswordSynchronized = true;
if (!databaseExists) {
await client.query(`CREATE DATABASE ${quotePgIdent(target.databaseName)} OWNER ${quotePgIdent(target.roleName)}`);
report.actions.databaseCreated = true;
}
await client.query(`GRANT CONNECT ON DATABASE ${quotePgIdent(target.databaseName)} TO ${quotePgIdent(target.roleName)}`);
report.actions.databaseConnectGranted = true;
} catch (error) {
addClassifiedAdminBlocker(report, error, "DEV DB provisioning apply could not complete role/database provisioning.");
return;
} finally {
if (admin) await admin.close();
}
let targetAdmin;
try {
targetAdmin = await openAdminClient(env, {
...options,
adminDbUrlOverride: withDatabaseInPostgresUrl(env[DEV_DB_ADMIN_SECRET_REF.env], target.databaseName)
});
await targetAdmin.client.query(`GRANT USAGE, CREATE ON SCHEMA public TO ${quotePgIdent(target.roleName)}`);
report.actions.schemaPrivilegesGranted = true;
} catch (error) {
addClassifiedAdminBlocker(report, error, "DEV DB provisioning apply could not grant target database schema privileges.");
return;
} finally {
if (targetAdmin) await targetAdmin.close();
}
report.provisioning.targetRole = {
checked: true,
exists: true,
created: report.actions.roleCreated,
ready: true,
targetNamePresent: true
};
report.provisioning.targetDatabase = {
checked: true,
exists: true,
created: report.actions.databaseCreated,
ready: true,
targetNamePresent: true
};
report.provisioning.privileges = {
checked: true,
ready: report.actions.databaseConnectGranted && report.actions.schemaPrivilegesGranted,
databaseConnectGranted: report.actions.databaseConnectGranted,
schemaPrivilegesGranted: report.actions.schemaPrivilegesGranted
};
report.provisioning.ready = report.provisioning.privileges.ready;
}
async function openAdminClient(env, options) {
if (options.adminClient) {
return {
client: options.adminClient,
close: async () => {}
};
}
const pool = await createPgPool({
dbUrl: options.adminDbUrlOverride ?? env[DEV_DB_ADMIN_SECRET_REF.env],
sslMode: env.HWLAB_CLOUD_DB_SSL_MODE,
timeoutMs: env.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS,
pgModuleLoader: options.pgModuleLoader
});
return {
client: pool,
close: async () => {
await pool.end();
}
};
}
async function createPgPool({ dbUrl, sslMode, timeoutMs, pgModuleLoader }) {
let pg;
try {
pg = await (pgModuleLoader ? pgModuleLoader() : import("pg"));
} catch (error) {
if (error?.code === "ERR_MODULE_NOT_FOUND") {
const driverError = new Error("Postgres provisioning requires the pg package");
driverError.code = "HWLAB_PG_DRIVER_MISSING";
throw driverError;
}
throw error;
}
const Pool = pg.Pool ?? pg.default?.Pool;
if (typeof Pool !== "function") {
const driverError = new Error("Postgres provisioning could not load pg.Pool");
driverError.code = "HWLAB_PG_DRIVER_MISSING";
throw driverError;
}
return new Pool(buildPostgresPoolConfig({
dbUrl,
sslMode,
timeoutMs
}));
}
function inferProvisioningFromRuntime(report) {
const runtimeReady = report.runtime?.ready === true && report.runtime?.durable === true;
const authReady = runtimeAuthSatisfied(report.runtime);
if (!runtimeReady && !authReady) return;
report.provisioning.targetRole = {
...report.provisioning.targetRole,
checked: report.provisioning.targetRole.checked || false,
exists: true,
ready: true,
inferredFromRuntime: true
};
report.provisioning.targetDatabase = {
...report.provisioning.targetDatabase,
checked: report.provisioning.targetDatabase.checked || false,
exists: true,
ready: true,
inferredFromRuntime: true
};
report.provisioning.ready = true;
}
function addRuntimeProvisioningBlocker(report) {
const blocker = report.runtime?.blocker;
if (!blocker) return;
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
addBlocker(report, "runtime_blocker", "runtime-provisioning-ssl", "DEV DB provisioning reached the target runtime adapter, but SSL mode blocked connection.", runtimeEvidence(report.runtime));
} else if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
addBlocker(report, "runtime_blocker", "runtime-provisioning-auth", "DEV DB target role/database exists check did not prove application authentication readiness.", runtimeEvidence(report.runtime));
} else if (blocker === RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING) {
addBlocker(report, "runtime_blocker", "runtime-provisioning-driver", "Postgres driver is unavailable for DEV DB provisioning verification.", runtimeEvidence(report.runtime));
}
}
function addMissingObjectBlocker(report, kind) {
const scope = kind === "role" ? "runtime-provisioning-role" : "runtime-provisioning-database";
addBlocker(report, "runtime_blocker", scope, `DEV DB target ${kind} is missing according to admin catalog inspection.`, {
exists: false,
created: false,
valueRedacted: true,
endpointRedacted: true,
affectedRuntimeBlocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
secretValuesPrinted: false
});
}
function addClassifiedAdminBlocker(report, error, summary) {
const classified = classifyProvisioningError(error);
addBlocker(report, "runtime_blocker", classified.scope, summary, {
blocker: classified.blocker,
errorCode: classified.errorCode,
valueRedacted: true,
endpointRedacted: true,
secretValuesPrinted: false
});
}
function classifyProvisioningError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "UNKNOWN";
if (errorCode === "HWLAB_PG_DRIVER_MISSING") {
return {
blocker: RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
scope: "runtime-provisioning-driver",
errorCode
};
}
if (isSslError(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
scope: "runtime-provisioning-ssl",
errorCode
};
}
if (["28P01", "28000", "42501"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
scope: "runtime-provisioning-auth",
errorCode
};
}
if (["3D000"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
scope: "runtime-provisioning-database",
errorCode
};
}
if (["42P01", "42703", "3F000"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
scope: "runtime-provisioning-schema",
errorCode
};
}
return {
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
scope: "runtime-provisioning-query",
errorCode
};
}
function runtimeEvidence(runtime) {
return {
blocker: runtime.blocker,
queryResult: runtime.connection?.queryResult ?? "unknown",
errorCode: runtime.connection?.errorCode ?? null,
schemaReady: runtime.schema?.ready === true,
migrationReady: runtime.migration?.ready === true,
durableReady: runtime.ready === true && runtime.durable === true,
secretValuesPrinted: false
};
}
async function objectExists(client, sql, params) {
const result = await client.query(sql, params);
return Array.isArray(result?.rows) && result.rows.length > 0;
}
function sourceOnlyGates(target) {
const sourceReady = target.ok;
const targetMissingInSourceOnly = target.present === false;
return {
ssl: notCheckedGate("source-only validation does not connect to Postgres"),
auth: sourceReady || targetMissingInSourceOnly ? notCheckedGate("auth requires live target runtime verification") : blockedGate("runtime_provisioning_target_blocked"),
role: sourceReady || targetMissingInSourceOnly ? notCheckedGate("role existence requires DEV admin catalog inspection") : blockedGate("runtime_provisioning_target_blocked"),
database: sourceReady || targetMissingInSourceOnly ? notCheckedGate("database existence requires DEV admin catalog inspection") : blockedGate("runtime_provisioning_target_blocked"),
schema: notCheckedGate("schema is verified by runtime migration/readiness after role/database provisioning"),
migration: notCheckedGate("migration ledger is verified by dev-runtime-migration"),
durability: notCheckedGate("durability is verified by post-provisioning runtime readiness and postflight")
};
}
function normalizeGate(gate = {}) {
if (gate.ready === true || gate.status === "ready") return readyGate();
if (gate.status === "blocked") return blockedGate(gate.blocker ?? "runtime_durable_adapter_blocked");
return notCheckedGate();
}
function readyGate(summary = "ready") {
return {
checked: true,
ready: true,
status: "ready",
blocker: null,
summary
};
}
function blockedGate(blocker, summary = "blocked") {
return {
checked: true,
ready: false,
status: "blocked",
blocker,
summary
};
}
function notCheckedGate(summary = "not checked") {
return {
checked: false,
ready: false,
status: "not_checked",
blocker: null,
summary
};
}
function emptyObjectState(targetNamePresent) {
return {
checked: false,
exists: false,
created: false,
ready: false,
targetNamePresent
};
}
function summarizeRuntime(runtime = {}) {
return {
adapter: runtime.adapter ?? "unknown",
durable: Boolean(runtime.durable),
durableRequested: Boolean(runtime.durableRequested),
ready: runtime.ready === true,
status: runtime.status ?? "unknown",
blocker: runtime.blocker ?? null,
liveRuntimeEvidence: Boolean(runtime.liveRuntimeEvidence),
fixtureEvidence: Boolean(runtime.fixtureEvidence),
connection: {
queryAttempted: Boolean(runtime.connection?.queryAttempted),
queryResult: runtime.connection?.queryResult ?? "unknown",
endpointRedacted: true,
valueRedacted: true,
errorCode: runtime.connection?.errorCode ?? null
},
schema: {
checked: Boolean(runtime.schema?.checked),
ready: Boolean(runtime.schema?.ready),
missingTables: Array.isArray(runtime.schema?.missingTables) ? [...runtime.schema.missingTables] : [],
missingColumns: Array.isArray(runtime.schema?.missingColumns) ? [...runtime.schema.missingColumns] : []
},
migration: {
checked: Boolean(runtime.migration?.checked),
ready: Boolean(runtime.migration?.ready),
requiredMigrationId: runtime.migration?.requiredMigrationId ?? null,
appliedMigrationId: runtime.migration?.appliedMigrationId ?? null,
missing: runtime.migration?.missing !== false
},
gates: runtime.gates ?? {}
};
}
function runtimeAuthSatisfied(runtime = {}) {
if (runtime.ready === true && runtime.durable === true) return true;
const blocker = runtime.blocker;
return [
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED
].includes(blocker) && runtime.connection?.queryAttempted === true;
}
function buildApplyBoundary(args) {
return {
defaultMode: "check",
mode: args.mode,
liveDbReads: args.mode === "apply" || (args.mode === "dry-run" && args.allowLiveDbRead),
liveDbWrites: args.mode === "apply",
requiresForLiveDbRead: [
"--confirm-dev",
`${appSecretRef.env} from ${appSecretRef.secretName}/${appSecretRef.secretKey}`,
`${DEV_DB_ADMIN_SECRET_REF.env} from ${DEV_DB_ADMIN_SECRET_REF.secretName}/${DEV_DB_ADMIN_SECRET_REF.secretKey} unless runtime readiness already proves auth`
],
requiresForApply: [
"--apply",
"--confirm-dev",
"--confirmed-non-production",
`${appSecretRef.env} from ${appSecretRef.secretName}/${appSecretRef.secretKey}`,
`${DEV_DB_ADMIN_SECRET_REF.env} from ${DEV_DB_ADMIN_SECRET_REF.secretName}/${DEV_DB_ADMIN_SECRET_REF.secretKey}`
],
writeScope: args.mode === "apply"
? [
"DEV Postgres target role named by the redacted cloud-api DB URL",
"DEV Postgres target database named by the redacted cloud-api DB URL",
"DEV target DB CONNECT and public schema CREATE/USAGE grants for that role"
]
: [],
noWriteScope: [
"Kubernetes Secret resources",
"PROD",
"service restarts",
"schema migrations",
"M3/M4/M5 acceptance"
],
forbidden: [
"printing DB URLs, passwords, tokens, Secret values, or kubeconfig material",
"manual psql or node stdin database writes",
"mutating PROD"
]
};
}
function buildSafety(args) {
return {
devOnly: true,
environment: ENVIRONMENT_DEV,
targetNamespace: DEV_DB_ENV_CONTRACT.dns.namespace,
prodAllowed: false,
sourceStoresSecretValues: false,
printsSecretValues: false,
secretValuesPrinted: false,
dbUrlValueRedacted: true,
adminUrlValueRedacted: true,
endpointRedacted: true,
readsKubernetesSecrets: false,
writesKubernetesSecrets: false,
liveDbWrites: args.mode === "apply",
fixtureEvidenceAllowed: false
};
}
function addBlocker(report, type, scope, summary, evidence = {}) {
report.blockers.push({
type,
scope,
status: "open",
summary,
sourceIssue: scope.includes("auth") ? "pikasTech/HWLAB#307" : issue,
evidence
});
}
function addSafetyRefusal(report, summary) {
report.safetyRefusal = true;
addBlocker(report, "safety_refusal", "runtime-provisioning-apply-boundary", summary, {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
});
}
function finalizeReport(report) {
const openBlockers = report.blockers.filter((blocker) => blocker.status === "open");
report.provisioning.ready = report.provisioning.ready || (
report.provisioning.targetRole.ready === true &&
report.provisioning.targetDatabase.ready === true &&
(report.provisioning.privileges.ready === true || report.mode !== "apply")
);
report.conclusion = {
status: openBlockers.length === 0 ? "ready" : "blocked",
summary: openBlockers.length === 0
? conclusionReadySummary(report)
: "DEV DB provisioning is blocked; see separated role/database/ssl/auth blockers.",
blockerCount: openBlockers.length
};
return report;
}
function conclusionReadySummary(report) {
if (report.mode === "apply") {
return "DEV DB role/database provisioning completed without printing secret values.";
}
if (report.mode === "dry-run" && report.actions.liveDbReadAttempted) {
return "DEV DB role/database provisioning read-only verification passed without DB writes or secret output.";
}
return "DEV DB provisioning source contract is ready; no DB connection or live write was attempted.";
}
function summarizeReport(report) {
return {
issue: report.issue,
mode: report.mode,
conclusion: report.conclusion,
db: report.db,
actions: report.actions,
provisioning: report.provisioning,
gates: report.gates,
downstreamRuntimeGates: report.downstreamRuntimeGates,
blockers: report.blockers,
safety: report.safety
};
}
function quotePgIdent(value) {
return `"${String(value).replaceAll('"', '""')}"`;
}
function quotePgLiteral(value) {
return `'${String(value).replaceAll("'", "''")}'`;
}
function withDatabaseInPostgresUrl(rawUrl, databaseName) {
const url = new URL(rawUrl);
url.pathname = `/${encodeURIComponent(databaseName)}`;
return url.toString();
}
function isSslError(error) {
const code = typeof error?.code === "string" ? error.code : "";
if (code.startsWith("ERR_SSL") || code.startsWith("ERR_TLS")) return true;
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
return [
"does not support ssl",
"ssl is not enabled",
"ssl negotiation",
"ssl off",
"ssl required",
"requires ssl",
"no ssl encryption",
"no encryption",
"hostssl",
"server requires ssl",
"tls handshake"
].some((pattern) => message.includes(pattern));
}
async function writeReport(report, reportPath, root) {
const absolute = path.resolve(root, reportPath);
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
}
function requireArgValue(value, flag) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`${flag} requires a value`);
}
return value;
}
function usage() {
return [
"Usage: node scripts/dev-runtime-provisioning.mjs [--check|--dry-run|--apply]",
"",
"Default --check validates redacted DEV DB provisioning inputs only and never connects to DB.",
"--dry-run is source-only unless --allow-live-db-read --confirm-dev is provided.",
"--apply requires --confirm-dev --confirmed-non-production, HWLAB_CLOUD_DB_URL, and HWLAB_CLOUD_DB_ADMIN_URL.",
"Reports never print DB URLs, passwords, tokens, Secret values, or kubeconfig material."
].join("\n");
}
export function formatDevRuntimeProvisioningFailure(error) {
const message = redactFailureText(error instanceof Error ? error.message : String(error));
return {
issue,
conclusion: {
status: "blocked",
summary: "DEV DB provisioning command failed before producing a report."
},
error: message,
safety: {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
},
trace: createHash("sha256").update(message).digest("hex").slice(0, 12)
};
}
function redactFailureText(value) {
return String(value ?? "")
.replace(/postgres(?:ql)?:\/\/[^\s"'<>]+/giu, "[redacted-postgres-url]")
.replace(/(password\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]")
.replace(/(token\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]")
.replace(/(kubeconfig\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]");
}
@@ -0,0 +1,270 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
buildDevRuntimeProvisioningReport,
parseArgs
} from "./dev-runtime-provisioning.mjs";
test("source check parses target role/database without leaking DB URL material", async () => {
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
env: {
HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("super") }),
HWLAB_CLOUD_DB_SSL_MODE: "disable"
},
now: () => "2026-05-23T00:00:00.000Z"
});
assert.equal(report.conclusion.status, "ready");
assert.equal(report.actions.liveDbReadAttempted, false);
assert.equal(report.actions.liveDbWriteAttempted, false);
assert.equal(report.db.targetRoleNamePresent, true);
assert.equal(report.db.targetDatabaseNamePresent, true);
assert.equal(report.db.targetPasswordPresent, true);
assert.equal(report.safety.secretValuesPrinted, false);
assertNoFixtureSecrets(report);
});
test("source check without injected DB URL is ready and performs no live access", async () => {
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
env: {},
now: () => "2026-05-23T00:00:00.000Z"
});
assert.equal(report.conclusion.status, "ready");
assert.equal(report.actions.liveDbReadAttempted, false);
assert.equal(report.actions.liveDbWriteAttempted, false);
assert.equal(report.blockers.length, 0);
assert.equal(report.db.targetUrlPresent, false);
assert.equal(report.gates.role.status, "not_checked");
});
test("live dry-run classifies missing target role separately from missing database", async () => {
const report = await buildDevRuntimeProvisioningReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: envWithAdmin(),
adminClient: createAdminClient({ roleExists: false, databaseExists: true }),
queryClient: createRuntimeClient("auth"),
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.actions.adminInspectionAttempted, true);
assert.equal(report.actions.liveDbWriteAttempted, false);
assert.equal(report.provisioning.targetRole.exists, false);
assert.equal(report.provisioning.targetDatabase.exists, true);
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-role"), true);
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-database"), false);
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-auth"), true);
assertNoFixtureSecrets(report);
});
test("live dry-run classifies missing target database separately from target role", async () => {
const report = await buildDevRuntimeProvisioningReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: envWithAdmin(),
adminClient: createAdminClient({ roleExists: true, databaseExists: false }),
queryClient: createRuntimeClient("auth"),
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.provisioning.targetRole.exists, true);
assert.equal(report.provisioning.targetDatabase.exists, false);
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-role"), false);
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-database"), true);
});
test("live dry-run separates SSL from auth/schema/migration", async () => {
const report = await buildDevRuntimeProvisioningReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: envWithAdmin({ sslMode: "require" }),
adminClient: createAdminClient({ roleExists: true, databaseExists: true }),
queryClient: createRuntimeClient("ssl"),
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.gates.ssl.status, "blocked");
assert.equal(report.gates.auth.status, "not_checked");
assert.equal(report.gates.schema.status, "not_checked");
assert.equal(report.gates.migration.status, "not_checked");
assert.equal(report.blockers.some((blocker) => blocker.scope === "runtime-provisioning-ssl"), true);
assert.equal(JSON.stringify(report).includes("does not support SSL"), false);
});
test("live dry-run leaves role/database ready while migration remains a downstream blocker", async () => {
const report = await buildDevRuntimeProvisioningReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: envWithAdmin(),
adminClient: createAdminClient({ roleExists: true, databaseExists: true }),
queryClient: createRuntimeClient("migration"),
now: () => "2026-05-23T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "ready");
assert.equal(report.provisioning.ready, true);
assert.equal(report.provisioning.targetRole.ready, true);
assert.equal(report.provisioning.targetDatabase.ready, true);
assert.equal(report.runtime.blocker, "runtime_durable_adapter_migration_blocked");
assert.equal(report.gates.migration.status, "blocked");
assert.equal(report.blockers.length, 0);
});
test("apply creates missing role/database and grants target privileges without printing secrets", async () => {
const adminClient = createAdminClient({ roleExists: false, databaseExists: false });
const targetAdminClient = createAdminClient({ roleExists: true, databaseExists: true });
const report = await buildDevRuntimeProvisioningReport(
parseArgs(["--apply", "--confirm-dev", "--confirmed-non-production"]),
{
env: envWithAdmin(),
adminClient,
queryClient: createRuntimeClient("ready"),
now: () => "2026-05-23T00:00:00.000Z",
adminDbUrlOverride: null,
pgModuleLoader: async () => {
throw new Error("pgModuleLoader should not be used when adminClient is injected");
}
}
);
assert.equal(targetAdminClient.calls.length, 0);
assert.equal(report.conclusion.status, "ready");
assert.equal(report.actions.roleCreated, true);
assert.equal(report.actions.databaseCreated, true);
assert.equal(report.actions.rolePasswordSynchronized, true);
assert.equal(report.actions.databaseConnectGranted, true);
assert.equal(report.actions.schemaPrivilegesGranted, true);
assert.equal(report.provisioning.ready, true);
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("CREATE ROLE ")));
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("CREATE DATABASE ")));
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("GRANT CONNECT ON DATABASE ")));
assert.ok(adminClient.calls.some((call) => call.sql.startsWith("GRANT USAGE, CREATE ON SCHEMA public ")));
assertNoFixtureSecrets(report);
});
function envWithAdmin({ sslMode = "disable" } = {}) {
return {
HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("app") }),
HWLAB_CLOUD_DB_ADMIN_URL: fixturePostgresUrl({
user: "postgres",
password: fixtureSecret("admin"),
database: "postgres"
}),
HWLAB_CLOUD_DB_SSL_MODE: sslMode
};
}
function fixturePostgresUrl({
user = "hwlab_app",
password = fixtureSecret("app"),
host = "db.example.test",
database = "hwlab_runtime"
} = {}) {
return `${["postgresql", "://"].join("")}${user}:${password}@${host}:5432/${database}`;
}
function fixtureSecret(kind) {
return `${kind}-${"secret"}`;
}
function assertNoFixtureSecrets(value) {
const text = JSON.stringify(value);
for (const forbidden of [
fixtureSecret("super"),
fixtureSecret("app"),
fixtureSecret("admin"),
"db.example.test",
"hwlab_app",
"hwlab_runtime"
]) {
assert.equal(text.includes(forbidden), false);
}
}
function createAdminClient({ roleExists, databaseExists }) {
const state = {
roleExists,
databaseExists,
calls: []
};
return {
get calls() {
return state.calls;
},
async query(sql, params = []) {
state.calls.push({ sql, params });
if (sql.includes("pg_catalog.pg_roles")) {
return { rows: state.roleExists ? [{ present: 1 }] : [] };
}
if (sql.includes("pg_catalog.pg_database")) {
return { rows: state.databaseExists ? [{ present: 1 }] : [] };
}
if (sql.startsWith("CREATE ROLE ")) {
state.roleExists = true;
return { rows: [] };
}
if (sql.startsWith("ALTER ROLE ")) {
return { rows: [] };
}
if (sql.startsWith("CREATE DATABASE ")) {
state.databaseExists = true;
return { rows: [] };
}
if (sql.startsWith("GRANT CONNECT ON DATABASE ") || sql.startsWith("GRANT USAGE, CREATE ON SCHEMA public ")) {
return { rows: [] };
}
throw new Error(`unexpected admin query: ${sql}`);
}
};
}
function createRuntimeClient(mode) {
return {
async query(sql) {
if (sql.includes("information_schema.columns")) {
if (mode === "ssl") {
const error = new Error("fixture SSL blocked: server does not support SSL");
error.code = "08P01";
throw error;
}
if (mode === "auth") {
const error = new Error("fixture auth blocked");
error.code = "28P01";
throw error;
}
return { rows: schemaRows() };
}
if (sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations")) {
if (mode === "migration") return { rows: [] };
return { rows: [{ id: "0001_cloud_core_skeleton", schema_version: "runtime-durable-postgres-v1" }] };
}
if (sql.startsWith("SELECT COUNT(*)::int AS count FROM ")) {
return { rows: [{ count: 0 }] };
}
throw new Error(`unexpected runtime query: ${sql}`);
}
};
}
function schemaRows() {
const schema = {
gateway_sessions: ["id", "project_id", "gateway_service_id", "status", "started_at", "ended_at", "gateway_session_json"],
box_resources: ["id", "project_id", "gateway_session_id", "resource_state", "labels_json", "resource_json", "updated_at"],
box_capabilities: ["id", "box_resource_id", "capability_type", "capability_json", "updated_at"],
hardware_operations: ["id", "project_id", "requested_by", "operation_type", "operation_json", "status", "requested_at", "updated_at"],
audit_events: ["id", "request_id", "actor", "source", "operation", "target", "result", "timestamp", "event_json"],
evidence_records: ["id", "project_id", "operation_id", "evidence_type", "uri", "metadata_json", "created_at"]
};
return Object.entries(schema).flatMap(([table, columns]) =>
columns.map((column) => ({ table_name: table, column_name: column }))
);
}
+9 -1
View File
@@ -199,11 +199,14 @@ function assertDurableRuntimeRunbook(value) {
for (const expected of [
"DB live readiness and durable runtime readiness are separate gates",
"runtime_durable_adapter_query_blocked",
"DEV DB Provisioning Automation",
"Source migration contract",
"Live provisioning apply",
"Static runtime boundary contract",
"Public health observation",
"Live DB read verification",
"Live migration/repair apply",
"Durable runtime postflight",
"Do not run `kubectl get secret`",
"secretValuesPrinted=false",
"Before any authorized live migration or repair",
@@ -212,10 +215,15 @@ function assertDurableRuntimeRunbook(value) {
"Moved:",
"Still blocked:",
"No full M3, M4, or M5 acceptance is allowed while runtime durability is blocked",
"node scripts/dev-runtime-provisioning.mjs --check",
"node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --write-report",
"node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --write-report",
"node scripts/dev-runtime-migration.mjs --check",
"node scripts/dev-runtime-migration.mjs --dry-run --write-report",
"node scripts/dev-runtime-migration.mjs --dry-run --allow-live-db-read --confirm-dev --write-report",
"node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --write-report"
"node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --write-report",
"node scripts/dev-runtime-postflight.mjs --check",
"node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --write-report"
]) {
assertIncludes(value, expected, label);
}