From b5b0e41ff9ed6c6d6160e0ed0f8284ec169b50f1 Mon Sep 17 00:00:00 2001 From: Matheus Henrique Caiser Barrozo Date: Fri, 7 Nov 2025 14:47:34 -0300 Subject: [PATCH] fix: restore blog posts that were accidentally removed by merge --- src/content/blog/ci-cd-saga.mdx | 151 +++++++++++++++++++++++++++ src/content/blog/merge-vs-rebase.mdx | 71 +++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 src/content/blog/ci-cd-saga.mdx create mode 100644 src/content/blog/merge-vs-rebase.mdx diff --git a/src/content/blog/ci-cd-saga.mdx b/src/content/blog/ci-cd-saga.mdx new file mode 100644 index 0000000..dab637c --- /dev/null +++ b/src/content/blog/ci-cd-saga.mdx @@ -0,0 +1,151 @@ +--- +title: "A Saga do CI/CD: Como Corrigimos uma Race Condition no GitHub Actions" +publishedAt: "2025-11-06" +summary: "Uma história real de depuração de um workflow de CI/CD, onde a concorrência de gatilhos e a ordem de execução causaram uma falha sutil, mas crítica, no nosso processo de release." +--- + +Todo desenvolvedor que já configurou um pipeline de CI/CD conhece a sensação: a satisfação de ver os checks verdes e a automação funcionando... e a frustração profunda quando uma falha misteriosa acontece. Recentemente, passei por uma dessas sagas ao tentar otimizar os workflows de um projeto, e a jornada para encontrar a solução foi cheia de lições valiosas. + +## O Problema Inicial: Workflows Lentos e Redundantes + +Tudo começou com um objetivo simples: fazer nossos workflows rodarem mais rápido. Em Pull Requests que alteravam apenas a documentação (arquivos `.md`), nossos jobs de teste, lint e deploy eram executados desnecessariamente, gastando tempo e recursos. + +A solução parecia óbvia: usar um filtro de caminho para pular os jobs se nenhuma alteração no código fosse detectada. + +## A Primeira Tentativa e o Primeiro Erro + +Implementamos a popular action `dorny/paths-filter`. A ideia era simples: se os arquivos alterados não estivessem em `src/`, `package.json`, etc., os steps seguintes seriam pulados. + +```yaml +- name: Check for code changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + code: + - 'src/**' + - '.github/workflows/**' + # ... outros caminhos + +- name: Install dependencies + if: steps.filter.outputs.code == 'true' + run: pnpm install +``` + +**Onde quebrou?** O workflow falhou com um erro enigmático: `Error: Can't find common ancestor`. + +- **Lição 1: O `fetch-depth` é Crucial.** A action `actions/checkout` por padrão faz um checkout "superficial" (`fetch-depth: 1`), baixando apenas o último commit. O `paths-filter` precisa do histórico completo para comparar a branch do PR com a branch base. A solução foi adicionar `fetch-depth: 0` ao checkout. + +## O Segundo Erro: A Duplicidade de Deployments + +Com o primeiro erro corrigido, notamos que cada PR criava **duas** entradas de deployment: uma atribuída a mim e outra ao bot `github-actions[bot]`. + +- **Lição 2: `environment:` Nativo vs. Actions Manuais.** Descobrimos que estávamos usando dois mecanismos para o mesmo fim. A chave `environment:` no nosso job já instruía o GitHub a criar um deployment (atribuído ao usuário que iniciou o workflow). Ao mesmo tempo, a action `bobheadxi/deployments` também criava um deployment (atribuído ao bot). A solução foi remover a action e confiar 100% no mecanismo nativo do GitHub, que hoje já é robusto o suficiente para gerenciar o ciclo de vida completo do deployment. + +## O Furo Final: A Race Condition + +Tudo parecia perfeito, até que uma análise mais profunda revelou uma falha crítica. Nosso workflow `preview.yml` era acionado por dois eventos: `push` em branches `release/*` e `pull_request`. + +O objetivo era: + +1. No `push` para `release/*`, criar uma tag beta com `semantic-release`. +2. No `pull_request`, apenas rodar testes e gerar um preview. + +O problema era a `concurrency: cancel-in-progress: true`. + +- **A Race Condition:** Quando um desenvolvedor fazia o push da branch de release e abria o PR em seguida, duas execuções do workflow eram disparadas. A `concurrency` cancelava a mais antiga (a do `push`), e a execução do `pull_request` continuava. Como o `semantic-release` só rodava no evento de `push`, a tag beta **nunca era criada**. + +- **Lição 3: Separe as Responsabilidades.** Um workflow não deve tentar servir a dois mestres. A solução foi dividir o `preview.yml` em dois: + 1. **`preview.yml`:** Focado apenas em validação de PRs (gatilho `pull_request`). + 2. **`create-beta-release.yml`:** Focado apenas em criar a release beta (gatilho `push`). + +Essa separação eliminou a competição entre os gatilhos e a race condition, garantindo que cada processo rode de forma confiável e independente. + +## O Capítulo Final: A Otimização Condicional + +Mas a saga não acabou aí. Após resolver a race condition, percebemos que nossa arquitetura ainda tinha uma falha fundamental: **os status checks ficavam inconsistentes**. + +### O Dilema dos Status Checks + +Quando implementamos o paths-filter no `preview.yml`, o job `deploy-preview` era completamente **pulados** quando não havia mudanças no código. Isso fazia com que os status checks aparecessem como "skipped" em vez de "success", quebrando a consistência dos required status checks. + +```yaml +# ❌ ANTES: Job pulado completamente +deploy-preview: + if: needs.test-and-lint.outputs.code-changed == 'true' # ← Isso fazia o job ser skipped + uses: ./.github/workflows/reusable-deploy-vercel.yml +``` + +### A Solução Arquitetural + +A resposta estava em mover o controle **para dentro** dos reusable workflows. Em vez de decidir no nível do job se rodar ou não, deixar os reusable workflows decidirem internamente quais steps executar. + +```yaml +# ✅ DEPOIS: Job sempre roda, controle interno +deploy-preview: + # Sem condição no job - sempre roda para status check consistente + uses: ./.github/workflows/reusable-deploy-vercel.yml +``` + +Dentro do `reusable-deploy-vercel.yml`, adicionamos o paths-filter e condições em todos os steps: + +```yaml +jobs: + deploy: + steps: + - name: Check for code changes + id: filter + uses: dorny/paths-filter@v3 + with: + filters: | + code: + - 'src/**' + - 'public/**' + - 'package.json' + # ... outros arquivos de código + + - name: Install Vercel CLI + if: steps.filter.outputs.code == 'true' # ← Condição no step + run: npm install --global vercel@latest + + - name: Build Project Artifacts + if: steps.filter.outputs.code == 'true' # ← Condição no step + run: vercel build + + - name: Deploy to Vercel + if: steps.filter.outputs.code == 'true' # ← Condição no step + run: vercel deploy +``` + +### O Resultado Perfeito + +Agora ambos os reusable workflows seguem o **mesmo padrão arquitetural**: + +- **`reusable-test-and-lint.yml`**: Paths-filter + steps condicionais +- **`reusable-deploy-vercel.yml`**: Paths-filter + steps condicionais + +**Comportamento final:** +- **PRs com mudanças no código**: Jobs rodam e fazem trabalho real ✅ +- **PRs só com documentação**: Jobs rodam (status checks verdes) mas pulam steps internos ✅ + +**Benefícios alcançados:** +- ✅ **Status checks consistentes** (sempre success) +- 💰 **Otimização de custos** (deploy condicional no Vercel) +- 🔄 **Arquitetura uniforme** nos reusable workflows +- 📋 **Compatível com required status checks** + +## Conclusão + +O que começou como uma simples otimização se tornou uma jornada profunda pela arquitetura do GitHub Actions. A lição final é clara: workflows de CI/CD são parte do código e merecem a mesma atenção à arquitetura, separação de responsabilidades e depuração que aplicamos à nossa aplicação. + +**Nunca subestime:** +- A importância de um bom `fetch-depth` +- O poder das race conditions em sistemas concorrentes +- A necessidade de status checks consistentes +- A beleza de uma arquitetura uniforme + +E lembre-se: em CI/CD, assim como na vida, **separar responsabilidades** é sempre a melhor solução! 🚀 + +--- + +\*Escrito com ❤️ por **Matheus Caiser, The Mr. Developer\*** diff --git a/src/content/blog/merge-vs-rebase.mdx b/src/content/blog/merge-vs-rebase.mdx new file mode 100644 index 0000000..393c174 --- /dev/null +++ b/src/content/blog/merge-vs-rebase.mdx @@ -0,0 +1,71 @@ +--- +title: "Merge Commit vs. Rebase: Qual a Melhor Estratégia para seu Histórico Git?" +publishedAt: "2025-11-06" +summary: "Um debate clássico no mundo Git: você deve preferir um histórico linear e limpo com rebase, ou um histórico rastreável e completo com merge commits? Vamos analisar os prós e contras de cada abordagem." +--- + +Se você já trabalhou em uma equipe de desenvolvimento, provavelmente já se deparou com este debate: qual é a maneira "certa" de incorporar as mudanças de uma feature branch na branch principal? A resposta geralmente se resume a duas estratégias principais oferecidas pelo GitHub: **Create a merge commit** e **Rebase and merge**. + +Ambas as abordagens têm o mesmo resultado final — o código da sua feature chega à branch de destino — mas elas contam a *história* de como ele chegou lá de maneiras drasticamente diferentes. + +## A Abordagem 1: "Create a merge commit" (O Historiador) + +Esta é a estratégia padrão do Git. Ela preserva a história exatamente como ela aconteceu. + +- **Como funciona:** Quando você faz o merge de um Pull Request, o Git cria um novo commit, o "merge commit". Este commit especial tem dois "pais": o último commit da branch de destino e o último commit da sua feature branch. Ele une os dois históricos. + +- **Como fica o `git log`:** + ``` + * Merge pull request #123 from feature/nova-feature (main) + |\ + | * feat: Adiciona nova funcionalidade (feature/nova-feature) + | * fix: Corrige bug na funcionalidade + * | commit anterior (main) + |/ + * ... + ``` + O histórico se torna um grafo, parecendo uma "árvore de natal". + +- **Prós:** + * **Rastreabilidade Absoluta:** É indiscutível *quando* o PR foi mergeado e de onde ele veio. O contexto do PR está permanentemente gravado no histórico do Git. + * **Não Reescreve a História:** Os commits originais da feature branch permanecem intocados, o que é considerado mais seguro por alguns. + +- **Contras:** + * **Histórico "Poluído":** O log fica cheio de commits de merge que, para alguns, são apenas ruído e dificultam a leitura da evolução linear do projeto. + +## A Abordagem 2: "Rebase and merge" (O Editor) + +Esta estratégia prioriza um histórico limpo e legível. + +- **Como funciona:** Antes de fazer o merge, o Git pega todos os commits da sua feature branch e os "reaplica", um por um, em cima do último commit da branch de destino. Depois disso, a branch de destino pode ser simplesmente "avançada" para incluir esses novos commits, sem a necessidade de um merge commit. + +- **Como fica o `git log`:** + ``` + * feat: Adiciona nova funcionalidade (main) + * fix: Corrige bug na funcionalidade + * commit anterior (main) + * ... + ``` + O histórico fica perfeitamente linear, como se todo o trabalho tivesse sido feito diretamente na branch principal. + +- **Prós:** + * **Histórico Limpo e Legível:** É extremamente fácil seguir a sequência de mudanças no projeto. + * **Facilita a Depuração:** Ferramentas como `git bisect` (para encontrar quando um bug foi introduzido) funcionam muito melhor em um histórico linear. + +- **Contras:** + * **Perda de Contexto do PR:** Você perde a informação explícita de "quando o PR #123 foi mergeado" diretamente no log do Git. Esse contexto passa a viver apenas na interface do GitHub. + * **Reescreve a História:** Os hashes dos seus commits originais são alterados durante o rebase. + +## Conclusão: Qual é o Melhor? + +Não há uma resposta "certa". É uma escolha filosófica para o seu projeto. + +- **Escolha "Merge Commit" se:** Você valoriza a rastreabilidade histórica e a integridade dos commits acima de tudo. É ótimo para projetos com auditorias rigorosas ou para equipes com muitos desenvolvedores juniores, pois é o fluxo mais simples de entender. + +- **Escolha "Rebase and Merge" se:** Você valoriza um histórico limpo e legível e está confortável em usar a interface do GitHub para encontrar o contexto de um PR. É uma abordagem muito popular em projetos de alta performance e equipes que priorizam a clareza do `git log`. + +No nosso projeto, optamos pelo **"Rebase and Merge"**. A clareza do histórico linear supera a perda do contexto do merge commit no log, e essa decisão nos ajuda a manter o projeto organizado e fácil de navegar. + +--- + +*Escrito com ❤️ por **Matheus Caiser, The Mr. Developer***