118 lines · 4.9 KB · TypeScript
RawDownload
/* oxlint-disable executor/no-try-catch-or-throw -- boundary: out-of-band deploy gate over a raw postgres connection */// ---------------------------------------------------------------------------// Deploy gate: make the membership mirror READY before the build that// authorizes from it goes live, and fail the deploy if it cannot be.////   bun run db:ensure-workos-mirror-ready:prod   # op run --env-file=.env.production//   (deploy.yml runs it after the migrations, before the cloud deploy)//// Readiness is the SAME rule the request path applies// (`src/auth/mirror-readiness-store.ts`): the one-off backfill has written// every organization (`workos_sync.backfill_completed_at`) AND the events// reconciler has drained the stream within its lag budget// (`workos_sync.drained_at`). Until both hold the deployed build reads// membership from WorkOS instead of the mirror, so an unready mirror never// locks anyone out or lets a revoked member in — but a deploy that leaves it// unready would run every request through that fallback, which is the state// this whole cutover exists to leave behind. So this gate://   1. reads the readiness row;//   2. if the backfill has not completed, RUNS it (scripts/backfill-workos-mirror.ts,//      idempotent) and reads again;//   3. if the reconciler has not drained recently, DRAINS the stream itself//      (scripts/drain-workos-events.ts: the same replay the Worker's cron//      runs, over this connection) and reads again — never merely waits for//      the cron: this gate runs BEFORE the build that carries the cron may//      have been deployed, and a gate that only waited could not pass until//      the reconciler build had shipped on its own, by hand. A cron that is//      already live is safe beside it (the cursor's compare-and-set gives//      the stream one owner at a time);//   4. exits 0 only when the mirror is ready, and 1 with the reason otherwise.// Needs DATABASE_URL and WORKOS_API_KEY (the backfill and the drain read WorkOS).// --------------------------------------------------------------------------- import { spawnSync } from "node:child_process";import { dirname, resolve } from "node:path";import { fileURLToPath } from "node:url"; import { drizzle } from "drizzle-orm/postgres-js";import postgres from "postgres";import { directDatabaseUrl, waitForDatabaseConnection } from "./database-connection"; import {  MirrorReadinessState,  describeMirrorReadiness,  readMirrorReadiness,} from "../src/auth/mirror-readiness-store"; const __dirname = dirname(fileURLToPath(import.meta.url));const BACKFILL_SCRIPT = resolve(__dirname, "backfill-workos-mirror.ts");const DRAIN_SCRIPT = resolve(__dirname, "drain-workos-events.ts"); const connectionString = process.env.DATABASE_URL;if (!connectionString) {  console.error("DATABASE_URL is not set");  process.exit(1);} const usesLocalDatabase =  connectionString.includes("127.0.0.1") || connectionString.includes("localhost"); const sql = postgres(directDatabaseUrl(connectionString), {  max: 1,  prepare: false,  connect_timeout: 10,  ...(usesLocalDatabase ? {} : { ssl: "require" as const }),});const db = drizzle(sql); const log = (line: string) => console.log(`[mirror-ready] ${line}`); const readiness = () => readMirrorReadiness(db, new Date()); // The backfill and drain scripts own their own WorkOS + database wiring;// running them as subprocesses (with this process's env) keeps that wiring// in one place.const runScript = (what: string, script: string) => {  if (!process.env.WORKOS_API_KEY) {    throw new Error(`WORKOS_API_KEY is not set; the mirror ${what} cannot run`);  }  const result = spawnSync("bun", ["run", script], {    stdio: "inherit",    env: process.env,  });  if (result.status !== 0) {    throw new Error(`the mirror ${what} exited with status ${result.status ?? "unknown"}`);  }}; try {  await waitForDatabaseConnection(sql, { log });  let state = await readiness();  log(describeMirrorReadiness(state));   if (MirrorReadinessState.$is("BackfillPending")(state)) {    log("backfill not completed; running scripts/backfill-workos-mirror.ts");    runScript("backfill", BACKFILL_SCRIPT);    state = await readiness();    log(describeMirrorReadiness(state));  }   if (MirrorReadinessState.$is("ReconcilerStale")(state)) {    log("events stream not drained recently; running scripts/drain-workos-events.ts");    runScript("drain", DRAIN_SCRIPT);    state = await readiness();    log(describeMirrorReadiness(state));  }   if (!MirrorReadinessState.$is("Ready")(state)) {    console.error(      `[mirror-ready] the membership mirror is not ready: ${describeMirrorReadiness(state)}. ` +        "The deployed build would read membership from WorkOS on every request until it is. " +        "Check that WorkOS is reachable and the backfill has run, then rerun the deploy.",    );    process.exit(1);  }  log("the membership mirror is ready");} finally {  await sql.end({ timeout: 5 });}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t