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).
- Identidade — espera o
/healthresponder comcommit == <sha desta entrega>.versaosozinho não distingue container velho de novo (um deploy sem bump serve a mesma versão), então ocommitentra no/health, alimentado peloXADM_COMMITque o CI passa como build-arg. Sem o campo, a camada degrada paraversaoquandoEXPECT_VERSIONfoi declarada, com o degradado escrito no relatório; sem ela, a ausência do campo REPROVA —/healthsemcommitnum 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). - Rotas críticas — o app declara em
docs/app.json→smoke.routesas 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. - 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. - 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é ocommitdo/health, areleaseno 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 literalHEAD, por cima doENVda imagem, e o/healthpassa a publicar uma identidade falsa com a imagem carregando o sha certo. Corrigido paraXADM_COMMITem 2026-09-09 (xadm-comum-web0.9.0), comHEADtratado como ausente dos dois lados — lib esmoke.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/healthcomcommitno 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@Scheduledque 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 sobrescrevedsn,environment,release,dist,serverNamee mais — qualquerSENTRY_*do ambiente passaria a sobrepor o que a lib fixou deliberadamente, inclusive o DSN e osendDefaultPiida observabilidade. A release é setada em código, doXADM_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_TOKENdo §5. commitdentro doversion.jsonno Flutter web: é o artefato que o app consome para auto-update, servidono-cachea 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)