import { Cli as incur_Cli, z } from 'incur' import * as Analytics from '../../analytics/Analytics.js' import * as Db from '../../db/Db.js' /** * Applies pending Postgres migrations, connecting directly via * `--database-url` or `DATABASE_URL` (never through Hyperdrive — the migrate * advisory lock needs session semantics); `--db-schema` scopes the session. * When ClickHouse migrate credentials are configured, also applies analytics * schema migrations ({@link Analytics.migrations}) over the HTTP interface. */ const migrate = incur_Cli.create('migrate', { description: 'Apply pending database migrations.', env: z.object({ CLICKHOUSE_DATABASE: z .string() .optional() .describe('ClickHouse database for analytics migrations.'), CLICKHOUSE_MIGRATE_PASSWORD: z .string() .optional() .describe('Password for the DDL-capable ClickHouse user.'), CLICKHOUSE_MIGRATE_USER: z .string() .optional() .describe('DDL-capable ClickHouse user for analytics migrations.'), CLICKHOUSE_URL: z.string().optional().describe('ClickHouse HTTPS endpoint.'), DATABASE_URL: z .string() .optional() .describe('Postgres connection string. Used when --database-url is omitted.'), }), options: z.object({ clickhouseDatabase: z .string() .optional() .describe('ClickHouse database. Overrides CLICKHOUSE_DATABASE.'), clickhouseMigratePassword: z .string() .optional() .describe('ClickHouse DDL password. Overrides CLICKHOUSE_MIGRATE_PASSWORD.'), clickhouseMigrateUser: z .string() .optional() .describe('ClickHouse DDL user. Overrides CLICKHOUSE_MIGRATE_USER.'), clickhouseUrl: z .string() .optional() .describe('ClickHouse HTTPS endpoint. Overrides CLICKHOUSE_URL.'), databaseUrl: z .string() .optional() .describe('Postgres connection string. Overrides DATABASE_URL.'), dbSchema: z.string().optional().describe('Postgres schema to create and scope the session to.'), }), async run(c) { const connectionString = c.options.databaseUrl ?? c.env.DATABASE_URL if (!connectionString) return c.error({ code: 'missing_database_url', message: 'Set --database-url or DATABASE_URL to the Postgres connection string.', }) const analytics = clickhouseConfig({ database: c.options.clickhouseDatabase ?? c.env.CLICKHOUSE_DATABASE, password: c.options.clickhouseMigratePassword ?? c.env.CLICKHOUSE_MIGRATE_PASSWORD, url: c.options.clickhouseUrl ?? c.env.CLICKHOUSE_URL, user: c.options.clickhouseMigrateUser ?? c.env.CLICKHOUSE_MIGRATE_USER, }) if (analytics?.missing) return c.error({ code: 'missing_clickhouse_config', message: `ClickHouse analytics migration is partially configured; set ${analytics.missing.join(', ')}.`, }) const db = Db.postgres({ connectionString, ...(c.options.dbSchema ? { schema: c.options.dbSchema } : {}), }) try { await db.migrate() } catch (error) { /* v8 ignore next */ const message = error instanceof Error ? error.message : 'Migration failed.' return c.error({ code: 'migrate_failed', message, }) } finally { await db.close() } if (!analytics) return { migrated: true } try { await Analytics.clickhouse(analytics.options).migrate() } catch (error) { /* v8 ignore next */ const message = error instanceof Error ? error.message : 'Migration failed.' return c.error({ code: 'migrate_failed', message, }) } return { analytics: true, migrated: true } }, }) /** * Super admin commands that run against a deployment's infrastructure rather * than the hosted API (database migrations, …). */ export const admin = incur_Cli .create('admin', { description: 'Super admin commands for a Tempo API deployment.', }) .command(migrate) /** * Resolves ClickHouse migrate config. Credentials gate the migration (absent * → skip); empty values count as unset since CI renders missing secrets as * empty strings. Partial config returns the missing variable names instead. */ function clickhouseConfig(input: { database?: string | undefined password?: string | undefined url?: string | undefined user?: string | undefined }): | { missing: readonly string[]; options?: undefined } | { missing?: undefined; options: Analytics.clickhouse.Options } | undefined { const database = input.database || undefined const password = input.password || undefined const url = input.url || undefined const user = input.user || undefined if (password === undefined && user === undefined) return undefined if (database === undefined || password === undefined || url === undefined || user === undefined) return { missing: [ ...(database === undefined ? ['CLICKHOUSE_DATABASE'] : []), ...(password === undefined ? ['CLICKHOUSE_MIGRATE_PASSWORD'] : []), ...(user === undefined ? ['CLICKHOUSE_MIGRATE_USER'] : []), ...(url === undefined ? ['CLICKHOUSE_URL'] : []), ], } return { options: { database, password, url, user } } }