Files
cfw-autumn/scripts/dw/helpers/migration.ts
amianthus 25e920bbd3 feat(dw): 🎸 parallel agent worktrees with isolated infra + test harness
bun dw spins up a fully isolated per-worktree dev stack: Neon DB branch,
Dragonfly + ElasticMQ via docker compose, portless aliases, tmux
session, and emulate.dev Google OAuth. Workers, dev server, vite,
checkout, and stripe-listen run as concurrently siblings. bun setup-test
auto-runs on first dw and seeds unit-test-org via
clearOrg+setupOrg+ensureDefaultStripeAccount with 13 features + team
invites for all 5 useautumn.com members. Hardcoded localhost URLs in
test scenarios now read AUTUMN_TEST_BASE_URL/AUTUMN_TEST_VITE_URL.
preload-env.ts loads .env.local so bun t/cm/setup-test route to the
worktree server. 1265-line scripts/dw.ts split into scripts/dw/ module
(index + commands + helpers). server/src/db/initDrizzle.ts reverted to
upstream (search_path concerns solved by per-branch Neon isolation).
Workers enabled in agent worktrees now that SQS is per-worktree via
ElasticMQ.
2026-05-16 02:02:43 +01:00

105 lines
3.1 KiB
TypeScript

import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { sh, fatal, log } from "./shell.ts";
import { SHARED_DIR, PROJECT_ROOT } from "../constants.ts";
export function listSqlFiles(dir: string): string[] {
const res = Bun.spawnSync(["ls", dir]);
const stdout = res.stdout ? new TextDecoder().decode(res.stdout).trim() : "";
if (!stdout) return [];
return stdout.split("\n").filter((f) => f.endsWith(".sql"));
}
export function writeTempDrizzleConfig(outDir: string): string {
const tmp = join(SHARED_DIR, `.dw-${process.pid}.config.ts`);
const content = `import { defineConfig } from "drizzle-kit";\nexport default defineConfig({\n\tdialect: "postgresql",\n\tout: ${JSON.stringify(outDir)},\n\tschema: "./db/schema.ts",\n\tdbCredentials: { url: process.env.DATABASE_URL! },\n});\n`;
writeFileSync(tmp, content);
return tmp;
}
export function generateAndApplyMigration(
branchName: string,
databaseUrl: string,
): void {
const outDir = join(SHARED_DIR, "drizzle-local", branchName);
if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });
const drizzleConfigPath = writeTempDrizzleConfig(outDir);
try {
log(`generating initial migration for ${branchName}`);
const gen = sh(
"bunx",
["drizzle-kit", "generate", "--config", drizzleConfigPath],
{
cwd: SHARED_DIR,
env: {
...(process.env as Record<string, string>),
NODE_OPTIONS: "--import tsx",
},
},
);
if (gen.code !== 0) {
fatal(`drizzle-kit generate failed:\n${gen.stdout}\n${gen.stderr}`);
}
const sqlFiles = listSqlFiles(outDir);
if (sqlFiles.length === 0) {
fatal(`no .sql files generated in ${outDir}`);
}
log(`applying ${sqlFiles.length} migration file(s) to ${branchName}`);
for (const f of sqlFiles) {
const p = join(outDir, f);
const sqlBody = readFileSync(p, "utf-8");
const mig = sh("psql", [databaseUrl, "-v", "ON_ERROR_STOP=1"], {
stdin: sqlBody,
});
if (mig.code !== 0) {
fatal(
`applying migration ${f} failed:\n${mig.stdout}\n${mig.stderr}`,
);
}
}
} finally {
if (existsSync(drizzleConfigPath)) rmSync(drizzleConfigPath);
}
}
export function loadDbFunctions(branchName: string, databaseUrl: string): void {
log(`loading DB functions into ${branchName}`);
const sqlDir = join(
PROJECT_ROOT,
"server",
"src",
"internal",
"balances",
"utils",
"sql",
);
const sqlFiles = [
"deductFromRollovers.sql",
"deductFromMainBalance.sql",
"unwindFromLockReceipt.sql",
"getTotalBalance.sql",
"deductFromAdditionalBalance.sql",
"getAvailableOverageFromSpendLimit.sql",
"performDeduction.sql",
"syncBalances.sql",
"syncBalancesV2.sql",
"resetCusEnts.sql",
];
for (const f of sqlFiles) {
const p = join(sqlDir, f);
const sqlBody = readFileSync(p, "utf-8");
const res = sh("psql", [databaseUrl, "-v", "ON_ERROR_STOP=1"], {
stdin: sqlBody,
});
if (res.code !== 0) {
fatal(
`loading DB function ${f} into ${branchName} failed:\n${res.stdout}\n${res.stderr}`,
);
}
}
}