deploy: prod-only pipeline with Drizzle migrations; dev refresh + preview
- push to main builds, backs up prod (pg_dump 17), runs db-migrate + seeds, preflights and deploys; rollback = workflow_dispatch with rollback_tag - staging workflow, promote step and migrate-*.ts / old SQL removed - DB_URL = app role, DB_MIGRATE_URL = owner role - scripts/db-refresh-dev.sh (prod dump -> local dev DB), scripts/preview.sh <branch>
This commit is contained in:
@@ -17,81 +17,108 @@ if (process.env.ORIGIN !== expectedOrigin || process.env.BETTER_AUTH_URL !== exp
|
||||
const client = postgres(process.env.DB_URL, { max: 1, connect_timeout: 10 });
|
||||
try {
|
||||
await client`select 1`;
|
||||
// Schema layout after the reorg (auth/money/house/ref/games/cs/animaldex/site/meta/lists).
|
||||
const required = [
|
||||
'auth.user',
|
||||
'auth.session',
|
||||
'auth.account',
|
||||
'auth.verification',
|
||||
'money.accounts',
|
||||
'money.transactions',
|
||||
'money.recurring',
|
||||
'money.forecasts',
|
||||
'money.utility_settings',
|
||||
'money.utility_readings',
|
||||
'money.wishlist',
|
||||
'money.insurances',
|
||||
'money.projection_settings',
|
||||
'house.listings',
|
||||
'house.price_history',
|
||||
'house.alert_rules',
|
||||
'house.worker_runs',
|
||||
'ref.pokemon',
|
||||
'ref.pokemon_games',
|
||||
'ref.pokemon_game_entries',
|
||||
'ref.pokemon_locations',
|
||||
'ref.pokemon_encounters',
|
||||
'ref.pokemon_evolutions',
|
||||
'ref.pokemon_research',
|
||||
'ref.pokemon_dex_numbers',
|
||||
'ref.pokemon_moves',
|
||||
'ref.pokemon_learnsets',
|
||||
'ref.pokemon_game_research',
|
||||
'ref.tracker_games',
|
||||
'ref.tracker_items',
|
||||
'ref.tracker_challenges',
|
||||
'games.catch_em_all_entries',
|
||||
'games.catch_em_all_games',
|
||||
'games.catch_em_all_settings',
|
||||
'games.pokemon_journeys',
|
||||
'games.pokemon_catches',
|
||||
'games.pokemon_milestones',
|
||||
'games.pokemon_seen',
|
||||
'games.tracker_progress',
|
||||
'games.tracker_ranks',
|
||||
'games.tracker_weapon_ranks',
|
||||
'games.tracker_challenge_progress',
|
||||
'games.osu_entries',
|
||||
'cs.items',
|
||||
'cs.price_observations',
|
||||
'cs.item_dimensions',
|
||||
'cs.price_streams',
|
||||
'animaldex.animals',
|
||||
'animaldex.sightings',
|
||||
'animaldex.sighting_photos',
|
||||
'site.blog_articles',
|
||||
'site.tasks',
|
||||
'site.topics',
|
||||
'site.task_topics',
|
||||
'meta.tables',
|
||||
'meta.columns'
|
||||
];
|
||||
const missing = await client<{ name: string }[]>`
|
||||
select name from unnest(${required}::text[]) as name where to_regclass(name) is null`;
|
||||
if (missing.length)
|
||||
throw new Error(`Schema reorg/migrations missing: ${missing.map((m) => m.name).join(', ')}`);
|
||||
const [schema] = await client`
|
||||
select
|
||||
(select count(*) from information_schema.columns
|
||||
where table_name = 'user' and column_name = 'role') as user_role,
|
||||
where table_schema = 'auth' and table_name = 'user' and column_name = 'role') as user_role,
|
||||
(select count(*) from information_schema.columns
|
||||
where table_name = 'account' and column_name = 'issuer') as account_issuer,
|
||||
where table_schema = 'money' and table_name = 'projection_settings'
|
||||
and column_name = 'include_recurring' and data_type = 'boolean') as include_recurring_bool,
|
||||
(select count(*) from information_schema.columns
|
||||
where table_name = 'blog_articles' and column_name = 'tags') as blog_tags,
|
||||
to_regclass('public.catch_em_all_settings') is not null as settings_ready,
|
||||
(select count(*) from information_schema.columns where table_name = 'lists' and column_name = 'slug') as list_slug,
|
||||
to_regclass('public.list_item_completions') is not null as list_completions,
|
||||
(select count(*) from information_schema.columns where table_name = 'tasks_to_topics' and column_name = 'id') as topic_link_id,
|
||||
(select count(*) from information_schema.columns
|
||||
where table_name in ('animals', 'sightings', 'photos')
|
||||
and column_name = 'id' and data_type = 'integer') as animal_serial_ids,
|
||||
(select count(*) from information_schema.tables
|
||||
where table_schema = 'public' and table_name in (
|
||||
'money_accounts', 'money_transactions', 'money_recurring', 'money_forecasts',
|
||||
'money_utility_settings', 'money_utility_readings', 'money_wishlist', 'money_insurances',
|
||||
'money_projection_settings'
|
||||
)) as money_tables,
|
||||
(select count(*) from information_schema.columns
|
||||
where table_name = 'money_utility_settings' and column_name = 'billing_start') as money_billing_start,
|
||||
(select count(*) from information_schema.tables
|
||||
where table_schema = 'public' and table_name in (
|
||||
'pokemon_locations', 'pokemon_encounters', 'pokemon_evolutions',
|
||||
'tracker_games', 'tracker_items', 'tracker_progress'
|
||||
)) as game_tracker_tables,
|
||||
(select count(*) from information_schema.tables where table_schema='public' and table_name in (
|
||||
'pokemon_research','pokemon_dex_numbers','pokemon_moves','pokemon_learnsets','pokemon_game_research',
|
||||
'pokemon_journeys','pokemon_catches','pokemon_milestones','tracker_ranks','tracker_weapon_ranks',
|
||||
'tracker_challenges','tracker_challenge_progress','osu_entries','pokemon_seen'
|
||||
)) as tracker_journal_tables,
|
||||
(select count(*) from information_schema.columns where table_name='pokemon_journeys' and column_name='completed') as tracker_completion_column,
|
||||
(select count(*) from information_schema.columns
|
||||
where table_name = 'pokemon_game_entries' and column_name = 'source') as entry_source,
|
||||
(select count(*) from information_schema.columns
|
||||
where (table_name = 'money_accounts' and column_name = 'kind')
|
||||
or (table_name = 'money_recurring' and column_name in ('starts_on', 'ends_on'))
|
||||
or (table_name = 'money_wishlist' and column_name = 'priority')
|
||||
or (table_name = 'money_projection_settings' and column_name = 'include_recurring')) as money_projection_columns
|
||||
where table_schema = 'games' and table_name = 'osu_entries'
|
||||
and column_name in ('accuracy', 'pp', 'rate') and data_type = 'numeric') as osu_numeric,
|
||||
(select count(*) from meta.tables where kind = 'list') as lists,
|
||||
has_function_privilege('meta.create_list(text, text, text, boolean, jsonb, text, text, text, integer)', 'execute') as can_create_list,
|
||||
has_schema_privilege('money', 'create') as app_has_ddl
|
||||
`;
|
||||
if (
|
||||
!Number(schema.user_role) ||
|
||||
!Number(schema.account_issuer) ||
|
||||
!Number(schema.blog_tags) ||
|
||||
!schema.settings_ready ||
|
||||
!Number(schema.list_slug) ||
|
||||
!schema.list_completions ||
|
||||
!Number(schema.topic_link_id) ||
|
||||
Number(schema.animal_serial_ids) !== 3 ||
|
||||
Number(schema.money_tables) !== 9 ||
|
||||
!Number(schema.money_billing_start) ||
|
||||
Number(schema.money_projection_columns) !== 5 ||
|
||||
Number(schema.game_tracker_tables) !== 6 ||
|
||||
Number(schema.tracker_journal_tables) !== 14 ||
|
||||
!Number(schema.tracker_completion_column) ||
|
||||
!Number(schema.entry_source)
|
||||
!Number(schema.include_recurring_bool) ||
|
||||
Number(schema.osu_numeric) !== 3 ||
|
||||
!Number(schema.lists) ||
|
||||
!schema.can_create_list
|
||||
) {
|
||||
throw new Error(
|
||||
'Required migrations or Animaldex ID conversion have not been applied to this database'
|
||||
'The schema reorg has not been applied to this database (or grants are missing)'
|
||||
);
|
||||
}
|
||||
if (schema.app_has_ddl)
|
||||
throw new Error('DB_URL must use the app role (personal_app), not the schema owner');
|
||||
const [catalog] = await client`
|
||||
select
|
||||
(select count(*) from pokemon) as pokemon,
|
||||
(select count(*) from games where source_id is not null) as games,
|
||||
(select count(*) from pokemon_game_entries) as entries,
|
||||
(select count(*) from pokemon_encounters) as encounters,
|
||||
(select count(*) from tracker_games where franchise = 'call-of-duty') as cod_games,
|
||||
(select count(*) from pokemon_research) as pokemon_research,
|
||||
(select count(*) from pokemon_moves) as pokemon_moves,
|
||||
(select count(*) from pokemon_learnsets) as pokemon_learnsets,
|
||||
(select count(*) from pokemon_dex_numbers) as pokemon_dex_numbers,
|
||||
(select count(*) from tracker_challenges) as tracker_challenges
|
||||
(select count(*) from ref.pokemon) as pokemon,
|
||||
(select count(*) from ref.pokemon_games where source_id is not null) as games,
|
||||
(select count(*) from ref.pokemon_game_entries) as entries,
|
||||
(select count(*) from ref.pokemon_encounters) as encounters,
|
||||
(select count(*) from ref.tracker_games where franchise = 'call-of-duty') as cod_games,
|
||||
(select count(*) from ref.pokemon_research) as pokemon_research,
|
||||
(select count(*) from ref.pokemon_moves) as pokemon_moves,
|
||||
(select count(*) from ref.pokemon_learnsets) as pokemon_learnsets,
|
||||
(select count(*) from ref.pokemon_dex_numbers) as pokemon_dex_numbers,
|
||||
(select count(*) from ref.tracker_challenges) as tracker_challenges
|
||||
`;
|
||||
if (
|
||||
Number(catalog.pokemon) < 1025 ||
|
||||
@@ -109,15 +136,15 @@ try {
|
||||
Number(catalog.tracker_challenges) < 3600
|
||||
)
|
||||
throw new Error(
|
||||
'Run db:migrate:tracker-journal, db:seed:research and db:seed:challenges before deploying the tracker expansion'
|
||||
'Run db:seed:research and db:seed:challenges before deploying the tracker expansion'
|
||||
);
|
||||
if (Number(catalog.cod_games) < 7) {
|
||||
throw new Error('Call of Duty catalog has not been seeded in this database');
|
||||
}
|
||||
const [bo2] = await client`select rules,
|
||||
(select count(*)::int from tracker_items i where i.game_id=g.id and i.kind='weapon') as weapons,
|
||||
(select count(*)::int from tracker_items i where i.game_id=g.id and i.kind='weapon' and i.icon is not null) as pictures
|
||||
from tracker_games g where slug='black-ops-2'`;
|
||||
(select count(*)::int from ref.tracker_items i where i.game_id=g.id and i.kind='weapon') as weapons,
|
||||
(select count(*)::int from ref.tracker_items i where i.game_id=g.id and i.kind='weapon' and i.icon is not null) as pictures
|
||||
from ref.tracker_games g where slug='black-ops-2'`;
|
||||
if (
|
||||
!bo2 ||
|
||||
bo2.rules.rankCaps?.weaponPrestige !== 2 ||
|
||||
|
||||
Reference in New Issue
Block a user