Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-db-migrate-status.mjs
T

197 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const MIGRATE_STATUS_COMMAND = Object.freeze({
command: "pnpm",
args: ["--filter", "@toolctrl/db", "prisma:migrate:status"],
});
const MAX_EXCERPT_LENGTH = 1200;
const ALLOW_UNREACHABLE_ENV = "DB_MIGRATE_STATUS_ALLOW_UNREACHABLE";
const DATABASE_URL_ASSIGNMENT_PATTERN = /\bDATABASE_URL\s*=\s*([^\s'\"]+|'[^']*'|\"[^\"]*\")/g;
const DATABASE_URL_JSON_PATTERN = /("DATABASE_URL"\s*:\s*")([^"]+)(")/g;
const DATABASE_URL_TEXT_PATTERN = /(DATABASE_URL(?:\s+(?:is|was|value|set to|configured as))?[:=]?\s+)([^\s]+)/gi;
const URL_WITH_CREDENTIALS_PATTERN = /\b([a-z][a-z0-9+.-]*:\/\/)([^\s/@:]+)(?::([^\s/@]*))?@/gi;
const UNREACHABLE_DATABASE_PATTERN = /\bP1001\b|Can't reach database server/i;
export function buildMigrateStatusCommand() {
return {
command: MIGRATE_STATUS_COMMAND.command,
args: [...MIGRATE_STATUS_COMMAND.args],
display: [MIGRATE_STATUS_COMMAND.command, ...MIGRATE_STATUS_COMMAND.args].join(" "),
};
}
function redactUrlCredentials(value) {
return value.replace(URL_WITH_CREDENTIALS_PATTERN, (_match, protocol) => `${protocol}<redacted>@`);
}
export function redactSecrets(value, env = {}) {
let redacted = String(value ?? "");
const databaseUrl = env.DATABASE_URL;
if (databaseUrl) {
redacted = redacted.split(databaseUrl).join("<redacted:DATABASE_URL>");
}
redacted = redacted
.replace(DATABASE_URL_JSON_PATTERN, `$1<redacted:DATABASE_URL>$3`)
.replace(DATABASE_URL_ASSIGNMENT_PATTERN, "DATABASE_URL=<redacted:DATABASE_URL>")
.replace(DATABASE_URL_TEXT_PATTERN, (_match, prefix) => `${prefix}<redacted:DATABASE_URL>`);
return redactUrlCredentials(redacted);
}
export function createOutputExcerpt(value, env = {}, maxLength = MAX_EXCERPT_LENGTH) {
const sanitized = redactSecrets(value, env).trim();
if (!sanitized) {
return "<empty>";
}
if (sanitized.length <= maxLength) {
return sanitized;
}
return `${sanitized.slice(0, maxLength)}… <truncated ${sanitized.length - maxLength} chars>`;
}
export function formatMigrateStatusResult(result) {
const status = result.ok ? "PASS" : "FAIL";
const lines = [
`${status} database migrate status verification`,
`phase: ${result.phase}`,
`command: ${result.commandDisplay}`,
`exitCode: ${result.exitCode ?? "n/a"}`,
`status: ${result.status}`,
`stdout: ${result.stdoutExcerpt}`,
`stderr: ${result.stderrExcerpt}`,
];
if (result.errorMessage) {
lines.push(`error: ${result.errorMessage}`);
}
lines.push("");
return lines.join("\n");
}
function allowsUnreachableDatabase(env = {}) {
return env[ALLOW_UNREACHABLE_ENV] === "1" || env[ALLOW_UNREACHABLE_ENV] === "true";
}
function isUnreachableDatabaseResult({ stdout = "", stderr = "" }) {
return UNREACHABLE_DATABASE_PATTERN.test(`${stdout}\n${stderr}`);
}
function runSpawn({ command, args, env, cwd, spawnImpl }) {
return new Promise((resolve) => {
let child;
try {
child = spawnImpl(command, args, {
cwd,
env: { ...env, CI: "1" },
stdio: ["ignore", "pipe", "pipe"],
});
} catch (error) {
resolve({ exitCode: null, stdout: "", stderr: "", error });
return;
}
let stdout = "";
let stderr = "";
let settled = false;
const settle = (payload) => {
if (!settled) {
settled = true;
resolve(payload);
}
};
child.stdout?.on?.("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr?.on?.("data", (chunk) => {
stderr += chunk.toString();
});
child.on?.("error", (error) => {
settle({ exitCode: null, stdout, stderr, error });
});
child.on?.("close", (exitCode) => {
settle({ exitCode, stdout, stderr, error: null });
});
});
}
export async function verifyDbMigrateStatus({ env = process.env, spawnImpl = spawn, cwd = process.cwd() } = {}) {
const command = buildMigrateStatusCommand();
if (!env.DATABASE_URL) {
return {
ok: false,
phase: "preflight",
commandDisplay: command.display,
exitCode: null,
status: "missing DATABASE_URL",
stdoutExcerpt: "<empty>",
stderrExcerpt: "DATABASE_URL is required for Prisma migrate status and was not provided.",
};
}
const spawnResult = await runSpawn({
command: command.command,
args: command.args,
env,
cwd,
spawnImpl,
});
if (spawnResult.error) {
return {
ok: false,
phase: "spawn",
commandDisplay: command.display,
exitCode: spawnResult.exitCode,
status: "spawn error",
stdoutExcerpt: createOutputExcerpt(spawnResult.stdout, env),
stderrExcerpt: createOutputExcerpt(spawnResult.stderr, env),
errorMessage: createOutputExcerpt(spawnResult.error.message, env),
};
}
const unreachableDatabaseAllowed =
spawnResult.exitCode !== 0 && allowsUnreachableDatabase(env) && isUnreachableDatabaseResult(spawnResult);
const ok = spawnResult.exitCode === 0 || unreachableDatabaseAllowed;
return {
ok,
phase: "prisma:migrate:status",
commandDisplay: command.display,
exitCode: spawnResult.exitCode,
status: unreachableDatabaseAllowed
? `database unreachable; skipped by ${ALLOW_UNREACHABLE_ENV}`
: ok
? "migrations status clean"
: "prisma migrate status failed",
stdoutExcerpt: createOutputExcerpt(spawnResult.stdout, env),
stderrExcerpt: createOutputExcerpt(spawnResult.stderr, env),
};
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = await verifyDbMigrateStatus();
const output = formatMigrateStatusResult(result);
if (result.ok) {
process.stdout.write(output);
} else {
process.stderr.write(output);
process.exitCode = 1;
}
}