|
| 1 | +// Run Prisma migrations against the dedicated NEW run-ops database (the second physical DB in the |
| 2 | +// split). It owns its own migration history, so it is migrated independently of the control-plane |
| 3 | +// DB. The connection is resolved the same way the webapp resolves it (RUN_OPS_DATABASE_URL, falling |
| 4 | +// back to TASK_RUN_DATABASE_URL) so migrations always target the DB the app connects to. |
| 5 | +// |
| 6 | +// Usage: node scripts/migrate.mjs [deploy|status] (defaults to deploy) |
| 7 | +import { spawnSync } from "node:child_process"; |
| 8 | +import { readFileSync } from "node:fs"; |
| 9 | +import { dirname, resolve } from "node:path"; |
| 10 | +import { fileURLToPath } from "node:url"; |
| 11 | + |
| 12 | +const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); |
| 13 | + |
| 14 | +// Read from local .env files so dev works without an exported env; deploy environments inject vars directly. |
| 15 | +function readFromEnvFiles(key) { |
| 16 | + for (const file of [resolve(packageRoot, ".env"), resolve(packageRoot, "../../.env")]) { |
| 17 | + let contents; |
| 18 | + try { |
| 19 | + contents = readFileSync(file, "utf8"); |
| 20 | + } catch { |
| 21 | + continue; |
| 22 | + } |
| 23 | + for (const line of contents.split("\n")) { |
| 24 | + const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/); |
| 25 | + if (!match || match[1] !== key) continue; |
| 26 | + let value = match[2]; |
| 27 | + if ( |
| 28 | + (value.startsWith('"') && value.endsWith('"')) || |
| 29 | + (value.startsWith("'") && value.endsWith("'")) |
| 30 | + ) { |
| 31 | + value = value.slice(1, -1); |
| 32 | + } |
| 33 | + if (value) return value; |
| 34 | + } |
| 35 | + } |
| 36 | + return undefined; |
| 37 | +} |
| 38 | + |
| 39 | +// Expand `${VAR}` refs (e.g. the repo .env's RUN_OPS_DATABASE_DIRECT_URL=${RUN_OPS_DATABASE_URL}); |
| 40 | +// our manual .env reader loads them literally, unlike Prisma's dotenv-expand. |
| 41 | +const expand = (value) => |
| 42 | + value?.replace(/\$\{(\w+)\}/g, (_, k) => process.env[k] ?? readFromEnvFiles(k) ?? ""); |
| 43 | +const resolveVar = (key) => expand(process.env[key] || readFromEnvFiles(key)); |
| 44 | +const redact = (url) => url.replace(/:\/\/[^@]*@/, "://***@"); |
| 45 | + |
| 46 | +const subcommand = process.argv[2] === "status" ? "status" : "deploy"; |
| 47 | + |
| 48 | +const databaseUrl = resolveVar("RUN_OPS_DATABASE_URL") || resolveVar("TASK_RUN_DATABASE_URL"); |
| 49 | +// Prefer the direct/unpooled endpoint for migrations (poolers break Prisma's advisory locks). |
| 50 | +const directUrl = |
| 51 | + resolveVar("RUN_OPS_DATABASE_DIRECT_URL") || |
| 52 | + resolveVar("TASK_RUN_DATABASE_DIRECT_URL") || |
| 53 | + databaseUrl; |
| 54 | + |
| 55 | +if (!databaseUrl) { |
| 56 | + // Single-DB installs never set these — safe no-op. A genuinely-expected DB is gated on by the caller. |
| 57 | + console.log( |
| 58 | + `run-ops migrate ${subcommand}: neither RUN_OPS_DATABASE_URL nor TASK_RUN_DATABASE_URL is set ` + |
| 59 | + "(checked env and .env). No dedicated run-ops database configured — skipping." |
| 60 | + ); |
| 61 | + process.exit(0); |
| 62 | +} |
| 63 | + |
| 64 | +console.log( |
| 65 | + `Running \`prisma migrate ${subcommand}\` against the run-ops database (${redact(databaseUrl)})` |
| 66 | +); |
| 67 | + |
| 68 | +const result = spawnSync("prisma", ["migrate", subcommand, "--schema", "prisma/schema.prisma"], { |
| 69 | + cwd: packageRoot, |
| 70 | + stdio: "inherit", |
| 71 | + env: { |
| 72 | + ...process.env, |
| 73 | + RUN_OPS_DATABASE_URL: databaseUrl, |
| 74 | + RUN_OPS_DATABASE_DIRECT_URL: directUrl, |
| 75 | + }, |
| 76 | +}); |
| 77 | + |
| 78 | +process.exit(result.status ?? 1); |
0 commit comments