Faz o CI executar a imagem, não só buildá-la - #89
Conversation
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>
O smoke test achou um bug na primeira execução
Consequência: a imagem publicada em Como o Corrigido em
A armadilha foi para o |
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>
Fecha a #81.
O job
dockerbuildava 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+workercomdocker compose up --waite verifica quatro coisas:--waitvolta sem errodb:preparefalhandocurlem/acessibilidadede fora do containerSECRET_KEY_BASE" da v2.0.0, que nada exercitavaworkerexecuta um job enfileirado pelowebA premissa da issue estava errada num ponto
A #81 diz que os dois bugs de build já vividos aqui (CRLF em
bin/*, plataforma ausente noGemfile.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ópriodocker build, que o CI já validava.O argumento que se sustenta é o vizinho:
bin/docker-entrypointdepende 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 doci.ymlem 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 rodardocker compose upsem--buildcom uma imagem antiga em disco sobe a antiga; o README já manda usar--build.SECRET_KEY_BASEvazio no job é o ponto, não descuido. Oenv: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 é
WebhookDeliveryJobpara a assinatura 0, que não existe:WebhookDeliveryrecusa 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,dbhealthy,webhealthy,workerup;curl --fail .../acessibilidade— exit 0;docker compose logs web | grep 'gerando uma chave'— exit 0;webe, no log doworker: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:checkAll 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
weblevou ~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-timeoutestá 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
mainnão tem branch protection, então nenhum required check quebra com isso.Vai junto a atualização de um comentário do
config/queue.ymlque a #78 deixou desatualizado — já não é só webhook que roda em background.