GitHub Actions Matrix: guia prático para testes paralelos em várias plataformas e versões

No ano passado, um projeto open source me procurou porque o arquivo de configuração de CI já tinha passado de 800 linhas. Abri o arquivo e era uma sequência densa de definições de job repetidas: Node 16 rodando no Ubuntu, Node 16 rodando no Windows, Node 16 rodando no macOS… depois tudo de novo para Node 18, e mais uma vez para Node 20. Mudar um comando de teste? Tinha que alterar 12 pontos. Adicionar uma versão nova? Mais alguns minutos copiando e colando.
Foi aí que caiu a ficha: muita gente ainda mantém testes em várias versões e plataformas na mão.
O recurso Matrix do GitHub Actions, resumindo, serve para expandir automaticamente essas configurações repetidas. Você define alguns sistemas operacionais e algumas versões de runtime, e ele roda todas as combinações. Parece simples, mas na prática há várias pegadinhas: explosão de combinações que vira explosão de custo, uma falha cancelando o restante da execução, dúvidas sobre como excluir combinações específicas… eu já passei por todas.
Este artigo começa pela sintaxe mais básica do Matrix e avança, passo a passo, por controle preciso com exclude/include, escolha da estratégia fail-fast, limite de concorrência com max-parallel e geração dinâmica de Matrix. No final, deixo 5 templates de workflow prontos para copiar para projetos reais, cobrindo cenários de projetos pessoais a aplicações empresariais.
2. Conceito central do Matrix: expandir vários jobs com uma configuração
A lógica central do Matrix é simples: você define algumas dimensões, e o GitHub Actions calcula o produto cartesiano automaticamente.
Imagine que seu projeto precisa ser testado em Ubuntu, Windows e macOS, além de ser compatível com Node.js 18, 20 e 22. Na abordagem tradicional, você escreveria 9 jobs na mão, repetindo ambiente de execução, etapas de instalação e comandos de teste. Com Matrix, basta isto:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm test
Com essas 10 linhas, o GitHub Actions expande automaticamente 3 x 3 = 9 jobs paralelos. Cada job recebe valores diferentes de matrix.os e matrix.node e executa sua própria combinação.
Naquele projeto com um arquivo de 800 linhas, a refatoração com Matrix reduziu a configuração para cerca de 120 linhas, uma queda de mais de 60% no código. A manutenção também ficou mais barata: para adicionar uma versão, basta colocar mais um número no array, sem copiar e colar uma pilha de definições de job.
O que Matrix ajuda você a fazer:
- Gerar combinações de testes em várias plataformas e versões com uma só configuração
- Expandir automaticamente as configurações e evitar código repetido escrito à mão
- Excluir combinações problemáticas com exclude
- Adicionar casos com configurações especiais usando include
- Controlar a quantidade de jobs paralelos para equilibrar velocidade e custo
O que ele não resolve:
- Se os testes foram mal escritos, Matrix não salva
- Combinações demais podem estourar a conta; controlar o número de dimensões continua sendo sua responsabilidade
- Instalação lenta de dependências precisa ser tratada com estratégia de cache
Sinceramente, o Matrix em si não é difícil de entender. O difícil é usá-lo para resolver problemas reais de engenharia. Vamos começar pela sintaxe básica e desmontar a ideia peça por peça.
3. Sintaxe básica: como funciona a combinação os x version
A regra de combinação do Matrix é o produto cartesiano da matemática. Cada dimensão que você define é combinada com todas as outras.
Uma dimensão, N valores -> N jobs
Duas dimensões, M x N valores -> M x N jobs
Três dimensões, A x B x C valores -> A x B x C jobs
Vamos a um exemplo concreto. Suponha que você tenha um projeto Python que precisa testar Python 3.9, 3.10, 3.11 e 3.12 em Linux e Windows, além de validar PostgreSQL e MySQL:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
database: [postgresql, mysql]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Setup ${{ matrix.database }}
run: |
# Inicia o serviço de banco de dados correspondente
if [ "${{ matrix.database }}" = "postgresql" ]; then
docker run -d -p 5432:5432 postgres
else
docker run -d -p 3306:3306 mysql
fi
shell: bash
- run: pip install -r requirements.txt
- run: pytest
Essa configuração gera 2 x 4 x 2 = 16 jobs. Cada job roda em um ambiente independente, sem interferir nos outros.
Como acessar variáveis do Matrix:
${{ matrix.os }}— obtém o sistema operacional do job atual${{ matrix.python-version }}— obtém a versão do Python do job atual${{ matrix.database }}— obtém o tipo de banco de dados do job atual
Essas variáveis podem ser usadas em runs-on, steps, env e outros pontos para ajustar dinamicamente o comportamento de cada job.
Uma pegadinha comum: muita gente acha que o Matrix resolve automaticamente a instalação de dependências. Na verdade, cada job roda em um ambiente separado, então a instalação é repetida. Isso cria um problema: se instalar dependências leva 2 minutos, 16 jobs representam 32 minutos de espera se fossem executados em série.
Há duas formas de aliviar isso:
- Usar cache — armazenar o diretório de dependências do
pipounpmpara evitar downloads repetidos - Reduzir combinações — usar exclude para remover combinações desnecessárias
Eu já expliquei estratégia de cache em detalhes no artigo [Estratégia de cache no GitHub Actions: acelere pipelines CI/CD em 5 vezes], então não vou abrir esse tema aqui. Vamos focar em como usar exclude/include para controlar as combinações com precisão.
4. exclude/include: controle preciso das combinações de teste
Por padrão, Matrix faz a combinação completa de todas as dimensões. Em projetos reais, porém, você frequentemente encontra situações em que algumas combinações não precisam ser testadas ou exigem tratamento especial.
4.1 exclude: remover combinações inválidas
Quando eu mantinha um projeto Python, bati neste problema: a combinação Windows + Python 3.9 falhava sempre, porque uma dependência tinha problema de compatibilidade nessa versão no Windows. Mas o projeto era voltado principalmente para deploy em servidores Linux; Windows era suporte secundário, e não valia gastar tempo corrigindo aquele bug específico.
É aí que exclude entra:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
exclude:
- os: windows-latest
python-version: '3.9'
- os: macos-latest
python-version: '3.9'
Essa configuração remove os testes de Python 3.9 no Windows e no macOS. O Matrix original teria 3 x 4 = 12 jobs; depois de excluir 2, ficam 10.
Cenários típicos para usar exclude:
- Problemas conhecidos de compatibilidade — certa versão não roda em um sistema específico
- Limite de recursos — poucos runners self-hosted disponíveis e necessidade de reduzir combinações
- Casos periféricos — combinações que quase nenhum usuário usa e que não merecem tempo de CI
4.2 include: adicionar configurações especiais
include faz o oposto: ajuda a adicionar combinações extras ou a complementar combinações específicas com variáveis adicionais.
Por exemplo, você quer ativar relatório de cobertura apenas nos testes com Python 3.12, sem fazer isso nas outras versões:
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12']
include:
- python-version: '3.12'
coverage: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements.txt
- name: Run tests
run: |
if [ "${{ matrix.coverage }}" = "true" ]; then
pytest --cov=src --cov-report=xml
else
pytest
fi
shell: bash
Aqui, include faz duas coisas:
- Adiciona uma nova combinação — o teste com Python 3.12
- Complementa essa combinação com uma variável extra —
coverage: true
Cenários típicos para usar include:
- Testes com versões experimentais — por exemplo, Python 3.13 preview, testado só em um sistema
- Configurações especiais — algumas combinações precisam de variáveis de ambiente ou parâmetros extras
- Cobertura de casos periféricos — combinações raras adicionadas separadamente em vez de geradas pelo produto cartesiano completo
4.3 exclude e include podem ser usados juntos
Em projetos reais, é comum usar exclude e include ao mesmo tempo. Por exemplo: remover todas as combinações com Python 3.9, mas adicionar separadamente um teste mínimo com Ubuntu + Python 3.9:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.10', '3.11', '3.12']
exclude:
- python-version: '3.9'
include:
- os: ubuntu-latest
python-version: '3.9'
minimal: true
A ordem de execução dessa configuração é: gerar todas as combinações -> aplicar exclude -> aplicar include. Resultado final: Ubuntu roda 4 versões; Windows roda 3 versões, sem 3.9.
5. Estratégia fail-fast: falhar rápido vs depurar tudo
Jobs de Matrix têm um comportamento padrão: quando um job falha, os outros jobs ainda em execução são cancelados. Esse comportamento se chama fail-fast e vem ativado por padrão.
strategy:
fail-fast: true # valor padrão, pode ser omitido
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
5.1 Quando usar fail-fast: true (padrão)
Testes de PR — alguém abriu um PR e você quer saber rápido se há problema. Se um job falha, há boa chance de os outros também falharem pelo mesmo erro de código, então não vale gastar tempo continuando.
Cenários sensíveis a custo — a cota gratuita do GitHub Actions é limitada, e os recursos de runners self-hosted também. Falhar rápido pode economizar um bom dinheiro.
Minha preferência pessoal é: fail-fast: true em testes de PR e fail-fast: false nos testes completos da branch main.
5.2 Quando usar fail-fast: false
Fase de depuração — seus jobs de Matrix falham com frequência, e você quer saber exatamente quais combinações falharam e por quê. Com fail-fast: true, você só enxerga o primeiro job que falhou; os demais são cancelados.
Testes de compatibilidade — você está validando várias versões e plataformas e quer o resultado de cada combinação. Mesmo que uma versão tenha problema, isso não deve impedir a coleta de informação sobre as outras.
Relatório completo — você precisa gerar, ao final do CI, um relatório com o status de aprovação/falha de todas as combinações.
strategy:
fail-fast: false # deixa todos os jobs terminarem
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
5.3 Um caso real
No ano passado, ajudei um projeto a investigar um problema de CI. Os testes sempre falhavam em Ubuntu + Node 18, mas as outras combinações passavam. Como fail-fast estava ativado por padrão, cada execução mostrava apenas a falha em Ubuntu + Node 18 e cancelava os outros jobs. Depois, eles quiseram saber se Windows + Node 18 também tinha problema, mudaram para fail-fast: false e descobriram que no Windows estava tudo certo. O problema era só no Ubuntu, causado por compatibilidade de maiúsculas/minúsculas em caminhos de arquivo.
Minha recomendação: na fase de desenvolvimento e depuração, use fail-fast: false para enxergar todos os problemas; quando estiver estável, use fail-fast: true para economizar dinheiro e tempo.
6. max-parallel: controle de concorrência e otimização de custo
Jobs de Matrix rodam em paralelo por padrão, e o GitHub inicia o máximo de jobs simultâneos possível. Para repositórios públicos, o limite de concorrência dos runners hospedados pelo GitHub é 20; para repositórios privados, contas gratuitas têm limite de 2.
Mas às vezes você precisa controlar manualmente a concorrência. É para isso que serve max-parallel.
6.1 Quando limitar a concorrência
Runner self-hosted com poucos recursos — seu servidor de runner tem 4 núcleos e 8 GB de RAM; rodar 8 jobs ao mesmo tempo pode derrubar a máquina.
Rate limit de serviço externo — seus testes chamam uma API de terceiros com limite de QPS, e concorrência alta pode gerar bloqueio.
Limite do pool de conexões do banco — seus testes conectam a um banco com pool de 10 conexões; jobs demais podem esgotar o pool.
strategy:
max-parallel: 4 # no máximo 4 jobs ao mesmo tempo
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
Essa configuração gera 6 jobs, mas no máximo 4 rodam ao mesmo tempo. Quando um termina, o próximo começa.
6.2 Um exemplo de cálculo de custo
Suponha que seu projeto precise testar 3 sistemas x 4 versões do Node = 12 jobs a cada execução de CI. Cada job leva, em média, 10 minutos.
Sem limite de concorrência (assumindo runners suficientes):
- 12 jobs rodam ao mesmo tempo
- Tempo total aproximado: 10 minutos
- Tempo total de computação = 12 x 10 = 120 minutos
Com max-parallel: 4:
- 12 jobs rodam em 3 lotes
- Tempo total aproximado: 30 minutos
- Tempo total de computação = 12 x 10 = 120 minutos (não muda)
Percebeu? max-parallel não reduz o tempo total de computação; ele aumenta a duração total da execução. Então por que usar?
Por causa de custo de pico de concorrência e limites de recursos.
O GitHub Actions cobra por minutos, mas se você usa runner self-hosted ou um provedor de nuvem que cobra pelo pico, controlar a concorrência é importante. Por exemplo: com 12 jobs simultâneos, seu banco precisa de 12 conexões; em lotes, precisa de apenas 4.
Minha prática:
- Repositórios públicos com runners hospedados pelo GitHub: não mexa em
max-parallel; deixe o GitHub agendar - Repositórios privados com cota gratuita: use
max-parallel: 2e rode mais devagar, sem estourar a cota - Runner self-hosted: limite
max-parallelconforme a máquina; em 4 núcleos, eu começaria com 2 a 4 jobs paralelos
7. Matrix dinâmico: técnica avançada com fromJSON
Até aqui, os exemplos de Matrix foram estáticos: você escreve no YAML quais versões serão testadas. Mas em alguns cenários, as combinações precisam ser geradas dinamicamente com base nas mudanças de código.
Imagine um monorepo com vários serviços, cada um com sua própria configuração de teste. Você quer testar apenas os serviços afetados pelo commit atual, não todos os serviços a cada execução.
7.1 Workflow em duas etapas para implementar matrix dinâmico
O GitHub Actions não tem uma sintaxe direta para “matrix dinâmico”, mas você pode gerar a configuração de matrix em um job e passá-la para outro. A chave é a função fromJSON().
jobs:
# Primeira etapa: detecta serviços alterados e gera a configuração do matrix
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # precisa obter o commit anterior
- name: Detect changed services
id: set-matrix
run: |
# Obtém os arquivos alterados neste commit
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
# Verifica quais serviços foram alterados
SERVICES="[]"
if echo "$CHANGED_FILES" | grep -q "services/auth/"; then
SERVICES=$(echo $SERVICES | jq '. + ["auth"]')
fi
if echo "$CHANGED_FILES" | grep -q "services/api/"; then
SERVICES=$(echo $SERVICES | jq '. + ["api"]')
fi
if echo "$CHANGED_FILES" | grep -q "services/web/"; then
SERVICES=$(echo $SERVICES | jq '. + ["web"]')
fi
# Se nenhum serviço mudou, testa todos por padrão
if [ "$SERVICES" = "[]" ]; then
SERVICES='["auth", "api", "web"]'
fi
echo "matrix={\"service\":$(echo $SERVICES)}" >> $GITHUB_OUTPUT
# Segunda etapa: usa o matrix gerado dinamicamente
test:
needs: detect
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Test ${{ matrix.service }}
run: |
cd services/${{ matrix.service }}
npm install
npm test
Esse workflow funciona assim:
- O job
detectverifica quais diretórios foram alterados neste commit - Com base nos diretórios alterados, gera uma configuração de matrix em JSON
- O job
testusafromJSON()para analisar essa configuração e criar os jobs correspondentes
7.2 Casos típicos para matrix dinâmico
Monorepo — testar apenas os serviços alterados e economizar tempo de CI
Deploy sob demanda — detectar mudanças em Dockerfile e construir/publicar apenas imagens atualizadas
Otimização de testes de Matrix — escolher combinações com base nos tipos de arquivo alterados, como rodar a matriz completa de versões apenas quando package.json muda
Pegadinhas que eu já encontrei:
fromJSON()só pode ser usado no valor destrategy.matrix, não em outros lugares- O JSON gerado precisa ser um matrix válido, como
{"service": ["auth", "api"]} - Se o matrix gerado ficar vazio, o workflow falha diretamente; sempre inclua um valor padrão
8. Biblioteca prática: 5 exemplos de workflow prontos para produção
A seguir estão 5 templates de workflow que você pode copiar diretamente, cobrindo de projetos pessoais a aplicações empresariais.
8.1 Template 1: testes Node.js em várias versões (básico)
Cenário adequado: biblioteca ou aplicação Node.js que precisa ser compatível com várias versões do Node
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 20, 22, 23]
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'
- run: npm ci
- run: npm run build --if-present
- run: npm test
- name: Upload coverage
if: matrix.node-version == 22
uses: codecov/codecov-action@v4
Pontos importantes:
- Use
npm ciem vez denpm installpara garantir versões travadas das dependências - Faça upload de cobertura apenas no Node 22 para evitar envios duplicados
cache: 'npm'ativa o cache do npm e acelera a instalação de dependências
8.2 Template 2: testes Python em várias plataformas e versões (intermediário)
Cenário adequado: projeto Python que precisa testar entre plataformas e versões
name: Python CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11', '3.12']
exclude:
- os: windows-latest
python-version: '3.10' # problema conhecido de compatibilidade
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: pytest -v
- name: Lint check
run: |
pip install ruff
ruff check .
Pontos importantes:
- Use
excludepara remover combinações com problemas conhecidos cache: 'pip'acelera a instalação de dependências do pip- Integre uma ferramenta de lint como ruff
8.3 Template 3: controle preciso com exclude/include (avançado)
Cenário adequado: quando você precisa controlar combinações com precisão, excluir algumas e adicionar testes especiais
name: Advanced Matrix
on:
push:
branches: [main]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.10', '3.11', '3.12']
exclude:
# Remove Windows + Python 3.10 (problema conhecido)
- os: windows-latest
python-version: '3.10'
include:
# Adiciona um teste experimental: Ubuntu + Python 3.13 preview
- os: ubuntu-latest
python-version: '3.13-dev'
experimental: true
# Adiciona relatório de cobertura ao Python 3.12
- python-version: '3.12'
coverage: true
continue-on-error: ${{ matrix.experimental == true }}
steps:
- uses: actions/checkout@v4
- name: Setup Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- run: pip install -r requirements.txt
- name: Run tests
run: |
if [ "${{ matrix.coverage }}" = "true" ]; then
pytest --cov=src --cov-report=xml
else
pytest
fi
shell: bash
Pontos importantes:
continue-on-errorfaz testes experimentais falharem sem afetar o status geralincludepode adicionar novas combinações e complementar variáveis ao mesmo temposhell: bashgarante comandos consistentes no Windows e no Linux
8.4 Template 4: matrix dinâmico + caching (avançado)
Cenário adequado: monorepo em que as combinações de teste são geradas dinamicamente conforme mudanças nos arquivos
name: Dynamic Matrix CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detect changed packages
id: set-matrix
run: |
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
PACKAGES="[]"
for dir in packages/*/; do
pkg=$(basename $dir)
if echo "$CHANGED_FILES" | grep -q "^packages/$pkg/"; then
PACKAGES=$(echo $PACKAGES | jq ". + [\"$pkg\"]")
fi
done
# Se não houver mudança, testa todos os pacotes
if [ "$PACKAGES" = "[]" ]; then
PACKAGES='["core", "utils", "cli"]'
fi
echo "matrix={\"package\":$(echo $PACKAGES)}" >> $GITHUB_OUTPUT
test:
needs: detect
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.detect.outputs.matrix) }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build --if-present
- name: Test ${{ matrix.package }}
run: |
cd packages/${{ matrix.package }}
npm test
Pontos importantes:
fetch-depth: 2obtém o commit anterior para comparação- Use
jqpara manipular arrays JSON - Forneça um valor padrão quando não houver mudanças, evitando erro de matrix vazio
8.5 Template 5: runner self-hosted + max-parallel (empresarial)
Cenário adequado: runner self-hosted com controle rígido de concorrência e recursos
name: Enterprise CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: [self-hosted, linux, x64]
strategy:
fail-fast: true
max-parallel: 4
matrix:
java-version: [11, 17, 21]
database: [postgresql, mysql]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3306:3306
options: >-
--health-cmd "mysqladmin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Java ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: 'maven'
- name: Run tests with ${{ matrix.database }}
env:
DB_TYPE: ${{ matrix.database }}
DB_HOST: localhost
DB_PORT: ${{ matrix.database == 'postgresql' && 5432 || 3306 }}
run: mvn test -Dspring.profiles.active=${{ matrix.database }}
- name: Archive test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.java-version }}-${{ matrix.database }}
path: target/surefire-reports
Pontos importantes:
runs-on: [self-hosted, linux, x64]especifica as labels do runner self-hostedmax-parallel: 4limita a concorrência e protege o servidor de runner- Use
servicespara iniciar containers de banco de dados durante os testes if: always()garante upload dos resultados mesmo quando os testes falham
9. Armadilhas comuns e boas práticas
Depois de usar Matrix por bastante tempo, já caí em várias armadilhas. Estas são as mais comuns.
9.1 Armadilha 1: explosão de combinações
A configuração mais exagerada que já vi tinha 4 sistemas operacionais x 5 versões de runtime x 3 bancos de dados x 2 estratégias de cache = 120 jobs. Cada execução de CI levava 45 minutos, e a conta subiu direto.
Como resolver:
- Rode o Matrix completo apenas na branch main; em PRs, rode só combinações principais
- Use
excludepara remover casos periféricos - Questione a necessidade de cada dimensão: você realmente precisa testar 4 sistemas operacionais?
# PR testa apenas combinações essenciais
on:
pull_request:
branches: [main]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest] # PR testa só Ubuntu
node: [20] # PR testa só Node 20
9.2 Armadilha 2: fail-fast atrapalha a depuração
O padrão fail-fast: true incomoda bastante na fase de depuração: um job falha, todos os outros são cancelados, e você não vê o relatório completo.
Como resolver: durante a depuração, mude manualmente para fail-fast: false; depois de corrigir, volte ao comportamento desejado.
Ou controle com uma variável de ambiente:
strategy:
fail-fast: ${{ github.event_name == 'pull_request' }}
9.3 Armadilha 3: falta de caching
Matrix executa o mesmo job várias vezes. Se cada execução reinstala dependências do zero, o custo de tempo fica alto. Já testei 12 combinações em que cada instalação levava 2 minutos; só instalar dependências consumia 24 minutos.
Como resolver: use cache do GitHub Actions ou uma action dedicada de cache.
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # essencial: ativa cache do npm
9.4 Resumo de boas práticas
Prática 1: Matrix pequeno em PR + Matrix completo na main
jobs:
test:
strategy:
matrix:
# PR testa apenas combinações principais
${{ github.event_name == 'pull_request' && fromJSON('{"os":["ubuntu-latest"],"node":[20]}') || fromJSON('{"os":["ubuntu-latest","windows-latest","macos-latest"],"node":[18,20,22]}') }}
Prática 2: usar exclude para combinações com problemas conhecidos
Ao encontrar problema de compatibilidade em uma combinação específica, pule com exclude, registre um TODO e corrija depois.
Prática 3: combinar com caching para reduzir tempo de instalação
Instalar dependências é uma das partes mais demoradas de cada job. Um bom cache pode reduzir esse tempo de minutos para segundos.
Prática 4: adicionar names significativos às combinações
O nome padrão do job fica em um formato como test (ubuntu-latest, 20). Você pode personalizar com name:
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
Assim, cada job fica mais fácil de identificar na interface do GitHub Actions.
10. Conclusão
GitHub Actions Matrix é uma ferramenta forte para testes em várias plataformas e versões. O núcleo se resume a quatro movimentos: definir dimensões, controlar combinações, gerenciar concorrência e gerar configurações dinamicamente.
Já vi muitos projetos ainda mantendo configurações repetidas de CI na mão, com um comando precisando ser alterado em mais de dez lugares. Matrix reduz centenas de linhas para algumas dezenas e baixa o custo de manutenção imediatamente.
Recapitulando os pontos principais:
- Sintaxe básica:
matrix.osematrix.nodeexpandem pelo produto cartesiano - Controle preciso:
excluderemove combinações inválidas;includeadiciona configurações especiais - Escolha de estratégia: ajuste
fail-fastao cenário; false para depurar, true para produção - Limite de concorrência:
max-parallelprotege runners self-hosted e controla custo - Geração dinâmica:
fromJSON()permite testes sob demanda e economiza recursos de CI
Os 5 templates deste artigo podem ser copiados diretamente para o seu projeto, desde o teste mais simples em várias versões do Node.js até uma configuração empresarial com runner self-hosted.
Se você está começando com Matrix, recomendo partir do Template 1. Depois que ele estiver rodando, adicione exclude e include; só então avance para geração dinâmica. Vá por etapas, sem complicar tudo logo de início.
Se tiver dúvidas, deixe um comentário ou consulte a documentação oficial do GitHub Actions para mais detalhes. Se você tiver aprendizados práticos com Matrix, também vale compartilhar.
Configurar testes GitHub Actions Matrix em várias plataformas e versões
Configure uma estratégia Matrix do zero para automatizar testes entre plataformas e versões.
⏱️ Estimated time: 30 min
- 1
Step 1: Definir as dimensões do Matrix
Adicione a configuração strategy.matrix ao job do workflow:
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
```
Isso gera 2 x 3 = 6 jobs paralelos. - 2
Step 2: Usar variáveis do Matrix
Referencie variáveis de matrix em runs-on e steps:
```yaml
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
```
Cada job recebe automaticamente os valores correspondentes de os e node. - 3
Step 3: Excluir combinações específicas (opcional)
Use exclude para remover combinações com problemas conhecidos:
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18
```
A combinação Windows + Node 18 é removida, gerando 5 jobs no final. - 4
Step 4: Configurar a estratégia de falha
Escolha a estratégia fail-fast de acordo com o cenário:
- Testes de PR: fail-fast: true (falha rápido e economiza custo)
- Fase de depuração: fail-fast: false (mostra todas as falhas)
- Branch main: fail-fast: false (relatório completo)
```yaml
strategy:
fail-fast: false
matrix:
# ...
``` - 5
Step 5: Limitar a concorrência (opcional)
Ao usar runner self-hosted ou recursos limitados, limite a concorrência:
```yaml
strategy:
max-parallel: 4
matrix:
# ...
```
No máximo 4 jobs rodam ao mesmo tempo, evitando sobrecarga no runner.
FAQ
Existe limite para o número de combinações do Matrix?
Qual é o valor padrão de fail-fast?
Qual é a ordem de execução de exclude e include?
Onde o fromJSON() pode ser usado em um matrix dinâmico?
max-parallel reduz o tempo total de computação?
Jobs do Matrix podem compartilhar cache?
Como adicionar um nome personalizado a jobs do Matrix?
• name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
Assim, a interface do GitHub Actions mostra nomes amigáveis como "Test (ubuntu-latest, Node 20)", facilitando identificar cada job.
1 min de leitura · Publicado em: 28 abr 2026 · Atualizado em: 14 jul 2026
Guia completo GitHub Actions
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
GitHub Actions Matrix: testes paralelos em várias versões na prática
Tutorial prático de GitHub Actions Matrix, com sintaxe básica, filtros exclude/include, otimização de fail-fast e controle de recursos com max-parallel, além de um pipeline completo para testes paralelos em várias versões.
Parte 4 de 10
Próximo
Estratégias de deploy com GitHub Actions: de VPS a plataformas de nuvem
Compare deploy em VPS por SSH, plataformas como Vercel, Cloudflare e Netlify e arquitetura híbrida com GitHub Actions, workflows e solução de falhas comuns.
Parte 6 de 10




Comentários
Entre com GitHub para comentar