O que é o GitHub Actions e por que ele fica lento

O GitHub Actions e a plataforma de CI/CD nativa do GitHub, lançada em outubro de 2018. Permite automatizar builds, testes e deploys diretamente no repositório, sem precisar configurar um servidor Jenkins ou CircleCI separado.

A grande vantagem e a integração direta: pull request abre, workflow roda, resultado aparece no próprio PR. Mas com o tempo o pipeline vai crescendo. Mais testes, mais dependências, mais etapas. O que era 3 minutos vira 20. E ai cada commit vira uma espera.

A boa notícia: a maioria dos problemas de lentidão tem solução rápida. Neste post vamos direto ao ponto com 8 técnicas que fazem diferença real no dia a dia.

Como funciona o GitHub Actions por baixo

Cada workflow e um arquivo YAML em .GitHub/workflows/. Quando um evento acontece (push, pull request, cron...), o GitHub provisiona um runner, clona o repositório do zero e executa os steps na ordem definida.

Esse modelo de runner efémero e ótimo para reproducibilidade mas péssimo para desempenho por padrão: sem cache, cada run baixa todas as dependências do zero. Um npm install com 500 pacotes pode levar 2-3 minutos sozinho.

O segredo esta em entender o que é custoso (download de dependências, compilação, testes pesados) e o que pode ser reutilizado entre runs ou rodado em paralelo.

Técnica 1: Cache de dependências com actions/cache

A otimização com maior impacto imediato e cachear a pasta de dependências entre runs. O GitHub disponibiliza até 10GB de cache por repositório gratuitamente.

- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

A chave usa o hash do package-lock.json: se o lock não mudou, o cache e reutilizado. Se mudou (nova dependência), o cache e invalidado automaticamente. Para projetos Java use ~/.m2, para Python use ~/.cache/pip.

💡
Dica

As actions oficiais de setup (setup-node, setup-Python, setup-java) já tem cache integrado. Passe cache: 'npm' diretamente no setup-node e ele configura o cache automaticamente, sem precisar do actions/cache separado.

Técnica 2: Jobs em paralelo

Por padrão os jobs de um workflow rodam em serie. Mas se seus testes unitários e de integração são independentes, eles podem rodar ao mesmo tempo.

jobs:
  test-unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test -- --testPathPattern='unit'

  test-integration:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test -- --testPathPattern='integration'

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

Os três jobs rodam em paralelo. O tempo total cai para o tempo do job mais lento, não a soma de todos. Se cada um leva 5 minutos, o pipeline inteiro termina em 5 minutos em vez de 15.

⚠️
Atenção

Cada job paralelo consome um runner separado. No plano gratuito do GitHub você tem 20 runners simultâneos para repositórios privados. Fique de olho no consumo de minutos do mes.

Técnica 3: Concurrency groups para cancelar runs obsoletas

Você fez 3 pushes seguidos enquanto o CI do primeiro ainda rodava. Resultado: 3 pipelines concorrentes, consumindo minutos e travando o merge. O concurrency resolve isso automaticamente.

concurrency:
  group: ${{ GitHub.workflow }}-${{ GitHub.ref }}
  cancel-in-progress: true

Com essa configuração, quando um novo push chega na mesma branch, o GitHub cancela o run anterior automaticamente. Você só paga pelo run que realmente importa: o último.

Para a branch main você pode querer manter todos os runs (não cancelar). Nesse caso use cancel-in-progress: ${{ GitHub.ref != 'refs/heads/main' }}.

Técnica 4: Filtros de path (só rodar quando necessário)

Por que rodar o pipeline inteiro quando você só mudou um arquivo de documentação? O filtro paths resolve isso.

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package*.json'
  pull_request:
    paths:
      - 'src/**'
      - 'tests/**'

Se o commit alterou apenas README.md, o workflow nem e disparado. O CI só roda quando arquivos relevantes mudam. Para monorepos isso é ainda mais impactante: cada serviço pode ter seu próprio workflow que só dispara quando sua pasta muda.

🚀
Pro tip

Combine paths com matrix strategy para testes em múltiplas versões do Node/Python. O GitHub roda cada combinação em paralelo. Testar nas versões 18, 20 e 22 do Node simultaneamente leva o mesmo tempo que testar em uma única versão.

Técnica 5: Checkout superficial (shallow clone)

Por padrão o actions/checkout clona todo o histórico do repositório. Para repos antigos com anos de commits isso pode ser lento. Use fetch-depth: 1 para clonar só o último commit.

- uses: actions/checkout@v4
  with:
    fetch-depth: 1

A economia varia bastante: em repos novos e imperceptivel, mas em repos com muitos anos de histórico pode economizar 30-60 segundos só no checkout. Cuidado se seu workflow precisar do histórico completo (por exemplo, para gerar changelogs ou calcular versão semântica com git tags).

Técnica 6: Runners maiores para builds pesados

Se você já otimizou tudo e o CI ainda demora, considere usar runners com mais CPU e RAM. O GitHub oferece runners com 4, 8 e 16 cores (pagos), e alguns projetos open source tem acesso a runners gratuitos mais potentes.

Para projetos que compilam C++, fazem builds de imagens Docker ou rodam suites de testes muito grandes, um runner de 4 cores pode ser 2-3x mais rápido que o runner padrão de 2 cores. O custo extra pode compensar o tempo economizado dos desenvolvedores esperando o CI.

💡
Dica

Runners self-hosted são gratuitos e podem usar sua própria infraestrutura. São ótimos se você tem servidores ociosos ou precisa de hardware específico (GPU, arm64). O custo de manutenção e maior, mas o custo por minuto e zero.

Técnica 7: Compartilhar artefatos entre jobs

Se você faz o build em um job e precisa usar o resultado em outro (por exemplo, rodar testes sobre o artefato compilado), use actions/upload-artifact e actions/download-artifact.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  test-e2e:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - run: npm run test:e2e

Isso evita fazer o build duas vezes. O job de testes recebe o artefato pronto em vez de recompilar do zero. Para artefatos grandes, o upload/download pode ser mais lento que recompilar, então avalie caso a caso.

Técnica 8: Dividir testes pesados com matrix

Tem uma suite com 1000 testes que leva 20 minutos? Divida em grupos e rode em paralelo com matrix strategy.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npx jest --shard=${{ matrix.shard }}/4

O Jest divide automaticamente os testes em 4 grupos iguais. Cada grupo roda em um runner separado em paralelo. O tempo cai de 20 para cerca de 5 minutos. O Playwright também tem suporte nativo a sharding com o parâmetro --shard.

🔴
Cuidado

Testes que dependem de estado compartilhado (banco de dados, arquivos temporários, portas de rede) podem ter conflitos ao rodar em paralelo. Certifique-se que cada shard usa recursos isolados antes de ativar o sharding.

Comparação com alternativas ao GitHub Actions

O GitHub Actions não e a única opcao de CI/CD. Cada plataforma tem seus pontos fortes.

CircleCI tem cache mais eficiente e uma interface de debug melhor, mas requer configuração separada do GitHub. GitLab CI e nativo no GitLab com mais controle sobre runners. Buildkite e excelente para empresas que querem total controle com runners próprios. Vercel e Netlify tem CI integrado focado em frontend, sem precisar configurar YAML.

A principal vantagem do GitHub Actions e a integração nativa com o ecossistema GitHub: Marketplace com milhares de actions prontas, integração com issues, PRs e Packages, e o fato de não precisar de conta extra. Para quem já usa GitHub, e o caminho natural.

Pontos positivos e limitações

Os pontos fortes são evidentes: integração total com GitHub, Marketplace rico, suporte a todos os sistemas operacionais, YAML simples para casos comuns, e plano gratuito generoso para repositórios públicos.

As limitações aparecem em escala: o plano gratuito tem 2.000 minutos/mes para repos privados, o que se esgota rápido em times médios. Debugar um runner remoto ainda e trabalhoso comparado a rodar localmente. Workflows complexos ficam verbosos rapidamente. E o limite de 6 horas por job pode ser um problema para builds muito pesados.

Casos de uso reais

Time de frontend React: usa jobs paralelos para lint, testes unitários e build de produção. Com cache do npm e paralelismo, o CI caiu de 18 para 6 minutos.

Startup com monorepo: filtros de path garantem que o CI do backend só roda quando arquivos de backend mudam. O CI do frontend e separado. PRs de documentação passam instantaneamente.

Projeto open source: usa matrix para testar em Ubuntu, Windows e macOS ao mesmo tempo, mais matrix de versões do Python (3.10, 3.11, 3.12). 9 combinações rodando em paralelo em menos tempo que uma rodava antes.

API Node.js em produção: usa concurrency groups para cancelar runs antigos em branches de feature, mas mantem todos os runs na main. Deploy automático só ocorre se todos os jobs passam.

Dicas e boas práticas

💡
Dica

Use act (GitHub.com/nektos/act) para testar workflows localmente antes de fazer push. Ele simula o ambiente do GitHub Actions no seu Docker local, economizando minutos e o ciclo de push-espera-veja-o-erro.

💡
Dica

Fixe a versão das actions com o hash SHA em vez de tag (ex: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683). Tags podem ser atualizadas por terceiros; o hash e imutável. Essencial em produção para evitar supply chain attacks.

🚀
Pro tip

Use Composite Actions para reutilizar steps comuns entre workflows. Em vez de copiar os mesmos 10 steps de setup em cada workflow, crie uma action local em .GitHub/actions/setup/action.yml e chame com uses: ./.GitHub/actions/setup.

Vale a pena otimizar o GitHub Actions?

Sim, especialmente se o CI esta acima de 10 minutos. O tempo que os desenvolvedores esperam o CI multiplicado pelo número de pessoas no time e pelo número de PRs por semana vira horas desperdiçadas toda semana.

Comece pelas técnicas de maior impacto e menor esforço: cache de dependências e concurrency groups podem ser implementados em 15 minutos e já dao resultados imediatos. Depois avalie paralelismo e filtros de path conforme a necessidade.

O próximo passo: abra o repositório, olhe o gráfico de tempo dos últimos workflows, identifique o step mais lento e aplique a técnica correspondente. Pequenas mudanças no YAML podem transformar o dia a dia do time.