From 9ebbbe765e26a686fd0851a99f01c50666eeb3b7 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 26 Oct 2025 08:09:47 +1100 Subject: [PATCH 1/4] Notify vercel --- .github/workflows/migrate-ops.yml | 5 +++++ .github/workflows/migrate-tenants.yml | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/.github/workflows/migrate-ops.yml b/.github/workflows/migrate-ops.yml index 4101b58..b415b7d 100644 --- a/.github/workflows/migrate-ops.yml +++ b/.github/workflows/migrate-ops.yml @@ -69,3 +69,8 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} DATABASE_SSL: ${{ secrets.DATABASE_SSL }} run: bun run migrate:ops + + - name: 'notify vercel' + uses: 'vercel/repository-dispatch/actions/status@v1' + with: + name: Vercel - manage: migrate-ops diff --git a/.github/workflows/migrate-tenants.yml b/.github/workflows/migrate-tenants.yml index a4c298f..b0bf142 100644 --- a/.github/workflows/migrate-tenants.yml +++ b/.github/workflows/migrate-tenants.yml @@ -71,3 +71,7 @@ jobs: DATABASE_SSL: ${{ secrets.DATABASE_SSL }} run: bun run migrate:tenants + - name: 'notify vercel' + uses: 'vercel/repository-dispatch/actions/status@v1' + with: + name: Vercel - manage: migrate-tenants \ No newline at end of file From b76c29814910ff1964dce4bcd8c6557640c4593a Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 26 Oct 2025 08:12:39 +1100 Subject: [PATCH 2/4] Fix yaml errors --- .github/workflows/migrate-ops.yml | 2 +- .github/workflows/migrate-tenants.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/migrate-ops.yml b/.github/workflows/migrate-ops.yml index b415b7d..75116fd 100644 --- a/.github/workflows/migrate-ops.yml +++ b/.github/workflows/migrate-ops.yml @@ -73,4 +73,4 @@ jobs: - name: 'notify vercel' uses: 'vercel/repository-dispatch/actions/status@v1' with: - name: Vercel - manage: migrate-ops + name: Vercel - manage-migrate-ops diff --git a/.github/workflows/migrate-tenants.yml b/.github/workflows/migrate-tenants.yml index b0bf142..715834b 100644 --- a/.github/workflows/migrate-tenants.yml +++ b/.github/workflows/migrate-tenants.yml @@ -74,4 +74,4 @@ jobs: - name: 'notify vercel' uses: 'vercel/repository-dispatch/actions/status@v1' with: - name: Vercel - manage: migrate-tenants \ No newline at end of file + name: Vercel - manage-migrate-tenants \ No newline at end of file From 2c15e6b1b012ff35b61367d6e101bb83fb138c3e Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 26 Oct 2025 08:16:42 +1100 Subject: [PATCH 3/4] Remove vercel notify --- .github/workflows/migrate-ops.yml | 5 ----- .github/workflows/migrate-tenants.yml | 5 ----- 2 files changed, 10 deletions(-) diff --git a/.github/workflows/migrate-ops.yml b/.github/workflows/migrate-ops.yml index 75116fd..4101b58 100644 --- a/.github/workflows/migrate-ops.yml +++ b/.github/workflows/migrate-ops.yml @@ -69,8 +69,3 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} DATABASE_SSL: ${{ secrets.DATABASE_SSL }} run: bun run migrate:ops - - - name: 'notify vercel' - uses: 'vercel/repository-dispatch/actions/status@v1' - with: - name: Vercel - manage-migrate-ops diff --git a/.github/workflows/migrate-tenants.yml b/.github/workflows/migrate-tenants.yml index 715834b..81d09d5 100644 --- a/.github/workflows/migrate-tenants.yml +++ b/.github/workflows/migrate-tenants.yml @@ -70,8 +70,3 @@ jobs: DATABASE_URL: ${{ secrets.DATABASE_URL }} DATABASE_SSL: ${{ secrets.DATABASE_SSL }} run: bun run migrate:tenants - - - name: 'notify vercel' - uses: 'vercel/repository-dispatch/actions/status@v1' - with: - name: Vercel - manage-migrate-tenants \ No newline at end of file From b828c24f62e98522b4d50e551f559083839771b0 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sun, 26 Oct 2025 08:29:07 +1100 Subject: [PATCH 4/4] terminate connection after migration --- scripts/migrate-all-tenants.ts | 157 +++++++++++++++++---------------- 1 file changed, 83 insertions(+), 74 deletions(-) diff --git a/scripts/migrate-all-tenants.ts b/scripts/migrate-all-tenants.ts index 6e85df7..d88112e 100644 --- a/scripts/migrate-all-tenants.ts +++ b/scripts/migrate-all-tenants.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; import * as schema from "../drizzle/schema"; import * as opsSchema from "../ops/drizzle/schema"; @@ -22,18 +23,20 @@ function sanitizeError(error: string, tenantId: string): string { } async function getOpsDatabase() { - return drizzle({ - connection: { - url: `${process.env.DATABASE_URL}/manage`, - ssl: process.env.DATABASE_SSL === "true", - }, - schema, + const client = postgres(`${process.env.DATABASE_URL}/manage`, { + ssl: process.env.DATABASE_SSL === "true" ? "require" : false, + max: 1, }); -} -async function getAllTenantIds(): Promise { - const opsDb = await getOpsDatabase(); + return { + db: drizzle({ client, schema: opsSchema }), + client, + }; +} +async function getAllTenantIds( + opsDb: ReturnType>, +): Promise { const [users, organizations] = await Promise.all([ opsDb.select({ id: opsSchema.opsUser.id }).from(opsSchema.opsUser), opsDb @@ -49,22 +52,19 @@ async function getAllTenantIds(): Promise { return tenantIds; } -async function migrateTenantDatabase(ownerId: string): Promise<{ +async function migrateTenantDatabase( + ownerId: string, + opsDb: ReturnType>, +): Promise<{ success: boolean; skipped?: boolean; error?: string; }> { + let tenantClient: ReturnType | null = null; + try { const databaseName = getDatabaseName(ownerId); - const opsDb = drizzle({ - connection: { - url: `${process.env.DATABASE_URL}/manage`, - ssl: process.env.DATABASE_SSL === "true", - }, - schema: opsSchema, - }); - const checkDb = await opsDb.execute( sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`, ); @@ -73,14 +73,13 @@ async function migrateTenantDatabase(ownerId: string): Promise<{ return { success: true, skipped: true }; } - const tenantDb = drizzle({ - connection: { - url: `${process.env.DATABASE_URL}/${databaseName}`, - ssl: process.env.DATABASE_SSL === "true", - }, - schema, + tenantClient = postgres(`${process.env.DATABASE_URL}/${databaseName}`, { + ssl: process.env.DATABASE_SSL === "true" ? "require" : false, + max: 1, }); + const tenantDb = drizzle({ client: tenantClient, schema }); + const migrationsFolder = path.resolve(process.cwd(), "drizzle"); await migrate(tenantDb, { migrationsFolder }); @@ -90,6 +89,10 @@ async function migrateTenantDatabase(ownerId: string): Promise<{ success: false, error: error instanceof Error ? error.message : String(error), }; + } finally { + if (tenantClient) { + await tenantClient.end(); + } } } @@ -101,65 +104,71 @@ async function main() { process.exit(1); } - const tenantIds = await getAllTenantIds(); - console.log(`Found ${tenantIds.length} tenant(s) to migrate\n`); - - if (tenantIds.length === 0) { - console.log("No tenants found. Nothing to migrate."); - return; - } + const { db: opsDb, client: opsClient } = await getOpsDatabase(); - let successCount = 0; - let skippedCount = 0; - let failureCount = 0; - const failures: Array<{ tenantId: string; error: string }> = []; - - for (let i = 0; i < tenantIds.length; i++) { - const tenantId = tenantIds[i]; - const maskedId = maskId(tenantId); - const progress = `[${i + 1}/${tenantIds.length}]`; - process.stdout.write(`${progress} Migrating ${maskedId}... `); + try { + const tenantIds = await getAllTenantIds(opsDb); + console.log(`Found ${tenantIds.length} tenant(s) to migrate\n`); - const result = await migrateTenantDatabase(tenantId); + if (tenantIds.length === 0) { + console.log("No tenants found. Nothing to migrate."); + return; + } - if (result.success) { - if (result.skipped) { - console.log("⊘ (no database)"); - skippedCount++; + let successCount = 0; + let skippedCount = 0; + let failureCount = 0; + const failures: Array<{ tenantId: string; error: string }> = []; + + for (let i = 0; i < tenantIds.length; i++) { + const tenantId = tenantIds[i]; + const maskedId = maskId(tenantId); + const progress = `[${i + 1}/${tenantIds.length}]`; + process.stdout.write(`${progress} Migrating ${maskedId}... `); + + const result = await migrateTenantDatabase(tenantId, opsDb); + + if (result.success) { + if (result.skipped) { + console.log("⊘ (no database)"); + skippedCount++; + } else { + console.log("✓"); + successCount++; + } } else { - console.log("✓"); - successCount++; + console.log("✗"); + const sanitizedError = sanitizeError( + result.error || "Unknown error", + tenantId, + ); + console.log(` Error: ${sanitizedError}`); + failureCount++; + failures.push({ tenantId: maskedId, error: sanitizedError }); } - } else { - console.log("✗"); - const sanitizedError = sanitizeError( - result.error || "Unknown error", - tenantId, - ); - console.log(` Error: ${sanitizedError}`); - failureCount++; - failures.push({ tenantId: maskedId, error: sanitizedError }); } - } - console.log(`\n${"=".repeat(60)}`); - console.log("Migration Summary:"); - console.log(` Total: ${tenantIds.length}`); - console.log(` Success: ${successCount}`); - console.log(` Skipped: ${skippedCount} (no database created yet)`); - console.log(` Failed: ${failureCount}`); - - if (failures.length > 0) { - console.log("\nFailed migrations:"); - for (const failure of failures) { - console.log(` - ${failure.tenantId}: ${failure.error}`); + console.log(`\n${"=".repeat(60)}`); + console.log("Migration Summary:"); + console.log(` Total: ${tenantIds.length}`); + console.log(` Success: ${successCount}`); + console.log(` Skipped: ${skippedCount} (no database created yet)`); + console.log(` Failed: ${failureCount}`); + + if (failures.length > 0) { + console.log("\nFailed migrations:"); + for (const failure of failures) { + console.log(` - ${failure.tenantId}: ${failure.error}`); + } + console.log("\n✗ Some migrations failed!"); + process.exit(1); } - console.log("\n✗ Some migrations failed!"); - process.exit(1); - } - console.log("\n✓ All tenant databases migrated successfully!"); - process.exit(0); + console.log("\n✓ All tenant databases migrated successfully!"); + process.exit(0); + } finally { + await opsClient.end(); + } } main().catch((error) => {