#!/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}@`); } export function redactSecrets(value, env = {}) { let redacted = String(value ?? ""); const databaseUrl = env.DATABASE_URL; if (databaseUrl) { redacted = redacted.split(databaseUrl).join(""); } redacted = redacted .replace(DATABASE_URL_JSON_PATTERN, `$1$3`) .replace(DATABASE_URL_ASSIGNMENT_PATTERN, "DATABASE_URL=") .replace(DATABASE_URL_TEXT_PATTERN, (_match, prefix) => `${prefix}`); return redactUrlCredentials(redacted); } export function createOutputExcerpt(value, env = {}, maxLength = MAX_EXCERPT_LENGTH) { const sanitized = redactSecrets(value, env).trim(); if (!sanitized) { return ""; } if (sanitized.length <= maxLength) { return sanitized; } return `${sanitized.slice(0, maxLength)}… `; } 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: "", 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; } }