Rebuild Staging a partir de Produção
Runbook canônico para reconstruir o ambiente de staging com dump canônico de produção. Substitui o anti-padrão
staging_mark_migrations.sql(deprecated).Cenário coberto aqui: staging existente (DROP + CREATE schema public + restore). Projeto Supabase NOVO (deletou+recriou): ver
REBUILD_FRESH_PROJECT.md.Última atualização: 2026-06-05
Por que esse runbook existe
Staging vinha sendo populado marcando 304 migrations como aplicadas sem rodá-las (staging_mark_migrations.sql). Toda migration que usa IF NOT EXISTS, CREATE OR REPLACE, ou depende de estado pré-existente diferente de prod diverge silenciosamente. Incidentes documentados:
escolas.limite_alunosvirouINTEGERem vez deGENERATED ALWAYS(2026-06-04)series_escolaresficou sem catálogo completo → quebrou auto-criação de turmas na importação em massa (8000 alunos, 0 sucessos)
A solução não é resolver caso-a-caso. É clonar o dump real de produção (via pg_dump -Fc + pg_restore em fases) e tratar staging como banco descartável que pode ser refeito em ~30 min.
Modo rápido (recomendado)
Tudo abaixo (passos 1→7) está orquestrado em um único script:
export PGURL_PROD="postgresql://postgres:...@db.mjvuzsizjlcalyfmbquy.supabase.co:5432/postgres"
export PGURL_STAGING="postgresql://postgres:...@db.<staging-ref>.supabase.co:5432/postgres"
# Via npm script (recomendado)
bun run staging:rebuild
# Ou direto
bash scripts/staging/rebuild-from-prod.shFlags: --yes (pula confirmação), --keep-dump (não apaga dump no fim), --skip-drift, --dump-dir DIR.
Qual input escolher no workflow Staging Rebuild?
O workflow é disparado manualmente (workflow_dispatch) em Actions → Staging Rebuild → Run workflow. A combinação de fresh_project e reset_auth depende do estado atual do banco staging:
| Estado do staging | fresh_project | reset_auth | Por quê |
|---|---|---|---|
Projeto Supabase recém-criado / deletado-e-recriado, public ainda vazio | true | true | pre-rebuild-check valida que public.escolas não existe ou está vazia; auth.users do provisioning Supabase precisa ser limpo para evitar colisão de e-mail no restore |
| Staging em uso com dados de QA — quero refazer | false | false | Modo destrutivo: DROP SCHEMA public CASCADE + restore. O CREATE SCHEMA public vem do próprio dump (pre-data). É o caminho default para staging já populado |
Staging com auth.users poluído por testes E2E | false | true | Destrói schema E limpa auth.users (Dev users precisam ser recriados via UI antes do smoke) |
⚠️
reset_auth≠ wipe completo. A checkboxreset_authaplica apenasTRUNCATE auth.users CASCADE. Ela NÃO substitui o reset do schemapublic. Marcar só essa caixa semfresh_project = falsemantém os dados de aplicação (escolas, alunos, etc.) intactos.
⚠️ Erro comum: disparar com
fresh_project = truequando o staging já tem dados. Opre-rebuild-checkaborta com fail-close (linha 65 do script) — isso é o comportamento correto, não bug. A correção é re-disparar comfresh_project = false. O guard existe para evitar sobrescrever dados de QA em andamento.
ℹ️ Quem cria o schema
public: o script apenas fazDROP SCHEMA public CASCADE. A recriação vem dopg_restore --section=pre-datado dump canônico de prod. Os grants do schema são reaplicados em[3b/7]após o restore.
Guards anti-acidente (todos abortam ANTES de qualquer escrita)
| Guard | O que verifica |
|---|---|
| PGURL_PROD == PGURL_STAGING | Aborta se variáveis idênticas |
| PROD ref check | PGURL_PROD DEVE apontar para mjvuzsizjlcalyfmbquy (ref de prod conhecido). Bloqueia variáveis trocadas. |
| STAGING ref check | PGURL_STAGING NÃO pode apontar para o ref de prod |
| Connectivity | SELECT 1 nos dois bancos antes do dump |
| Sanity de contagem | Aborta se staging.escolas > prod.escolas (sintoma de inversão) |
Confirmação REBUILD | Operador digita literal antes do DROP (a menos que --yes) |
| Trap SIGINT/SIGTERM | Ctrl+C aborta com mensagem clara, prod intocada |
| LGPD dump cleanup | rm -P no fim (overwrite); preserva dump só em erro ou --keep-dump |
Backup manual de prod ANTES de rodar (paranoia recomendada)
O script só LÊ de prod (via pg_dump), nunca escreve. Mesmo assim, para a primeira execução:
# 1. Backup full de prod, salvo fora do repo
pg_dump "$PGURL_PROD" -Fc --no-owner --no-acl \
-f /mnt/documents/prod-backup-$(date +%Y%m%d-%H%M).dump
# 2. Verifica que o arquivo foi criado e tem tamanho razoável
ls -lh /mnt/documents/prod-backup-*.dump
# 3. (Opcional mas recomendado) testar restore num banco descartável
# Se você tiver um terceiro banco vazio (PGURL_TEST):
# pg_restore --no-owner --no-acl -d "$PGURL_TEST" /mnt/documents/prod-backup-*.dump
# 4. Rodar o rebuild
bun run staging:rebuild
# 5. Verificar drift
bun run staging:driftBackup também disponível via Supabase Dashboard → Database → Backups (PITR no plano Pro+).
Etapas 8 (deploy edge functions/worker) e 9 (smoke E2E) ficam fora do script — exigem credenciais separadas e revisão humana. Comandos prontos são impressos ao final.
O passo-a-passo manual abaixo permanece como referência canônica.
Pré-requisitos
pg_dump≥ versão do servidor Supabase (hoje 17.x —pg_dump16 aborta com "server version mismatch"). O runnerubuntu-latestvem com 16 pré-instalado; o workflow remove e instala explicitamente opostgresql-client-17.supabaseCLI v1.200+wranglerCLI (para Worker)- Acesso à connection string de prod (Database Settings → Connection string)
- Projeto Supabase staging vazio OU pronto para ter dados resetados
Pipeline
1. pg_dump prod → prod_dump_YYYYMMDD.dump (custom)
2. Reset staging → drop schema public + recreate
3. pg_restore pre/data/post → staging recebe schema + seed completo
(com limpeza de órfãos conhecidos antes do post-data)
4. sanitize.sql → PII, OTPs, MP, logs
5. seed_test_users.sql → usuários Dev
6. supabase migration repair → alinha schema_migrations
7. check-drift.ts → BLOCKERS deve ser 0
8. deploy edge fns + worker
9. smoke E2EPasso a passo
1. Exportar dump de produção
export PGURL_PROD="postgresql://postgres:PWD@db.mjvuzsizjlcalyfmbquy.supabase.co:5432/postgres"
DATE=$(date +%Y%m%d)
pg_dump "$PGURL_PROD" \
--format=custom \
--schema=public \
--no-owner --no-privileges \
--exclude-table-data='logs_transacoes' \
--exclude-table-data='sms_log' \
--exclude-table-data='email_outbox' \
--exclude-table-data='email_eventos' \
--exclude-table-data='email_unsubscribe_tokens' \
--exclude-table-data='incidentes_resolucoes' \
--exclude-table-data='ntfy_alertas' \
--exclude-table-data='login_otps' \
--exclude-table-data='portal_otps' \
--exclude-table-data='cadastro_tokens' \
-f "prod_dump_${DATE}.dump"Excluímos data de tabelas grandes/efêmeras na origem para reduzir tamanho. Estrutura dessas tabelas vem normalmente.
⚠️ Regra de paridade pai/filho (FK). Ao adicionar uma FK que aponta para uma tabela em
--exclude-table-data, o filho TAMBÉM precisa ser excluído. Caso contrário o POST-DATA falha ao recriar a constraint porque o filho referencia um pai que não veio no dump. Casos atuais:email_unsubscribe_tokens → email_outboxeincidentes_resolucoes → logs_transacoes. O scriptscripts/staging/rebuild-from-prod.shtem sentinela defensiva em[3c/7]que aborta com a lista de FKs faltantes.
⚠️ LGPD — Retenção do dump. O arquivo
prod_dump_*.dumpcontém PII bruta (CPF, telefone, e-mail) antes do passo 4 (sanitize). Regras obrigatórias:
- Armazenar apenas localmente durante a execução do runbook, NUNCA em bucket público, Drive compartilhado, anexo de chat ou repositório.
- Apagar (
rm -P prod_dump_*.dump) imediatamente após confirmar que o passo 7 (drift check) passou. Máximo absoluto: 24 h no disco do operador.- Se for necessário arquivar para auditoria, usar bucket privado com retenção de 7 dias e criptografia at-rest (não fazemos isso por padrão).
- Em CI: o workflow
staging-rebuild(se for criado) DEVE rodar o sanitize no mesmo job que faz o restore, e nunca persistir o dump como artifact.
2. Resetar schema public em staging
export PGURL_STAGING="postgresql://postgres:PWD@db.nrgcajnhpjmwilfcmqyb.supabase.co:5432/postgres"
psql "$PGURL_STAGING" <<'SQL'
DROP SCHEMA public CASCADE;
CREATE SCHEMA public;
GRANT ALL ON SCHEMA public TO postgres;
GRANT USAGE ON SCHEMA public TO anon, authenticated, service_role;
SQL3. Restaurar dump
pg_restore --dbname="$PGURL_STAGING" \
--no-owner --no-privileges \
--section=pre-data \
--single-transaction \
--exit-on-error \
"prod_dump_${DATE}.dump"
pg_restore --dbname="$PGURL_STAGING" \
--no-owner --no-privileges \
--data-only \
--disable-triggers \
--superuser=postgres \
--single-transaction \
--exit-on-error \
"prod_dump_${DATE}.dump"
psql "$PGURL_STAGING" -c "DELETE FROM public.aluno_turma_historico h WHERE NOT EXISTS (SELECT 1 FROM public.alunos a WHERE a.id = h.aluno_id)"
pg_restore --dbname="$PGURL_STAGING" \
--no-owner --no-privileges \
--section=post-data \
--single-transaction \
--exit-on-error \
"prod_dump_${DATE}.dump"4. Sanitize (PII + tokens + MP + logs)
psql "$PGURL_STAGING" -f docs/staging/sanitize.sql5. Seed de usuários de teste
psql "$PGURL_STAGING" -f docs/staging/seed_test_users.sql6. Alinhar schema_migrations
supabase CLI grava o estado em supabase_migrations.schema_migrations. Após restaurar de prod, a tabela já vem com as 304 versões aplicadas. Confirmar:
psql "$PGURL_STAGING" -c "SELECT count(*) FROM supabase_migrations.schema_migrations"
# deve bater com: ls supabase/migrations/ | wc -lSe divergir, rodar:
supabase link --project-ref nrgcajnhpjmwilfcmqyb
supabase db push # idempotente — não deve aplicar nada se schema já confere7. Drift check obrigatório
PGURL_PROD="..." PGURL_STAGING="..." \
bun run scripts/staging/check-drift.tsCritério de aceite: BLOCKERS: 0. Se houver BLOCKER, não prosseguir — significa que o dump+restore não pegou tudo (raro, mas reportar e investigar).
8. Deploy edge functions + worker
for dir in supabase/functions/*/; do
fname=$(basename "$dir")
[ "$fname" = "_shared" ] && continue
supabase functions deploy "$fname" --project-ref nrgcajnhpjmwilfcmqyb
done
cd workers/olp-msgqueue && wrangler deploy --env staging9. Smoke E2E
# Pelo menos esses 4 fluxos têm que passar:
# - login senha (Dev Admin)
# - /me retorna escolas e papéis
# - GET escola-dados de uma escola seed
# - importação de 10 alunos (não 8000 — só smoke)
bunx playwright test --grep "@smoke"Quando refazer
| Gatilho | Ação |
|---|---|
| Cron diário de drift abriu issue (BLOCKERS > 0) | Refazer no mesmo dia |
| Migration relevante deployada em prod | Refazer antes da próxima sessão de QA em staging |
| Mais de 7 dias desde última sincronização | Refazer antes de stress test |
| Bug em staging que não reproduz em prod | Suspeitar drift, rodar check-drift.ts ANTES de debugar |
Não fazer
- ❌ Marcar migrations como aplicadas sem rodar (
staging_mark_migrations.sqlé DEPRECATED) - ❌ Fix manual no SQL Editor sem registrar em
drift-report.md - ❌ Refazer staging "do zero" rodando todas as migrations sequencialmente — o catálogo (
series_escolares,papeis, etc.) e funções com data não-trivial podem não estar 100% nas migrations. Usar dump real é mais seguro.
Referências
- Memória:
mem://infrastructure/staging/drift-detector-and-rebuild-protocol - Memória:
mem://infrastructure/staging/limite-alunos-generated-column-drift - Script:
scripts/staging/check-drift.ts - Sanitize:
docs/staging/sanitize.sql - Cron:
.github/workflows/staging-drift.yml