Módulo 4: La matriz — múltiples versiones y entornos

5. `include`, `exclude` y `fail-fast`

Descripción

Ya sabes generar una cuadrícula: dos listas se multiplican en N celdas. Pero una cuadrícula completa rara vez es exactamente lo que quieres. A veces sobra una celda —una combinación que no aplica, como una versión vieja de Python en Windows que nadie usa—. A veces falta una celda especial que no cabe en el producto —un job extra que corre solo en una combinación, con un paso de más—. Y siempre está la pregunta de qué hacer cuando una celda se pone roja: ¿cancelas las demás para ahorrar, o las dejas correr para ver el mapa completo? Esta lección te da las tres herramientas para eso: exclude, include y fail-fast.

Al terminar vas a poder recortar la cuadrícula quitando combinaciones con exclude, sumar celdas puntuales con include sin multiplicar la matriz entera, y elegir conscientemente el comportamiento de fail-fast —el default que cancela las celdas hermanas al primer rojo, o false para dejarlas correr y ver todos los fallos a la vez—. Vas a ver el YAML de cada uno (contenido honesto), cómo se leería el log de CI en cada caso, y una demostración local real del primo de fail-fast: la bandera -x de pytest, que detiene la corrida al primer fallo, para que sientas el trade-off "parar temprano vs. ver todo" con salida ejecutada de verdad.

Conexión con el módulo: la lección 4 te dio la cuadrícula cruda (3×3 = 9). Esta te da el bisturí para esculpirla: quitar lo que no aplica, agregar lo puntual, y decidir cómo se comporta ante un rojo. La lección 6 leerá los resultados de la matriz que aquí terminas de moldear. Y la lección 7 usará exclude como una de sus herramientas para recortar una matriz inflada hasta dejar solo lo que paga. Así que aquí no cambias qué prueba la matriz (eso es la 7), sino que aprendes las palancas de YAML para expresarlo con precisión.

El sastre que ajusta un traje de talla estándar

Compras un traje de una talla estándar. Te queda casi bien: el cuerpo perfecto, pero las mangas un poco largas y te falta un bolsillo interior donde guardas los boletos. No compras otro traje; vas al sastre. El sastre hace tres cosas: quita tela de las mangas (sobra), agrega el bolsillo que faltaba (una pieza puntual que la talla estándar no traía), y —si estuviera armando varios trajes en serie y uno saliera mal— decide si detiene la línea para revisar o deja que salgan todos y luego inspecciona.

La matriz de la lección 4 es el traje de talla estándar: el producto cartesiano completo, útil pero rara vez exacto. exclude es quitar tela: elimina una combinación que sobra. include es coser el bolsillo: agrega una celda puntual con su propia configuración, sin rehacer el traje entero. Y fail-fast es la decisión sobre la línea de producción: al primer rojo, ¿paras todo o dejas correr para ver cuántos salieron mal? Un buen sastre —y un buen ingeniero de CI— usa las tres con intención, no deja el traje como vino de fábrica.

exclude: quitar una celda del producto

exclude toma la cuadrícula completa y le resta combinaciones específicas. Se declara como una lista de diccionarios, cada uno describiendo la celda que quieres eliminar por sus coordenadas.

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    python-version: ["3.11", "3.12", "3.13"]
    exclude:
      - os: windows-latest
        python-version: "3.11"      # quita SOLO la celda (windows, 3.11)

La cuadrícula base son 3 × 3 = 9 celdas. El exclude quita una: la esquina (windows-latest, 3.11). Quedan ocho. Fíjate en la precisión: no quita todo Windows ni todo 3.11, solo la intersección exacta donde ambas coordenadas coinciden. La cuadrícula resultante:

test (ubuntu-latest, 3.11)    test (ubuntu-latest, 3.12)    test (ubuntu-latest, 3.13)
test (macos-latest, 3.11)     test (macos-latest, 3.12)     test (macos-latest, 3.13)
   [excluida]                 test (windows-latest, 3.12)   test (windows-latest, 3.13)

¿Cuándo excluyes una celda? Cuando esa combinación específica no aplica o no vale la pena: una versión vieja que en un sistema concreto nadie usa, una dependencia que no tiene rueda para ese par exacto, un runner caro (los de macOS suelen costar más minutos) donde una versión intermedia no aporta. exclude te deja quedarte con "casi todo el producto, menos estas esquinas".

include: sumar una celda especial sin multiplicar

include hace lo contrario: agrega celdas. Pero tiene una propiedad clave que lo hace valioso: una celda agregada con include no multiplica la matriz. Es una fila extra, puntual, que puede traer su propia configuración adicional.

strategy:
  matrix:
    os: [ubuntu-latest]
    python-version: ["3.11", "3.12", "3.13"]
    include:
      - os: ubuntu-latest
        python-version: "3.13"
        coverage: true            # <- una clave EXTRA que solo esta celda tiene

La matriz base es 1 × 3 = 3 celdas (Linux, tres versiones). El include agrega una celda más: (ubuntu-latest, 3.13) con una clave extra coverage: true que las otras no tienen. Total: cuatro jobs. Dentro del job, podrías leer ${{ matrix.coverage }} para correr un paso extra —por ejemplo, medir cobertura— solo en esa celda. Las otras tres celdas no tienen esa clave, así que se saltan ese paso.

La diferencia con agregar un valor a una lista es enorme y vale la pena grabarla:

Agregar un valor a una lista de la matriz MULTIPLICA (un valor más en una lista de 3, con otra lista de 3, suma 3 celdas). Agregar una celda con include SUMA UNA (una fila puntual, con su propia config, sin tocar el producto).

Por eso include es la herramienta para "una celda especial": el job de cobertura que solo corre en una combinación, el experimento con una versión pre-release, el build que sube un artefacto solo en Linux. Si pusieras coverage como una dimensión más (coverage: [true, false]), duplicarías toda la matriz. Con include, agregas exactamente una celda.

Un detalle útil: include también sirve para expandir una celda existente con claves extra sin crear una nueva, cuando la combinación ya está en el producto. Pero su uso más común, y el que importa aquí, es el de arriba: sumar una fila puntual.

fail-fast: parar al primer rojo, o ver el mapa completo

Cuando una celda de la matriz se pone roja, GitHub tiene que decidir qué hacer con las otras celdas que todavía están corriendo. Ese comportamiento lo controla fail-fast, y vive en el bloque strategy (hermano de matrix).

fail-fast: true — es el default. En cuanto una celda falla, GitHub cancela todas las celdas hermanas que siguen corriendo. La idea: si ya sabes que el cambio está roto, ¿para qué gastar minutos terminando las otras ocho corridas? Ahorras tiempo y costo. El precio: solo ves el primer fallo; si el bug afecta a tres celdas, verás una en rojo y las otras dos como "canceladas", sin saber que también habrían fallado.

fail-fast: false — deja correr todas las celdas hasta el final, aunque una ya haya fallado. La idea: quieres el mapa completo —saber exactamente cuáles celdas pasan y cuáles fallan—, porque eso te dice si el bug es de una versión específica o de todas. El precio: gastas los minutos de las celdas que ya sabías que podrían fallar.

strategy:
  fail-fast: false      # deja correr todas las celdas, aunque una falle
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    python-version: ["3.11", "3.12", "3.13"]

Así se leería el log de CI en cada caso, con un bug que rompe las tres celdas de Windows. Con fail-fast: true (default):

tests · push a main   (fail-fast: true)
  ✓ test (ubuntu-latest, 3.11)     ✓ test (ubuntu-latest, 3.12)     ✓ test (ubuntu-latest, 3.13)
  ✓ test (macos-latest, 3.11)      ✓ test (macos-latest, 3.12)      ✓ test (macos-latest, 3.13)
  ✗ test (windows-latest, 3.11)    ⊘ test (windows-latest, 3.12)    ⊘ test (windows-latest, 3.13)
                                     (canceladas al primer rojo)

Ves una celda de Windows roja y dos canceladas (). Sabes que Windows falla, pero no confirmaste que las tres versiones de Windows fallan —podría ser solo 3.11—. Con fail-fast: false:

tests · push a main   (fail-fast: false)
  ✓ test (ubuntu-latest, 3.11)     ✓ test (ubuntu-latest, 3.12)     ✓ test (ubuntu-latest, 3.13)
  ✓ test (macos-latest, 3.11)      ✓ test (macos-latest, 3.12)      ✓ test (macos-latest, 3.13)
  ✗ test (windows-latest, 3.11)    ✗ test (windows-latest, 3.12)    ✗ test (windows-latest, 3.13)

Ahora ves el mapa entero: las tres celdas de Windows en rojo, las seis de Linux/macOS en verde. El diagnóstico es inmediato y preciso: el bug es de Windows, en todas las versiones —no de una versión suelta—. Ese mapa completo vale los minutos extra cuando estás cazando de qué depende un fallo.

La regla práctica: fail-fast: false cuando quieres diagnosticar (ver todas las celdas que fallan para localizar el patrón), el default true cuando quieres feedback rápido y barato (basta saber que algo se rompió para que no se mergee). Muchos equipos usan false en la rama principal, donde el diagnóstico importa, y true en ramas de trabajo, donde solo quieren la señal rápida.

max-parallel: cuántas celdas a la vez

Un primo cercano, para completar el panorama. Por defecto GitHub corre tantas celdas en paralelo como le permitan tus runners disponibles. max-parallel limita ese número:

strategy:
  max-parallel: 3       # como maximo 3 celdas corriendo a la vez
  matrix:
    python-version: ["3.11", "3.12", "3.13", "3.14"]

Con nueve celdas y max-parallel: 3, corren de tres en tres. ¿Para qué limitarlo? Para no saturar una cuota de runners compartida con otros proyectos, o para no golpear a la vez un recurso externo que la suite toca (una API de pruebas con límite de peticiones). El costo: la matriz tarda más en total, porque no todo corre a la vez. Es una palanca de "ancho de banda", no de "qué se prueba".

El primo local de fail-fast: la bandera -x de pytest

No tenemos un runner para ver fail-fast cancelar celdas de verdad, pero pytest tiene el mismo trade-off un nivel más abajo, dentro de una sola corrida, y ese sí lo podemos ejecutar. La bandera -x (o --exitfirst) le dice a pytest: "detente en cuanto un test falle, no sigas con los demás". Es, a nivel de tests dentro de un job, lo que fail-fast es a nivel de celdas dentro de una matriz: parar temprano para ahorrar, a cambio de no ver todos los fallos.

Ejemplo trabajado

Corramos una tanda de tests que incluye uno roto en medio, con -x, para ver a pytest detenerse:

python -m pytest -x tests/test_pricing.py tests_red/test_report_path_bug.py tests/test_refunds.py

Qué esperar. En Python 3.14.0, medido de verdad (el test roto compara una ruta contra un separador de Windows, así que en macOS falla a propósito):

=================================== FAILURES ===================================
_____________________ test_report_path_hardcoded_separator _____________________

    def test_report_path_hardcoded_separator():
        # BUG: compara contra un separador '/' escrito a mano. Pasa en POSIX,
        # se rompe en Windows. Aqui lo forzamos a fallar para ver una celda ROJA.
        path = os.path.join("reports", "2026-08", "daily.txt")
>       assert path == "reports\\2026-08\\daily.txt"  # separador de Windows, en macOS falla
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       AssertionError: assert 'reports/2026-08/daily.txt' == 'reports\\2026-08\\daily.txt'

tests_red/test_report_path_bug.py:8: AssertionError
=========================== short test summary info ============================
FAILED tests_red/test_report_path_bug.py::test_report_path_hardcoded_separator - AssertionError: assert 'reports/2026-08/daily.txt' == 'reports\\2026-08\\da...
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
========================= 1 failed, 3 passed in 0.02s =========================

Lee la penúltima línea: !!! stopping after 1 failures !!!. pytest corrió los tres tests de test_pricing.py (los 3 passed), llegó al test roto, falló, y se detuvo ahí —nunca corrió los tests de test_refunds.py que venían después—. Eso es exactamente el espíritu de fail-fast: true: al primer rojo, no gastes más, corta. El resumen 1 failed, 3 passed lo confirma: de los siete tests que había en la lista, solo se ejecutaron cuatro antes de parar.

Si hubieras corrido lo mismo sin -x, pytest habría ejecutado los siete, mostrándote todos los que pasan y todos los que fallan —el "mapa completo", como fail-fast: false—. Mismo trade-off, un nivel más abajo: -x es a los tests lo que fail-fast es a las celdas de la matriz. Sentir uno en local te da intuición del otro en el CI.

Una aclaración honesta para no confundir capas: -x detiene los tests dentro de un job; fail-fast cancela celdas (jobs) dentro de una matriz. Son mecanismos distintos, en niveles distintos, que comparten la misma filosofía —parar temprano vs. ver todo—. No los mezcles en un mismo YAML esperando que uno haga el trabajo del otro.

Errores comunes

Usar una dimensión extra cuando querías include. Qué pasa: quieres un solo job de cobertura en (ubuntu, 3.13), y lo agregas como una dimensión coverage: [true, false]. Eso duplica toda la matriz: cada celda ahora existe con coverage: true y con coverage: false, el doble de jobs. Por qué pasa: se confunde "agregar una opción" con "agregar una celda". Cómo detectarlo: si tu número de celdas se duplicó al querer añadir un solo caso especial, usaste una dimensión donde querías include. Cómo corregirlo: pon la celda especial en include, que suma una fila con su propia config, sin multiplicar.

exclude con coordenadas que no existen. Qué pasa: excluyes os: windows-latest, python-version: "3.10", pero tu lista de versiones es ["3.11", "3.12", "3.13"] —3.10 no está—. El exclude no quita nada (no hay tal celda), y sigues teniendo las nueve, creyendo que quitaste una. Por qué pasa: un typo en la versión, o copiar un exclude de otro proyecto. Cómo detectarlo: cuenta las celdas después del exclude; si el número no bajó, tus coordenadas no coinciden con ninguna celda real. Cómo corregirlo: asegúrate de que cada coordenada del exclude sea un valor que de verdad está en las listas de la matriz.

Dejar fail-fast: true cuando estás diagnosticando de qué depende un fallo. Qué pasa: un test falla de forma rara y quieres saber si es solo en 3.11 o en todas las versiones, pero con el default true, la primera celda roja cancela las demás y nunca ves el patrón completo. Por qué pasa: el default cancela para ahorrar, que es bueno para feedback rápido pero malo para diagnosticar. Cómo detectarlo: ves una celda roja y varias "canceladas", y no puedes concluir en qué versiones falla. Cómo corregirlo: pon fail-fast: false mientras diagnosticas, para que corran todas y te den el mapa completo de rojos y verdes; regresa al default cuando termines.

Ejercicios

Ejercicio 1 — Cuenta las celdas después de esculpir. Para cada matriz, di cuántos jobs resultan: (a) os: [ubuntu, macos, windows] × python-version: ["3.11", "3.12", "3.13"] con exclude de (windows, 3.11) y (macos, 3.11); (b) os: [ubuntu] × python-version: ["3.11", "3.12"] con un include de (ubuntu, 3.13, coverage: true); (c) python-version: ["3.11", "3.12", "3.13"] (una sola dimensión) con un include de (3.14, experimental: true).

Ver solución
  • (a) 9 − 2 = 7 jobs. El producto base es 3 × 3 = 9; el exclude quita dos celdas concretas —(windows, 3.11) y (macos, 3.11)—, dejando siete.
  • (b) 2 + 1 = 3 jobs. La base es 1 × 2 = 2 celdas (Linux, 3.11 y 3.12); el include suma una celda puntual —(ubuntu, 3.13) con coverage: true—, sin multiplicar. Total: tres.
  • (c) 3 + 1 = 4 jobs. Una sola dimensión de tres valores da tres celdas; el include agrega una fila más —3.14 con experimental: true—, sin tocar las otras. Total: cuatro.

La regla que se repite: exclude resta celdas del producto; include suma celdas puntuales al producto. Ninguno de los dos multiplica. Multiplicar solo lo hace agregar un valor a una lista de dimensión.

Ejercicio 2 — Elige fail-fast para el objetivo. Para cada situación, di si conviene fail-fast: true (default, cancela hermanas) o fail-fast: false (deja correr todas) y por qué: (a) un push a una rama de trabajo donde solo quieres saber rápido si algo se rompió, sin gastar de más; (b) estás investigando si un bug nuevo afecta solo a Windows o a los tres sistemas; (c) el pipeline de la rama principal, donde quieres el reporte más completo posible antes de aprobar un merge.

Ver solución
  • (a) fail-fast: true (el default). En una rama de trabajo, la señal que buscas es binaria —"¿pasa o no?"—; en cuanto una celda falla, ya sabes que no debe mergearse, así que cancelar las demás ahorra minutos sin perder información que necesites. Feedback rápido y barato.
  • (b) fail-fast: false. Estás diagnosticando de qué depende el fallo, y para eso necesitas el mapa completo: ver si las tres celdas de Windows fallan (bug de Windows) o solo una versión (bug de versión). Con true, la primera roja cancelaría las demás y nunca verías el patrón. Aquí los minutos extra compran el diagnóstico.
  • (c) fail-fast: false. En la rama principal quieres el reporte más completo antes de aprobar: saber todas las celdas que fallan, no solo la primera, para no arreglar una y descubrir otra en el siguiente push. El costo extra se justifica por la importancia de la rama.

La regla mecánica: true para feedback rápido/barato, false para diagnóstico/reporte completo. Muchos equipos combinan: true en ramas de trabajo, false en main.

Ejercicio 3 — Traduce un requisito a exclude/include. Un equipo quiere: probar Python 3.11, 3.12 y 3.13 en Linux y Windows; pero una dependencia no tiene rueda para Windows con 3.11, así que esa celda no puede correr; y además quiere un único job extra que mida cobertura, solo en Linux con 3.13. Escribe la sección strategy.matrix que cumple exactamente eso y di cuántas celdas resultan.

Ver solución
strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    python-version: ["3.11", "3.12", "3.13"]
    exclude:
      - os: windows-latest
        python-version: "3.11"        # la dependencia no tiene rueda para este par
    include:
      - os: ubuntu-latest
        python-version: "3.13"
        coverage: true                # unico job extra de cobertura

Cuenta de celdas: la base es 2 × 3 = 6; el exclude quita (windows, 3.11)5; el include suma una fila puntual de cobertura → 6 jobs en total. Las cinco celdas del producto corren la suite normal; la sexta, la de include, corre además el paso de cobertura (leyendo ${{ matrix.coverage }}). Fíjate en que la celda de cobertura reutiliza (ubuntu, 3.13), que ya está en el producto: con include conviven una celda "normal" (ubuntu, 3.13) y la extra con la clave coverage. El requisito quedó expresado exacto: exclude para la combinación imposible, include para el job especial.

Resumen y siguiente paso

En esta lección aprendiste a esculpir la cuadrícula con tres herramientas. exclude resta celdas concretas del producto —para quitar una combinación que no aplica o no vale la pena— por sus coordenadas exactas. include suma celdas puntuales, con su propia configuración extra, sin multiplicar la matriz —la herramienta para el job especial de cobertura o el experimento de una versión—. Y fail-fast decide qué pasa ante un rojo: el default true cancela las hermanas para feedback rápido y barato; false las deja correr para darte el mapa completo cuando diagnosticas. Vimos también max-parallel, la palanca de cuántas celdas corren a la vez.

Grabaste la distinción clave: agregar un valor a una lista multiplica, include suma una, exclude resta. Y sentiste el trade-off de fail-fast ejecutando su primo local, la bandera -x de pytest: al correr una tanda con un test roto en medio, pytest se detuvo con stopping after 1 failures y 1 failed, 3 passed, sin llegar a los tests posteriores —parar temprano para ahorrar, a cambio de no ver todo—, exactamente la filosofía de fail-fast un nivel más abajo.

Antes de avanzar deberías poder: escribir un exclude y un include correctos; contar las celdas resultantes; explicar por qué include no multiplica; elegir fail-fast según si buscas feedback rápido o diagnóstico completo; y relacionar -x de pytest con fail-fast de la matriz sin confundir las capas.

Lo que sigue, en la lección 6, es leer lo que esta matriz produce: los N resultados. Cómo GitHub nombra cada celda, cómo se lee una cuadrícula toda verde o con una sola celda roja, y qué te dice esa celda roja —en qué versión o sistema vive el bug— para localizarlo y, de ahí, reproducirlo en local igualando ese entorno.

Recursos