Pular para o conteúdo

Armadilhas e correções

Bugs que passaram por typecheck e por testes de unidade, e só apareceram executando. Estão aqui porque a maioria não é óbvia em revisão de código, e porque o padrão que os une é mais útil que cada caso isolado.

O padrão. Todos envolvem a fronteira entre o que o código declara e o que o ambiente faz: um erro embrulhado por biblioteca, um schema cujo default não é o que parece, uma ferramenta que loga o que recebe. Nenhum é pego por tipo.


Sintoma. oren run --verbose imprimia o token em texto puro.

Causa. O motor de execução loga o conteúdo de withNewFile, e o input.json ia por ali. Toda a redação cuidadosa da CLI era anulada por uma flag.

✔ withNewFile /oren/input.json (contents: "{\"token\":\"super-secreto\"}")

Correção. Entrada com campo secreto viaja como secret montado, que o motor nunca imprime. Entrada sem segredo continua como arquivo comum — visível e cacheável, sem pagar o custo à toa.

Lição. Redigir na saída da própria aplicação não basta quando uma camada abaixo também loga.


Sintoma. Um output declarado secret era corretamente redigido, mas ao ser interpolado numa string comum aparecia em texto.

saída {"biggest":"***"} ← redigido
entrada {"nota":"o maior arquivo foi /source/c.txt"} ← vazou

Causa. O resolver de expressões calculava o taint corretamente e o runner o descartava, redigindo apenas pelos campos declarados secret no contrato.

Correção. O sigilo de um input vem de duas fontes: a declaração no contrato e a contaminação por um valor secreto usado na expressão que o preencheu.

Lição. Propagação de taint precisa de granularidade por campo. Um booleano agregado “esta estrutura tem algo secreto” não diz o que redigir.


Sintoma. No pipeline que originou o projeto, major e patch nunca foram incrementados. Todas as tags são 0.X.0.

Causa. O worker lia incremenMajorVersion e o pipeline enviava incrementMajorVersion. Sem contrato, o campo desconhecido era simplesmente ignorado.

A parte pior. O modelo novo aceitava o mesmo bug. O spec declarava additionalProperties: false como padrão, mas JSON Schema assume o contrário quando a chave está ausente — e o validador não a forçava.

Correção. Inputs e outputs são fechados por padrão, com sugestão de nome:

campo desconhecido: "incremenMajorVersion" — você quis dizer "incrementMajorVersion"?

Lição. Um default declarado em prosa não é um default aplicado. Vale testar o comportamento que a documentação promete.


Sintoma. Um worker quebrava e a saída era exit code: 1, nada mais. Era preciso reproduzir o container à mão para descobrir o motivo.

Causa. O ExecError do Dagger já carrega stdout, stderr e exitCode — não estávamos lendo.

Lição. Quando uma biblioteca embrulha o erro, o objeto costuma ter mais do que a mensagem mostra. Vale inspecionar antes de assumir que a informação se perdeu.

Correção relacionada. O LogOutput do motor mostra as operações do grafo (withExec, withNewFile), não a saída do worker. Filtrar aquele log jamais teria funcionado — a informação não estava lá. A saída do worker precisa ser capturada e atribuída ao step.


Sintoma. --yes imprimia “gravando em .oren/consent.json” e não gravava nada.

Causa. O flush usava uma segunda instância do store; os grants estavam na primeira.

Lição. Efeito colateral anunciado em log não é efeito colateral verificado. O teste que pega isso não olha a mensagem, olha o arquivo.


Sintoma. conteudo: "criado pelo oren" chegava ao Terraform como -var=pelo e -var=oren.

Causa. for pair in $(jq ...) faz word splitting.

Correção. Argumentos posicionais com set --, que preservam espaços. Estava em terraform-plan, terraform-apply e build-docker-image — este último teria falhado com qualquer buildArg contendo espaço.

Lição. O padrão é comum o bastante para virar checklist ao escrever worker em shell.


O motor não estava pronto quando disse que estava

Seção intitulada “O motor não estava pronto quando disse que estava”

Sintoma. ETIMEDOUT na primeira execução depois de criar o engine.

Causa. Um container responde a docker ps antes de o socket do engine existir. Conectar nesse intervalo falha com um timeout que não explica nada.

Por que passou despercebido. Toda primeira execução cria o container — era a experiência de estreia de qualquer usuário novo. Só não aparecia em desenvolvimento porque o engine já estava de pé.

Correção. Aguardar o socket antes de devolver o controle.

Lição. Testar a partida a frio, não só o caminho quente.


Sintoma. oren doctor retornava 0 mesmo com Docker ausente.

Causa. process.exitCode era definido depois de o processo já ter terminado.

Correção. Lançar erro e deixar o handler decidir o código.


Ferramentas que não funcionam dentro de container

Seção intitulada “Ferramentas que não funcionam dentro de container”

buildah rootless precisa de unshare(CLONE_NEWUSER), indisponível num container não privilegiado. Era minha primeira escolha para construir imagem sem daemon; só descobri executando. kaniko foi feito exatamente para isso.

Lição. “Rootless” e “funciona dentro de container sem privilégios” não são a mesma propriedade.


Sintoma. O build gravava docker-archive e o push esperava oci-archive.

Causa. O formato do arquivo não estava em contrato nenhum. Uma implementação de build que gravasse OCI quebraria qualquer push que esperasse o outro, sem nada indicando o porquê.

Correção. archiveFormat virou output obrigatório do build e input do push.

Lição. Quando duas tasks trocam um artefato, o formato dele é parte do contrato — não só o caminho.


Sintoma. localhost:5000/app era rejeitado.

Causa. Meu pattern não previa porta no host — que é a forma de todo registry que não seja o Docker Hub.

Correção. Pattern novo, validado contra dez referências reais antes de aplicar.

Lição. Regex de formato conhecido merece uma bateria de casos reais, não só os que vieram à cabeça.


O entryCount do cache do motor reportava 4328 entradas para 1,2 MB em disco. Não consegui explicar o número, e removi da saída em vez de exibi-lo.

Lição. Número que você não sabe explicar é pior que número nenhum — o usuário vai tentar interpretá-lo.