Matrix de GitHub Actions: guía práctica de pruebas paralelas multiplataforma y multiversión

El año pasado un proyecto open source me contactó: su archivo de configuración de CI ya superaba las 800 líneas. Lo abrí y estaba lleno de definiciones de jobs repetidas — Node 16 en Ubuntu, Node 16 en Windows, Node 16 en macOS… y luego lo mismo con Node 18 y Node 20. ¿Cambiar un comando de test? Había que tocar 12 sitios. ¿Añadir una versión nueva? Copiar y pegar durante un cuarto de hora.
En ese momento me di cuenta de que mucha gente sigue manteniendo pruebas multiplataforma y multiversión a mano.
La función Matrix de GitHub Actions, en esencia, te ayuda a expandir automáticamente esas configuraciones repetidas. Defines unos sistemas operativos y unas versiones de runtime, y la plataforma ejecuta todas las combinaciones por ti. Suena sencillo, pero al usarla de verdad aparecen bastantes trampas: explosión de combinaciones que dispara la factura, un fallo que tumba toda la cadena, no saber cómo excluir combinaciones concretas… Yo me he topado con todas.
Este artículo empieza por la sintaxis básica de Matrix y te lleva paso a paso por exclude/include para un control preciso, la elección de estrategia fail-fast, la limitación de concurrencia con max-parallel y la generación dinámica de Matrix. Al final tienes 5 plantillas de workflow listas para copiar en tu proyecto, desde proyectos personales hasta escenarios empresariales.
2. Conceptos clave de Matrix: expandir múltiples tareas con un clic
La lógica central de Matrix es simple: defines varias dimensiones y GitHub Actions hace automáticamente el producto cartesiano.
Por ejemplo, tu proyecto debe probarse en Ubuntu, Windows y macOS, y ser compatible con Node.js 18, 20 y 22. Con el enfoque tradicional escribirías 9 jobs a mano, repitiendo entorno, instalación y comandos de test. Con Matrix basta con esto:
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
Estas 10 líneas se expanden en 3 x 3 = 9 tareas en paralelo. Cada tarea recibe distintos valores de matrix.os y matrix.node y ejecuta todas las combinaciones.
El proyecto de 800 líneas que mencioné quedó en unos 120 tras refactorizar con Matrix — más del 60% menos de código. El mantenimiento también baja: añadir una versión nueva es meter un número en el array, sin copiar montones de jobs.
Lo que Matrix puede hacer por ti:
- Generar combinaciones de prueba multiplataforma y multiversión con un clic
- Expandir toda la configuración automáticamente y evitar código repetido
- Excluir combinaciones problemáticas con exclude
- Añadir casos con configuración especial mediante include
- Controlar la concurrencia para equilibrar velocidad y coste
Lo que no resuelve:
- Tests mal escritos — Matrix no los arregla
- Demasiadas combinaciones y factura descontrolada — debes limitar tú las dimensiones
- Instalación lenta de dependencias — hace falta estrategia de caché
La verdad es que Matrix no cuesta entenderlo; lo difícil es usarlo para problemas reales de ingeniería. Empecemos por la sintaxis básica.
3. Sintaxis básica: el principio de combinación os x versión
Las reglas de combinación de Matrix son el producto cartesiano de las matemáticas. Cada dimensión se combina con todas las demás.
Una dimensión, N valores -> N tareas
Dos dimensiones, M x N valores -> M x N tareas
Tres dimensiones, A x B x C valores -> A x B x C tareas
Un ejemplo concreto. Tienes un proyecto Python que debe probarse en Linux y Windows con Python 3.9, 3.10, 3.11 y 3.12, y además con PostgreSQL y 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: |
# Iniciar el servicio de base de datos correspondiente
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
Esta configuración genera 2 x 4 x 2 = 16 tareas. Cada una corre en un entorno independiente, sin interferencias.
Formas de acceder a las variables de Matrix:
${{ matrix.os }}— sistema operativo de la tarea actual${{ matrix.python-version }}— versión de Python de la tarea actual${{ matrix.database }}— tipo de base de datos de la tarea actual
Estas variables puedes usarlas en runs-on, steps, env, etc., para ajustar dinámicamente el comportamiento de cada tarea.
Una trampa habitual: mucha gente cree que Matrix resuelve solo la instalación de dependencias. En realidad cada tarea es un entorno independiente y la instalación se repite. Si instalar dependencias tarda 2 minutos, 16 tareas implican 32 minutos de espera (suponiendo ejecución en serie).
Hay dos soluciones:
- Usar caché — cachear directorios de
piponpmy evitar descargas repetidas - Reducir combinaciones — excluir pruebas innecesarias con exclude
La estrategia de caché la expliqué en detalle en [Estrategia de caché en GitHub Actions: acelera tu pipeline CI/CD 5 veces]; aquí nos centramos en exclude/include para controlar con precisión las combinaciones de prueba.
4. exclude/include: control preciso de las combinaciones de prueba
Matrix hace por defecto todas las permutaciones de las dimensiones, pero en proyectos reales a menudo hay combinaciones que no hace falta probar o que requieren un tratamiento especial.
4.1 exclude: excluir combinaciones inválidas
Mantuve un proyecto Python donde Windows + Python 3.9 fallaba siempre: una dependencia tenía problemas de compatibilidad en Windows con 3.9. El proyecto apuntaba sobre todo a servidores Linux; Windows era soporte secundario, no merecía arreglar ese bug concreto.
Ahí entra exclude:
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'
Esta configuración excluye las pruebas de Python 3.9 en Windows y macOS. De 3 x 4 = 12 tareas pasas a 10.
Casos típicos de exclude:
- Problemas de compatibilidad conocidos — una versión no funciona en un sistema concreto
- Límites de recursos — pocos runners self-hosted, hay que reducir combinaciones
- Escenarios marginales — combinaciones que casi nadie usa, no merecen tiempo de CI
4.2 include: añadir configuraciones especiales
include hace lo contrario: añade combinaciones extra o variables adicionales para combinaciones concretas.
Por ejemplo, quieres informes de cobertura solo en las pruebas de Python 3.12:
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
Aquí include hace dos cosas:
- Añade una combinación — prueba de Python 3.12
- Añade una variable extra —
coverage: true
Casos típicos de include:
- Pruebas de versiones experimentales — p. ej. vista previa de Python 3.13 solo en un sistema
- Configuración especial — variables de entorno o parámetros extra en ciertas combinaciones
- Cobertura de casos marginales — combinaciones poco frecuentes, añadidas aparte en lugar de por permutación total
4.3 exclude e include pueden usarse juntos
En la práctica sueles necesitar ambos. Por ejemplo: excluir todas las combinaciones con Python 3.9, pero añadir una prueba mínima 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
El orden es: generar todas las combinaciones -> aplicar exclude -> aplicar include. Resultado: 4 versiones en Ubuntu, 3 en Windows (sin 3.9).
5. Estrategia fail-fast: fallo rápido frente a depuración completa
Por defecto, si una tarea de Matrix falla, las demás en ejecución se cancelan. Eso es fail-fast, activado por defecto.
strategy:
fail-fast: true # Valor por defecto, se puede omitir
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
5.1 Cuándo usar fail-fast: true (por defecto)
Pruebas en PR — un desarrollador abre un PR y quieres saber rápido si los tests pasan. Si una tarea falla, las demás probablemente también (mismo código), no tiene sentido seguir gastando tiempo.
Escenarios sensibles al coste — la cuota gratuita de GitHub Actions es limitada, y los runners self-hosted también. Fallar rápido ahorra dinero.
Mi hábito: fail-fast: true en PR, fail-fast: false en pruebas completas de main.
5.2 Cuándo usar fail-fast: false
Fase de depuración — las tareas de Matrix fallan a menudo y quieres ver qué combinaciones fallan y por qué. Con fail-fast: true solo ves la primera; el resto se cancela.
Pruebas de compatibilidad — pruebas multiplataforma y multiversión donde necesitas el resultado de cada combinación. Un fallo en una versión no debe ocultar el resto.
Informes completos — al terminar el CI necesitas un informe con el estado de todas las combinaciones.
strategy:
fail-fast: false # Dejar que todas las tareas terminen
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
5.3 Un caso real
El año pasado ayudé a depurar el CI de un proyecto: los tests fallaban siempre en Ubuntu + Node 18, pero el resto pasaba. Con fail-fast por defecto solo veían ese fallo y se cancelaban las demás tareas. Querían saber si Windows + Node 18 también fallaba, cambiaron a fail-fast: false, y Windows estaba bien — solo Ubuntu fallaba. Al final era un problema de mayúsculas/minúsculas en rutas de archivo.
Mi consejo: en depuración usa fail-fast: false para ver todos los problemas; en operación estable, fail-fast: true para ahorrar tiempo y dinero.
6. max-parallel: control de concurrencia y optimización de costes
Las tareas de Matrix se ejecutan en paralelo por defecto; GitHub lanza tantas simultáneas como pueda. En repos públicos el límite de runners hospedados es 20; en privados, la cuenta gratuita suele tener límite 2.
A veces necesitas limitar la concurrencia manualmente: ahí entra max-parallel.
6.1 Cuándo limitar la concurrencia
Recursos limitados en runners self-hosted — tu servidor tiene 4 núcleos y 8 GB; 8 tareas a la vez lo saturan.
Rate limiting de servicios externos — los tests llaman APIs de terceros con límite de QPS; demasiada concurrencia te bloquea.
Límite del pool de conexiones a base de datos — el pool tiene 10 conexiones; demasiadas tareas las agotan.
strategy:
max-parallel: 4 # Como máximo 4 tareas a la vez
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
Esta configuración genera 6 tareas, pero solo 4 corren a la vez. Termina una, arranca la siguiente.
6.2 Un ejemplo de cálculo de coste
Supón un proyecto donde cada CI prueba 3 sistemas x 4 versiones de Node = 12 tareas. Cada tarea tarda de media 10 minutos.
Sin límite de concurrencia (runners suficientes):
- 12 tareas en paralelo
- Tiempo total ~10 minutos
- Tiempo de cómputo total = 12 x 10 = 120 minutos
Con max-parallel: 4:
- 12 tareas en 3 lotes
- Tiempo total ~30 minutos
- Tiempo de cómputo total = 12 x 10 = 120 minutos (igual)
¿Ves? max-parallel no reduce el tiempo de cómputo total, solo alarga la duración. ¿Por qué usarlo entonces?
Por el coste de pico de concurrencia y las limitaciones de recursos.
GitHub Actions factura por minutos, pero con runners self-hosted o proveedores que cobran por pico, controlar la concurrencia importa. Doce tareas a la vez pueden exigir 12 conexiones a la base de datos; en lotes de 4, bastan 4.
Mi experiencia práctica:
- Repos públicos con runners de GitHub: no te preocupes por
max-parallel, deja que programe solo - Repos privados con cuota gratuita:
max-parallel: 2, despacio, sin pasar la cuota - Runners self-hosted: limita según el servidor; con 4 núcleos, 2-4 concurrentes
7. Matrix dinámico: técnicas avanzadas con fromJSON
Hasta aquí Matrix era estático: versiones fijas en el YAML. En algunos escenarios necesitas generar combinaciones según los cambios del código.
Por ejemplo, un monorepo con varios servicios, cada uno con su configuración de tests. Quieres probar solo los servicios tocados en el commit, no todos.
7.1 Workflow en dos pasos para Matrix dinámico
GitHub Actions no tiene sintaxis directa de matrix dinámico, pero un job puede generar la configuración y pasarla a otro. La clave es fromJSON().
jobs:
# Paso 1: detectar servicios modificados y generar la matrix
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Necesitas el commit anterior
- name: Detect changed services
id: set-matrix
run: |
# Archivos modificados en este commit
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
# Qué servicios cambiaron
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
# Si no hay cambios, probar todos por defecto
if [ "$SERVICES" = "[]" ]; then
SERVICES='["auth", "api", "web"]'
fi
echo "matrix={\"service\":$(echo $SERVICES)}" >> $GITHUB_OUTPUT
# Paso 2: usar la matrix generada dinámicamente
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
Cómo funciona:
- El job
detectve qué directorios cambiaron en el commit - Genera un JSON de matrix según esos cambios
- El job
testusafromJSON()para parsearlo y crear las tareas
7.2 Casos típicos de matrix dinámico
Monorepo — probar solo servicios modificados, ahorrar tiempo de CI
Despliegue bajo demanda — detectar cambios en Dockerfile y construir solo imágenes actualizadas
Optimización de pruebas en matrix — decidir combinaciones según tipo de archivo (p. ej. matrix completa solo si cambia package.json)
Trampas que he pisado:
fromJSON()solo vale en el valor destrategy.matrix, no en otros sitios- El JSON debe ser un matrix válido, p. ej.
{"service": ["auth", "api"]} - Si la matrix queda vacía, el workflow falla — pon valores por defecto
8. Biblioteca de plantillas: 5 workflows listos para producción
Aquí van 5 plantillas que puedes copiar directamente, desde proyectos personales hasta uso empresarial.
8.1 Plantilla 1: pruebas multiversión en Node.js (básico)
Escenario: librería o app Node.js compatible con varias versiones de 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
Puntos clave:
- Usa
npm cien lugar denpm installpara versiones bloqueadas - Sube cobertura solo en Node 22, evita duplicados
cache: 'npm'acelera la instalación
8.2 Plantilla 2: Python multiplataforma y multiversión (intermedio)
Escenario: proyecto Python con pruebas cruzadas de SO y versión
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 de compatibilidad conocido
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 .
Puntos clave:
excludepara combinaciones problemáticas conocidascache: 'pip'acelera pip- Integración con ruff para lint
8.3 Plantilla 3: control preciso con exclude/include (avanzado)
Escenario: control fino de combinaciones, exclusiones y pruebas especiales
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:
# Excluir Windows + Python 3.10 (problema conocido)
- os: windows-latest
python-version: '3.10'
include:
# Prueba experimental: Ubuntu + vista previa Python 3.13
- os: ubuntu-latest
python-version: '3.13-dev'
experimental: true
# Cobertura en 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
Puntos clave:
continue-on-errorpara que pruebas experimentales no tumben el estado globalincludepuede añadir combinaciones y variables a la vezshell: bashunifica comandos en Windows y Linux
8.4 Plantilla 4: matrix dinámico + caching (avanzado)
Escenario: monorepo con combinaciones según archivos modificados
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
# Sin cambios, probar todos los paquetes
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
Puntos clave:
fetch-depth: 2para comparar con el commit anteriorjqpara manipular arrays JSON- Valores por defecto si no hay cambios, evita matrix vacía
8.5 Plantilla 5: runner self-hosted + max-parallel (empresarial)
Escenario: runners self-hosted con control estricto de concurrencia y 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
Puntos clave:
runs-on: [self-hosted, linux, x64]etiquetas del runner propiomax-parallel: 4protege el servidorserviceslevanta contenedores de BD de pruebaif: always()sube resultados aunque fallen los tests
9. Trampas habituales y buenas prácticas
Llevo tiempo con Matrix y he caído en bastantes. Estas son las más comunes.
9.1 Trampa 1: explosión de combinaciones
La configuración más extrema que vi: 4 SO x 5 runtimes x 3 bases de datos x 2 esquemas de caché = 120 tareas. Cada CI tardaba 45 minutos y la factura explotó.
Soluciones:
- Matrix completo solo en main; en PR, combinaciones clave
excludepara escenarios marginales- ¿De verdad necesitas 4 sistemas operativos en cada dimensión?
# PR solo prueba lo esencial
on:
pull_request:
branches: [main]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest] # PR solo Ubuntu
node: [20] # PR solo Node 20
9.2 Trampa 2: fail-fast dificulta la depuración
fail-fast: true por defecto molesta al depurar: un fallo cancela el resto y no ves el informe completo.
Solución: pon fail-fast: false mientras depuras y vuelve a true después.
O controla con expresiones:
strategy:
fail-fast: ${{ github.event_name == 'pull_request' }}
9.3 Trampa 3: falta de caching
Matrix repite el mismo job muchas veces; reinstalar dependencias cada vez cuesta caro. Una vez probé 12 combinaciones, 2 minutos de instalación cada una: 24 minutos solo en instalar.
Solución: caché de GitHub Actions o actions dedicadas.
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # Clave: caché npm
9.4 Resumen de buenas prácticas
Práctica 1: Matrix pequeño en PR + Matrix completo en main
jobs:
test:
strategy:
matrix:
# PR solo combinaciones clave
${{ github.event_name == 'pull_request' && fromJSON('{"os":["ubuntu-latest"],"node":[20]}') || fromJSON('{"os":["ubuntu-latest","windows-latest","macos-latest"],"node":[18,20,22]}') }}
Práctica 2: exclude para combinaciones problemáticas conocidas
Ante un fallo de compatibilidad en una combinación concreta, usa exclude, anota un TODO y arréglalo después.
Práctica 3: caching para reducir tiempo de instalación
Instalar dependencias en cada job es de lo más lento; un buen caché pasa de minutos a segundos.
Práctica 4: nombres significativos para cada combinación
El nombre por defecto es tipo test (ubuntu-latest, 20). Personaliza con name:
jobs:
test:
name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
Así identificas cada tarea más fácil en la interfaz de GitHub Actions.
10. Conclusión
Matrix en GitHub Actions es una herramienta potente para pruebas multiplataforma y multiversión. En el fondo son pocas cosas: definir dimensiones, controlar combinaciones, gestionar concurrencia y generar dinámicamente.
He visto demasiados proyectos manteniendo CI repetitivo a mano: cambiar un comando implica tocar docenas de sitios. Matrix comprime cientos de líneas en decenas y baja el coste de mantenimiento.
Repaso de lo esencial:
- Sintaxis básica:
matrix.osymatrix.nodehacen el producto cartesiano - Control preciso:
excludequita combinaciones inválidas,includeañade configuraciones especiales - Estrategia: elige
fail-fastsegún el escenario — false al depurar, true en producción - Concurrencia:
max-parallelprotege runners self-hosted y controla costes - Generación dinámica:
fromJSON()permite pruebas bajo demanda y ahorra recursos de CI
Las 5 plantillas de este artículo puedes copiarlas tal cual, desde Node.js multiversión hasta configuración empresarial con runners propios.
Si empiezas con Matrix, te sugiero la plantilla 1, que funcione, luego añade exclude e include, y por último prueba la generación dinámica. Paso a paso; no te lances de golpe a lo más complejo.
Si tienes dudas, déjalas en los comentarios o consulta la documentación oficial de GitHub Actions. Si tienes trucos con Matrix, compártelos.
Configurar pruebas multiplataforma y multiversión con Matrix en GitHub Actions
Configurar la estrategia Matrix desde cero para pruebas automatizadas multiplataforma y multiversión
⏱️ Estimated time: 30 min
- 1
Step 1: Definir dimensiones de Matrix
Añade strategy.matrix al job del workflow:
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
```
Esto genera 2 x 3 = 6 tareas en paralelo. - 2
Step 2: Usar variables de Matrix
Referencia las variables en runs-on y steps:
```yaml
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
```
Cada tarea obtiene automáticamente los valores de os y node correspondientes. - 3
Step 3: Excluir combinaciones concretas (opcional)
Usa exclude para quitar combinaciones problemáticas conocidas:
```yaml
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18
```
Excluye Windows + Node 18; quedan 5 tareas. - 4
Step 4: Configurar estrategia de fallo
Elige fail-fast según el escenario:
- PR: fail-fast: true (fallo rápido, ahorro)
- Depuración: fail-fast: false (ver todos los fallos)
- Rama main: fail-fast: false (informe completo)
```yaml
strategy:
fail-fast: false
matrix:
# ...
``` - 5
Step 5: Limitar concurrencia (opcional)
Con runners self-hosted o recursos limitados, limita la concurrencia:
```yaml
strategy:
max-parallel: 4
matrix:
# ...
```
Como máximo 4 tareas simultáneas, evita saturar el runner.
FAQ
¿Hay un límite en el número de combinaciones de Matrix?
¿Cuál es el valor por defecto de fail-fast?
¿En qué orden se ejecutan exclude e include?
¿Dónde se puede usar fromJSON() en un matrix dinámico?
¿max-parallel reduce el tiempo total de cómputo?
¿Las tareas de Matrix pueden compartir caché?
¿Cómo poner nombres personalizados a las tareas de Matrix?
• name: Test (${{ matrix.os }}, Node ${{ matrix.node }})
En la interfaz verás nombres claros como "Test (ubuntu-latest, Node 20)" y será más fácil identificar cada tarea.
16 min de lectura · Publicado el: 28 abr 2026 · Actualizado el: 21 ago 2026
Guía completa de GitHub Actions
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Matrix en GitHub Actions: pruebas paralelas multi-versión en la práctica
Tutorial de Matrix en GitHub Actions: sintaxis básica, filtros exclude/include, optimización fail-fast y control max-parallel, con plantilla completa de pipeline de pruebas multi-versión.
Parte 4 de 10
Siguiente
Estrategias de despliegue con GitHub Actions: pipeline CD de VPS a plataformas cloud
Guía detallada de tres estrategias de despliegue con GitHub Actions: SSH en VPS, plataformas gestionadas (Vercel/Cloudflare/Netlify) y arquitectura híbrida, con workflows completos y resolución de problemas frecuentes
Parte 6 de 10



Comentarios
Inicia sesión con GitHub para dejar un comentario