962bc48738
## What
Adds the ability to **automatically migrate the dedicated run-ops
database** (the NEW DB in the run-ops split), matching how every other
database in the system is migrated. Follow-up to the run-ops split
activation.
## Changes
- **Migrate runner** — new
`internal-packages/run-ops-database/scripts/migrate.mjs`, exposed as
`db:migrate:deploy` / `db:migrate:status`. Connects via
`RUN_OPS_DATABASE_URL` (the same var the app uses) and expands `${VAR}`
refs like Prisma's dotenv.
- **Self-host** — `docker/scripts/entrypoint.sh` runs the run-ops
migration on boot when the DB is configured, gated by
`SKIP_RUN_OPS_MIGRATIONS`. Single-DB installs never set the URL, so it's
a clean no-op.
- **Single env-var family** — the run-ops DB is now addressed by one
canonical `RUN_OPS_*` family, connect path and migrations resolving the
identical URL:
- `RUN_OPS_DATABASE_URL` (writer) — replaces `TASK_RUN_DATABASE_URL`
- `RUN_OPS_LEGACY_DATABASE_URL` — replaces
`TASK_RUN_LEGACY_DATABASE_URL`
- `RUN_OPS_DATABASE_READ_REPLICA_URL` — replaces
`TASK_RUN_DATABASE_READ_REPLICA_URL`
- the old `TASK_RUN_*` aliases, the `??` coalesce, the
`runOpsNewDatabaseUrl` indirection, and the migrate-only `directUrl` are
all removed (consumers read `env.RUN_OPS_DATABASE_URL` directly).
`directUrl` was dropped because it was only ever used by `prisma
migrate` (never the app runtime) to bypass a pooler for advisory locks —
premature here since the run-ops connection isn't wired to the app yet.
If a pooler is later introduced for the app, a direct URL can be
reintroduced then.
## Safety
- **Pure rename** — nothing deployed sets any `TASK_RUN_*` var (the
split isn't activated anywhere yet; `.env.example`, docker-compose, and
cloud already use `RUN_OPS_*`), so there is no config migration.
- **Single-DB / self-host** — no new required env var; entrypoint and
migrate are no-ops when `RUN_OPS_DATABASE_URL` is unset.
- **Cloud** — runs migrations as pre-deploy ECS tasks (companion cloud
PR), calling these same `db:migrate:deploy` / `db:migrate:status`
commands.
## Verification
- Live migration against a fresh scratch DB with only
`RUN_OPS_DATABASE_URL` set: both migrations applied, no `P1012`/`P1013`;
`${VAR}` expansion, idempotent re-run, `status`, and no-op skip all
pass.
- Schema parity 4/4; `typecheck --filter webapp` 18/18; affected
split/replication tests 34/34.
## Scope
This delivers automatic migrations only. Enabling the app to *use* the
new DB (setting `RUN_OPS_DATABASE_URL` + `RUN_OPS_SPLIT_ENABLED` on the
service) is a separate activation step.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
73 lines
2.6 KiB
JavaScript
73 lines
2.6 KiB
JavaScript
// Run Prisma migrations against the dedicated NEW run-ops database (the second physical DB in the
|
|
// split). It owns its own migration history, so it is migrated independently of the control-plane
|
|
// DB. Connects via RUN_OPS_DATABASE_URL — the same var the webapp uses — so migrations always
|
|
// target the DB the app connects to.
|
|
//
|
|
// Usage: node scripts/migrate.mjs [deploy|status] (defaults to deploy)
|
|
import { spawnSync } from "node:child_process";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
// Read from local .env files so dev works without an exported env; deploy environments inject vars directly.
|
|
function readFromEnvFiles(key) {
|
|
for (const file of [resolve(packageRoot, ".env"), resolve(packageRoot, "../../.env")]) {
|
|
let contents;
|
|
try {
|
|
contents = readFileSync(file, "utf8");
|
|
} catch {
|
|
continue;
|
|
}
|
|
for (const line of contents.split("\n")) {
|
|
const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*?)\s*$/);
|
|
if (!match || match[1] !== key) continue;
|
|
let value = match[2];
|
|
if (
|
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
(value.startsWith("'") && value.endsWith("'"))
|
|
) {
|
|
value = value.slice(1, -1);
|
|
}
|
|
if (value) return value;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// Expand `${VAR}` refs in env-file values (our manual reader loads them literally, unlike Prisma's
|
|
// dotenv-expand), so a `.env` like RUN_OPS_DATABASE_URL=${DATABASE_URL} still resolves.
|
|
const expand = (value) =>
|
|
value?.replace(/\$\{(\w+)\}/g, (_, k) => process.env[k] ?? readFromEnvFiles(k) ?? "");
|
|
const resolveVar = (key) => expand(process.env[key] || readFromEnvFiles(key));
|
|
const redact = (url) => url.replace(/:\/\/[^@]*@/, "://***@");
|
|
|
|
const subcommand = process.argv[2] === "status" ? "status" : "deploy";
|
|
|
|
const databaseUrl = resolveVar("RUN_OPS_DATABASE_URL");
|
|
|
|
if (!databaseUrl) {
|
|
// Single-DB installs never set it — safe no-op. A genuinely-expected DB is gated on by the caller.
|
|
console.log(
|
|
`run-ops migrate ${subcommand}: RUN_OPS_DATABASE_URL is not set (checked env and .env). ` +
|
|
"No dedicated run-ops database configured — skipping."
|
|
);
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(
|
|
`Running \`prisma migrate ${subcommand}\` against the run-ops database (${redact(databaseUrl)})`
|
|
);
|
|
|
|
const result = spawnSync("prisma", ["migrate", subcommand, "--schema", "prisma/schema.prisma"], {
|
|
cwd: packageRoot,
|
|
stdio: "inherit",
|
|
env: {
|
|
...process.env,
|
|
RUN_OPS_DATABASE_URL: databaseUrl,
|
|
},
|
|
});
|
|
|
|
process.exit(result.status ?? 1);
|