Pular para conteúdo

Smoke de produção pós-deploy

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-10-01

Aplica-se a: perfil app com build.targets (todo app deployável).

O e2e-local prova o fio antes do deploy. O smoke roda depois, contra produção, e decide se o pipeline fica verde. Norma: Entrega · decisão 0031.

1. O que ele existe para pegar

/health verde é liveness — o processo subiu —, não "as telas funcionam": tela nova em 500 e segredo ausente (que só aparece como 401 no cliente) passam por ele. E o deploy pelo control-plane é assíncrono: o POST /api/ci/deploy retorna sucesso antes de o container novo servir.

O smoke é HTTP puro (smoke.py, stdlib, baixado da toolchain) e roda no runner do GitHub, que alcança o app e o bug.xadm.biz pela HTTPS pública.

2. As quatro camadas

1 — Identidade. Espera o /health responder vivo e com commit == <sha desta entrega>. versao sozinha não discrimina (um dispatch sem bump serve a mesma versão), por isso o commit está no shape do /health. Sem o campo, a camada degrada para versao e escreve o degradado no relatório.

2 — Rotas críticas. Cada rota declarada responde o status esperado, sem seguir redirect — seguir 302 → /login → 200 faria rota protegida passar por saudável.

3 — Log-guarantee. Nenhuma exceção nova no GlitchTip desde esta entrega:

  • critério primário: firstRelease == <esta release> — imune a desvio de relógio e a atraso de propagação;
  • fallback: sem firstRelease, firstSeen >= T0 (o instante do deploy);
  • reincidência avisa, não reprova: exceção antiga que voltou entra no relatório; reprovar por atividade na janela deixaria o smoke vermelho para sempre por um erro crônico.

Declarar a camada 3 e não configurá-la é defeito

O bloco smoke.glitchtip é ato explícito. Sem o GLITCHTIP_API_TOKEN no ambiente, a camada não rodaria e o job ficaria verde provando 2 de 3 camadas — então o smoke reprova (defeito de configuração, sem reverter) dizendo qual secret falta. A base do GlitchTip vem do features.glitchtip.dsn do app.json, e o token segue o nome pelo destino (Segurança §Segredos).

Guarda de fail-open

Se nenhum issue do projeto trouxer firstRelease e a release estiver configurada, o smoke não conclui "nada de novo": avisa alto e cai no fallback. Sem isso, app que não marca release teria o filtro nunca casando e o log-guarantee verde para sempre.

4 — Veredito. Reprovou em qualquer camada, em qualquer base: o job falha e o relatório diz o alvo de reversão (seção 5).

Ambiente do smoke

O smoke é o último step do job deploy do pipeline.yml: roda só se os steps de deploy passaram, no mesmo runner, e lê T0, PREVIOUS e TARGETS do step que precede o POST. Esse step preenche estas variáveis; quem roda o smoke.py à mão passa as mesmas.

Variável Papel
EXPECT_COMMIT sha desta entrega (camada 1 e release esperada no GlitchTip)
EXPECT_VERSION fallback declarado da camada 1 quando o /health não expõe commit
T0 instante do deploy, fallback do log-guarantee
PREVIOUS JSON {base: commit} lido do /health antes do POST de deploy — o alvo de reversão
IMAGE, TARGETS imagem e alvos (jar,native) para montar a tag imutável <sha>-<target>
SMOKE_TOKEN, SMOKE_M2M_TOKEN credenciais das rotas session e m2m
GLITCHTIP_API_TOKEN token da camada 3; GLITCHTIP_URL só vale quando o app.json não traz DSN
HEALTH_TRIES, HEALTH_INTERVAL espera da camada 1 (padrão 40 × 3 s)

3. O manifesto — declarado uma vez, lido por dois gates

Todo app deployável declara smoke.routes: o /health e, de cada classe de credencial que o app serve, ao menos uma rota GET sem efeito colateral. O /health sozinho não pega a tela em 500, o segredo ausente nem a rota que o AOT podou. O valida-frontmatter.py reprova app com build.targets sem o bloco, e a guarda Classe servida sem rota no smoke do gate reprova classe servida sem rota: session quando o app usa a xadm-seguranca e tem view, m2m quando tem app.api-token ou ROLE_API. Classe que só tem rota com efeito colateral (um POST que processa) declara o motivo em smoke.sem_rota, ex. {"m2m": "só POST que processa o arquivo"}.

"smoke": {
  "bases": ["https://webstorm.thoms.xadm.biz"],
  "glitchtip": { "org": "x-adm" },
  "routes": [
    { "path": "/health", "class": "public", "status": 200 },
    { "path": "/login",  "class": "public" },
    { "path": "/api/integrador/incidentes", "class": "m2m" },
    { "path": "/home",   "class": "session" }
  ]
}
  • bases é lista: app multi-instância declara todas — é onde deploy parcial se esconde.
  • glitchtip leva o slug da org; o projeto é o features.glitchtip.project do app.json.
  • class é a credencial que a rota exige, não o público da tela: public (sem credencial) · m2m (Bearer <APP>_API_TOKEN) · session (ponto de injeção, seção 4). method padrão GET, status padrão 200.

class errada vira reversão de um deploy sadio

Rota anônima declarada session passa enquanto o ponto de injeção funciona; no dia em que ele degradar, o smoke bate sem credencial, vê 200 e classifica como rota exposta. Confira cada rota contra o que o app trata como público (intercept-url-map, regra de view). O smoke sonda rota m2m/session também sem credencial: 200 anônimo prova que a class mente e é defeito de manifesto — falha o job sem reverter. A sonda não roda em public.

A mesma lista serve o e2e native:

Gate Quando Afirma
e2e native (e2e-local) antes do deploy, no binário rota m2m/session: o status declarado com credencial; rota public: ≠ 404
smoke depois do deploy, em produção o status declarado

4. O seam de sessão

Login Google é interativo. O xadm-seguranca expõe POST /smoke/session, que troca uma credencial dedicada (SMOKE_TOKEN, secret do repo no GitHub) por sessão de um principal smoke — nunca um usuário real. A sessão volta em Set-Cookie.

  • A credencial vai no cabeçalho X-Smoke-Token, nunca Authorization: Bearer: o Bearer entraria na cadeia de autenticação por token antes do controller e viraria 401.
  • ROLE_SMOKE acompanha o papel de leitura do app, e um filtro por método responde 403 smoke_read_only a POST/PUT/PATCH/DELETE do principal. O corte é por verbo: garante "não escreve por método", não "não escreve" — GET com efeito colateral passa. A ordem do filtro é um literal numérico, e a lib tem teste que o afirma contra a constante do framework.
  • Nasce desligado: @Requires(property = "smoke.token", pattern = ".+") — pattern, nunca notEquals (Java/Micronaut).
  • ponto de injeção ausente não é sempre 404: com regra de view global, a rota inexistente sai como 401 ou 302. Gate sobre o ponto de injeção afirma "não emite sessão" (404/401/403/302); 5xx reprova.

Sem o ponto de injeção, rota session vira asserção negativa: sem credencial responde 302/401; 200 é rota exposta e 5xx é a negação quebrada. Vale o mesmo para m2m sem token.

5. Rollback — pendente no control-plane

Rollback automático: pendente no control-plane. O smoke reprova e avisa; o operador reverte apontando <sha>-<target> no recurso e fazendo Redeploy.

O smoke calcula e reporta o alvo: a tag imutável do que estava no ar, <IMAGE>:<commit anterior>-<target>, com o <commit anterior> lido do /health antes do POST de deploy — o que estava de fato no ar, não o que o git supõe. A tag móvel (native-amd64) não serve de alvo. O relatório não sugere reverter quando:

Guarda Por quê
o commit no ar já não é o desta entrega outro deploy assumiu — a concurrency do workflow não cancela run de tag, e reverter voltaria dois commits
as bases divergem no commit anterior é desvio entre instâncias, achado operacional
não há commit anterior /health sem commit ou primeiro deploy
o manifesto está inválido, a class mente, ou falta o token do GlitchTip defeito de configuração, não do que subiu
o GlitchTip está fora do ar indisponibilidade do observador não é defeito do deploy

6. Identidade de build — de onde vem o commit

O CI passa --build-arg XADM_COMMIT=<sha>; o Dockerfile o promove a ENV; a xadm-comum-web o lê e o emite no /health. O mesmo valor é a release no Sentry — um identificador atravessa pipeline, imagem, /health e GlitchTip.

O nome é XADM_COMMIT — SOURCE_COMMIT é do Coolify

Em recurso pull-only, o Coolify injeta SOURCE_COMMIT=HEAD em runtime, por cima do ENV da imagem: a camada 1 nunca casaria, a release do Sentry viraria HEAD e o alvo de reversão seria uma tag inexistente. A lib trata HEAD como ausente (omite o campo em vez de mentir), e o smoke.py também. O site de docs, que o Coolify builda, usa o SOURCE_COMMIT dele como build-arg — lá o valor é real.

Declare o ARG tarde, e leia a env dentro do método

ARG invalida o cache de toda camada abaixo dele: vai depois do COPY, no estágio final, senão o nativeCompile roda de novo em todo build. A leitura em código nunca vai num static final (sob native-image o inicializador pode rodar no build).

A release do Sentry se marca em código (options.setRelease(VersaoInfo.commitAtual())), não por ENV SENTRY_RELEASE + enableExternalConfiguration: a config externa sobrescreveria dsn, environment, release e serverName depois da programática. O /health do Flutter web está em Versionamento.

7. O que o smoke NÃO prova

  • Regressão visual: afirma rota e exceção, não pixel.
  • Que o container velho não respondeu: durante a troca, o proxy pode entregar ao antigo; o retry mitiga em parte.
  • A stack compose: ela tem gate próprio no repo de config.
  • Que a lista de rotas basta: nunca basta — por isso todo 5xx e todo 404 autenticado chegam ao GlitchTip, e é essa rede que a camada 3 lê.