Alternar tema

GitHub Actions Matrix: testes paralelos em várias versões na prática

Easton editorial illustration: hardened server operations console

Na semana passada, colocamos um projeto em produção. Antes mesmo do fim da tarde, um usuário avisou que a página ficava completamente em branco no Node 16. Na hora, bateu aquele desespero.

Passei duas horas investigando. Revirei os logs e comparei o código três vezes, até descobrir que uma API tratava o comportamento de JSON.stringify() de forma diferente no Node 16 e no Node 20: a versão antiga lançava um erro ao encontrar uma referência circular, enquanto a nova lidava com ela silenciosamente. Nosso pipeline de CI testava apenas o Node 20, então esse problema de compatibilidade passou direto.

Na retrospectiva, fiquei pensando: se tivéssemos configurado testes paralelos em várias versões, o problema teria aparecido antes da publicação. Foi a partir daí que comecei a estudar com atenção o recurso Matrix do GitHub Actions. O nome pode parecer complicado, mas, na prática, ele apenas divide uma tarefa automaticamente em várias e as executa ao mesmo tempo em versões e plataformas diferentes.

Este artigo explica Matrix do básico ao avançado. Vamos passar pela sintaxe fundamental, pelos filtros de combinações com exclude/include, pela escolha da estratégia fail-fast e pelo controle de recursos com max-parallel. No fim, você terá um modelo completo de testes em várias versões do Node.js, pronto para copiar. A leitura leva cerca de 10 minutos; depois disso, você já poderá adaptar seu CI para trabalhar em paralelo.

Fundamentos de Matrix — comece em 5 minutos

Matrix pode ser resumido em uma frase: você escreve um job, e o GitHub Actions o expande automaticamente em várias tarefas paralelas.

Veja um exemplo. Ao definir três versões do Node.js, [18, 20, 22], Matrix cria três tarefas de teste independentes, executadas respectivamente com Node 18, Node 20 e Node 22. As três começam ao mesmo tempo, rodam em paralelo e não interferem umas nas outras.

A configuração mínima é esta:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci && npm test

Observe principalmente o bloco strategy.matrix. node-version é um nome de variável definido por você, e o array [18, 20, 22] contém os valores que ela pode assumir. O GitHub Actions percorre esse array, atribui um valor por vez a matrix.node-version e cria a instância de job correspondente.

A sintaxe ${{ matrix.node-version }} referencia o valor atual. Na primeira execução, ele vale 18; na segunda, 20; e na terceira, 22.

Quando usei esse recurso pela primeira vez, fiquei com uma dúvida: as três tarefas rodam em série ou em paralelo? Por padrão, em paralelo. A cada push, o GitHub inicia três Runners simultaneamente, e as três tarefas rodam juntas. Nos meus testes, executar as três versões em série levava 15 minutos; com Matrix, caiu para 5 minutos, porque tudo roda ao mesmo tempo e a duração total passa a ser a da tarefa mais lenta.

Há, porém, um detalhe importante: a execução paralela consome mais minutos de Runner. Três tarefas significam três vezes o consumo. Se você usa a cota gratuita de 2.000 minutos por mês, uma matriz grande pode esgotá-la rapidamente. Voltaremos a esse ponto na seção sobre max-parallel.

Filtros exclude/include — controle preciso da matriz de testes

Quando você começa a combinar várias dimensões, o número de combinações de Matrix cresce muito rápido.

Por exemplo: três versões do Node [16, 18, 20] e três sistemas operacionais [ubuntu, windows, macos] resultam em 3 x 3 = 9 tarefas. Se acrescentarmos as suítes [unit, integration, e2e], chegamos a 27. É bastante? Para um projeto de código aberto, talvez não. Para uma equipe pequena, porém, minutos de Runner representam custo.

Além disso, algumas combinações não fazem sentido. O Node 16 já chegou ao EOL (End of Life), então testá-lo no Windows e no macOS pode ser puro desperdício. É aí que entra exclude, usado para remover essas combinações.

strategy:
  matrix:
    node-version: [16, 18, 20]
    os: [ubuntu-latest, windows-latest, macos-latest]
    exclude:
      - node-version: 16
        os: windows-latest
      - node-version: 16
        os: macos-latest

Em exclude, basta listar as combinações que devem ser removidas. A configuração acima elimina Node 16 + Windows e Node 16 + macOS. As 9 tarefas originais caem para 7, uma economia direta de 22% nos minutos de Runner.

include faz o movimento inverso: adiciona combinações específicas ou variáveis extras. Imagine que você queira testar uma versão experimental do Node 23, mas apenas no Ubuntu:

strategy:
  matrix:
    node-version: [16, 18, 20]
    os: [ubuntu-latest, windows-latest]
    include:
      - node-version: 23
        os: ubuntu-latest
        experimental: true

Há um detalhe: include não serve apenas para adicionar combinações. Ele também permite anexar variáveis extras a uma combinação específica. No exemplo, experimental: true existe apenas na tarefa do Node 23. É possível consultar essa variável em uma etapa posterior para que uma falha na versão experimental não interrompa o workflow inteiro:

- name: Run tests
  run: npm test
  continue-on-error: ${{ matrix.experimental == true }}

Já tropecei na prioridade entre exclude e include. O GitHub Actions primeiro aplica include para adicionar combinações e depois usa exclude para removê-las. Portanto, se uma combinação estiver em include e também for excluída em exclude, ela não será criada. Se você imaginar a ordem ao contrário, o resultado pode surpreender.

Em resumo:

  • exclude: remove combinações desnecessárias para economizar dinheiro e tempo
  • include: adiciona combinações especiais e permite definir variáveis extras para tratamentos diferentes

fail-fast e max-parallel — ajuste da estratégia de paralelismo

Matrix tem um comportamento padrão que pode passar despercebido: fail-fast: true.

O que isso significa? Quando qualquer tarefa da matriz falha, o GitHub Actions cancela imediatamente as demais tarefas ainda em execução. Se você tiver 10 tarefas paralelas e a terceira falhar após um minuto, as 7 restantes serão encerradas na hora.

Esse comportamento é bom ou ruim? Depende do cenário.

Em verificações de PR, fail-fast é útil. Se alguém envia código e os testes do Node 18 falham, não é necessário esperar as outras versões terminarem. O autor recebe o retorno imediatamente e pode corrigir o problema. Isso poupa tempo e recursos.

Já em testes Nightly ou regressões periódicas, fail-fast pode atrapalhar. O objetivo é obter um relatório completo: quais versões têm problemas e quais não têm. Se uma falha no Node 18 interromper tudo, você não saberá se o Node 20 apresenta o mesmo defeito. Nesse caso, configure fail-fast: false.

strategy:
  fail-fast: false
  matrix:
    node-version: [16, 18, 20]

max-parallel, por sua vez, controla quantas tarefas podem rodar simultaneamente. Por padrão, não há limite, e o GitHub tenta iniciar todas de uma vez. Em uma matriz grande, com 30 combinações, talvez você não queira ocupar todos os recursos de Runner de uma só vez.

strategy:
  fail-fast: true
  max-parallel: 6
  matrix:
    node-version: [16, 18, 20, 22]
    test-suite: [unit, integration, e2e]

A configuração acima permite no máximo 6 tarefas simultâneas. As 30 combinações são executadas em lotes de 6. A vantagem é manter o uso dos Runners sob controle, sem consumir toda a cota de uma vez. A desvantagem é o aumento do tempo total.

Preparei uma tabela simples para ajudar na escolha:

Cenáriofail-fastmax-parallelMotivo
Verificação de PRtruesem limiteRetorno rápido; uma falha interrompe tudo e economiza tempo
Teste Nightlyfalse4-6Coleta um relatório completo e identifica todos os bugs
Matriz grande (>20 combinações)true4Limita o consumo de recursos e evita esgotar a cota
Teste de versão experimentalfalsesem limiteA falha da versão experimental não prejudica a avaliação geral

Na maioria dos casos, o padrão fail-fast: true já resolve. Só é preciso mudar para false quando você necessita de um diagnóstico completo. Em matrizes pequenas, com até 10 tarefas, max-parallel faz pouca diferença; ele exige mais atenção apenas nas matrizes maiores.

Um alerta: max-parallel limita apenas quantas tarefas o GitHub Actions inicia ao mesmo tempo, e não a quantidade de Runners. Se você usa um self-hosted runner (Runner próprio), um valor baixo demais pode criar uma fila e atrasar toda a execução. Essa configuração faz mais sentido com Runners públicos.

Modelo completo — pipeline de testes paralelos em várias versões do Node.js

Até aqui, vimos os conceitos em partes. Agora vamos reuni-los em uma configuração completa, pronta para copiar.

O modelo inclui:

  • Três versões do Node (16, 18 e 20)
  • Duas suítes de testes (unit e integration)
  • Dois sistemas operacionais (Ubuntu e Windows)
  • Cache automático para acelerar a instalação de dependências
  • Exclusão dos testes do Node 16 no Windows (versão em EOL)
name: Multi-Version Test Matrix

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      max-parallel: 6
      matrix:
        node-version: [16, 18, 20]
        test-suite: [unit, integration]
        os: [ubuntu-latest, windows-latest]
        exclude:
          - node-version: 16
            os: windows-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ${{ matrix.test-suite }} tests
        run: npm run test:${{ matrix.test-suite }}

Alguns pontos merecem atenção:

runs-on: ${{ matrix.os }}: o sistema operacional também é dinâmico; cada tarefa seleciona o Runner correspondente à combinação da matriz.

cache: 'npm': esse recurso de cache já vem com setup-node. Ele usa o hash de package-lock.json para armazenar as dependências do npm e, na execução seguinte, reutiliza o conteúdo sem baixá-lo novamente. Nos meus testes, isso reduziu em mais de 50% o tempo de instalação.

fail-fast: false: o valor false é proposital, porque o objetivo do teste em várias versões é encontrar todos os problemas. Mesmo que uma versão falhe, as demais devem terminar.

npm run test:${{ matrix.test-suite }}: o exemplo pressupõe que o package.json contenha os comandos test:unit e test:integration. Matrix chama cada um deles conforme a combinação atual.

Quantas tarefas essa configuração cria?

3 versões x 2 suítes x 2 sistemas operacionais = 12 tarefas. Excluindo Node 16 + Windows, que aparece em 2 suítes, restam 10 tarefas.

Dados práticos: usei essa configuração em alguns projetos e, com o cache, o tempo de CI caiu de cerca de 25 minutos em série para aproximadamente 8 minutos. O ganho veio principalmente da execução paralela e do cache de dependências.

Se o projeto for maior e tiver mais combinações, você pode:

  • Aumentar o limite de max-parallel, por exemplo para 8 ou 10
  • Separar os testes e2e em outro job para que não atrasem o restante
  • Usar continue-on-error para lidar com falhas em versões experimentais

Copie este modelo para .github/workflows/test.yml no seu projeto e ajuste os números das versões e os nomes das suítes conforme necessário. Isso deve ser suficiente para colocar tudo em funcionamento.

Conclusão

Depois de todos esses detalhes, vale resumir os pontos principais:

Matrix transforma automaticamente um job em várias tarefas paralelas. A configuração é simples e o resultado é direto: a cada push, três versões rodam ao mesmo tempo, e o tempo de CI cai para a duração da tarefa mais lenta.

exclude/include oferecem controle preciso. Quando houver combinações demais, use exclude para remover as desnecessárias e economizar mais de 20% dos minutos de Runner. Já include adiciona combinações especiais e permite definir variáveis extras para tratamentos diferentes.

fail-fast usa true por padrão e interrompe as outras tarefas assim que uma falha. Para verificar PRs, o padrão funciona bem; em testes Nightly, use false para obter o relatório completo. max-parallel controla o limite de concorrência e só merece atenção especial em matrizes grandes.

Cache é indispensável. O recurso integrado de cache do setup-node exige apenas uma linha de configuração e pode cortar pela metade o tempo de instalação das dependências.

Como próximo passo, copie o modelo completo acima para o diretório .github/workflows/ do seu projeto e comece com testes em três versões do Node.js. Depois de confirmar que tudo funciona, amplie gradualmente para várias plataformas e suítes de testes. Se encontrar dificuldades com o cache, consulte o artigo da série “Estratégias de cache no GitHub Actions: acelere pipelines de CI/CD em até 5 vezes”, que traz técnicas mais detalhadas.

Quanto antes você configurar testes em várias versões, melhor. Não espere um problema chegar à produção para se arrepender — só de lembrar daquele bug da tela em branco, ainda me dá dor de cabeça.

Configurar testes em várias versões com GitHub Actions Matrix

Crie do zero um pipeline de testes paralelos para Node.js 16/18/20 nas plataformas Ubuntu e Windows

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Criar o arquivo de workflow

    Crie o arquivo `.github/workflows/test.yml` na raiz do projeto:

    • Verifique se a estrutura de diretórios está correta: `.github/workflows/`
    • O nome do arquivo é livre; recomenda-se `test.yml` ou `ci.yml`
  2. 2

    Step 2: Configurar os gatilhos

    Defina quando os testes serão executados:

    ```yaml
    on:
    push:
    branches: [main]
    pull_request:
    ```

    • Um push para a branch main dispara o workflow
    • A criação ou atualização de um PR também dispara o workflow
  3. 3

    Step 3: Definir a matriz

    Configure versões, plataformas e suítes de testes:

    ```yaml
    strategy:
    fail-fast: false
    max-parallel: 6
    matrix:
    node-version: [16, 18, 20]
    test-suite: [unit, integration]
    os: [ubuntu-latest, windows-latest]
    exclude:
    - node-version: 16
    os: windows-latest
    ```

    • fail-fast: false coleta todos os resultados dos testes
    • max-parallel: 6 controla o limite de concorrência
    • exclude remove combinações inválidas
  4. 4

    Step 4: Configurar as etapas de teste

    Defina as etapas que executarão os testes:

    ```yaml
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
    with:
    node-version: ${{ matrix.node-version }}
    cache: 'npm'
    - run: npm ci
    - run: npm run test:${{ matrix.test-suite }}
    ```

    • cache: 'npm' ativa o cache de dependências
    • As variáveis de matrix configuram as versões dinamicamente
  5. 5

    Step 5: Fazer commit e validar

    Envie o código para disparar os testes:

    • Faça commit na branch main ou crie um PR
    • Acompanhe a execução das tarefas paralelas na página do GitHub Actions
    • Confira se os testes passaram em todas as versões

FAQ

Uma matriz consome mais minutos de Runner?
Sim. Cada tarefa criada pela matriz é contabilizada separadamente. Por exemplo, 3 versões x 2 plataformas = 6 tarefas; se cada uma levar 5 minutos, o consumo total será de 30 minutos, e não 5. Em compensação, o tempo de espera cai bastante porque as tarefas são executadas em paralelo.
Devo configurar fail-fast como true ou false?
Depende do cenário:

• Verificação de PR: recomenda-se true (falha rápida e retorno imediato)
• Teste Nightly: recomenda-se false (coleta o relatório completo)
• Versão experimental: recomenda-se false (evita prejudicar a avaliação geral)

O padrão é true e atende à maioria dos cenários de PR.
Qual é executado primeiro: exclude ou include?
Primeiro, include adiciona as combinações; depois, exclude as remove. Portanto, se você adicionar uma combinação em include e também a excluir em exclude, ela não aparecerá. Vale a pena listar todas as combinações antes de decidir quais remover.
Qual valor usar em max-parallel?
Valores de referência:

• Matriz pequena (<10 combinações): não é necessário configurar; use o padrão
• Matriz média (10-20 combinações): configure entre 6 e 8
• Matriz grande (>20 combinações): configure entre 4 e 6

Um valor muito baixo aumenta o tempo total de espera; um valor alto demais pode esgotar todos os recursos de Runner de uma só vez.
Como usar cache para acelerar uma Matrix?
Basta adicionar o parâmetro `cache: 'npm'` em `actions/setup-node` para armazenar as dependências automaticamente. A action usa o hash de package-lock.json para determinar se há um cache disponível. Na prática, isso pode reduzir em mais de 50% o tempo de instalação das dependências. Se você usa pnpm ou yarn, altere para `cache: 'pnpm'` ou `cache: 'yarn'`.
Quais tipos de variável são aceitos em Matrix?
Há três tipos:

• Array: `[18, 20, 22]`
• Array de objetos: `[{name: 'a', value: 1}, {name: 'b', value: 2}]`
• String: deve ser adicionada com include

Recomenda-se usar arrays e arrays de objetos, que são mais fáceis de ler.

10 min de leitura · Publicado em: 8 abr 2026 · Atualizado em: 8 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog