Skip to content

Faz o CI executar a imagem, não só buildá-la - #89

Merged
Hirley merged 4 commits into
mainfrom
claude-hirley/smoke-test-imagem
Sep 1, 2026
Merged

Hirley merged 4 commits into
mainfrom
claude-hirley/smoke-test-imagem

Conversation

@Hirley

@Hirley Hirley commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Fecha a #81.

O job docker buildava a imagem de produção e nunca a subia. Tudo que só falha em runtime passava batido: entrypoint, db:prepare, permissões do usuário não-root, healthcheck, publicação de porta, variável de ambiente faltando.

Depois da #77 isso ficou pior: o deploy passou a ter dois processos, e o segundo — o worker do Solid Queue — nunca foi exercitado por nada automatizado. Um worker que sobe e não consome nada é invisível: a tela responde normalmente e a entrega só não acontece.

O que o job faz agora

Sobe db + web + worker com docker compose up --wait e verifica quatro coisas:

Verificação O que quebraria sem ela
--wait volta sem erro container que morre no boot, entrypoint quebrado, healthcheck quebrado, db:prepare falhando
curl em /acessibilidade de fora do container porta não publicada, Puma escutando só em loopback
a linha de chave efêmera no log do entrypoint o caminho "sem SECRET_KEY_BASE" da v2.0.0, que nada exercitava
o worker executa um job enfileirado pelo web worker que sobe e não consome nada

A premissa da issue estava errada num ponto

A #81 diz que os dois bugs de build já vividos aqui (CRLF em bin/*, plataforma ausente no Gemfile.lock) teriam sido pegos por um smoke test. Não teriam — não precisariam: o Dockerfile executa ./bin/rails assets:precompile, então os dois quebravam o próprio docker build, que o CI já validava.

O argumento que se sustenta é o vizinho: bin/docker-entrypoint depende da mesma normalização de LF e só roda quando o container sobe — um problema ali passa por um build verde. Corrigi o texto no README e no comentário do ci.yml em vez de repetir a premissa.

Decisões

O build ficou em duas etapas. A primeira carrega a imagem no daemon local (load) para o smoke test rodar contra o artefato de verdade; a segunda publica e, como o contexto é o mesmo, acerta o cache inteiro e custa segundos. O ganho não é tempo: é que uma imagem que não sobe nunca chega ao registry.

image: ${APP_IMAGE:-task-keeper-api:local} em web e worker, para o compose usar a imagem já buildada em vez de buildar de novo — buildar duas vezes o mesmo artefato dobraria o job. Efeito colateral local: quem rodar docker compose up sem --build com uma imagem antiga em disco sobe a antiga; o README já manda usar --build.

SECRET_KEY_BASE vazio no job é o ponto, não descuido. O env: do workflow descreve o ambiente de teste, e aqui sobe a imagem de produção, então o job sobrescreve as três variáveis que não valem para essa stack.

O job enfileirado é WebhookDeliveryJob para a assinatura 0, que não existe: WebhookDelivery recusa antes de tocar a rede. O que se verifica é o percurso (web enfileira → Postgres guarda → worker executa), não a entrega.

Verificação

Rodei os passos exatos do workflow na máquina local, contra a imagem buildada do Dockerfile:

  • docker compose up --detach --wait — exit 0, db healthy, web healthy, worker up;
  • curl --fail .../acessibilidade — exit 0;
  • docker compose logs web | grep 'gerando uma chave' — exit 0;
  • job enfileirado pelo web e, no log do worker: Performed WebhookDeliveryJob (Job ID: 9b6156ec-…) from SolidQueue(default) in 15.31ms;
  • docker compose down --volumes — exit 0.

Gates de sempre no container: RuboCop 110 arquivos / 0 ofensas, zeitwerk:check All is good!, RSpec 384 exemplos / 0 falhas.

O que não dá para verificar daqui: o tempo do job num runner do GitHub. Localmente o web levou ~3,5 min para ficar saudável, mas isso é a máquina — o próprio timeout de 5s do healthcheck estourou em 28s, com a VM do Docker Desktop saturada. O --wait-timeout está em 240s; se num runner isso apertar, o número é o que muda.

Nota: o nome do check muda de "Build da imagem Docker" para "Build e smoke test da imagem Docker". A main não tem branch protection, então nenhum required check quebra com isso.

Vai junto a atualização de um comentário do config/queue.yml que a #78 deixou desatualizado — já não é só webhook que roda em background.

O job docker buildava a imagem de produção e nunca a subia, então tudo
que só falha em runtime passava batido: entrypoint, db:prepare,
permissões do usuário não-root, healthcheck, publicação de porta,
variável de ambiente faltando. Depois da #77 isso ficou pior: o deploy
passou a ter dois processos, e o segundo — o worker do Solid Queue —
nunca foi exercitado por nada automatizado. Um worker que sobe e não
consome nada é invisível, porque a tela responde normalmente e a entrega
simplesmente não acontece.

O job agora sobe db + web + worker com `docker compose up --wait` e
verifica quatro coisas: a stack fica saudável; /acessibilidade responde
200 pela porta publicada (de fora do container, o que o healthcheck
interno não cobre); o log traz a linha de chave efêmera do entrypoint; e
o worker executa um job enfileirado pelo web.

Sobre a premissa da issue: ela dizia que os dois bugs de build já
vividos por este projeto (CRLF em bin/*, plataforma ausente no
Gemfile.lock) teriam sido pegos por um smoke test. Não é verdade — o
Dockerfile executa ./bin/rails assets:precompile, então os dois
quebravam o próprio build, que o CI já validava. O argumento que se
sustenta é o vizinho: bin/docker-entrypoint depende da mesma
normalização de LF e só roda quando o container SOBE, então um problema
ali passaria por um build verde. Corrigi o texto no README e no
comentário do ci.yml em vez de repetir a premissa.

Decisões:

O build ficou em duas etapas. A primeira carrega a imagem no daemon
local (load) para o smoke test rodar contra o artefato de verdade; a
segunda publica, e como o contexto é o mesmo acerta o cache inteiro,
custando segundos. O ganho não é tempo: é que uma imagem que não sobe
nunca chega ao registry.

Para o compose usar essa imagem em vez de buildar de novo, web e worker
ganharam `image: ${APP_IMAGE:-task-keeper-api:local}`. Buildar duas
vezes o mesmo artefato dobraria o job. Efeito colateral local: quem
rodar `docker compose up` sem --build com uma imagem antiga em disco
sobe a antiga — o README já manda usar --build.

SECRET_KEY_BASE vazio no job é o ponto, não descuido: é o que exercita o
caminho de chave efêmera que nasceu na v2.0.0. O env do workflow
descreve o ambiente de teste, então o job sobrescreve as três variáveis
que não valem para uma stack de produção.

O job enfileirado é WebhookDeliveryJob para a assinatura 0, que não
existe: WebhookDelivery recusa antes de tocar a rede. O que se verifica
é o percurso (web enfileira, Postgres guarda, worker executa), não a
entrega.

Verificado rodando os passos exatos do workflow na máquina local, contra
a imagem buildada do Dockerfile: `up --wait` verde nos três serviços,
curl 200, grep da chave efêmera, e o log do worker com "Performed
WebhookDeliveryJob ... in 15.31ms" para o job enfileirado pelo web.
Gates de sempre no container: RuboCop 110 arquivos sem ofensas,
zeitwerk:check limpo, RSpec 384 exemplos e 0 falhas.

Vai junto a atualização de um comentário do config/queue.yml que a #78
deixou desatualizado: já não é só webhook que roda em background.

Closes #81

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
O smoke test do commit anterior falhou na primeira execução com
"container task_keeper_api-worker-1 exited (126)" — 126 é "comando
encontrado, mas não executável".

bin/jobs entrou no repositório como 100644, enquanto todos os outros
scripts de bin/ são 100755. O arquivo foi criado no Windows na #77, e o
sistema de arquivos de lá não tem bit de execução, então o Git registrou
o modo sem ele. Um `docker build` nesta máquina não percebe: o contexto
vem do disco, onde tudo parece executável — foi por isso que a
verificação da #77 passou aqui. Um checkout num runner Linux respeita o
modo do índice, e aí o worker não sobe.

Ou seja: a imagem publicada em main a partir da #87 builda perfeitamente
e não tem worker. Como o web responde normalmente e a falha é do outro
processo, o sintoma seria "os jobs não saem da fila" — exatamente o
modo de falha silencioso que a #77 veio eliminar.

Duas camadas, mesmo critério já usado para o CRLF:

  * `git update-index --chmod=+x bin/jobs` corrige a causa, no índice;
  * `chmod +x bin/*` no Dockerfile, ao lado do `sed` que normaliza LF,
    protege a imagem do próximo script criado no Windows.

A armadilha foi para o CLAUDE.md, junto com outra que apareceu na
verificação: exportar MSYS_NO_PATHCONV=1 no shell inteiro faz o curl do
Git Bash falhar com "(23)" em `-o /dev/null`, o que parece erro da
aplicação e não é.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Hirley

Hirley commented Sep 1, 2026

Copy link
Copy Markdown
Owner Author

O smoke test achou um bug na primeira execução

container task_keeper_api-worker-1 exited (126) — 126 é "comando encontrado, mas não executável".

100755 bin/docker-entrypoint
100644 bin/jobs          ← 
100755 bin/rails
100755 bin/rspec
100755 bin/setup

bin/jobs entrou no repositório sem bit de execução. Foi criado no Windows na #77, e o sistema de arquivos de lá não tem esse bit, então o Git registrou 100644.

Consequência: a imagem publicada em main desde a #87 builda perfeitamente e não tem worker. Um docker build no Windows não expõe isso (o contexto vem do disco, onde tudo parece executável) — foi por isso que a verificação da #77 passou na minha máquina. Um checkout num runner Linux respeita o modo do índice.

Como o web responde normalmente e quem morre é o outro processo, o sintoma em produção seria "os jobs não saem da fila": exatamente o modo de falha silencioso que a #77 veio eliminar.

Corrigido em 0ce4814, com duas camadas — mesmo critério já usado para o CRLF:

  • git update-index --chmod=+x bin/jobs resolve a causa, no índice;
  • chmod +x bin/* no Dockerfile, ao lado do sed que normaliza LF, protege a imagem do próximo script criado no Windows.

A armadilha foi para o CLAUDE.md, e o README ganhou um terceiro item na lista de bugs de build — o único dos três que não quebrava o docker build, e portanto o primeiro que justifica de fato este PR. A discussão sobre a premissa da issue, na descrição acima, envelheceu bem: o exemplo que faltava apareceu sozinho.

Hirley and others added 2 commits August 31, 2026 23:11
O smoke test falhou com "container task_keeper_api-worker-1 has no
healthcheck configured". O `--wait` do compose exige healthcheck em todo
serviço nomeado, e o worker não tem um: ele não serve HTTP, então o
HEALTHCHECK do Dockerfile (curl na 3000) está desligado nele de
propósito.

Isso não apareceu na verificação local porque o compose do Docker
Desktop aceita a mesma combinação e reporta o serviço como "Healthy" —
o do runner trata como erro. É a segunda vez neste PR que o ambiente
Windows esconde uma diferença que só o Linux mostra.

Não inventei um healthcheck de processo para o worker só para satisfazer
o --wait: seria uma verificação de pulso, e já existe uma de trabalho.
Agora o job espera db e web ficarem saudáveis, sobe o worker em seguida,
e quem prova que ele está vivo continua sendo "O worker consome um job
da fila".

Entre os dois entrou uma checagem de estado do container, com retry
curto. Ela não acrescenta cobertura — acrescenta diagnóstico: um worker
que morre no boot passa a falhar em ~2s com o exit code na mensagem, em
vez de virar um timeout de 60s dizendo apenas "não consumiu o job". Era
esse o sintoma do bin/jobs sem bit de execução, no commit anterior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Com push=false, o passo que publica fazia um build inteiro — ainda que
todo de cache — cujo resultado era descartado. O Dockerfile já foi
validado pelo build local do passo anterior, e a imagem, pelo smoke
test; refazer não acrescenta cobertura, só ~17s por PR.

O passo de metadados continua rodando nos dois casos, então as regras de
tag (a do latest e as de semver, que já falharam duas vezes neste
projeto) seguem sendo exercitadas em pull request.

Tempos medidos no runner, agora que dá para medir: o smoke test em si
custa ~26s — 18s esperando db e web ficarem saudáveis, 3s subindo o
worker, 3s para o job ir do web ao worker, 1s de teardown. O que pesa é
o build com `load`, ~1m45s contra ~1m10s de um build que não exporta a
imagem. Esses 35s são o preço de testar exatamente o artefato que vai
ser publicado, e não uma cópia dele.

Com isso o job fica em ~2m50s e o CI completo em ~4min, contra os ~3min
que a issue estabeleceu como alvo. Registro a diferença em vez de
esconder: o alvo foi escrito sem saber o custo do `load`, e o caminho
para cumpri-lo seria publicar antes de testar, que é exatamente o que
este PR existe para não fazer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Hirley
Hirley merged commit 9c7adfa into main Sep 1, 2026
3 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in @Hirley's task_keeper_api Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant