Skip to content

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_alunos virou INTEGER em vez de GENERATED ALWAYS (2026-06-04)
  • series_escolares ficou 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:

bash
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.sh

Flags: --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 stagingfresh_projectreset_authPor quê
Projeto Supabase recém-criado / deletado-e-recriado, public ainda vaziotruetruepre-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 refazerfalsefalseModo 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 E2EfalsetrueDestrói schema E limpa auth.users (Dev users precisam ser recriados via UI antes do smoke)

⚠️ reset_auth ≠ wipe completo. A checkbox reset_auth aplica apenas TRUNCATE auth.users CASCADE. Ela NÃO substitui o reset do schema public. Marcar só essa caixa sem fresh_project = false mantém os dados de aplicação (escolas, alunos, etc.) intactos.

⚠️ Erro comum: disparar com fresh_project = true quando o staging já tem dados. O pre-rebuild-check aborta com fail-close (linha 65 do script) — isso é o comportamento correto, não bug. A correção é re-disparar com fresh_project = false. O guard existe para evitar sobrescrever dados de QA em andamento.

ℹ️ Quem cria o schema public: o script apenas faz DROP SCHEMA public CASCADE. A recriação vem do pg_restore --section=pre-data do 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)

GuardO que verifica
PGURL_PROD == PGURL_STAGINGAborta se variáveis idênticas
PROD ref checkPGURL_PROD DEVE apontar para mjvuzsizjlcalyfmbquy (ref de prod conhecido). Bloqueia variáveis trocadas.
STAGING ref checkPGURL_STAGING NÃO pode apontar para o ref de prod
ConnectivitySELECT 1 nos dois bancos antes do dump
Sanity de contagemAborta se staging.escolas > prod.escolas (sintoma de inversão)
Confirmação REBUILDOperador digita literal antes do DROP (a menos que --yes)
Trap SIGINT/SIGTERMCtrl+C aborta com mensagem clara, prod intocada
LGPD dump cleanuprm -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:

bash
# 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:drift

Backup 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_dump 16 aborta com "server version mismatch"). O runner ubuntu-latest vem com 16 pré-instalado; o workflow remove e instala explicitamente o postgresql-client-17.
  • supabase CLI v1.200+
  • wrangler CLI (para Worker)
  • Acesso à connection string de prod (Database Settings → Connection string)
  • Projeto Supabase staging vazio OU pronto para ter dados resetados

Pipeline

text
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 E2E

Passo a passo

1. Exportar dump de produção

bash
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_outbox e incidentes_resolucoes → logs_transacoes. O script scripts/staging/rebuild-from-prod.sh tem sentinela defensiva em [3c/7] que aborta com a lista de FKs faltantes.

⚠️ LGPD — Retenção do dump. O arquivo prod_dump_*.dump conté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

bash
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;
SQL

3. Restaurar dump

bash
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)

bash
psql "$PGURL_STAGING" -f docs/staging/sanitize.sql

5. Seed de usuários de teste

bash
psql "$PGURL_STAGING" -f docs/staging/seed_test_users.sql

6. 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:

bash
psql "$PGURL_STAGING" -c "SELECT count(*) FROM supabase_migrations.schema_migrations"
# deve bater com: ls supabase/migrations/ | wc -l

Se divergir, rodar:

bash
supabase link --project-ref nrgcajnhpjmwilfcmqyb
supabase db push     # idempotente — não deve aplicar nada se schema já confere

7. Drift check obrigatório

bash
PGURL_PROD="..." PGURL_STAGING="..." \
  bun run scripts/staging/check-drift.ts

Crité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

bash
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 staging

9. Smoke E2E

bash
# 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

GatilhoAção
Cron diário de drift abriu issue (BLOCKERS > 0)Refazer no mesmo dia
Migration relevante deployada em prodRefazer antes da próxima sessão de QA em staging
Mais de 7 dias desde última sincronizaçãoRefazer antes de stress test
Bug em staging que não reproduz em prodSuspeitar 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