Pular para conteúdo

0031 — Smoke de produção pós-deploy

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-09-08

Contexto

O pipeline da casa prova tudo menos produção. O job gate roda análise estática e testes; o e2e-local exercita o fio multi-app antes do deploy; o e2e native prova o binário. E mesmo assim a frota entregou, em produção: uma tela nova em 500 com o app healthy, e um INTEGRADOR_API_TOKEN ausente que só apareceu como 401 no cliente.

/health é liveness: prova que o processo subiu e o Flyway passou — não que as telas renderizam nem que os segredos foram injetados. Some a isso que o deploy pelo control-plane (0026) é assíncrono: o POST /api/ci/deploy retorna sucesso e o pipeline fica verde antes de o container novo estar servindo. O CI dizia "deployado" sem nada ter verificado o resultado.

Do outro lado, a política de glitch (2026-09-08) fez todo 5xx e todo 404 autenticado chegarem ao GlitchTip exatamente 1×. O sinal passou a existir; ninguém o lia no momento em que agir é mais barato — logo depois do deploy que o causou.

Decisão

Recurso deployável só encerra o deploy com smoke verde. Um job smoke roda depois do deploy, em produção, por HTTP, em quatro camadas — e falha reverte o deploy.

Correção 2026-10-01: o smoke deixou de ser job próprio e é o último step do job deploy, no mesmo runner: o GitHub cobra cada job arredondado ao minuto. A decisão não muda; o deploy só encerra verde com o smoke verde (smoke de produção).

  1. Identidade — espera o /health responder com commit == <sha desta entrega>. versao sozinho não distingue container velho de novo (um deploy sem bump serve a mesma versão), então o commit entra no /health, alimentado pelo XADM_COMMIT que o CI passa como build-arg. Sem o campo, a camada degrada para versao quando EXPECT_VERSION foi declarada, com o degradado escrito no relatório; sem ela, a ausência do campo REPROVA — /health sem commit num app que deveria publicá-lo é o container velho ainda no ar, e passar ali seria verde contra o build que nem subiu (foi o que aconteceu na primeira entrega instrumentada; corrigido em 2026-09-09).
  2. Rotas críticas — o app declara em docs/app.json → smoke.routes as rotas que não podem quebrar, por classe (public, m2m, session). A mesma lista serve o e2e native, que afirma ≠404 no binário antes do deploy: declarada uma vez, dois gates.
  3. Log-guarantee — nenhuma exceção nova no GlitchTip desde esta entrega. Critério primário é a release (firstRelease), que responde exatamente "esta exceção nasceu nesta entrega?"; o corte por tempo (firstSeen >= T0) é o fallback de quem ainda não marca release.
  4. Veredito — reprovou, reverte para <IMAGE>:<commit anterior>-<target> (a tag imutável que todo build publica), confirma que a reversão aconteceu, e falha o job.

A reversão é conservadora nas bordas. Ela não acontece: sob deploy concorrente (o commit no ar já não é o desta entrega), sob divergência de previous entre instâncias, sem alvo, por erro de configuração do manifesto, nem por indisponibilidade do GlitchTip. A automação agressiva fica no caso claro; o ambíguo vira vermelho com diagnóstico.

O seam de sessão é endpoint dedicado, desligado por default. POST /smoke/session no xadm-seguranca troca uma credencial dedicada por sessão de um principal smoke — guardado por @Requires(property="smoke.token", pattern=".+") e com rate-limit. A credencial vai em cabeçalho próprio (X-Smoke-Token), não em Authorization: Bearer, que naquela rota entraria na cadeia de autenticação por token antes do controller e viraria 401. O papel de smoke acompanha o papel de leitura do app em vez de substituí-lo (isolado, não passaria em @Secured nenhum), e o "só-leitura" é imposto por filtro de método (403 em POST/PUT/PATCH/DELETE) — garantia por verbo, não prova de ausência de efeito colateral em GET. Login Google é interativo e não se automatiza; sem o seam, rota de sessão vira asserção negativa (sem credencial tem de responder 302/401 — 200 é rota exposta, 5xx é caminho de negação quebrado).

Consequências

  • O deploy passa a ter veredito. Antes, "verde" significava "o POST foi aceito"; agora significa "produção respondeu o que devia". O custo é ~2–3 min por deploy, só em deploy.
  • Um identificador atravessa tudo. O XADM_COMMIT é o commit do /health, a release no GlitchTip e o sufixo da tag imutável — um valor, quatro usos, nada a sincronizar.
  • O nome da env é do namespace da casa, e isso não é estética. A primeira versão usou SOURCE_COMMIT, que é variável predefinida do Coolify: em recurso pull-only ele a injeta em runtime com o literal HEAD, por cima do ENV da imagem, e o /health passa a publicar uma identidade falsa com a imagem carregando o sha certo. Corrigido para XADM_COMMIT em 2026-09-09 (xadm-comum-web 0.9.0), com HEAD tratado como ausente dos dois lados — lib e smoke.py. Detalhe em coolify §Carimbar o commit.
  • A tag imutável vira ativo operacional, não sobra de build: é o alvo do rollback, e por isso ganhou norma de retenção (constituição §9).
  • A frota ganha um endpoint de auth a mais (/smoke/session) — desligado por default, revogável por rotação do secret, e vigiado pela política de glitch como qualquer rota.
  • A metade Flutter web é receita, não template — não há templates/dockerfile-web (o fluxo de sourcemap diverge entre os apps), então o /health com commit no web depende de os repos seguirem a receita, sem gate do central cobrando.

Alternativas descartadas

  • Só avisar, sem reverter. Era a recomendação conservadora: o veredito ainda não tem histórico, e reversão errada custa mais que o bug. Preterida — a janela de exposição de um defeito em produção medida em horas é o problema que a decisão existe para resolver; as quatro guardas cobrem o falso- positivo previsível, e o relatório sempre nomeia a camada que motivou.
  • Reprovar por atividade na janela (lastSeen). Reprovaria deploy limpo por ruído crônico — um @Scheduled que erra de hora em hora deixaria o smoke vermelho para sempre, e a reação previsível é desligá-lo. Reincidência avisa; só o que nasceu nesta entrega reprova.
  • Marcar a release por ENV SENTRY_RELEASE + enableExternalConfiguration. Parecia de graça e é armadilha: com o flag ligado, a config externa é fundida depois da configuração programática e sobrescreve dsn, environment, release, dist, serverName e mais — qualquer SENTRY_* do ambiente passaria a sobrepor o que a lib fixou deliberadamente, inclusive o DSN e o sendDefaultPii da observabilidade. A release é setada em código, do XADM_COMMIT.
  • Bearer de smoke aceito em qualquer rota de view, em vez do endpoint dedicado: toda view viraria porta de entrada do token, e sumiria a fronteira com o <APP>_API_TOKEN do §5.
  • commit dentro do version.json no Flutter web: é o artefato que o app consome para auto-update, servido no-cache a todo cliente — o /health é o contrato de operação, e é lá que a identidade de build mora.
  • Perguntar o status do deploy ao control-plane (endpoint novo no central-backend): acoplaria o smoke ao orquestrador quando o alvo da prova é o app.
  • Estender o smoke à stack compose: o gate de config (lint das sync rules + smoke de carga) já é dono daquele veredito; dois donos é pior que um.

Referências

  • Receita e mecânica: smoke de produção
  • Norma: constituição §9 (Entrega/Deploy) · definição de pronto §6
  • 0022 (native, tag da imagem) · 0026 (control-plane) · e2e-local §10 (a lista de rotas, pré-deploy)