<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[aguillarOps | Blog]]></title><description><![CDATA[Espaço onde documento como investigo e resolvo problemas reais em DevOps.]]></description><link>https://aguillarops.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69b4e7f9210c74252fb569ae/e7d91144-8023-44bc-95ac-6e707458779f.png</url><title>aguillarOps | Blog</title><link>https://aguillarops.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 11 May 2026 18:00:03 GMT</lastBuildDate><atom:link href="https://aguillarops.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Por que o Slot de QA não é mais o lugar certo para o Rollback]]></title><description><![CDATA[Chegou mais uma segunda-feira, dia de entrega, e com ela, a rotina de deploy. Nosso processo padrão envolve subir a nova versão em QA e, em seguida, fazer o swap para produção. Simples, certo? Não exa]]></description><link>https://aguillarops.dev/rollback-seguro</link><guid isPermaLink="true">https://aguillarops.dev/rollback-seguro</guid><category><![CDATA[Devops]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure-app-services]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[deployment]]></category><dc:creator><![CDATA[Vinicius Aguilar]]></dc:creator><pubDate>Mon, 23 Mar 2026 12:00:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/c2d71249-0a7c-4134-b7b1-c54974cf0056.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Chegou mais uma segunda-feira, dia de entrega, e com ela, a rotina de deploy. Nosso processo padrão envolve subir a nova versão em QA e, em seguida, fazer o swap para produção. Simples, certo? Não exatamente. Acontece que o ambiente de QA, que deveria ser um espelho da produção para validação final, começou a ser usado para testes diversos, por diferentes equipes. Isso significa que, no momento crucial do swap, a versão que esperávamos encontrar em QA para garantir um rollback seguro, muitas vezes, já não era a versão estável que havíamos validado. Uma dor de cabeça que precisava ser resolvida antes que se tornasse um problema maior.</p>
<hr />
<h2>O problema</h2>
<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">A dor era clara: a falta de um ambiente imutável para rollback.</mark> Nosso processo de deploy, que dependia do ambiente de QA para um swap seguro, estava vulnerável. Se algo desse errado em produção, a versão em QA poderia não ser a versão estável anterior, impossibilitando um rollback rápido e seguro. Precisávamos de um "porto seguro", um slot de deploy que guardasse a versão estável da produção, intocável, para qualquer eventualidade.</p>
<hr />
<h2>A investigação</h2>
<p>Minha primeira dedução foi que precisávamos de um novo slot de implantação, dedicado exclusivamente para ser um fallback. Sem rota configurada, sem uso para testes, apenas um guardião da versão estável. A jornada começou na Azure, nosso provedor de serviços em nuvem. Naveguei até o App Service, onde nossas aplicações residem, e então para os slots de implantação da aplicação em questão. Tudo via interface gráfica, um legado que ainda estamos trabalhando para modernizar com IaC, mas que, por enquanto, é o caminho.</p>
<p>Criei o slot e o batizei de fallback. A ideia era que ele serviria apenas para subir a versão nova e fazer o swap para produção. Em caso de necessidade de rollback, ele estaria lá, imutável, com a versão que estava em produção antes do deploy. Parecia um plano sólido.</p>
<p>Com o slot criado, o próximo passo era ajustar o Azure DevOps. Nossas configurações de swap ainda apontavam para QA, e isso precisava mudar. Fui em Releases, selecionei a release que estávamos trabalhando (ainda em modo clássico, outro ponto para futuras melhorias com YAML) e cliquei em editar. Para agilizar, clonei o stage de deploy para QA, mas o deixei sem trigger, para ser executado apenas manualmente. O ponto crucial aqui foi trocar o Slot de QA para fallback.</p>
<p>Em seguida, na task que realiza o swap para produção, alterei o Source Slot de QA para fallback. Confirmei que o checkbox Swap with Production estava ativo. <mark class="bg-yellow-200 dark:bg-yellow-500/30">Nesse momento, senti que estávamos no caminho certo.</mark> Salvei as alterações e o trabalho, por enquanto, estava feito. Agora era esperar a próxima entrega para validar se tudo funcionaria como o esperado.</p>
<p>Chegou a segunda-feira, dia da entrega. 8:40 da manhã, o deploy agendado para as 09:00. Dei uma última lida na documentação que havíamos feito, para me certificar de que não havia esquecido de nada. O plano era claro: verificar a versão em produção, subir a nova versão no slot fallback, fazer o swap para produção e, por fim, verificar se a versão havia sido trocada. Acontece kkk, que a teoria é uma coisa, a prática é outra.</p>
<p>O Microsoft Azure App Service, no processo de swap, troca o código (artefato) e algumas configurações (dependendo do que está marcado como "slot setting"). A lógica é:</p>
<aside><div style="font-family:monospace;font-size:13px;border:1px solid #333;border-radius:8px;padding:16px 20px;line-height:2.2">Produção → versão A
Fallback → versão B
Swap (Deploy) → Produção fica com B, fallback fica com A
Swap novamente (Rollback) → Produção volta para A, fallback volta para B
<span style="color:#ff7043">Tecnicamente, isso funciona perfeitamente.</span></div></aside>

<p>Com o plano em mãos, executei os passos. Subi o projeto no slot fallback e, em seguida, realizei o swap.</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#6a9955"># Build anterior em produção</span>
<span style="color:#c9d1d9">956270768</span>
<span style="color:#6a9955"># Novo build em produção após o swap</span>
<span style="color:#febc2e">961267768</span></div></div></aside>

<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">Ufa, deu certo!</mark> A versão foi atualizada com sucesso e o slot fallback agora continha a versão anterior, pronta para um rollback caso necessário. A tensão diminuiu, e a sensação de ter resolvido um problema crítico foi gratificante.</p>
<hr />
<h2>Ficou em aberto</h2>
<p>Embora a solução para o problema do slot de fallback tenha sido implementada com sucesso, ainda há um caminho a percorrer. A dependência da interface gráfica da Azure e o uso de releases em modo clássico no Azure DevOps são pontos que precisam ser endereçados. A transição para IaC (Infrastructure as Code) e a conversão das releases para arquivos YAML versionados são os próximos passos para modernizar nossa infraestrutura e processos, garantindo maior automação e controle.</p>
<hr />
<h2>Referências</h2>
<ul>
<li><p><a href="https://learn.microsoft.com/pt-br/azure/app-service/deploy-staging-slots">Azure App Service - Slots de Implantação</a> - documentação oficial sobre como configurar e realizar o swap entre slots de produção e staging</p>
</li>
<li><p><a href="https://learn.microsoft.com/pt-br/azure/devops/pipelines/release/?view=azure-devops">Azure DevOps - Release Pipelines (Clássico)</a> - guia sobre a criação e edição de pipelines de release no modo clássico</p>
</li>
<li><p><a href="https://learn.microsoft.com/pt-br/azure/app-service/deploy-staging-slots#which-settings-are-swapped">Azure App Service - Configurações de Slot</a> - detalhes técnicos sobre quais configurações de artefatos e variáveis são trocadas durante o swap</p>
</li>
</ul>
<hr />
<p><em>Vinicius Aguilar — DevOps Engineer</em></p>
]]></content:encoded></item><item><title><![CDATA[DirectoryNotFoundException: quando o teste do dev conhece só a máquina dele]]></title><description><![CDATA[A pipeline falhou na task de testes. O erro apontava para um diretório de templates de email que não existia no agente. O problema não estava na pipeline. Estava no código. E sem saber ler C#, eu nunc]]></description><link>https://aguillarops.dev/directorynotfoundexception-funciona-na-minha-maquina</link><guid isPermaLink="true">https://aguillarops.dev/directorynotfoundexception-funciona-na-minha-maquina</guid><category><![CDATA[Devops]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[dotnet]]></category><category><![CDATA[Pipeline]]></category><dc:creator><![CDATA[Vinicius Aguilar]]></dc:creator><pubDate>Sat, 21 Mar 2026 02:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/33ed40b8-1153-48f4-ade0-b05d615992b3.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<aside><div style="border-left:3px solid #00bcd4;padding:16px 20px;border-radius:0 8px 8px 0;font-size:15px;font-style:italic;line-height:1.7">A pipeline falhou na task de testes. O erro apontava para um diretório de templates de email que não existia no agente. O problema não estava na pipeline. Estava no código. E sem saber ler C#, eu nunca teria chegado até lá.</div></aside>

<p>Me avisaram que a pipeline de um projeto tinha quebrado. Abri e fui direto na task que havia falhado e a mensagem era clara:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#ff5f57">System.IO.DirectoryNotFoundException : Email templates directory was not found: 'C:\agent\_work\8\sharedfiles\email-templates'.</span></div></div></aside>

<p>Antes de qualquer coisa, avisei no grupo do time que a pipeline estava com problema e que eu estava verificando.</p>
<hr />
<h2>O problema</h2>
<p>Bom, a mensagem de erro dizia que o agente não encontrou o diretório de templates de email. Bem tranquilo de entender, mas o contexto ajudou muito. No dia anterior, o dev tinha mandado no grupo:</p>
<blockquote>
<p>Galera, esse projeto aqui fica os templates de email. Se alterar o arquivo por outro projeto, os commits serão bloqueados. Quando precisar alterar, altere no repositório real e depois atualize nos outros projetos.</p>
</blockquote>
<p>Com isso, eu já tinha uma hipótese antes de abrir os arquivos: algum teste novo passou a depender desses templates na hora de rodar.</p>
<hr />
<h2>A investigação</h2>
<h3>Entendendo o template de pipeline</h3>
<p>A task de testes vinha de um template centralizado que eu mesmo montei:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre">- template: pipelines/templates/steps/dotnet-test-report.yml@platform
  parameters:
    configuration: '$(build.configuration)'
    framework: '$(target.framework)'
    ...</div></div></aside>

<p>Fui até o repositório central, para relembrar o que o template faz. Ele descobre automaticamente os projetos de teste procurando por arquivos <code>.csproj</code>, e para cada um monta e executa o <code>dotnet test</code>. Se qualquer projeto crítico falha, a pipeline inteira cai com <code>exit 1</code>.</p>
<p>Nada suspeito aqui. Mas foi importante para ter contexto. Quanto mais contexto eu absorvo, mais rápido consigo fechar o diagnóstico.</p>
<h3>O que mudou na PR</h3>
<p>Fui olhar a PR que havia sido mergeada antes da falha. Tinha vários testes novos. Peguei qualquer um e encontrei:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre">var html = AppDataHelper.RenderEmailTemplate(
    EmailTemplatePreviewSettings.TemplatesRootPath,
    templateName,
    model);</div></div></aside>

<p>Fui até onde <code>TemplatesRootPath</code> era definido:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre">internal static string TemplatesRootPath =&gt;
    Path.GetFullPath(Path.Combine(
        AppContext.BaseDirectory,
        "..",
        "..",
        "..",
        "..",
        "..",
        "sharedfiles",
        "email-templates"));</div></div></aside>

<p><mark class="bg-yellow-200 dark:bg-yellow-500/30">Olha que interessante, cinco </mark> <code>..</code> <mark class="bg-yellow-200 dark:bg-yellow-500/30">navegando para fora do diretório. O dev provavelmente construiu o caminho com base na estrutura de pastas da máquina dele.</mark></p>
<p>Na máquina dele, subindo cinco níveis a partir do <code>BaseDirectory</code>, chegava no repositório oficial dos templates. No agente de CI, esse caminho não existe. Daí o <code>DirectoryNotFoundException</code>. Isso bateu com a mensagem do dia anterior no grupo. O código não estava tentando chegar na pasta réplica usada no agente. Estava tentando alcançar o repositório oficial dos templates, do jeito que existe na estrutura local do dev.</p>
<p>Não era problema de pipeline. Era problema de código.</p>
<h3>A solução: variável de ambiente com fallback</h3>
<p>O dev é tech lead e tem uma pipeline parada. Não era hora de questionar a arquitetura da solução local dele. A saída mais rápida e menos invasiva: <mark class="bg-yellow-200 dark:bg-yellow-500/30">fazer o código consultar uma variável de ambiente antes de tentar o caminho relativo</mark>, e configurar essa variável no grupo de variáveis compartilhado já existente, apontando para a pasta réplica correta no agente.</p>
<p>Primeiro, o ajuste no <code>TemplatesRootPath</code>:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre">internal static string TemplatesRootPath =&gt;
    Environment.GetEnvironmentVariable("EMAIL_TEMPLATES_PATH")?.Trim()
    ?? Path.GetFullPath(Path.Combine(
        AppContext.BaseDirectory,
        "..",
        "..",
        "..",
        "..",
        "..",
        "sharedfiles",
        "email-templates"));</div></div></aside>

<p>Depois, no grupo de variáveis, adicionei <code>EMAIL_TEMPLATES_PATH</code> apontando para <code>AppData/SharedFiles</code>. E no template da pipeline, passei a variável para o step de testes:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre">- template: pipelines/templates/steps/dotnet-test-report.yml@platform
  parameters:
    configuration: '$(build.configuration)'
    framework: '$(target.framework)'
    ...
    environmentVariables:
      EMAIL_TEMPLATES_PATH: '$(email.templates.path)'
      ...</div></div></aside>

<p>Rodei. Os erros de <code>DirectoryNotFoundException</code> sumiram. Mas a pipeline ainda falhou, agora com 6 testes quebrando onde antes eram 25.</p>
<h3>Lendo os logs do xUnit sem enlouquecer</h3>
<p>Uma dica que vale guardar: o xUnit roda testes em paralelo, então o output fica entrelaçado. O padrão é mais ou menos assim:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#888">[ANÚNCIO] TestA falhou   ← só avisa</span>
<span style="color:#888">[ANÚNCIO] TestB falhou   ← só avisa</span>
[DETALHES] Failed TestA  ← explica o TestA
<span style="color:#888">[ANÚNCIO] TestC falhou   ← chegou enquanto explicava o A</span>
[DETALHES] Failed TestB  ← explica o TestB
[DETALHES] Failed TestC  ← explica o TestC</div></div></aside>

<p>É muito fácil se confundir. A dica é dar um <strong>Ctrl+F</strong> e procurar por <code>Failed</code>. Ignore os blocos <code>[xUnit.net]</code>, principalmente os que aparecem no meio das explicações: eles são só avisos que interrompem o output de outro teste. Não sei por que é assim <strong>xD</strong>, é muito ruim. Mas sabendo disso, fica gerenciável.</p>
<p>E sobre o stack trace: leia <strong>de baixo para cima</strong>. Ignore tudo que começa com <code>System.</code> ou <code>Microsoft.</code>, isso é infraestrutura do .NET que você não controla. Foca na primeira linha que aponta para um arquivo do seu projeto.</p>
<p>Quando o valor não nasce no arquivo que eu estou olhando, eu já sei que ele veio de fora. E aí o stack trace vira mapa: eu subo uma chamada por vez até encontrar a origem.</p>
<h3>Os 6 erros restantes: todos a mesma causa</h3>
<p><strong>Erros 1, 2 e 3</strong> (<code>AppDataUnitTest.cs</code>)</p>
<p>Os três apontavam para variações do mesmo padrão: um método <code>GetSharedFilesPath</code> dentro dos testes usando os mesmos cinco <code>..</code> para montar o caminho. O dev copiou a lógica do <code>TemplatesRootPath</code> que já havíamos corrigido. A correção foi idêntica: ler de <code>EMAIL_TEMPLATES_PATH</code> com fallback para o caminho relativo.</p>
<aside>
  <div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px">
    <div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center">
      <span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span>
      <span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span>
      <span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span>
    </div>
    <div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#6a9955"># antes: cinco .. navegando para a estrutura local do dev</span>
private static string GetSharedFilesPath(string relativePath)
{
    return Path.GetFullPath(Path.Combine(
        AppContext.BaseDirectory,
        "..", "..", "..", "..", "..",
        "sharedfiles", "email-templates",
        relativePath.Replace('/', Path.DirectorySeparatorChar)));
}
<span style="color:#6a9955"># depois: variável de ambiente com fallback</span>
private static string GetSharedFilesPath(string relativePath)
{
    var templatesRoot = Environment.GetEnvironmentVariable("EMAIL_TEMPLATES_PATH")?.Trim()
        ?? Path.GetFullPath(Path.Combine(
            AppContext.BaseDirectory,
            "..", "..", "..", "..", "..",
            "sharedfiles", "email-templates"));
    return Path.Combine(templatesRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
}</div>
  </div>
</aside>

<p><strong>Erros 4, 5 e 6</strong> (<code>RenderPDFUnitTest.cs</code> --&gt; <code>invoice.cshtml</code>, <code>invoice-email.cshtml</code>, <code>invoice-usd.cshtml</code>)</p>
<p>Stack trace idêntico para os três, só mudava o arquivo de template. A rota era:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#888">RenderPDFUnitTest.cs:line 75    ← 1. o teste chamou RenderPDF</span>
<span style="color:#888">InvoiceClosed.cs:line 132       ← 2. que chamou GetRazorEngineCompiledTemplate</span>
<span style="color:#888">AppData.cs:line 97              ← 3. que chamou GetFilePath</span>
<span style="color:#ff5f57">AppData.cs:line 68              ← 4. GetFilePath não achou o arquivo → estouro</span></div></div></aside>

<p>O teste passava um caminho montado com <code>GetSharedFilesPath</code> usando os mesmos cinco <code>..</code>. O <code>GetRazorEngineCompiledTemplate</code> tentava o caminho direto, não achava, caía no <code>GetFilePath</code>, que também não achava, e estourava. Mesma raiz, mesma correção: aplicar o padrão de variável de ambiente com fallback no <code>GetSharedFilesPath</code> do <code>RenderPDFUnitTest</code>.</p>
<p>No total: <strong>três arquivos de teste</strong>, <strong>mesma lógica copiada</strong>, <strong>mesma correção aplicada</strong> em cada um.</p>
<aside>
  <div style="border:1px solid #00bcd4;border-radius:8px;padding:14px 18px;margin:1rem 0">
    <div style="margin:0 0 8px 0;color:#00bcd4 !important;font-weight:700">INSIGHT</div>
    <div style="margin:0">
      Esse caso é um bom exemplo de por que DevOps precisa saber ler código. A pipeline foi só o lugar onde o problema apareceu. A causa estava nos testes, e sem conseguir rastrear o stack trace e entender o que cada método fazia, eu teria ficado horas tentando resolver isso na camada de configuração, sem nunca chegar na raiz.
    </div>
  </div>
</aside>

<p>Subi as correções, rodei a pipeline. Build finalizado com sucesso. Abri a PR, avisei o dev, ela foi mergeada. Rodei a branch <code>develop</code> para validar uma última vez.</p>
<p><strong>O real problema estava no código do teste. A pipeline foi só o mensageiro.</strong></p>
<hr />
<h2>Ficou em aberto</h2>
<p><strong>A lógica dos cinco</strong> <code>..</code> <strong>está correta na máquina do dev?</strong> Não investiguei se a estrutura de diretórios local realmente funciona como o código assume. Pode ser que funcione, pode ser que o dev nunca tenha rodado esses testes localmente de verdade.</p>
<p><strong>Existe uma abordagem melhor para referenciar arquivos compartilhados em testes?</strong> A variável de ambiente com fallback resolve o problema de pipeline, mas o padrão de navegar para fora do projeto para achar um arquivo irmão em repositório diferente tem cheiro de problema esperando para acontecer de novo. Uma solução mais robusta provavelmente envolveria copiar os templates como parte do build de teste, ou usar um fixture de teste que isola esse caminho. Não avaliei isso a fundo.</p>
<p><strong>Quantos outros lugares no projeto usam a mesma lógica?</strong> Corrigi três arquivos. Pode haver mais. Não fiz uma busca global por <code>"..", "..", "..", "..", ".."</code> no repositório.</p>
<hr />
<h2>Referências</h2>
<ul>
<li><p><a href="https://xunit.net/docs/running-tests-in-parallel">xUnit - Parallelism in MSBuild</a> - documentação sobre execução paralela e por que o output fica entrelaçado</p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups?view=azure-devops">Azure DevOps - Variable Groups</a> - como configurar variáveis compartilhadas entre pipelines</p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/template-parameters?view=azure-devops">Azure Pipelines - Template parameters</a> - como passar variáveis de ambiente via parâmetros de template</p>
</li>
</ul>
<hr />
<p><em>Vinicius Aguilar — DevOps Engineer</em></p>
]]></content:encoded></item><item><title><![CDATA[Como editar o título de uma PR derrubou todos os builds do dia]]></title><description><![CDATA[Pipeline parada. Dev sem conseguir buildar. E um erro apontando para um diretório que não deveria existir. O que parecia ser um problema na VM era o rastro de três ações em uma PR que ninguém tinha pe]]></description><link>https://aguillarops.dev/enotempty-npm-pipeline-pr-autocancelamento</link><guid isPermaLink="true">https://aguillarops.dev/enotempty-npm-pipeline-pr-autocancelamento</guid><category><![CDATA[Devops]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[npm]]></category><category><![CDATA[Pipeline]]></category><dc:creator><![CDATA[Vinicius Aguilar]]></dc:creator><pubDate>Fri, 20 Mar 2026 02:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/9935f374-1ac9-42b1-ae24-36803fc169c6.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<aside><div style="border-left:3px solid #00bcd4;padding:16px 20px;border-radius:0 8px 8px 0;font-size:15px;font-style:italic;line-height:1.7">Pipeline parada. Dev sem conseguir buildar. E um erro apontando para um diretório que não deveria existir. O que parecia ser um problema na VM era o rastro de três ações em uma PR que ninguém tinha percebido.</div></aside>

<p>A pipeline de build de uma aplicação Angular parou de funcionar. O famoso <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">"DO NADA"</mark></strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/71e574ef-01b3-4bd1-86e9-388313592160.gif" alt="" style="display:block;margin:0 auto" />

<p>Um dev me pingou: <em>"tá dando um erro estranho na pipe, consegue dar uma olhada?"</em> 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.</p>
<hr />
<h2>O problema</h2>
<p>Acessei o build e de primeira já verifiquei que todos falhavam no mesmo ponto: a etapa de instalação do <strong>Angular CLI</strong>. Isso já me deu o palpite de que podia ser um problema na VM, pois essa pipeline usa um agente self-hosted.</p>
<hr />
<h2>A investigação</h2>
<p>Abri a task de instalação do Angular CLI no Azure DevOps. A primeira coisa que saltou foi <code>##[error]Error: Npm failed with return code: 217</code>:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#888">2000-00-00T13:00:55Z ##[section]Starting: Install Angular CLI</span>
<span style="color:#888">2000-00-00T13:00:55Z Task         : npm</span>
<span style="color:#888">2000-00-00T13:00:55Z Version      : X.XXX.1 | Author: Microsoft Corporation</span>
<span style="color:#888">2000-00-00T13:00:57Z [command] npm install -g @angular/cli@XX.X.1</span>
<span style="color:#ff5f57">2000-00-00T13:01:15Z npm error code ENOTEMPTY</span>
<span style="color:#ff5f57">2000-00-00T13:01:15Z npm error syscall rename</span>
<span style="color:#ff5f57">2000-00-00T13:01:15Z npm error path   .../node_modules/@angular/cli</span>
<span style="color:#ff5f57">2000-00-00T13:01:15Z npm error dest   .../node_modules/@angular/.cli-Xuqet0PR</span>
<span style="color:#ff5f57">2000-00-00T13:01:15Z npm error errno  -39</span>
<span style="color:#febc2e">2000-00-00T13:01:15Z npm error ENOTEMPTY: directory not empty, rename '.../cli' -&gt; '.../cli-Xuqet0PR'</span>
<span style="color:#ff5f57">2000-00-00T13:01:16Z ##[error]Error: Npm failed with return code: 217</span>
<span style="color:#888">2000-00-00T13:01:16Z ##[section]Finishing: Install Angular CLI</span></div></div></aside>

<p>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 <em>antes</em> do 217 e encontrei a mensagem: <code>directory not empty, rename '.../cli' -&gt; '.../cli-Xuqet0PR'</code></p>
<p>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 (<code>cli</code>) para um temporário (<code>.cli-Xuqet0PR</code>). Só que esse temporário já existia com conteúdo. O rename falhou, o processo morreu, o build foi junto.</p>
<p><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">A grande pergunta não era o que quebrou. Era por que isso estava ali.</mark></strong></p>
<p>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 <strong>o último lote de builds com sucesso</strong> e identifiquei o momento exato em que as falhas começaram. Abri o build correspondente, acessei a PR em andamento e encontrei <strong>uma alteração no YAML da pipeline: o dev tinha removido a etapa de instalação do Angular.</strong></p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#6a9955"># trecho removido da pipeline pelo dev</span>
- task: Npm@1
  inputs:
    command: 'custom'
    customCommand: 'install -g @angular/cli@XX.X.1'
  displayName: 'Install Angular CLI'</div></div></aside>

<p><em><strong>Por que esse dev precisou retirar isso?</strong></em> Esse tipo de mudança na pipeline normalmente é um sinal de que o problema foi interpretado sem contexto suficiente.</p>
<h3>Antes de investigar mais a fundo, preciso liberar o fluxo</h3>
<p>Pipeline parada é prioridade máxima. Como DevOps, minha função é acelerar o ciclo de entrega do time, e build travado é o oposto disso.</p>
<p>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.</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#28c840">$</span> ssh -i [chave-de-acesso] [usuario]@[ip-privado]
<span style="color:#28c840">$</span> cd /home/vm-admin/myagent/_work/_tool/node/XX.XX.X/x64/lib/node_modules/@angular/
<span style="color:#28c840">$</span> ls -a
<span style="color:#febc2e">.  ..  .cli-Xuqet0PR  cli</span></div></div></aside>

<p>Tava lá. O diretório temporário abandonado, exatamente onde o erro dizia.</p>
<p>Antes de rodar qualquer comando, fui pesquisar o que era esse <code>.cli-Xuqet0PR</code>. 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:</p>
<blockquote>
<p>O diretório temporário que o npm utiliza durante uma instalação global <strong>s</strong>erve como um espaço de trabalho intermediário para processar o pacote antes de movê-lo para o local definitivo no sistema.</p>
</blockquote>
<p>Ótimo. O fluxo normal do npm é criar o temporário <strong>(.cli-xxxxx)</strong>, processar o pacote nele e mover tudo para o diretório definitivo <strong>(cli)</strong>. <strong>Se o processo é interrompido no meio, o temporário fica lá.</strong> Na próxima execução, quando o npm tenta criar de novo, encontra um diretório não vazio e explode com <code>ENOTEMPTY</code>.</p>
<p>Agora que entendi o processo e sei qual é o problema, a solução é deletar as duas pastas, a <code>cli</code> e a temporária.</p>
<aside>
  <div style="border:1px solid #ff7043;border-radius:8px;padding:14px 18px;margin:1rem 0">
    <div style="margin:0 0 8px 0;color:#ff7043 !important;font-weight:700">ATENÇÃO</div>
    <p>
      O comando <code>rm -rf</code> deleta recursivamente e sem confirmação. Confirme o caminho antes de executar. Não tem como desfazer.
    </p>
  </div>
</aside>

<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#6a9955"># removendo o diretório definitivo corrompido e o temporário abandonado</span>
<span style="color:#28c840">$</span> rm -rf .../node_modules/@angular/cli
<span style="color:#28c840">$</span> rm -rf .../node_modules/@angular/.cli-Xuqet0PR</div></div></aside>

<p>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 <code>cli</code> 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.</p>
<h3>Quem interrompeu o processo?</h3>
<p>Um princípio que carrego pra sempre na minha carreira é que <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">nada acontece do nada.</mark></strong></p>
<p>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. <strong>O que provavelmente aconteceu?</strong> 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.</p>
<p>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. <strong>Mas quem pode ter interrompido a execução, se não tem build anterior ao que falhou?</strong></p>
<p>Fui ver nos logs do agente na VM. Aqui aprendi algo que eu não sabia na prática: <strong>a VM está em UTC e eu estou em UTC-3</strong>. O build que eu via no Azure DevOps como falho às <strong>08:53</strong> correspondia a <strong>11:53</strong> na VM. <em><strong>Isso importa na hora de correlacionar logs.</strong></em></p>
<p>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 <code>Running job</code>:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#28c840">$</span> cat /home/vm-admin/myagent/_diag/Agent_*.log | grep "Running job"
<span style="color:#888">[2000-00-00 01:00:39Z] Running job: Build Frontend</span>
<span style="color:#888">[2000-00-00 01:03:25Z] Running job: Build Frontend</span>
<span style="color:#888">[2000-00-00 01:05:58Z] Running job: Build Frontend</span>
<span style="color:#888">[2000-00-00 02:00:20Z] Running job: Job</span>
<span style="color:#888">[2000-00-00 02:10:37Z] Running job: Job</span>
<span style="color:#888">[2000-00-00 02:20:45Z] Running job: Job</span>
<span style="color:#febc2e">[2000-00-00 11:49:37Z] Running job: Job</span>
<span style="color:#febc2e">[2000-00-00 11:50:35Z] Running job: Job</span>
[2000-00-00 11:53:12Z] Running job: Job   &lt;-- Job que falhou</div></div></aside>

<p>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.</p>
<p>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. <strong>Por que eles rodaram antes da primeira pipeline do dia?</strong> 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.</p>
<p>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.</p>
<p>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.</p>
<p>Foi só nesse momento que a minha ficha caiu e eu olhei para o número do build que havia falhado: <code>#20000000.3</code>. <strong>Estava na minha cara o tempo todo. O sufixo já indicava que era a terceira execução do dia.</strong> 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.</p>
<h3>Lendo os logs dos Workers</h3>
<p>Na pasta <code>_diag/</code> existem logs de <strong>Workers</strong> além dos logs do agente. Basicamente, um arquivo de log por build, mas com um detalhe muito útil: <em><strong>o nome do arquivo já contém o horário de início do build, em UTC.</strong></em></p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#28c840">$</span> ls /home/vm-admin/myagent/_diag/
<span style="color:#febc2e">Worker_20000000-114938-utc.log</span>
<span style="color:#febc2e">Worker_20000000-115038-utc.log</span>
<span style="color:#888">...</span></div></div></aside>

<p>Listei os arquivos e identifiquei esses dois na hora. Os segundos não batem exatamente, mas os minutos batem com <strong>11:49</strong> e <strong>11:50 dos logs do agente</strong>, só podiam ser esses.</p>
<p>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: <em>"o que aconteceu com esse build? Foi deletado, cancelado, crashou? E quem fez isso?"</em> <em><strong>A IA retornou:</strong></em></p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#888">Pipeline:  [empresa] - Dashboard (Staging)</span>
<span style="color:#888">Build:     20000000.1 | Motivo: PullRequest</span>
<span style="color:#888">Branch:    [empresa]-4873 -&gt; develop</span>
<span style="color:#ff5f57">Resultado: Canceled</span>
<span style="color:#888">Momento:   durante o passo Cache npm</span>
<span style="color:#888">Consequência: todos os passos seguintes foram skipped</span>
<br />
<span style="color:#febc2e">Nesse log não aparece quem cancelou.</span>
<span style="color:#888">O log mostra apenas que o agente recebeu um sinal de cancelamento.</span></div></div></aside>

<p>Agora eu sabia que os dois builds foram <strong>cancelados</strong>. Não foi crash, não foi a VM. Receberam um sinal externo limpo. <strong>Mas de quem?</strong></p>
<h3>O culpado</h3>
<p>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 <strong>API REST</strong> 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 <strong>JSON</strong> 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á:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#febc2e">"deletedBy": "Microsoft.VisualStudio.Services.TFS"</span>
<span style="color:#888">"deletedReason": "The build was manually deleted."</span>
<span style="color:#ff5f57">"pr.autoCancel": "true"</span></div></div></aside>

<p><strong>Pois é. O culpado não era o dev novato. Não era a máquina. Não era ninguém do time.</strong> Foi o <code>pr.autoCancel</code> 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.</p>
<p>Esse recurso <strong>cancela automaticamente</strong> <strong>qualquer build em andamento toda vez que a PR é atualizada.</strong> 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:</p>
<aside>
  <div style="font-family:monospace;font-size:14px;border:1px solid #333;border-radius:8px;padding:16px 20px;line-height:1.9">
    <div><strong>01.</strong> Dev abriu a PR → build <code>20000000.1</code> inicia</div>
    <div><strong>02.</strong> Dev marcou a PR como <code>DRAFT</code> → auto-cancel derruba <code>build 1</code> → build <code>20000000.2</code> inicia</div>
    <div><strong>03.</strong> Dev editou o título da PR → auto-cancel derruba <code>build 2</code> durante o <code>npm install</code></div>
    <div style="padding-left:2.2em">→ <code>.cli-Xuqet0PR</code> fica incompleto na VM</div>
    <div style="padding-left:2.2em">→ build <code>20000000.3</code> inicia</div>
    <div style="padding-left:2.2em;color:#ff7043">→ <code>ENOTEMPTY</code> → todos os builds seguintes quebram no mesmo ponto</div>
  </div>
</aside>

<aside>
  <div style="border:1px solid #00bcd4;border-radius:8px;padding:14px 18px;margin:1rem 0">
    <div style="margin:0 0 8px 0;color:#00bcd4 !important;font-weight:700">INSIGHT</div>
    <div style="margin:0">
      <mark>Editar o título de uma PR também dispara um novo build.</mark>
      Qualquer atualização na PR enquanto um build está em andamento aciona o <code>pr.autoCancel</code>, que derruba o build anterior antes de iniciar o novo. Se o cancelamento ocorre no meio de um processo com estado em disco, como o <code>npm install -g</code>, o rastro fica na VM e quebra todos os builds seguintes até alguém limpar manualmente.
    </div>
  </div>
</aside>

<p>Feito, problema 100% rastreado.<br />O que aprendemos com tudo isso? O que aprendemos com tudo isso? <strong>A pipeline precisa ser resiliente o suficiente pra sobreviver a cancelamentos. xD</strong></p>
<p>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 <code>pr.autoCancel</code> lá no servidor. É claro que a pipeline tem uma fragilidade por não saber limpar a sujeira que o npm deixou para trás. ..  </p>
<p>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: <strong>job que não consegue sobreviver a uma interrupção vai quebrar cedo ou tarde.</strong></p>
<hr />
<h2>Ficou em aberto</h2>
<p><strong>Por que o npm não faz cleanup ao receber o sinal de cancelamento?</strong> 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...</p>
<p><strong>O</strong> <code>pr.autoCancel</code> <strong>tem configuração granular?</strong> Não verifiquei se é possível desativar por pipeline específica ou se é uma configuração global.</p>
<p><strong>Dá para tornar o step idempotente?</strong> Adicionar uma limpeza antes do <code>npm install -g</code> eliminaria o problema independentemente de qualquer cancelamento futuro:</p>
<aside><div style="background:#1e1e1e;border-radius:10px;overflow:hidden;font-family:monospace;font-size:12.5px"><div style="background:#2d2d2d;padding:10px 16px;display:flex;gap:8px;align-items:center"><span style="width:12px;height:12px;border-radius:50%;background:#ff5f57;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#febc2e;display:inline-block;flex-shrink:0"></span><span style="width:12px;height:12px;border-radius:50%;background:#28c840;display:inline-block;flex-shrink:0"></span></div><div style="padding:16px 20px;color:#c9d1d9;line-height:1.9;overflow-x:auto;white-space:pre"><span style="color:#6a9955"># step de limpeza antes do npm install -g</span>
- script: |
    rm -rf $(node_modules_path)/@angular/cli
    rm -rf $(node_modules_path)/@angular/.cli-*
  displayName: 'Cleanup Angular CLI antes de instalar'</div></div></aside>

<p>Não implementei ainda. Não sei se gera efeito colateral em builds paralelos no mesmo agente.</p>
<hr />
<h2>Referências</h2>
<ul>
<li><p><a href="https://learn.microsoft.com/en-us/rest/api/azure/devops/build/builds?view=azure-devops-rest-7.1">Azure DevOps REST API - Builds</a> - usado para consultar builds deletados e cancelados que não aparecem na UI</p>
</li>
<li><p><a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/repos/bitbucket?view=azure-devops&amp;tabs=yaml#pr-triggers">Azure Pipelines - PR Triggers e autoCancel</a> - documentação do comportamento de auto-cancelamento em PRs</p>
</li>
<li><p><a href="https://docs.npmjs.com/cli/v10/commands/npm-install">npm CLI - npm install</a> - comportamento de instalação global e uso de diretórios temporários</p>
</li>
</ul>
<hr />
<p><em>Vinicius Aguilar — DevOps Engineer</em></p>
]]></content:encoded></item><item><title><![CDATA[EM DESENVOLVIMENTO]]></title><description><![CDATA[asdasd]]></description><link>https://aguillarops.dev/em-desenvolvimento</link><guid isPermaLink="true">https://aguillarops.dev/em-desenvolvimento</guid><dc:creator><![CDATA[Vinicius Aguilar]]></dc:creator><pubDate>Sat, 14 Mar 2026 07:42:32 GMT</pubDate><content:encoded><![CDATA[<p>asdasd</p>
]]></content:encoded></item><item><title><![CDATA[O profissional insubstituível]]></title><description><![CDATA[Uma mentira que alimentamos...
Segunda-feira, 09h da manhã. Deploy em produção. Todos os devs na call e alguém solta: "Ninguém é insubstituível mesmo.". Todo mundo fica em silêncio... e a pessoa conti]]></description><link>https://aguillarops.dev/o-profissional-insubstituivel</link><guid isPermaLink="true">https://aguillarops.dev/o-profissional-insubstituivel</guid><category><![CDATA[Devops]]></category><category><![CDATA[AI]]></category><category><![CDATA[carreira]]></category><dc:creator><![CDATA[Vinicius Aguilar]]></dc:creator><pubDate>Sat, 14 Mar 2026 07:30:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/94aec83e-c4db-4916-9987-be682d712e0e.svg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h2>Uma mentira que alimentamos...</h2>
<p>Segunda-feira, 09h da manhã. Deploy em produção. Todos os devs na call e alguém solta: <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">"Ninguém é insubstituível mesmo."</mark></strong>. Todo mundo fica em silêncio... e a pessoa continua: chegou o e-mail do desligamento de um brother do time. Aquele silêncio pesado.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/f5320a4b-5b04-43bc-819d-8776d17928d9.gif" alt="" style="display:block;margin:0 auto" />

<p>Pois é, e eu achava esse colega essencial no time. O problema é que dois meses depois, ninguém mais lembrava o nome dele...</p>
<p>Ora ou outra, ouvimos essa frase. E, sinceramente, em algumas situações ela nos faz acreditar nela. O principal setor que alimenta essa narrativa de que "ninguém é insubstituível" é o mercado de trabalho, porque não importa o quanto você atue, execute múltiplas tarefas, seja o primeiro a chegar e o último a sair: <strong>sempre haverá outra pessoa que consegue entregar o mesmo que você, ou até mais.</strong></p>
<p>Um erro que eu cometi foi achar que, quando eu saísse de uma empresa <em>(principalmente por falta de reconhecimento)</em>, ela iria sentir de alguma forma. E vamos ser sinceros: lá no fundo, existe um desejo de descobrir que a empresa passou por dificuldade depois que você saiu. E isso é puro ego. A verdade que dói é que <strong>não somos insubstituíveis no mercado de trabalho,</strong> e possivelmente a empresa nem sentiu a nossa falta. Com o tempo, a maturidade chega, e percebemos que, se a empresa não reconhecia nosso valor, talvez fosse simplesmente uma questão de alinhamento. Não é pessoal. É mercado.</p>
<p>Essa sempre foi a regra do jogo. <strong>Até que veio a inteligência artificial e reescreveu tudo.</strong></p>
<hr />
<h2>O cenário <strong>assustador</strong></h2>
<p>Hoje, em tech, quem não usa IA está ficando pra trás em produtividade. Uma pessoa com conhecimento básico usando IA, possivelmente vai conseguir entregar mais do que um dev júnior que não usa IA.</p>
<p>Ao mesmo tempo, tem gente perdendo o emprego pelo mesmo motivo: uso da IA. Então fica a dúvida: <em><strong>como ser um bom profissional nesse cenário?</strong></em></p>
<p>A lógica é simples: se eu não uso IA, fico para trás em produtividade e competitividade. Mas se eu me encosto nela de forma excessiva e sem critério, o risco de adquirir <strong>preguiça cognitiva</strong> e virar um simples <strong>copiador de prompts</strong> é gigante.</p>
<p>E qual o resultado prático disso no dia a dia? Sistemas sem integridade. Começa a entregar blocos de código que até rodam na superfície, mas que ignoram a arquitetura e atropelam as regras de negócio. No curto prazo, isso gera um ecossistema frágil e todo remendado. No longo prazo, você decreta a própria perda de relevância.</p>
<p>Pensa bem... se eu entrego um componente que não entendo a fundo e não sei explicar como funciona, minha mão de obra passa a ser facilmente substituível. Afinal de contas, fazer perguntas aleatórias para a IA qualquer um faz... agora, fazer as <em>perguntas certas</em> exige conhecimento técnico. Não tem atalho para isso.</p>
<p>Relutar contra a IA é fazer igual aquele <strong>cara da infra,</strong> de dez anos atrás, que se recusou a aprender Cloud. Aquele cara que dizia que configurar servidor na mão via SSH era o jeito ‘certo’, enquanto o mercado migrava pra infraestrutura como código. Hoje, ele leva uma semana pra fazer o que um pipeline resolve em minutos.</p>
<p><strong>Relutar contra a IA é repetir esse erro.</strong> Não importa o quanto você seja bom ou rápido no teclado, você não vai ultrapassar a produtividade de uma IA, por mais básica que ela seja. Atualmente, velocidade virou pré-requisito, não diferencial.</p>
<p>Tá, mas se eu não posso relutar contra a IA, terceirizar o cérebro pra ela é a solução? <strong>Longe disso.</strong> E é exatamente nesse ponto que o mercado separa o profissional genérico e o profissional com diferencial real.</p>
<hr />
<h2>O que a IA não consegue fazer</h2>
<p><strong>Raciocínio sistêmico.</strong> A capacidade de enxergar o todo, e não só a parte. De entender que cada componente de um sistema está conectado aos outros, e que uma ação em um ponto pode gerar consequências em cadeia em lugares que você nem estava olhando.</p>
<p><strong>Vou te dar um exemplo prático:</strong> recebi um alerta crítico de latência alta numa aplicação em produção. A primeira reação de quem depende 100% de IA seria colar o log no chat. Ela devolveria o óbvio: otimize a query, ajuste o índice, escale a máquina. Mas o problema estava fora do radar dela.</p>
<p>Foi fazendo as perguntas certas, como <strong>"Isso começou quando?", "Teve deploy hoje?" e "Quem mais compartilha recursos com esse banco?"</strong>, cheguei na raiz do problema. Um pipeline rodando em background travou as conexões do pool. Não era a query, era contenção de recurso causada por outra peça da infraestrutura. A IA não faz essa correlação sozinha. Ela não tem o contexto do todo. E com a pressão do sistema caindo, você não tem tempo de explicar isso pra ela.</p>
<p>Nesse cenário, o melhor foi <strong>não usar IA</strong>. Porque simplesmente não era necessário. Eu precisei somente me questionar com o básico para encontrar o problema raiz. Depois de entender a causa, se precisasse de algo mais minucioso, aí sim eu usaria a IA.</p>
<p>Esse tipo de raciocínio é o que responde a perguntas que a IA não sabe formular:</p>
<ul>
<li><p>Por que o sistema está se comportando assim na raiz do problema?</p>
</li>
<li><p>Onde as falhas vão surgir quando escalar? Qual a previsibilidade?</p>
</li>
<li><p>Como uma alteração mínima pode gerar um efeito dominó e causar um outage?</p>
</li>
</ul>
<p><em><strong>No fim das contas, o segredo é usar IA com profundidade técnica suficiente para saber quando ela está errando.</strong></em></p>
<p><strong>Quantas vezes você já jogou o erro no chat da IA sem nem ler a mensagem antes?</strong> É automático: deu erro, cola lá.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69b4e7f9210c74252fb569ae/3488023d-2e28-4df3-ac92-9af74ff624fe.gif" alt="" style="display:block;margin:0 auto" />

<p>O profissional que se diferencia é o que para, lê, entende o contexto e só depois decide se precisa de ajuda. Ele percebe quando a IA está alucinando, presa em loop, ou dando uma volta ao mundo para algo que se resolve com um único comando. A primeira pergunta que ele se faz é: 'Isso faz sentido?'. Ele domina a regra de negócio e a arquitetura, conhece os gaps e não sai executando código às cegas.</p>
<hr />
<h2>O que mudou na minha cabeça</h2>
<p>Essa clareza sobre IA e diferencial técnico é só metade do problema. A outra metade, que demorei mais para aceitar, <strong>é entender o meu real tamanho perante o mercado.</strong></p>
<p>Trabalho como DevOps, tenho projetos bacanas. Mas sei que ainda tem um nível acima do que eu sou hoje e chegar lá exige mais. Durante muito tempo, caí na armadilha da <strong>"síndrome do protagonista"</strong>, aquela ilusão de que somos o alecrim dourado, a exceção à regra, e de que o sucesso virá por atalhos ou de forma mágica. Mas quando você olha para a complexidade da nossa área, para a arquitetura dos sistemas e para a velocidade com que a IA evolui, a ficha cai: <em><strong>não há espaço para quem acha que sabe tudo.</strong></em></p>
<p>Existe uma sensação silenciosa que assombra muitos profissionais de tech: por fora, tudo certo. Entregas acontecendo, reconhecimento vindo, destaque no time. Mas por dentro, uma pergunta que não sai da cabeça: <em><strong>o quanto disso fui realmente eu?</strong></em> Essa dúvida se intensifica com o uso da IA. Porque quando a ferramenta entrega rápido demais, fica difícil separar onde termina a IA e onde começa o seu raciocínio.</p>
<p>Perceber que não existe atalho, milagre ou ‘hack’ pra salvar a carreira é <strong>libertador</strong>. Isso elimina a ilusão de que você já sabe o suficiente e deixa apenas um caminho inevitável: estudar. Isso não é autopiedade, é autoconsciência.</p>
<p>Como já dizia o filósofo <strong>Clóvis de Barros Filho: <em>'Você tem brio?'</em></strong>. Aquele brio de olhar para um problema e pensar: 'Como pode alguém ter criado isso e eu não dar conta de entender? Não tem como'. E então sentar, ler, refazer uma, duas, três vezes até dominar. Sem atalho. Sem plano B.</p>
<p>Mas dominar a técnica através do estudo é só a base. O seu verdadeiro diferencial em relação a uma IA entra no passo seguinte: a sua capacidade de pensamento e tomada de decisão no mundo real. Infelizmente, não conseguimos mais criar um cluster Kubernetes mais rápido que uma IA. No entanto, nós temos o raciocínio sistêmico, a mentalidade de engenharia e, principalmente, o contexto vivido. A IA não sabe que aquele microsserviço de pagamento não pode ser reiniciado durante o fechamento do mês porque derruba a conciliação inteira. Ela não sabe que um cliente em particular não tolera nem 30 segundos de indisponibilidade. Ela não carrega o histórico de decisões ruins que o time tomou há dois anos e que assombram a arquitetura até hoje. Esse contexto não se prompta. Ele se constrói com tempo, atenção e presença. E é exatamente isso que te mantém no jogo.</p>
<hr />
<h2>A conclusão</h2>
<p><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">"Ninguém é insubstituível."</mark></strong> Eu comecei esse artigo com essa frase. E continuo acreditando nela. No mercado de trabalho, <strong>ninguém é.</strong> Mas descobri que a pergunta certa nunca foi <strong>"como ser insubstituível?"</strong>. A pergunta certa é: <strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">"o que eu preciso dominar pra que me substituir custe caro demais?"</mark></strong></p>
<p>E a resposta não está em trabalhar mais horas, nem em abraçar a IA cegamente, nem em fingir que a IA não existe. <strong>Está em construir um raciocínio que a IA não replica, uma profundidade técnica que não se copia com prompt, e uma autoconsciência que te impede de estagnar.</strong></p>
<p><em><strong>Domine as tecnologias como pré-requisito. Mude sua forma de pensar como diferencial.</strong></em></p>
<p>O caminho pra ter esse diferencial é mais simples do que parece. Você simplesmente precisa estudar e se colocar em problemas... Pensar fora da caixa em DevOps não é usar a ferramenta mais nova ou montar o pipeline mais elaborado. É questionar o que todo mundo aceita como verdade. Por que esse processo existe? Ele ainda faz sentido? Existe uma forma mais simples de resolver isso? O DevOps que pensa assim não espera o problema virar incidente, ele enxerga o atrito antes, propõe a solução antes, e entrega antes. <em><strong>Essa antecipação é o que nenhuma IA faz sozinha, porque ela responde a perguntas. Você formula as perguntas certas.</strong></em></p>
<p>E caso você que está lendo precise de uma direção, eu tenho uma sugestão simples:</p>
<p><strong>ESTUDA → DOCUMENTA → ENSINA → PUBLICA</strong></p>
<ol>
<li><p><strong>Estuda.</strong> Tanto tecnologias quanto métodos. Se não sabe por onde começar, siga um <a href="https://roadmap.sh/devops">roadmap.</a></p>
</li>
<li><p><strong>Documenta tudo o que você aprende.</strong> O conhecimento que não está registrado se perde.</p>
</li>
<li><p><strong>Ensina antes de se sentir pronto.</strong> Ensinar expõe as lacunas que você nem sabia que tinha e, como bônus, fixa o que aprendeu. <em><strong>Inclusive, esse artigo é o meu passo 3.</strong></em> Escrevi antes de me sentir pronto. <strong>E foi exatamente isso que me forçou a organizar o que eu realmente sabia e expôs o que eu ainda precisava entender melhor.</strong></p>
</li>
<li><p><strong>Entrega publicamente o que você está aprendendo.</strong> Compromisso público vira combustível. Posta. Mostra o seu trabalho. Porque essa é a única prova que você pode dar para o mercado de trabalho de que você realmente sabe o que diz que sabe. Ensine. Tem gente que só vai entender tal conceito com a sua forma de explicar. Poste vídeos, artigos, posts, suba projetos no GitHub, mas publique o que você anda fazendo.</p>
</li>
</ol>
<p>Bom, esse é o meu primeiro artigo. E ele nasce exatamente do ponto onde eu parei de me enganar e comecei a construir de verdade, sem plano B, sem saída. <strong>Se algo aqui te incomodou, ótimo.</strong> <em><strong><mark class="bg-yellow-200 dark:bg-yellow-500/30">Incômodo é o primeiro sinal de que você está prestando atenção.</mark></strong></em></p>
<p>Os próximos artigos vão ser diferentes desse. Menos reflexão, mais terminal. Cada tópico que eu estudar vira conteúdo aqui: com os comandos que rodei, os erros que cometi e o raciocínio que usei pra resolver. Se você quer aprender DevOps, sem filtros, pode me acompanhar nessa caminhada. Vai valer o seu tempo.</p>
<hr />
<p><em>Vinicius Aguilar — DevOps Engineer</em></p>
]]></content:encoded></item></channel></rss>