Merge remote-tracking branch 'origin/main' into fix/cd-artifact-fastpath-20260524

# Conflicts:
#	package.json
This commit is contained in:
Codex
2026-05-24 06:18:34 +00:00
11 changed files with 219 additions and 27 deletions
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import {
formatDevRuntimeMigrationFailure,
runDevRuntimeMigrationCli
} from "../../scripts/src/dev-runtime-migration.mjs";
try {
process.exitCode = await runDevRuntimeMigrationCli(process.argv.slice(2));
} catch (error) {
process.stdout.write(`${JSON.stringify(formatDevRuntimeMigrationFailure(error), null, 2)}\n`);
process.exitCode = 1;
}
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import {
formatDevRuntimeProvisioningFailure,
runDevRuntimeProvisioningCli
} from "../../scripts/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;
}
+8 -5
View File
@@ -466,15 +466,18 @@ 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 --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json
node cmd/hwlab-cloud-api/provision.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json
node cmd/hwlab-cloud-api/migrate.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --report /tmp/hwlab-dev-gate/report.json
```
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.
Lease. Provisioning and migration run from the current `hwlab-cloud-api` image
as stable image-internal operator entrypoints; they receive DB input only
through SecretRef-backed env and print only redacted structured status. 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:
+41 -13
View File
@@ -220,11 +220,23 @@ below are true.
## DEV DB Provisioning Automation
The repo-owned DEV DB provisioning entrypoint is:
The cloud-api image owns a stable DEV DB provisioning entrypoint:
```sh
node cmd/hwlab-cloud-api/provision.mjs --check
node cmd/hwlab-cloud-api/provision.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
node cmd/hwlab-cloud-api/provision.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
```
This is the preferred image-internal operator command for `hwlab-cloud-api`.
It delegates to the same repo-owned provisioning implementation as the root
script and receives admin/target DB inputs only through SecretRef-backed env.
The repo-level compatibility entrypoint remains:
```sh
node scripts/dev-runtime-provisioning.mjs --check
node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
```
The default `--check` path validates the redacted target DB URL contract only:
@@ -237,7 +249,7 @@ kubeconfig material.
An authorized DEV provisioning apply is:
```sh
node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
```
The apply path uses `HWLAB_CLOUD_DB_URL` from
@@ -258,11 +270,27 @@ untracked manual DB write.
## Runtime Migration Automation
The repo-owned runtime migration entrypoint is:
The cloud-api image owns a stable runtime migration entrypoint:
```sh
node cmd/hwlab-cloud-api/migrate.mjs --check
node cmd/hwlab-cloud-api/migrate.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
node cmd/hwlab-cloud-api/migrate.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
```
This is the preferred image-internal operator command for `hwlab-cloud-api`.
It delegates to the same repo-owned migration implementation as the root
script, so there is only one migration contract and one output format. The
cloud-api artifact build copies `cmd/`, `scripts/`, `internal/`, `package.json`,
and runtime dependencies into the image; the DEV CD runtime migration Job uses
`node cmd/hwlab-cloud-api/migrate.mjs` from the current cloud-api image and
injects DB inputs only through SecretRef-backed env.
The repo-level compatibility entrypoint remains:
```sh
node scripts/dev-runtime-migration.mjs --check
node scripts/dev-runtime-migration.mjs --dry-run --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-migration.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
```
Those default paths are source-only. They validate
@@ -275,14 +303,14 @@ An authorized operator may run a read-only live verification only after
injecting the DEV DB URL through the existing Secret/env path:
```sh
node scripts/dev-runtime-migration.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-migration.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
```
A real DEV migration apply is intentionally separate and requires all explicit
write flags:
```sh
node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-migration.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
```
The apply command is scoped to DEV Postgres schema objects declared by
@@ -306,7 +334,7 @@ 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 --report /tmp/hwlab-dev-gate/report.json
node scripts/dev-runtime-postflight.mjs --live --confirm-dev --confirmed-non-production --target api --report /tmp/hwlab-dev-gate/dev-runtime-postflight-report.json
```
Default `--check` is source-only. Live mode first reads
@@ -335,13 +363,13 @@ authorizes the DEV apply command and the preconditions below are already true.
| Check | Safe for normal runners | Evidence allowed | Not allowed |
| --- | --- | --- | --- |
| Source migration contract | Yes | `node scripts/dev-runtime-migration.mjs --check` and `node scripts/dev-runtime-migration.mjs --dry-run --report /tmp/hwlab-dev-gate/report.json` | DB connection, Secret read, DEV/PROD write |
| Source migration contract | Yes | `node scripts/dev-runtime-migration.mjs --check` and `node scripts/dev-runtime-migration.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json` | 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 --report /tmp/hwlab-dev-gate/report.json` 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 --report /tmp/hwlab-dev-gate/report.json` 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 --report /tmp/hwlab-dev-gate/report.json` 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 --report /tmp/hwlab-dev-gate/report.json` 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 |
| Live provisioning apply | No, unless explicitly authorized by commander/operator | `node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json` 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 --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json` 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 --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json` 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 --report /tmp/hwlab-dev-gate/dev-runtime-postflight-report.json` 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
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -2687,7 +2687,7 @@ export async function runDevCdApply(argv, io = {}) {
kind: "runtime-k8s-job",
jobNamePrefix: "hwlab-runtime-provision",
commandArgs: [
"scripts/dev-runtime-provisioning.mjs",
"cmd/hwlab-cloud-api/provision.mjs",
"--apply",
"--confirm-dev",
"--confirmed-non-production",
@@ -2704,7 +2704,7 @@ export async function runDevCdApply(argv, io = {}) {
kind: "runtime-k8s-job",
jobNamePrefix: "hwlab-runtime-migrate",
commandArgs: [
"scripts/dev-runtime-migration.mjs",
"cmd/hwlab-cloud-api/migrate.mjs",
"--apply",
"--confirm-dev",
"--confirmed-non-production",
+14 -4
View File
@@ -1337,8 +1337,7 @@ test("transaction runs phases, allows internal side-effect env, releases lock, a
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 provisioningCall = commandLog.find((entry) => entry.args.includes("cmd/hwlab-cloud-api/provision.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);
@@ -1355,8 +1354,19 @@ test("transaction runs phases, allows internal side-effect env, releases lock, a
);
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");
const provisioningJob = JSON.parse(provisioningJobApply.input);
const migrationJob = JSON.parse(migrationJobApply.input);
assert.equal(provisioningJob.spec.template.spec.containers[0].args[0], "cmd/hwlab-cloud-api/provision.mjs");
assert.equal(migrationJob.spec.template.spec.containers[0].args[0], "cmd/hwlab-cloud-api/migrate.mjs");
assert.equal(
provisioningJob.spec.template.spec.containers[0].env.some((entry) => entry.name === "HWLAB_CD_TRANSACTION_ID"),
true
);
assert.equal(
migrationJob.spec.template.spec.containers[0].env.some((entry) => entry.name === "HWLAB_CD_TRANSACTION_ID"),
true
);
assert.equal(provisioningCall, undefined);
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");
+11 -1
View File
@@ -699,13 +699,15 @@ function usage() {
}
export function formatDevRuntimeMigrationFailure(error) {
const message = redactFailureText(error instanceof Error ? error.message : String(error));
return {
issue,
conclusion: {
status: "blocked",
summary: "DEV runtime migration command failed before producing a report."
},
error: error instanceof Error ? error.message : String(error),
error: message,
traceId: sha256(message).slice(0, 16),
safety: {
devOnly: true,
prodAllowed: false,
@@ -713,3 +715,11 @@ export function formatDevRuntimeMigrationFailure(error) {
}
};
}
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]");
}
@@ -1,5 +1,8 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import path from "node:path";
import test from "node:test";
import { promisify } from "node:util";
import {
buildDevRuntimeMigrationReport,
@@ -11,6 +14,9 @@ import {
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "../../internal/db/schema.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
test("source check is non-secret and does not attempt live DB access", async () => {
const report = await buildDevRuntimeMigrationReport(parseArgs(["--check"]), {
env: {
@@ -33,6 +39,48 @@ test("source check is non-secret and does not attempt live DB access", async ()
assert.equal(JSON.stringify(report).includes("db.example.test"), false);
});
test("cloud-api image migration entrypoint exposes the same non-secret source check", async () => {
const { stdout } = await execFileAsync(
process.execPath,
["cmd/hwlab-cloud-api/migrate.mjs", "--check"],
{
cwd: repoRoot,
env: {
...process.env,
HWLAB_CLOUD_DB_URL: "postgresql://hwlab:secret-password@db.example.test:5432/hwlab?sslmode=require",
HWLAB_CLOUD_DB_SSL_MODE: "disable"
}
}
);
const report = JSON.parse(stdout);
assert.equal(report.conclusion.status, "ready");
assert.equal(report.actions.liveDbReadAttempted, false);
assert.equal(report.actions.liveDbWriteAttempted, false);
assert.equal(report.safety.printsSecretValues, false);
assert.equal(report.safety.dbUrlValueRedacted, true);
assert.equal(report.migration.id, CLOUD_CORE_MIGRATION_ID);
assert.equal(JSON.stringify(report).includes("secret-password"), false);
assert.equal(JSON.stringify(report).includes("db.example.test"), false);
});
test("cloud-api image migration entrypoint redacts invalid DSN-shaped argv failures", async () => {
const secretArg = "postgresql://hwlab:secret-password@db.example.test:5432/hwlab?sslmode=require";
await assert.rejects(
execFileAsync(process.execPath, ["cmd/hwlab-cloud-api/migrate.mjs", secretArg], { cwd: repoRoot }),
(error) => {
const report = JSON.parse(error.stdout);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.error, "unknown argument: [redacted-postgres-url]");
assert.match(report.traceId, /^[0-9a-f]{16}$/u);
assert.equal(report.safety.secretValuesPrinted, false);
assert.equal(JSON.stringify(report).includes("secret-password"), false);
assert.equal(JSON.stringify(report).includes("db.example.test"), false);
return true;
}
);
});
test("apply mode refuses missing DEV confirmation before touching query client", async () => {
const queryClient = createFakeQueryClient({ migrationReady: false });
const report = await buildDevRuntimeMigrationReport(parseArgs(["--apply"]), {
@@ -1,11 +1,17 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import path from "node:path";
import test from "node:test";
import { promisify } from "node:util";
import {
buildDevRuntimeProvisioningReport,
parseArgs
} from "./dev-runtime-provisioning.mjs";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
test("source check parses target role/database without leaking DB URL material", async () => {
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
env: {
@@ -25,6 +31,31 @@ test("source check parses target role/database without leaking DB URL material",
assertNoFixtureSecrets(report);
});
test("cloud-api image provisioning entrypoint exposes non-secret source check", async () => {
const { stdout } = await execFileAsync(
process.execPath,
["cmd/hwlab-cloud-api/provision.mjs", "--check"],
{
cwd: repoRoot,
env: {
...process.env,
HWLAB_CLOUD_DB_URL: fixturePostgresUrl({ password: fixtureSecret("super") }),
HWLAB_CLOUD_DB_SSL_MODE: "disable"
}
}
);
const report = JSON.parse(stdout);
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: {},
+38
View File
@@ -215,6 +215,13 @@ function assertDurableRuntimeRunbook(value) {
"Moved:",
"Still blocked:",
"No full M3, M4, or M5 acceptance is allowed while runtime durability is blocked",
"node cmd/hwlab-cloud-api/provision.mjs --check",
"node cmd/hwlab-cloud-api/provision.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"node cmd/hwlab-cloud-api/provision.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"node cmd/hwlab-cloud-api/migrate.mjs --check",
"node cmd/hwlab-cloud-api/migrate.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
"node cmd/hwlab-cloud-api/migrate.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
"DEV CD runtime migration Job uses",
"node scripts/dev-runtime-provisioning.mjs --check",
"node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"node scripts/dev-runtime-provisioning.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
@@ -229,6 +236,31 @@ function assertDurableRuntimeRunbook(value) {
}
}
function assertCloudApiImageRuntimeEntrypoints({ packageJson, ciJson, artifactPublisher, devCdApplySource }) {
const label = "cloud-api image runtime maintenance entrypoints";
for (const expected of [
"COPY internal ./internal",
"COPY cmd ./cmd",
"COPY scripts ./scripts",
"COPY package.json ./package.json"
]) {
assertIncludes(artifactPublisher, expected, `${label} artifact publisher`);
}
for (const expected of [
"cmd/hwlab-cloud-api/provision.mjs",
"cmd/hwlab-cloud-api/migrate.mjs"
]) {
assertIncludes(devCdApplySource, expected, `${label} DEV CD job command`);
assertIncludes(packageJson.scripts?.check, `node --check ${expected}`, `${label} package check`);
assertIncludes(packageJson.scripts?.check, `node ${expected} --check`, `${label} package runtime check`);
}
assert.equal(
ciJson.commands?.some((command) => command.run === "npm run check"),
true,
`${label} CI must run npm run check`
);
}
function assertGuardFixture(guard) {
assert.equal(guard.guard, "hwlab-runtime-boundary");
assert.equal(guard.schemaVersion, "v1");
@@ -406,6 +438,12 @@ assertProdDisabled(
await readJSON(guard.requiredK3sSkeleton.prodDisabledPath)
);
assertDurableRuntimeRunbook(await readText("docs/reference/dev-runtime-boundary.md"));
assertCloudApiImageRuntimeEntrypoints({
packageJson: await readJSON("package.json"),
ciJson: await readJSON("CI.json"),
artifactPublisher: await readText("scripts/dev-artifact-publish.mjs"),
devCdApplySource: await readText("scripts/src/dev-cd-apply.mjs")
});
console.log(
`validated HWLAB runtime boundary guard for ${SERVICE_IDS.length} services; forbidden substitutes: ${forbiddenRuntimeSubstitutes.join(", ")}`