Pre-Deploy Validator
Checklist de validações que DEVE rodar antes de qualquer gcloud run deploy, terraform apply em prod, ou tag de release. Baseada em falhas reais.
Quando invocar
- Antes de
docker buildx build --pushpra image de prod - Antes de
gcloud run deploypra service em prod - Antes de tagear release (v1.0.0+)
- Quando user fala "deploy", "subir em prod", "release"
Checklist (23 items)
🔴 P0 — Bloqueia deploy
1. Env vars Vite no build apontam pro DOMÍNIO PÚBLICO, não run.app interno
# ANTES de docker buildx, confirmar:
echo "VITE_SERVER_URL=https://meudominio.com" # NÃO https://servico-xxxx.run.app
echo "VITE_AUTH_URL=https://meudominio.com"
Por quê: CSP connect-src 'self' bloqueia chamadas pro run.app. Browser entra em retry infinito → ERR_INSUFFICIENT_RESOURCES.
2. CSP connect-src inclui o domínio do backend
Inspecionar apps/web/nginx.conf ou middleware CSP:
connect-src 'self' https://meudominio.com
3. CSP nonce placeholder substituído ou removido
grep -rn "%%CSP_NONCE%%" apps/web/dist apps/web/index.html
# Deve retornar 0 matches (ou remover o placeholder)
4. Migrations slot livre (sem colisão entre PRs)
# Confirmar próximo slot:
ls packages/db/migrations/*.sql | tail -3
# Pegar último N, usar N+1 na nova migration
# Verificar _journal.json sincronizado com files
5. Rotas Hono novas estão MONTADAS em index.ts
# Pra cada novo arquivo em apps/server/src/routes/, confirmar:
grep "novoArquivo" apps/server/src/index.ts
# Se não tem, é 404 silencioso em prod
6. Paths internos não duplicam o segmento do mount
// BUG: router.get('/deals/:id') montado em app.route('/api/deals') = /api/deals/deals/:id
// CORRETO: router.get('/:id') montado em app.route('/api/deals') = /api/deals/:id
Comando de verificação:
for f in apps/server/src/routes/*.ts; do
recurso=$(basename $f .ts | sed 's/-.*//')
grep -q "router\.\(get\|post\|patch\|delete\)('/$recurso" "$f" && echo "BUG: $f duplica /$recurso"
done
7. Cloud Run traffic apontado pra LATEST após deploy
gcloud run services update-traffic <service> --to-latest --region=<region> --project=<project>
# Sem isso, deploy cria rev nova mas tráfego fica na anterior
🟡 P1 — Investigar antes
8. JWT claims que código backend espera estão sendo emitidos
// Decodar JWT real do user e verificar claims:
const decoded = jwt.decode(token);
// Se backend espera org_id mas JWT só tem app_metadata.org_id, ou vice-versa,
// é bug. GoTrue auto-hospedado precisa Custom Access Token Hook pra injetar claims.
9. RBAC Guard é SÍNCRONO (não lazy/toast-then-redirect)
// BUG: renderiza componente, dispara toast, redireciona depois → flash conteúdo sensível
// CORRETO: redirect imediato ANTES de render
if (role && !allow.includes(role)) return <Navigate to="/dashboard" replace />;
if (!role) return <Skeleton />; // loading
10. Mocks/placeholders foram removidos
grep -rn "MOCK_\|FAKE_\|hardcoded\|TODO.*mock\|Ana Lima\|Carlos Mendes\|R\$ 297" apps/web/src
# Esperado: zero matches em prod
11. Rate limit é DISTRIBUÍDO (Redis), não in-memory
grep -rn "new Map()" apps/server/src/middleware/rate-limit*
# Se aparecer Map(), não escala entre instâncias Cloud Run
12. Dev secrets com fail-fast em produção
// BUG: process.env['SECRET'] ?? 'dev-fallback-min-32-chars'
// CORRETO:
function requireEnv(name: string): string {
const v = process.env[name];
if (!v && process.env['NODE_ENV'] === 'production') {
throw new Error(`Missing required env var: ${name}`);
}
return v || `dev-fallback-${name.toLowerCase()}`;
}
13. CORS allowlist explícita, sem * com credentials
Backend Hono cors():
cors({ origin: ['https://meudominio.com'], credentials: true })
14. Security headers HSTS+CSP+XFO+nosniff+Referrer
curl -sI https://meudominio.com/ | grep -iE "strict-transport|x-frame|content-security|x-content|referrer"
# Esperado: 5 headers presentes
15. PostgREST/serviços internos NÃO expostos publicamente
Verificar URL Map / Cloud Armor — /rest/v1/* deve estar bloqueado ou autenticado.
⚪ P2 — Polish
16. Frontmatter status: implemented tem código real
# Pra spec com status: implemented, grep palavras-chave do nome:
grep -rn "<slug-keyword>" apps/server/src apps/web/src packages/
# Se zero matches, frontmatter mente
17. Worktrees git órfãos limpados
git worktree list | grep -v "$(pwd)"
du -sh ../proj-* # Cada worktree ~500MB
18. Branches mergeadas deletadas
gh pr list --state merged --limit 50 --json headRefName -q '.[].headRefName' | while read b; do
gh api repos/OWNER/REPO/branches/$b >/dev/null 2>&1 && echo "$b ainda existe"
done
19. PostgREST schema cache atualizado após mudança DB
NOTIFY pgrst, 'reload schema';
-- OU redeploy do service postgrest
20. Maestri agents não ficam idle aguardando "GO"
Briefings devem ter "GO autoridade total modo overnight, decide+implementa+valida". Caso contrário agent fica esperando confirmação.
21. Smoke E2E REAL via browser (não só curl)
Playwright headless executa o JS bundle. Curl só testa HTTP path — pode dar false positive enquanto bundle JS crasha em runtime.
22. Cloud SQL Private IP + VPC Connector (não 0.0.0.0/0)
gcloud sql instances describe <inst> --format='value(ipAddresses[].type)'
# Esperado: ['PRIVATE'] (sem PUBLIC/PRIMARY)
23. Backup automatizado configurado
Cloud SQL → automated backups daily + PITR ≥7d.
Output
Reporta cada item ✅ ou ❌, com fix sugerido pra cada ❌. Se algum P0 falha, BLOQUEAR DEPLOY.
Lições adicionais
- Validar build args ANTES do push: rodar
docker run --rm <image> grep -r "run.app" /usr/share/nginx/htmlpra detectar URL interna vazada - CDN/LB cache: após deploy, fazer hard refresh + cache_bust query string pra validar bundle novo
--to-latesté obrigatório: Cloud Run não promove tráfego automaticamente, mesmo com--platform=managedgh pr mergesem--delete-branch: deixa branches acumulando — sempre usar- Múltiplos PRs paralelos com mesma migration slot: precisam rebasear sequencialmente, não simultâneo
- Buildx cache do runner stage: quando
Dockerfileé multi-stage (builder+runner),buildxcom driverdocker-containercacheia o runner stage mesmo quando o builder produz dist nova com hash de conteúdo igual. Bundle antigo pode ser servido apesar de--no-cacheno buildx. Mitigação obrigatória: adicionarARG CACHEBUST=1no Dockerfile antes doCOPY --from=builder ..., e passar--build-arg CACHEBUST=$(date +%s)em TODO build de prod. Sem isso, prod pode servir bundle com env vars ou código de PRs anteriores. (Descoberto em CRM_OS web:v23 — bundle antigo persistiu mesmo após PR mergeado com fix.) - Cloud Armor URL Map removal NÃO basta: se o path matcher é apenas removido do URL Map, o tráfego cai no
defaultService(frontend SPA). Pra bloqueio explícito 403, adicionar regra Cloud Armordeny-403no path. Defense in depth. - PoC de security audit pode deixar resíduo no DB: agents de pentest fazem INSERT/UPDATE pra provar exploit. Sempre exigir que o agent REVERTA cada mudança ao final + logue PoC com timestamp pra cleanup posterior.
- State de auth descentralizado = dessincronia visual: se múltiplos hooks consomem
sessionpor caminhos diferentes (auth-context vs cache singleton vs localStorage direto), eles ficam dessincronizados no primeiro render. Sintoma típico: sidebar mostra org/user, dashboard mostra "Selecione organização". Regra: único source of truth via auth-context React; todos os hooks DERIVAM dele. Cache singleton em módulo só serve pra memoização secundária após session estabilizada, nunca como primeiro source. - Cloud Armor rule com expression vazio bloqueia TUDO: ao criar rule via
gcloud compute security-policies rules createse o flag--expressionfor omitido ou aceito vazio, a rule ficamatch: { expr: { expression: '' }}que matcha tudo. Validar SEMPRE após criar:gcloud compute security-policies rules describe <priority> --format='value(match.expr.expression)'— não pode estar vazio. ??não trata string vazia como falsy:const X = import.meta.env.VITE_FOO ?? 'fallback'falha quando Vite injeta''(de--build-arg VITE_FOO=sem valor).??só faz fallback em null/undefined. Use(env as string \| undefined)?.trim() \|\| 'fallback'. Validar bundle pós-build:curl bundle.js \| grep -oE 'https://[^"]*'deve listar URLs absolutas esperadas. Bug encontrado em CRM_OS web:v29 (login quebrado) — fix em PR #116.- Fix de Zod schema sem atualizar consumers do parsed value: ao trocar tipo de validação (ex:
z.string().date()→z.string().datetime()), buscar TODOS os call sites que consomem o valor parseado — formato muda. Em CRM_OS PR #117 corrigiu schema de/api/admin/audit-logdedate(YYYY-MM-DD) pradatetime(ISO completo), masbuildFilters()continuou fazendonew Date(\${from}T00:00:00.000Z`)(concatenava sufixo presumindo formato date) — virouDate(NaN), drizzle quebrava com RangeError, endpoint passou de 400 pra 500. Fix completo em PR #118. **Regra:** após mudar schema,grep` o nome do campo no arquivo todo + buscar consumers que assumem formato antigo. Pre-deploy: rodar a request real com payload do frontend antes de declarar pronto.