Files
cfw-autumn/scripts/dw/helpers/neon.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

119 lines
3.1 KiB
TypeScript

import { sh, fatal, log } from "./shell.ts";
import {
NEON_PROJECT_ID,
NEON_TEMPLATE_BRANCH,
NEON_PARENT_BRANCH,
BRANCH_NAME_RE,
} from "../constants.ts";
import type { NeonBranch } from "../types.ts";
function neon(args: string[]): { stdout: string; stderr: string; code: number } {
return sh("neon", args);
}
export function listBranches(): NeonBranch[] {
const res = neon([
"branches",
"list",
"--project-id",
NEON_PROJECT_ID,
"--output",
"json",
]);
if (res.code !== 0) {
fatal(`neon branches list failed: ${res.stderr || res.stdout}`);
}
try {
return JSON.parse(res.stdout) as NeonBranch[];
} catch {
fatal(`could not parse neon branches list output:\n${res.stdout}`);
}
}
export function findBranchByName(name: string): NeonBranch | undefined {
return listBranches().find((b) => b.name === name);
}
export function createBranch(name: string, parent: string): NeonBranch {
if (!BRANCH_NAME_RE.test(name) && name !== NEON_TEMPLATE_BRANCH) {
fatal(`refusing to create branch with unexpected name: ${name}`);
}
log(`creating neon branch ${name} (parent: ${parent})`);
const res = neon([
"branches",
"create",
"--project-id",
NEON_PROJECT_ID,
"--name",
name,
"--parent",
parent,
"--output",
"json",
]);
if (res.code !== 0) {
fatal(`neon branches create failed: ${res.stderr || res.stdout}`);
}
try {
const parsed = JSON.parse(res.stdout) as { branch?: NeonBranch };
const branch = parsed.branch ?? (parsed as unknown as NeonBranch);
if (!branch?.id) fatal(`unexpected neon create output:\n${res.stdout}`);
return branch;
} catch {
fatal(`could not parse neon create output:\n${res.stdout}`);
}
}
export function deleteBranch(idOrName: string): void {
const res = neon([
"branches",
"delete",
idOrName,
"--project-id",
NEON_PROJECT_ID,
]);
if (res.code !== 0) {
console.error(
`[dw] neon branches delete ${idOrName} failed: ${res.stderr || res.stdout}`,
);
} else {
log(`deleted neon branch ${idOrName}`);
}
}
export function connectionString(
branchName: string,
opts: { pooled?: boolean } = {},
): string {
const args = [
"connection-string",
branchName,
"--project-id",
NEON_PROJECT_ID,
];
if (opts.pooled) args.push("--pooled");
const res = neon(args);
if (res.code !== 0) {
fatal(
`neon connection-string for ${branchName} failed: ${res.stderr || res.stdout}`,
);
}
return res.stdout.trim();
}
export function ensureTemplateBranch(): void {
const branch = findBranchByName(NEON_TEMPLATE_BRANCH);
if (branch) return;
log(`bootstrap: ${NEON_TEMPLATE_BRANCH} missing, creating empty parent`);
createBranch(NEON_TEMPLATE_BRANCH, NEON_PARENT_BRANCH);
// Wipe the inherited schema so children start truly empty.
const url = connectionString(NEON_TEMPLATE_BRANCH);
const reset = sh("psql", [url, "-v", "ON_ERROR_STOP=1"], {
stdin: `DROP SCHEMA IF EXISTS public CASCADE;\nCREATE SCHEMA public;\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\n`,
});
if (reset.code !== 0) {
fatal(`failed to reset ${NEON_TEMPLATE_BRANCH}:\n${reset.stderr}`);
}
log(`${NEON_TEMPLATE_BRANCH} ready (empty + pg_trgm)`);
}