Files
pikasTech-HWLAB/scripts/src/dev-runtime-migration.mjs
T
2026-05-24 06:10:48 +00:00

726 lines
24 KiB
JavaScript

import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "../../internal/db/schema.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 { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const migrationPath = "internal/db/migrations/0001_cloud_core_skeleton.sql";
const defaultReportPath = tempReportPath("dev-runtime-migration-report.json");
const issue = "pikasTech/HWLAB#164";
export async function runDevRuntimeMigrationCli(argv = process.argv.slice(2), options = {}) {
const args = parseArgs(argv);
if (args.help) {
process.stdout.write(`${usage()}\n`);
return 0;
}
const report = await buildDevRuntimeMigrationReport(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 buildDevRuntimeMigrationReport(args = {}, options = {}) {
const parsed = normalizeArgs(args);
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
const root = options.repoRoot ?? repoRoot;
const sql = await readFile(path.join(root, migrationPath), "utf8");
const sourceCheck = validateMigrationSource(sql);
const db = summarizeDbEnv(env);
const report = {
issue,
mode: parsed.mode,
generatedAt: now(),
target: {
environment: ENVIRONMENT_DEV,
namespace: DEV_DB_ENV_CONTRACT.dns.namespace,
prodAllowed: false,
dbUrlEnv: "HWLAB_CLOUD_DB_URL",
dbUrlSecretRef: DEV_DB_ENV_CONTRACT.secretRefs[0],
dbEndpointAuthority: DEV_DB_ENV_CONTRACT.endpointAuthority.source
},
migration: {
id: CLOUD_CORE_MIGRATION_ID,
path: migrationPath,
sha256: sha256(sql),
schemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
ledgerTable: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
runtimeAdapter: "postgres"
},
db,
sourceCheck,
applyBoundary: buildApplyBoundary(parsed),
safety: buildSafety(parsed),
actions: {
sourceValidated: sourceCheck.ready,
liveDbReadAttempted: false,
liveDbWriteAttempted: false,
migrationApplied: false,
readinessVerified: false
},
gates: sourceOnlyGates(sourceCheck),
blockers: [],
safetyRefusal: false
};
if (!sourceCheck.ready) {
addBlocker(report, "contract_blocker", "runtime-migration-source", "Runtime migration source does not match the durable runtime schema contract.", {
missing: sourceCheck.missing
});
}
if (parsed.mode === "check" || (parsed.mode === "dry-run" && !parsed.allowLiveDbRead)) {
return finalizeReport(report);
}
if (parsed.mode === "dry-run") {
if (!parsed.confirmDev) {
addSafetyRefusal(report, "dry-run live DB verification requires --confirm-dev.");
return finalizeReport(report);
}
if (!db.urlPresent && !options.queryClient) {
addBlocker(report, "runtime_blocker", "runtime-migration-auth", "Live DB read verification requires HWLAB_CLOUD_DB_URL from the DEV Secret reference.", {
env: "HWLAB_CLOUD_DB_URL",
valueRedacted: true
});
return finalizeReport(report);
}
await verifyRuntimeReadiness(report, env, options);
return finalizeReport(report);
}
if (parsed.mode === "apply") {
if (!parsed.confirmDev || !parsed.confirmedNonProduction) {
addSafetyRefusal(report, "DEV runtime migration apply requires --confirm-dev and --confirmed-non-production.");
return finalizeReport(report);
}
if (!db.urlPresent && !options.queryClient) {
addBlocker(report, "runtime_blocker", "runtime-migration-auth", "Runtime migration apply requires HWLAB_CLOUD_DB_URL from the DEV Secret reference.", {
env: "HWLAB_CLOUD_DB_URL",
valueRedacted: true
});
return finalizeReport(report);
}
if (!sourceCheck.ready) {
addSafetyRefusal(report, "Refusing to apply because source migration validation is blocked.");
return finalizeReport(report);
}
await applyMigration(report, sql, env, options);
if (!report.safetyRefusal && report.actions.migrationApplied) {
await verifyRuntimeReadiness(report, env, options);
}
return finalizeReport(report);
}
addSafetyRefusal(report, `Unsupported runtime migration 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 === "--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");
args.writeReport = true;
} else if (arg.startsWith("--report=")) {
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
args.writeReport = true;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
return args;
}
export function validateMigrationSource(sql) {
const missing = [];
for (const [table, columns] of Object.entries(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS)) {
const tableMatch = sql.match(new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\s*\\(([\\s\\S]*?)\\);`, "u"));
if (!tableMatch) {
missing.push(`table:${table}`);
continue;
}
for (const column of columns) {
if (!new RegExp(`\\b${column}\\b`, "u").test(tableMatch[1])) {
missing.push(`column:${table}.${column}`);
}
}
}
const ledgerMatch = sql.match(
new RegExp(`CREATE TABLE IF NOT EXISTS ${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}\\s*\\(([\\s\\S]*?)\\);`, "u")
);
if (!ledgerMatch) {
missing.push(`table:${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}`);
} else {
for (const column of CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS) {
if (!new RegExp(`\\b${column}\\b`, "u").test(ledgerMatch[1])) {
missing.push(`column:${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}.${column}`);
}
}
}
if (!new RegExp(`'${CLOUD_CORE_MIGRATION_ID}'`, "u").test(sql)) {
missing.push(`ledger-id:${CLOUD_CORE_MIGRATION_ID}`);
}
if (!new RegExp(`'${CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION}'`, "u").test(sql)) {
missing.push(`schema-version:${CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION}`);
}
if (!/ON CONFLICT\s*\(id\)\s*DO UPDATE/iu.test(sql)) {
missing.push("ledger-upsert:on-conflict-id");
}
return {
ready: missing.length === 0,
checked: true,
migrationPath,
requiredMigrationId: CLOUD_CORE_MIGRATION_ID,
requiredSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
requiredTables: Object.keys(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS),
requiredLedgerTable: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
missing,
connectsToDatabase: false,
fixtureEvidence: false
};
}
function normalizeArgs(args) {
return {
...parseArgs([]),
...args
};
}
async function verifyRuntimeReadiness(report, env, options) {
report.actions.liveDbReadAttempted = 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.gates = gatesFromRuntime(runtime);
if (runtime.ready !== true) {
addBlocker(report, "runtime_blocker", blockerScope(runtime.blocker), runtime.reason ?? "Runtime durable readiness is blocked.", {
blocker: runtime.blocker ?? "runtime_durable_adapter_blocked",
adapter: runtime.adapter,
durable: Boolean(runtime.durable),
schemaReady: Boolean(runtime.schema?.ready),
migrationReady: Boolean(runtime.migration?.ready),
readinessReady: runtime.ready === true,
queryResult: runtime.connection?.queryResult ?? "unknown",
secretValuesPrinted: false
});
}
}
async function applyMigration(report, sql, env, options) {
report.actions.liveDbWriteAttempted = true;
let client;
let closeClient = async () => {};
try {
if (options.queryClient) {
client = options.queryClient;
} else {
const migrationClient = await createMigrationClient(env, options);
client = migrationClient.client;
closeClient = migrationClient.close;
}
await client.query("BEGIN");
await client.query(sql);
await client.query("COMMIT");
report.actions.migrationApplied = true;
} catch (error) {
await rollbackQuietly(client);
const classified = classifyMigrationError(error);
addBlocker(report, "runtime_blocker", blockerScope(classified.blocker), classified.summary, {
blocker: classified.blocker,
errorCode: classified.errorCode,
secretValuesPrinted: false
});
} finally {
await closeClient();
}
}
async function createMigrationClient(env, options) {
const pool = await createPgPool(env, options);
let client;
try {
client = await pool.connect();
} catch (error) {
await pool.end();
throw error;
}
return {
client,
close: async () => {
try {
client.release();
} finally {
await pool.end();
}
}
};
}
async function createPgPool(env, options) {
let pg;
try {
pg = await (options.pgModuleLoader ? options.pgModuleLoader() : import("pg"));
} catch (error) {
if (error?.code === "ERR_MODULE_NOT_FOUND") {
const driverError = new Error("Postgres migration apply 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 migration apply could not load pg.Pool");
driverError.code = "HWLAB_PG_DRIVER_MISSING";
throw driverError;
}
return new Pool(buildPostgresPoolConfig({
dbUrl: env.HWLAB_CLOUD_DB_URL,
sslMode: env.HWLAB_CLOUD_DB_SSL_MODE,
timeoutMs: normalizeTimeoutMs(env.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS)
}));
}
async function rollbackQuietly(client) {
if (!client) return;
try {
await client.query("ROLLBACK");
} catch {
// The original migration failure is reported without leaking connection details.
}
}
function gatesFromRuntime(runtime = {}) {
const gates = runtime.gates ?? {};
return {
auth: normalizeGate(gates.auth),
schema: normalizeGate(gates.schema),
migration: normalizeGate(gates.migration),
readiness: normalizeGate(gates.durability)
};
}
function sourceOnlyGates(sourceCheck) {
return {
auth: notCheckedGate("source-only validation does not read DB credentials or connect to Postgres"),
schema: sourceCheck.ready
? readyGate("source migration declares required durable runtime tables and columns")
: blockedGate("runtime_durable_schema_source_blocked"),
migration: sourceCheck.ready
? readyGate("source migration declares the required ledger upsert")
: blockedGate("runtime_durable_migration_source_blocked"),
readiness: notCheckedGate("runtime readiness requires a separate authorized DEV DB read or apply")
};
}
function normalizeGate(gate = {}) {
if (gate.ready === true) 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 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: summarizeRuntimeSchema(runtime.schema),
migration: summarizeRuntimeMigration(runtime.migration)
};
}
function summarizeRuntimeSchema(schema = {}) {
return {
checked: Boolean(schema.checked),
ready: Boolean(schema.ready),
requiredTables: Array.isArray(schema.requiredTables) ? [...schema.requiredTables] : Object.keys(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS),
missingTables: Array.isArray(schema.missingTables) ? [...schema.missingTables] : [],
missingColumns: Array.isArray(schema.missingColumns) ? [...schema.missingColumns] : []
};
}
function summarizeRuntimeMigration(migration = {}) {
return {
checked: Boolean(migration.checked),
ready: Boolean(migration.ready),
table: migration.table ?? CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
requiredMigrationId: migration.requiredMigrationId ?? CLOUD_CORE_MIGRATION_ID,
requiredSchemaVersion: migration.requiredSchemaVersion ?? CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
appliedMigrationId: migration.appliedMigrationId ?? null,
appliedSchemaVersion: migration.appliedSchemaVersion ?? null,
missing: migration.missing !== false
};
}
function summarizeDbEnv(env = {}) {
const urlPresent = typeof env.HWLAB_CLOUD_DB_URL === "string" && env.HWLAB_CLOUD_DB_URL.trim().length > 0;
const sslMode = env.HWLAB_CLOUD_DB_SSL_MODE || DEV_DB_ENV_CONTRACT.nonSecretDefaults.HWLAB_CLOUD_DB_SSL_MODE;
return {
requiredEnv: DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => ({
name,
present: name === "HWLAB_CLOUD_DB_URL" ? urlPresent : typeof env[name] === "string" && env[name].trim().length > 0,
redacted: name === "HWLAB_CLOUD_DB_URL",
source: name === "HWLAB_CLOUD_DB_URL" ? "k8s-secret-ref" : "runtime-env"
})),
urlPresent,
urlValueRedacted: true,
sslMode,
sslModeSecret: false,
secretRef: DEV_DB_ENV_CONTRACT.secretRefs[0],
endpointAuthority: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
optionalPublicDnsAliasRequired: DEV_DB_ENV_CONTRACT.dns.requiredForReadiness
};
}
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", "HWLAB_CLOUD_DB_URL from hwlab-cloud-api-dev-db/database-url"],
requiresForApply: ["--apply", "--confirm-dev", "--confirmed-non-production", "HWLAB_CLOUD_DB_URL from hwlab-cloud-api-dev-db/database-url"],
writeScope: args.mode === "apply"
? [
"DEV Postgres schema objects declared by internal/db/migrations/0001_cloud_core_skeleton.sql",
"DEV hwlab_schema_migrations ledger row 0001_cloud_core_skeleton"
]
: [],
noWriteScope: [
"Kubernetes Secret resources",
"PROD",
"service restarts",
"M3/M4/M5 acceptance"
],
forbidden: [
"printing DB URLs, passwords, tokens, Secret values, or kubeconfig material",
"using fixture output as DEV-LIVE runtime evidence",
"mutating PROD"
]
};
}
function buildSafety(args) {
return {
devOnly: true,
environment: ENVIRONMENT_DEV,
targetNamespace: DEV_DB_ENV_CONTRACT.dns.namespace,
prodAllowed: false,
sourceStoresSecretValues: false,
printsSecretValues: false,
dbUrlValueRedacted: 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#49" : issue,
evidence
});
}
function addSafetyRefusal(report, summary) {
report.safetyRefusal = true;
addBlocker(report, "safety_refusal", "runtime-migration-apply-boundary", summary, {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
});
}
function finalizeReport(report) {
const openBlockers = report.blockers.filter((blocker) => blocker.status === "open");
report.conclusion = {
status: openBlockers.length === 0 ? "ready" : "blocked",
summary: openBlockers.length === 0
? conclusionReadySummary(report)
: "DEV runtime migration automation is blocked; see separated auth/schema/migration/readiness blockers.",
blockerCount: openBlockers.length
};
return report;
}
function conclusionReadySummary(report) {
if (report.mode === "apply") {
return "DEV runtime migration applied and durable runtime readiness was verified without printing secret values.";
}
if (report.mode === "dry-run" && report.actions.liveDbReadAttempted) {
return "DEV runtime migration read-only verification passed without DB writes or secret output.";
}
return "Runtime migration source contract is ready; no DB connection or live write was attempted.";
}
function blockerScope(blocker) {
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) return "runtime-migration-ssl";
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) return "runtime-migration-auth";
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) return "runtime-migration-schema";
if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) return "runtime-migration-ledger";
if (blocker === RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING) return "runtime-migration-driver";
return "runtime-migration-readiness";
}
function classifyMigrationError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "UNKNOWN";
if (errorCode === "HWLAB_PG_DRIVER_MISSING") {
return {
blocker: RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
errorCode,
summary: "Postgres driver is unavailable for DEV runtime migration apply."
};
}
if (isMigrationSslError(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
errorCode,
summary: "DEV runtime migration reached Postgres, but SSL negotiation or SSL mode compatibility blocked apply."
};
}
if (["28P01", "28000", "42501"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
errorCode,
summary: "DEV runtime migration reached Postgres but auth or authorization blocked apply."
};
}
if (["42P01", "42703", "3F000"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
errorCode,
summary: "DEV runtime migration apply was blocked by schema/search_path state."
};
}
return {
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
errorCode,
summary: "DEV runtime migration apply could not complete."
};
}
function isMigrationSslError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "";
if (errorCode.startsWith("ERR_SSL") || errorCode.startsWith("ERR_TLS")) {
return true;
}
if ([
"DEPTH_ZERO_SELF_SIGNED_CERT",
"SELF_SIGNED_CERT_IN_CHAIN",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED"
].includes(errorCode)) {
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",
"before secure tls connection",
"tls handshake"
].some((pattern) => message.includes(pattern));
}
async function writeReport(report, reportPath, root) {
const absolute = ensureNotRepoReportsPath(root, reportPath, "--report");
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
}
function summarizeReport(report) {
return {
issue: report.issue,
mode: report.mode,
conclusion: report.conclusion,
migration: report.migration,
actions: report.actions,
gates: report.gates,
blockers: report.blockers,
safety: report.safety
};
}
function normalizeTimeoutMs(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1200;
}
function requireArgValue(value, flag) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`${flag} requires a value`);
}
return value;
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
function usage() {
return [
"Usage: node scripts/dev-runtime-migration.mjs [--check|--dry-run|--apply]",
"",
"Default --check validates the source migration contract 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 and HWLAB_CLOUD_DB_URL.",
"Reports never print DB URLs, passwords, tokens, Secret values, or kubeconfig material."
].join("\n");
}
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: message,
traceId: sha256(message).slice(0, 16),
safety: {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
}
};
}
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]");
}