snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
201
apps/dotcom/zero-cache/migrate.ts
Normal file
201
apps/dotcom/zero-cache/migrate.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/* eslint-disable no-console */
|
||||
import { existsSync, readFileSync, readdirSync } from 'fs'
|
||||
import { createServer } from 'http'
|
||||
import { Kysely, PostgresDialect, sql } from 'kysely'
|
||||
import pg from 'pg'
|
||||
import { hasTransactionBlock } from './migrationSql'
|
||||
|
||||
const postgresConnectionString =
|
||||
process.env.BOTCOM_POSTGRES_POOLED_CONNECTION_STRING ||
|
||||
'postgresql://user:password@127.0.0.1:6543/postgres'
|
||||
console.log('Using connection string:', postgresConnectionString)
|
||||
|
||||
const migrationsPath = `./migrations`
|
||||
if (!existsSync(migrationsPath)) {
|
||||
throw new Error(`Migrations path not found: ${migrationsPath}`)
|
||||
}
|
||||
|
||||
const init = `
|
||||
CREATE SCHEMA IF NOT EXISTS migrations;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS migrations.applied_migrations (
|
||||
filename VARCHAR PRIMARY KEY,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
|
||||
/**
|
||||
INSERT INTO migrations.applied_migrations (filename) VALUES
|
||||
('000_seed.sql'),
|
||||
('001_replicator_boot.sql'),
|
||||
('002_add_user_id.sql'),
|
||||
('003_make_published_slug_unique.sql'),
|
||||
('004_guest_column_on_file_state.sql'),
|
||||
('005_update_file_trigger.sql'),
|
||||
('006_add_file_soft_delete.sql'),
|
||||
('007_update_file_owner_details.sql')
|
||||
ON CONFLICT DO NOTHING;
|
||||
*/
|
||||
|
||||
// Tell Zero about schema changes so it updates its replica in place instead of a
|
||||
// full reset (Supabase doesn't fire event triggers for ALTER PUBLICATION). Guarded
|
||||
// because migrations run before Zero boots, so zero_0 may not exist yet on a fresh
|
||||
// database. to_regprocedure resolves the no-arg overload (to_regproc returns NULL
|
||||
// when the name is ambiguous, silently skipping the call).
|
||||
// https://zero.rocicorp.dev/docs/connecting-to-postgres#schema-change-hooks
|
||||
const notifyZeroOfSchemaChange = `
|
||||
DO $$
|
||||
BEGIN
|
||||
IF to_regprocedure('zero_0.update_schemas()') IS NOT NULL THEN
|
||||
PERFORM zero_0.update_schemas();
|
||||
END IF;
|
||||
END $$;
|
||||
`
|
||||
|
||||
const shouldSignalSuccess = process.argv.includes('--signal-success')
|
||||
const dryRun = process.argv.includes('--dry-run')
|
||||
|
||||
const DRY_RUN_ROLLBACK = new Error('dry-run-rollback')
|
||||
|
||||
const db = new Kysely({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new pg.Pool({
|
||||
connectionString: postgresConnectionString,
|
||||
application_name: 'migrate',
|
||||
idleTimeoutMillis: 10_000,
|
||||
max: 1,
|
||||
}),
|
||||
}),
|
||||
log: ['error'],
|
||||
})
|
||||
|
||||
async function waitForPostgres() {
|
||||
let attempts = 0
|
||||
do {
|
||||
try {
|
||||
await sql`SELECT 1`.execute(db)
|
||||
break
|
||||
} catch (_e) {
|
||||
if (attempts++ > 100) {
|
||||
throw new Error('Failed to connect to postgres')
|
||||
}
|
||||
console.log('Waiting for postgres' + '.'.repeat(attempts))
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
} while (true)
|
||||
await sql.raw(init).execute(db)
|
||||
}
|
||||
|
||||
async function migrate(summary: string[]) {
|
||||
await db.transaction().execute(async (tx) => {
|
||||
const appliedMigrations = await sql<{
|
||||
filename: string
|
||||
}>`SELECT filename FROM migrations.applied_migrations`.execute(tx)
|
||||
const migrations = readdirSync(migrationsPath).sort()
|
||||
if (migrations.length === 0) {
|
||||
throw new Error('No migrations found')
|
||||
}
|
||||
|
||||
// check that all applied migrations exist
|
||||
for (const appliedMigration of appliedMigrations.rows) {
|
||||
if (!migrations.includes(appliedMigration.filename)) {
|
||||
throw new Error(`Previously-applied migration ${appliedMigration.filename} not found`)
|
||||
}
|
||||
}
|
||||
|
||||
// We hardcode the `zero_0` shard schema. If Zero is ever reconfigured with a
|
||||
// different app id or shard, that name changes and our guards would silently
|
||||
// stop firing, reverting us to full resets. Fail loudly if it drifts. (No rows
|
||||
// means Zero hasn't booted yet, expected on a fresh database.)
|
||||
const shardSchemas = await sql<{ nspname: string }>`
|
||||
SELECT DISTINCT n.nspname FROM pg_proc p
|
||||
JOIN pg_namespace n ON n.oid = p.pronamespace
|
||||
WHERE p.proname = 'update_schemas'
|
||||
`.execute(tx)
|
||||
const schemaNames = shardSchemas.rows.map((r) => r.nspname)
|
||||
if (schemaNames.length > 0 && !schemaNames.includes('zero_0')) {
|
||||
throw new Error(
|
||||
`Expected Zero shard schema "zero_0" but found ${schemaNames.join(', ')}. Update the hardcoded schema name in migrate.ts and add a new migration to enable ddlDetection on the new schema.`
|
||||
)
|
||||
}
|
||||
|
||||
// DDL on a replicated table takes ACCESS EXCLUSIVE. Without a lock timeout, one long-running
|
||||
// reader makes the deploy hang while every room persist queues behind the waiting DDL.
|
||||
// Failing here aborts the deploy before Zero and the sync-worker roll, and a rerun picks up
|
||||
// where it left off. SET LOCAL is transaction-scoped, so it holds through a
|
||||
// transaction-mode pooler.
|
||||
await sql`SET LOCAL lock_timeout = '10s'`.execute(tx)
|
||||
|
||||
let appliedNewMigration = false
|
||||
for (const migration of migrations) {
|
||||
if (appliedMigrations.rows.some((m) => m.filename === migration)) {
|
||||
summary.push(`🏃 ${migration} already applied`)
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const migrationSql = readFileSync(`${migrationsPath}/${migration}`, 'utf8')
|
||||
if (hasTransactionBlock(migrationSql)) {
|
||||
throw new Error(
|
||||
`Migration ${migration} contains a transaction block. Migrations run in transactions, so you don't need to include them in the migration file.`
|
||||
)
|
||||
}
|
||||
await sql.raw(migrationSql).execute(tx)
|
||||
await sql`INSERT INTO migrations.applied_migrations (filename) VALUES (${migration})`.execute(
|
||||
tx
|
||||
)
|
||||
appliedNewMigration = true
|
||||
summary.push(`✅ ${migration} applied`)
|
||||
} catch (e) {
|
||||
summary.push(`❌ ${migration} failed`)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Notify Zero once after all DDL has run (see notifyZeroOfSchemaChange).
|
||||
if (appliedNewMigration) {
|
||||
await sql.raw(notifyZeroOfSchemaChange).execute(tx)
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
throw DRY_RUN_ROLLBACK
|
||||
}
|
||||
})
|
||||
await db.destroy()
|
||||
}
|
||||
async function run() {
|
||||
try {
|
||||
await waitForPostgres()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const summary: string[] = []
|
||||
try {
|
||||
await migrate(summary)
|
||||
console.log(summary.join('\n'))
|
||||
// need to do this to close the db connection
|
||||
if (shouldSignalSuccess) {
|
||||
const s = createServer((_, res) => {
|
||||
res.end('ok')
|
||||
})
|
||||
s.listen(7654)
|
||||
} else {
|
||||
process.exit(0)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e === DRY_RUN_ROLLBACK) {
|
||||
console.log(summary.join('\n'))
|
||||
console.log('🧹 Rolling back dry run...')
|
||||
process.exit(0)
|
||||
}
|
||||
console.error(e)
|
||||
console.error(summary.join('\n'))
|
||||
console.error('🧹 Rolling back...')
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
Reference in New Issue
Block a user