Como editar o título de uma PR derrubou todos os builds do dia
A pipeline de build de uma aplicação Angular parou de funcionar. O famoso "DO NADA".
Um dev me pingou: "tá dando um erro estranho na pipe, consegue dar uma olhada?" Pensei: não teve mudança recente na pipeline, nem na VM. Estranho. Mas como o assunto é fluxo do time, então é prioridade. Parei o que estava fazendo e fui verificar.
O problema
Acessei o build e de primeira já verifiquei que todos falhavam no mesmo ponto: a etapa de instalação do Angular CLI. Isso já me deu o palpite de que podia ser um problema na VM, pois essa pipeline usa um agente self-hosted.
A investigação
Abri a task de instalação do Angular CLI no Azure DevOps. A primeira coisa que saltou foi ##[error]Error: Npm failed with return code: 217:
Uma pesquisa rápida e descubro que é um erro genérico, ou seja, pode ser qualquer coisa. Tudo bem, mas isso significa que a resposta não estava ali. Voltei para o log, continuei lendo o que estava antes do 217 e encontrei a mensagem: directory not empty, rename '.../cli' -> '.../cli-Xuqet0PR'
Perfeito. Agora eu tinha uma pista. A própria mensagem de erro é intuitiva, o npm tentou instalar o Angular CLI e, para fazer isso, primeiro tenta renomear o diretório atual (cli) para um temporário (.cli-Xuqet0PR). Só que esse temporário já existia com conteúdo. O rename falhou, o processo morreu, o build foi junto.
A grande pergunta não era o que quebrou. Era por que isso estava ali.
Me fazer essa pergunta me fez enxergar a origem do problema em vez de sair procurando solução cegamente. Fui na lista de builds no Azure DevOps, procurei o último lote de builds com sucesso e identifiquei o momento exato em que as falhas começaram. Abri o build correspondente, acessei a PR em andamento e encontrei uma alteração no YAML da pipeline: o dev tinha removido a etapa de instalação do Angular.
Por que esse dev precisou retirar isso? Esse tipo de mudança na pipeline normalmente é um sinal de que o problema foi interpretado sem contexto suficiente.
Antes de investigar mais a fundo, preciso liberar o fluxo
Pipeline parada é prioridade máxima. Como DevOps, minha função é acelerar o ciclo de entrega do time, e build travado é o oposto disso.
Dei um change request na PR, chamei o dev no privado para avisar sobre o erro e comuniquei ao time no grupo que havia um problema na pipeline e que eu já estava investigando. Com isso feito, ativei a VPN e acessei a VM via SSH.
Tava lá. O diretório temporário abandonado, exatamente onde o erro dizia.
Antes de rodar qualquer comando, fui pesquisar o que era esse .cli-Xuqet0PR. Uma regra de ouro aqui é sempre questionar as próprias decisões. Sair apagando arquivos sem entender o impacto é pedir para ter problema. Fiz a pesquisa e retornou essa resposta:
O diretório temporário que o npm utiliza durante uma instalação global serve como um espaço de trabalho intermediário para processar o pacote antes de movê-lo para o local definitivo no sistema.
Ótimo. O fluxo normal do npm é criar o temporário (.cli-xxxxx), processar o pacote nele e mover tudo para o diretório definitivo (cli). Se o processo é interrompido no meio, o temporário fica lá. Na próxima execução, quando o npm tenta criar de novo, encontra um diretório não vazio e explode com ENOTEMPTY.
Agora que entendi o processo e sei qual é o problema, a solução é deletar as duas pastas, a cli e a temporária.
Rodei a pipeline de novo e passou. Fui conferir a pasta na VM logo após o step de instalação do Angular CLI concluir e apenas o diretório cli estava lá, sozinho. Ou seja, o temporário foi criado, extraiu o pacote e depois foi renomeado para cli. Problema resolvido, time avisado no grupo. Agora que o fluxo do time voltou, podemos entender o que causou isso.
Quem interrompeu o processo?
Um princípio que carrego pra sempre na minha carreira é que nada acontece do nada.
De cara, minha dedução foi que o problema estava na alteração do YAML feita pelo dev. Mas fui checar o histórico da PR e notei que ele só removeu a instalação do Angular no terceiro commit. O erro 217 já estava quebrando a pipeline antes disso. O que provavelmente aconteceu? Sem saber como resolver o problema original, ele colou o log na IA. Ela, completamente sem contexto da nossa aplicação, deu a solução mais genérica possível: mandar apagar a instalação do Angular CLI.
Descartada a primeira ideia, pensei que uma possível causa fosse o processo de instalação ter sido abortado no meio. O diretório temporário ficou órfão lá no agente de build. Todos os pipelines seguintes tentavam fazer o rename e quebravam justamente porque esbarravam num destino que já não estava vazio. Mas quem pode ter interrompido a execução, se não tem build anterior ao que falhou?
Fui ver nos logs do agente na VM. Aqui aprendi algo que eu não sabia na prática: a VM está em UTC e eu estou em UTC-3. O build que eu via no Azure DevOps como falho às 08:53 correspondia a 11:53 na VM. Isso importa na hora de correlacionar logs.
Dei um cat nos arquivos de logs do agente e o arquivo era enorme, sem chance de ficar procurando linha por linha. Pedi ajuda a uma IA para refinar o comando, e ela sugeriu filtrar pelo Running job:
Esse comando retornava os logs de todas as vezes que um build foi iniciado usando o agente da VM. Aqui foi fundamental entender a diferença de fuso horário, porque a máquina roda em UTC. Eu precisava isolar os logs e encontrar o primeiro build do dia.
Comecei a caçar pela data e esbarrei em vários logs antes das 11:53, que era o horário da nossa falha. Precisei analisar um por um por processo de eliminação. Primeiro, vi três execuções às 01:00 UTC. Por que eles rodaram antes da primeira pipeline do dia? Simples, foi por conta do fuso (UTC-3), esses builds na verdade rodaram às 22:00 do dia anterior no meu horário local. Eram jobs de outra pipeline que utiliza também esse agente.
Depois, achei mais três jobs às 02:00 UTC. Mesma lógica, eram das 23:00 da noite anterior, referentes aos agendamentos que configurei para rodar em três branches diferentes. Tudo certo até aqui.
Eliminando esse histórico noturno e o build das 11:53 que falhou, sobraram exatamente dois jobs nos logs (às 11:49 e 11:50) que simplesmente não apareciam na interface do Azure DevOps.
Foi só nesse momento que a minha ficha caiu e eu olhei para o número do build que havia falhado: #20000000.3. Estava na minha cara o tempo todo. O sufixo já indicava que era a terceira execução do dia. Eu não tinha prestado atenção nisso antes... mas acontece xD. Isso confirmava tudo, o log me levou na direção certa e realmente tínhamos dois builds perdidos.
Lendo os logs dos Workers
Na pasta _diag/ existem logs de Workers além dos logs do agente. Basicamente, um arquivo de log por build, mas com um detalhe muito útil: o nome do arquivo já contém o horário de início do build, em UTC.
Listei os arquivos e identifiquei esses dois na hora. Os segundos não batem exatamente, mas os minutos batem com 11:49 e 11:50 dos logs do agente, só podiam ser esses.
Rodei o comando para ver o conteúdo e percebi que era denso demais para ler direto. Copiei o log e perguntei para a IA: "o que aconteceu com esse build? Foi deletado, cancelado, crashou? E quem fez isso?" A IA retornou:
Agora eu sabia que os dois builds foram cancelados. Não foi crash, não foi a VM. Receberam um sinal externo limpo. Mas de quem?
O culpado
Ou seja, tínhamos builds cancelados e que sumiram da interface do Azure DevOps. Mas o rastro sempre fica na infraestrutura. Fui atrás e descobri que o Azure DevOps tem uma API REST que expõe justamente esses dados fantasmas. Abri a documentação, levantei os parâmetros necessários e usei a IA para me ajudar a montar a URL rapidão. Como eu já estava logado no portal, a chamada retornou um JSON gigantesco com os detalhes da execução. Joguei tudo num formatador online, dei um Ctrl+F pelo ID do build e a resposta estava lá:
Pois é. O culpado não era o dev novato. Não era a máquina. Não era ninguém do time. Foi o pr.autoCancel em ação... fazendo exatamente o que foi configurado pra fazer. O Azure cumpriu o contrato. O problema real é que a pipeline não estava preparada pra lidar com essa interrupção, e deixou rastro em disco.
Esse recurso cancela automaticamente qualquer build em andamento toda vez que a PR é atualizada. Então fui até a PR no Bitbucket para ver se a linha de pensamento estava certa, e quando cheguei lá vi o seguinte processo:
Feito, problema 100% rastreado.
O que aprendemos com tudo isso? O que aprendemos com tudo isso? A pipeline precisa ser resiliente o suficiente pra sobreviver a cancelamentos. xD
O que aconteceu aqui foi que a PR passou por varias etapas uma em seguida da outra. Ela foi aberta, passou para draft, mudou o título. O dev não percebeu que cada clique inofensivo dele na tela disparava um webhook que mandava o Azure DevOps executar o pr.autoCancel lá no servidor. É claro que a pipeline tem uma fragilidade por não saber limpar a sujeira que o npm deixou para trás. ..
PR é lugar de iterar. Abrir, ajustar o título, passar pra draft, receber review, commitar de novo... isso é o fluxo normal e esperado. O dev não fez nada errado ao atualizar a PR. O que falhou foi a pipeline assumir que o ambiente sempre estaria limpo, sem verificar o estado antes de instalar. É claro que entender que cada clique na interface pode disparar um webhook que impacta um processo rodando em disco lá na VM é um aprendizado valioso. Mas a lição principal aqui é sistêmica: job que não consegue sobreviver a uma interrupção vai quebrar cedo ou tarde.
Ficou em aberto
Por que o npm não faz cleanup ao receber o sinal de cancelamento? Dependendo de como o sinal é entregue e em que ponto exato do rename o processo estava, o cleanup pode não acontecer. Não investiguei isso a fundo. Mas é interessante...
O pr.autoCancel tem configuração granular? Não verifiquei se é possível desativar por pipeline específica ou se é uma configuração global.
Dá para tornar o step idempotente? Adicionar uma limpeza antes do npm install -g eliminaria o problema independentemente de qualquer cancelamento futuro:
Não implementei ainda. Não sei se gera efeito colateral em builds paralelos no mesmo agente.
Referências
Azure DevOps REST API - Builds - usado para consultar builds deletados e cancelados que não aparecem na UI
Azure Pipelines - PR Triggers e autoCancel - documentação do comportamento de auto-cancelamento em PRs
npm CLI - npm install - comportamento de instalação global e uso de diretórios temporários
Vinicius Aguilar — DevOps Engineer

