Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
169 lines
5.9 KiB
JavaScript
Executable File
169 lines
5.9 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// ─────────────────────────────────────────────────────────────
|
|
// apply-migrations — Apply committed migrations to the local
|
|
// Supabase instance.
|
|
//
|
|
// Usage:
|
|
// node apply-migrations.mjs
|
|
//
|
|
// Behaviour:
|
|
// Migrations are already committed to supabase/migrations/ on
|
|
// the base branch (written by the deploy-time migration round and
|
|
// merged by the merger) before this script runs. There is nothing
|
|
// to promote — this script only applies what is already there.
|
|
//
|
|
// If Supabase is not running: start it (initial start applies
|
|
// every migration in supabase/migrations/).
|
|
// If Supabase is already running: run `npx supabase migration up`.
|
|
//
|
|
// After applying, the PostgREST schema cache is reloaded so
|
|
// new columns/tables are visible immediately.
|
|
//
|
|
// Portability (works on both crm-builder and a bare AtomicCRM checkout):
|
|
// APP_DIR — repo root. Defaults to $CLAUDE_PROJECT_DIR, then /app.
|
|
// Migrations are always applied regardless of the app's data mode:
|
|
// crm-builder runs in demo (FakeRest) by default at migration time,
|
|
// but the committed migrations must still land in local Supabase so
|
|
// they are ready when the app is switched to live data. The demo|full
|
|
// MODE distinction belongs only to the live-switch step, not here.
|
|
// Supabase is driven via the `supabase` CLI, which manages its own
|
|
// Docker containers — we do NOT gate on /var/run/docker.sock so the
|
|
// same script works wherever the CLI is available (e.g. `make`).
|
|
// ─────────────────────────────────────────────────────────────
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync } from "node:fs";
|
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
|
|
// Resolve the repo root robustly. Prefer an explicit override / the env var, then
|
|
// the git toplevel — works on any surface (local, devcontainer, CRM Builder)
|
|
// without relying on CLAUDE_PROJECT_DIR being EXPORTED to this node process. The
|
|
// old "/app" hardcoded fallback was a CRM-Builder-ism: on a bare checkout where the
|
|
// env var wasn't exported, it chdir'd into a non-writable /app → EACCES on mkdir.
|
|
// git is only spawned as the fallback — when an env var is set (the common harness
|
|
// case) we never shell out.
|
|
const gitToplevel = () => {
|
|
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
encoding: "utf8",
|
|
});
|
|
return (r.status === 0 && r.stdout.trim()) || "";
|
|
};
|
|
const APP_DIR =
|
|
process.env.APP_DIR ||
|
|
process.env.CLAUDE_PROJECT_DIR ||
|
|
gitToplevel() ||
|
|
process.cwd();
|
|
|
|
const GREEN = "\x1b[0;32m";
|
|
const YELLOW = "\x1b[1;33m";
|
|
const RED = "\x1b[0;31m";
|
|
const BOLD = "\x1b[1m";
|
|
const NC = "\x1b[0m";
|
|
|
|
const log = (msg) => process.stdout.write(`${msg}\n`);
|
|
const err = (msg) => process.stderr.write(`${msg}\n`);
|
|
|
|
process.chdir(APP_DIR);
|
|
mkdirSync("supabase/migrations", { recursive: true });
|
|
|
|
// Run a command, capturing combined stdout+stderr as text.
|
|
function run(cmd, args) {
|
|
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
const out = (res.stdout || "") + (res.stderr || "");
|
|
return { out, code: res.status ?? (res.error ? 1 : 0) };
|
|
}
|
|
|
|
async function isSupabaseUp() {
|
|
try {
|
|
await fetch("http://localhost:54321", {
|
|
signal: AbortSignal.timeout(2000),
|
|
});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function reloadSchemaCache() {
|
|
const { code } = run("npx", [
|
|
"supabase",
|
|
"db",
|
|
"query",
|
|
"--local",
|
|
"SELECT pg_notify('pgrst', 'reload schema');",
|
|
]);
|
|
if (code === 0) {
|
|
log(`${GREEN}PostgREST schema cache reloaded.${NC}`);
|
|
} else {
|
|
log(`${YELLOW}Could not reload PostgREST schema cache (non-fatal).${NC}`);
|
|
}
|
|
}
|
|
|
|
if (await isSupabaseUp()) {
|
|
// ── Apply migrations to running Supabase ──
|
|
log(`${BOLD}Applying pending migrations to running Supabase...${NC}`);
|
|
let { out, code } = run("npx", ["supabase", "migration", "up", "--local"]);
|
|
|
|
if (code !== 0) {
|
|
// Auto-repair phantom versions that are recorded in Supabase but absent from
|
|
// the local migrations/ directory (happens when a prior session's worktree was
|
|
// cleaned up after applying the migration but before it landed in git).
|
|
if (
|
|
out.includes(
|
|
"Remote migration versions not found in local migrations directory",
|
|
)
|
|
) {
|
|
const phantomVersions = [...new Set(out.match(/\d{14}/g) ?? [])];
|
|
if (phantomVersions.length > 0) {
|
|
log(
|
|
`${YELLOW}Auto-repairing phantom migration versions: ${phantomVersions.join(" ")}${NC}`,
|
|
);
|
|
run("npx", [
|
|
"supabase",
|
|
"migration",
|
|
"repair",
|
|
"--local",
|
|
"--status",
|
|
"reverted",
|
|
...phantomVersions,
|
|
]);
|
|
({ out, code } = run("npx", [
|
|
"supabase",
|
|
"migration",
|
|
"up",
|
|
"--local",
|
|
]));
|
|
}
|
|
}
|
|
if (code !== 0) {
|
|
err(out);
|
|
err(`${RED}Migration up failed.${NC}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
log(`${GREEN}Migrations applied.${NC}`);
|
|
reloadSchemaCache();
|
|
} else {
|
|
// ── Initial start applies all migrations ──
|
|
log(
|
|
`${BOLD}Starting Supabase (initial start applies all migrations)...${NC}`,
|
|
);
|
|
log(`${YELLOW}First run can take up to ~2 min to pull images.${NC}`);
|
|
const { out } = run("npx", ["supabase", "start"]);
|
|
for (const line of out.split("\n")) {
|
|
if (/✓|✗|Error|Started|API URL/.test(line)) log(line);
|
|
}
|
|
|
|
log(`${BOLD}Waiting for Supabase API (localhost:54321)...${NC}`);
|
|
let retries = 120;
|
|
while (!(await isSupabaseUp())) {
|
|
retries -= 1;
|
|
if (retries <= 0) {
|
|
err(`${RED}Supabase did not respond after 120s.${NC}`);
|
|
process.exit(1);
|
|
}
|
|
await sleep(1000);
|
|
}
|
|
log(`${GREEN}Supabase ready and all migrations applied.${NC}`);
|
|
reloadSchemaCache();
|
|
}
|