Módulo 7: CI Integration con GitHub Actions

Matrix Testing y Caching

Matrix Testing y Caching

Descripción de la cápsula

Tu workflow corre en una sola versión de Python. ¿Pero tu proyecto soporta 3.10, 3.11 y 3.12? ¿Quieres asegurarte de que el código funcione en todas? Y cada vez que el workflow corre, instala pip, crea el entorno, descarga dependencias desde cero. Eso puede sumar minutos. Si cambias una línea de código y solo ejecutas tests, ¿por qué reinstalar todo?

Esta cápsula cubre matrix testing (ejecutar el mismo job en múltiples versiones de Python en paralelo) y caching (guardar dependencias pip entre ejecuciones para builds más rápidos). Aprenderás la estrategia matrix, fail-fast, cómo funciona el cache de pip, la clave basada en requirements.txt, una comparación antes/después de caching, y un workflow completo que combina matrix y cache.


¿Qué es Matrix Testing?

Definición

Matrix strategy en GitHub Actions permite ejecutar un job múltiples veces, variando uno o más parámetros. Cada combinación corre en un runner independiente, en paralelo. Para Python, el parámetro típico es la versión: 3.10, 3.11, 3.12.

Por qué importa

  • Compatibilidad: Aseguras que el código funcione en las versiones que tu proyecto declara soportar.
  • Detección temprana: Si usas una API deprecada en 3.10 que aún funciona pero fue removida en 3.12, el matrix lo detecta.
  • Confianza para upgrades: Cuando decides subir el mínimo requerido de 3.10 a 3.11, ya sabes que 3.11 pasa.

Cuándo usarlo

  • Proyectos que soportan múltiples versiones de Python (común en librerías).
  • Proyectos que quieren validar en la versión más reciente antes de adoptarla.
  • Cuando el tiempo de CI no es crítico (cada versión añade ~1-2 min por ejecución, pero corren en paralelo).

Sintaxis de Matrix

Ejemplo básico

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v

Cada valor en python-version genera una ejecución del job. Tres versiones = tres ejecuciones en paralelo. matrix.python-version se sustituye en cada run por "3.10", "3.11" o "3.12".

Múltiples dimensiones

strategy:
  matrix:
    python-version: ["3.10", "3.11"]
    os: [ubuntu-latest, macos-latest]

Eso genera 2 × 2 = 4 combinaciones: (3.10, ubuntu), (3.10, macos), (3.11, ubuntu), (3.11, macos). Cada una corre en paralelo. Útil para proyectos multi-plataforma.

Excluir combinaciones

strategy:
  matrix:
    python-version: ["3.10", "3.11", "3.12"]
    os: [ubuntu-latest, macos-latest]
    exclude:
      - python-version: "3.10"
        os: macos-latest

Excluye (3.10, macos). Útil si una combinación es problemática o innecesaria.

Incluir combinaciones adicionales

strategy:
  matrix:
    python-version: ["3.10", "3.11"]
    include:
      - python-version: "3.12"
        experimental: true

Añade una combinación extra. experimental sería un parámetro que puedes usar en los steps si lo necesitas.


fail-fast

Comportamiento por defecto

Por defecto, fail-fast: true en una estrategia matrix: si una combinación falla, las demás se cancelan. Ahorra minutos cuando ya sabes que hay un fallo.

Desactivar fail-fast

strategy:
  fail-fast: false
  matrix:
    python-version: ["3.10", "3.11", "3.12"]

Con fail-fast: false, si (3.10) falla, (3.11) y (3.12) siguen corriendo. Verás el resultado completo de todas las versiones. Útil para saber qué versiones específicamente fallan.

Cuándo usar cada uno

  • fail-fast: true (default): CI rápido; cuando una versión falla, el resto no aporta mucho. Bueno para desarrollo iterativo.
  • fail-fast: false: Cuando necesitas el reporte completo (ej. release que debe soportar todas las versiones).

Caching de dependencias pip

El problema

Cada vez que el workflow corre, el runner es nuevo. No tiene pip cache, no tiene paquetes instalados. pip install -r requirements.txt descarga todo desde PyPI. Con 50 paquetes, eso puede ser 1-2 minutos por ejecución. Si el workflow corre 10 veces al día, son 10-20 minutos de downloads repetidos.

La solución: cache

GitHub Actions permite cachear directorios entre ejecuciones. La acción actions/cache@v4 guarda una carpeta (por ejemplo, el cache de pip) bajo una clave. Si la clave coincide en la siguiente ejecución, restaura el cache. pip install entonces reutiliza paquetes ya descargados y solo baja lo nuevo.

Cómo funciona la clave del cache

La clave típica se basa en:

  • Sistema operativo del runner (ubuntu, macos, etc.)
  • Versión de Python
  • Contenido o hash de requirements.txt

Si cambias requirements.txt, la clave cambia, el cache no coincide, y pip instala de nuevo. Eso es correcto: dependencias nuevas requieren install fresco. Si no cambias requirements, el cache se reutiliza.

Hash de requirements.txt

- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-
  • path: Dónde pip guarda su cache por defecto en Linux (~/.cache/pip).
  • key: Clave única. runner.os diferencia ubuntu de macos. hashFiles('requirements.txt') genera un hash del archivo. Si requirements.txt cambia, el hash cambia, nueva clave, nuevo cache.
  • restore-keys: Si no hay cache exacto, usa el prefijo ubuntu-pip- para restaurar cualquier cache de pip en ubuntu. Puede ser un cache de una versión anterior de requirements; pip instalará solo lo que falte.

Ubicación del cache en distintos OS

OSPath típico
Ubuntu~/.cache/pip
macOS~/Library/Caches/pip
Windows~\AppData\Local\pip\Cache

La acción actions/setup-python con cache: 'pip' maneja esto automáticamente. Verás esa opción más abajo.


Usar actions/setup-python con cache

La forma más simple de cachear pip es usar el cache integrado de actions/setup-python:

- name: Set up Python
  uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: "pip"
    cache-dependency-path: "requirements.txt"

Con cache: "pip" y cache-dependency-path, setup-python cachea automáticamente según requirements.txt. No necesitas actions/cache manual para pip.

Con pyproject.toml

- name: Set up Python
  uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: "pip"
    cache-dependency-path: "pyproject.toml"

Usa el hash de pyproject.toml para la clave. Si tienes requirements-dev.txt aparte, puedes usar un string con varios paths:

cache-dependency-path: |
  requirements.txt
  requirements-dev.txt

Fallback: cache manual

Si tu proyecto tiene una estructura de dependencias más compleja, usa actions/cache manual:

- name: Cache pip
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-py${{ matrix.python-version }}-pip-

- name: Install dependencies
  run: pip install -r requirements.txt

Antes y después del caching

Sin cache (típico)

Set up Python:      8s
Install dependencies: 90s  ← descarga todo
Run tests:          25s
Total:              ~2 min

Con cache (primera vez, cache miss)

Set up Python:      8s
Cache pip:          2s (restore intent, miss)
Install dependencies: 85s
Run tests:          25s
Total:              ~2 min (similar, pero guarda cache para después)

Con cache (segunda vez, cache hit)

Set up Python:      8s
Cache pip:          15s (restore, hit)
Install dependencies: 12s  ← la mayoría ya está en cache
Run tests:          25s
Total:              ~1 min

Puedes ahorrar 50-70% del tiempo de "Install dependencies" cuando el cache da hit. En workflows que corren muchas veces al día, eso se traduce en minutos u horas ahorradas.


Cuándo no usar matrix

Matrix no siempre es la opción correcta:

  • Proyectos que solo soportan una versión: Si tu pyproject.toml declara requires-python = ">=3.12", no necesitas matrix con 3.10 o 3.11. Un solo job con 3.12 basta.
  • CI muy lento: Tres jobs consumen 3x minutos de tiempo de runner (aunque corran en paralelo, cada uno usa su propio runner). Si tu plan de GitHub tiene límites de minutos, matrix puede agotarlos rápido.
  • Dependencias incompatibles: Algunas librerías no soportan versiones antiguas de Python. Si 3.10 falla por una dependencia que solo soporta 3.11+, excluye 3.10 o no uses matrix.
  • Proyectos internos: Si todo el equipo usa 3.12 y el proyecto no se distribuye, validar en 3.10 puede ser overkill. Un solo job es más simple.

Métricas reales: ejemplo con proyecto típico

Un proyecto Python con ~30 dependencias (FastAPI, SQLAlchemy, pytest, etc.):

EscenarioSet up PythonInstall depsRun testsTotal
Sin cache, 1ª vez8s95s28s~2m 10s
Con cache, 1ª vez (miss)8s92s28s~2m 08s
Con cache, 2ª vez (hit)8s14s28s~50s
Matrix 3 versiones, sin cache3 × ~2m--~2m (paralelo)
Matrix 3 versiones, con cache hit3 × ~50s--~50s (paralelo)

El ahorro con cache es de ~1 minuto por run. En 50 pushes al día, son 50 minutos menos de espera. El matrix triplica el consumo de minutos de Actions pero da confianza en compatibilidad multi-versión.


Workflow completo: matrix + caching

# .github/workflows/tests.yml
name: Tests

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

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.10", "3.11", "3.12"]

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
          cache-dependency-path: "requirements.txt"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run tests
        run: pytest tests/ -v --tb=short

Con pyproject.toml

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
          cache-dependency-path: "pyproject.toml"
      - name: Install dependencies
        run: pip install -e ".[dev]"

Práctica guiada: añadir matrix y cache a un workflow existente

Sigue estos pasos para transformar un workflow básico en uno con matrix y caching.

1. Tener un workflow que funcione

Asegúrate de tener checkout, setup Python, install, run pytest. Que pase en al menos una versión.

2. Añadir la estrategia matrix

Envuelve el job con strategy y matrix.python-version:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]

3. Usar matrix en setup-python

Cambia el step de setup para usar ${{ matrix.python-version }}:

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

4. Añadir cache a setup-python

Añade cache y cache-dependency-path:

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
          cache-dependency-path: "requirements.txt"

5. Push y verificar

Haz push. En Actions verás tres jobs en paralelo (3.10, 3.11, 3.12). La primera vez el cache será miss; la segunda, deberías ver "Cache hit" en el step de setup y un tiempo de install menor.


Comparación de tiempos: ejemplo real

En un proyecto con ~30 dependencias en requirements.txt:

FaseSin cacheCon cache (1ª vez)Con cache (2ª+)
Checkout5s5s5s
Setup Python10s10s10s
Cache (restore)-3s (miss)12s (hit)
Install deps85s82s18s
Run tests28s28s28s
Total~2.1 min~2.1 min~1.2 min

El ahorro es de ~45% en ejecuciones sucesivas sin cambios en requirements. Con matrix (3 versiones), son 3 jobs en paralelo; cada uno con su propio cache. El tiempo total del workflow es el del job más lento, no la suma.


Cuándo no usar matrix

  • Proyecto single-versión: Si solo soportas Python 3.12 y no planeas cambiar, matrix añade complejidad sin beneficio.
  • CI muy lento: Si ya tarda 5+ minutos, añadir 2 versiones más puede no valer la pena. Valida al menos en la versión mínima y la máxima que soportas.
  • Dependencias incompatibles: Si una librería crítica no soporta 3.10, exclúyela del matrix o no uses esa versión.

Orden de los steps: cache antes de install

El orden correcto es:

  1. Checkout (para tener requirements.txt)
  2. Cache (restore) — si existe, restaura ~/.cache/pip
  3. Setup Python
  4. Install — pip usa el cache restaurado y solo baja lo nuevo

Si pones cache después de install, el restore no tiene efecto en esa ejecución. Si usas actions/setup-python con cache: "pip", setup-python hace el cache por ti; no necesitas un step separado.


Flujo de decisión: ¿usar matrix?

¿Tu proyecto soporta múltiples versiones de Python?
├── No (solo 3.12) → Un job con python-version: "3.12"
└── Sí
    ├── ¿Es una librería o paquete distribuible? → Sí → Matrix 3.10, 3.11, 3.12
    └── ¿Es una app interna?
        ├── ¿Todo el equipo usa la misma versión? → Sí → Un solo job
        └── ¿Quieres preparar upgrade futuro? → Matrix para validar

Cache con múltiples archivos de dependencias

Si tienes requirements.txt, requirements-dev.txt y opcionalmente requirements-prod.txt, la clave del cache debe reflejar todos los que usa el job:

- uses: actions/setup-python@v5
  with:
    python-version: "3.12"
    cache: "pip"
    cache-dependency-path: |
      requirements.txt
      requirements-dev.txt

O con cache manual:

key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt', 'requirements-dev.txt') }}

Si solo incluyes uno, cambios en el otro no invalidarán el cache y podrías tener dependencias desactualizadas.


Errores frecuentes al configurar matrix

"Invalid versión" en setup-python

Causa: Escribiste "3.12" pero la sintaxis esperada puede variar. Usa strings con el formato exacto: "3.10", "3.11", "3.12". No uses 3.12 sin comillas si falla (aunque a veces YAML lo acepta).

Matrix genera 0 jobs

Causa: Todas las combinaciones fueron excluidas con exclude, o hay un typo en la definición del matrix.

Solución: Revisa que matrix.python-version tenga al menos un valor. Si usas include sin matrix base, puede haber comportamientos inesperados. Prueba con un matrix mínimo primero.

Diferentes versiones de Python, mismo cache key

Causa: La clave no incluye matrix.python-version. Python 3.10 y 3.12 pueden tener wheels distintos; compartir cache puede causar incompatibilidades.

Solución: Incluye la versión en la clave cuando uses actions/cache manual: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('requirements.txt') }}. Con setup-python y cache: "pip", la acción maneja esto automáticamente por versión de Python.


Consejos para optimizar tiempos de CI

  1. Cache siempre que tengas 5+ dependencias: Con proyectos pequeños, el ahorro es mínimo (10-15s). Con 20+ paquetes, el ahorro puede ser de 1-2 min por ejecución. En workflows que corren decenas de veces al día, eso suma.
  2. Usa fail-fast: true durante desarrollo iterativo: Cuando estás arreglando un fallo y una versión ya falló, cancelar el resto ahorra minutos. Cambia a fail-fast: false cuando prepares un release y quieras el reporte completo.
  3. paths-ignore para archivos que no afectan tests: Si solo cambias README.md, docs/ o .gitignore, evita correr CI. Configura paths-ignore en los triggers. Cada run que evitas ahorra minutos del plan de Actions.
  4. Paraleliza jobs con cautela: Si unit e integration pueden correr en paralelo, hazlo. Pero si integration necesita el build de unit, usa needs para no duplicar trabajo.
  5. Revisa el consumo de minutos: En GitHub Settings → Billing → Plans and usage puedes ver cuántos minutos consumen tus workflows. Con plan gratuito (2000 min/mes para privados), matrix de 3 versiones puede agotarlos rápido en repos muy activos.

Resumen de opciones de cache para pip

MétodoVentajasCuándo usarlo
actions/setup-python con cache: "pip"Simple, un solo step, maneja path por OSProyecto estándar con requirements.txt o pyproject.toml
actions/cache manualControl total de la clave, múltiples pathsDependencias en varios archivos o estructura no estándar
Sin cacheSin complejidad, siempre install frescoProyecto muy pequeño (<5 deps) o cuando el cache da problemas

Ejemplo numérico: proyecto con 15 dependencias

Supongamos un proyecto con requirements.txt que incluye FastAPI, SQLAlchemy, pytest, pytest-cov, httpx y unas 10 dependencias más (unas 15 en total).

Sin cache:

  • Install dependencies: ~55 segundos
  • Run tests: ~20 segundos
  • Total por job: ~1 min 20 s

Con cache (segunda ejecución, cache hit):

  • Setup Python + restore cache: ~18 segundos
  • Install dependencies: ~12 segundos (la mayoría desde cache)
  • Run tests: ~20 segundos
  • Total por job: ~50 segundos

Ahorro: ~30 segundos por job. Con matrix de 3 versiones son 3 jobs; cada uno ahorra ~30 s. En 20 pushes al día, son 10 minutos menos de tiempo de runner. Si tu plan tiene límite de minutos, el cache te permite más ejecuciones dentro del límite.

Cuándo el cache no ayuda tanto: Proyectos con 2-3 dependencias (pytest, requests) instalan en 10-15 s de todas formas. El ahorro puede ser solo 5-8 s. Sigue siendo útil configurarlo; no cuesta nada y en proyectos que crecen el beneficio aparece.


Verificar que el cache funciona

Tras configurar el cache, haz dos pushes seguidos sin cambiar requirements.txt:

  1. Primera ejecución: En el step "Set up Python" (o "Cache pip") deberías ver algo como "Cache not found" o "Cache restored from key: ..." con estado "miss".
  2. Segunda ejecución: Deberías ver "Cache hit" con la misma clave. El step de install será notablemente más rápido.

Si siempre ves "miss", revisa que la clave sea estable. Si usas hashFiles('requirements.txt'), no debe cambiar entre pushes si no modificaste el archivo. Si la clave incluye algo aleatorio (fecha, run_id), cada ejecución tendrá clave distinta y nunca dará hit.


Referencia rápida: sintaxis matrix y cache

Para consulta rápida al implementar:

Matrix básico:

strategy:
  matrix:
    python-version: ["3.10", "3.11", "3.12"]
# Usar en steps: ${{ matrix.python-version }}

Matrix con exclude:

strategy:
  matrix:
    python-version: ["3.10", "3.11", "3.12"]
    os: [ubuntu-latest, macos-latest]
    exclude:
      - python-version: "3.10"
        os: macos-latest

Cache con setup-python:

- uses: actions/setup-python@v5
  with:
    python-version: ${{ matrix.python-version }}
    cache: "pip"
    cache-dependency-path: "requirements.txt"

fail-fast:

strategy:
  fail-fast: false  # Todos los jobs corren aunque uno falle
  matrix:
    python-version: ["3.10", "3.11", "3.12"]

Ejercicios

Ejercicio 1: Matrix básico (Básico)

Añade una estrategia matrix al workflow de tests para que corra en Python 3.10 y 3.11. Haz push y verifica que ambos jobs aparezcan en Actions.

Ver solución
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v

En Actions verás "test (3.10)" y "test (3.11)" como jobs separados.

Ejercicio 2: fail-fast false (Básico)

Configura fail-fast: false en el matrix. Introduce un test que falle solo en Python 3.10 (por ejemplo, usando sintaxis que existe en 3.11+ pero no en 3.10). Verifica que los otros jobs sigan corriendo cuando 3.10 falla.

Ver solución
strategy:
  fail-fast: false
  matrix:
    python-version: ["3.10", "3.11", "3.12"]

Para hacer fallar solo 3.10, podrías usar algo que cambió entre versiones. Ejemplo simplificado: un test que hace assert sys.version_info >= (3, 11) — fallará en 3.10. O usa match (disponible desde 3.10) de forma que un bug en tu código solo se manifieste en 3.10. La idea es ver que con fail-fast: false, 3.11 y 3.12 completan aunque 3.10 falle.

Ejercicio 3: Cache con setup-python (Intermedio)

Configura el cache de pip usando la opción integrada de actions/setup-python. Ejecuta el workflow dos veces seguidas sin cambiar requirements.txt. Compara el tiempo del step "Install dependencies" entre la primera y la segunda ejecución.

Ver solución
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: "pip"
          cache-dependency-path: "requirements.txt"

La primera vez: Cache miss o "Cache not found". Install puede tardar 60-90s. La segunda vez: Cache hit. Install puede bajar a 10-25s. La diferencia depende del tamaño de requirements.txt.

Ejercicio 4: Cache manual con hash (Intermedio)

En lugar de cache: "pip" en setup-python, usa actions/cache@v4 manualmente. Configura la clave con hashFiles('requirements.txt') y el path ~/.cache/pip. Verifica que el cache funcione.

Ver solución
      - name: Cache pip
        uses: actions/cache@v4
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install -r requirements.txt

El orden importa: cache antes de setup-python, e install después. hashFiles genera un hash del contenido de requirements.txt. Si lo cambias, nueva clave, nuevo cache.

Ejercicio 5: Matrix con pyproject.toml (Intermedio)

Tu proyecto usa pyproject.toml con [project.optional-dependencies] dev = ["pytest", "pytest-cov"]. Configura matrix para 3.10, 3.11, 3.12 y cache usando cache-dependency-path: "pyproject.toml". El step de install debe usar pip install -e ".[dev]".

Ver solución
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: "pip"
          cache-dependency-path: "pyproject.toml"
      - run: pip install -e ".[dev]"
      - run: pytest tests/ -v

Ejercicio 6: Excluir una combinación (Avanzado)

Tienes matrix con python 3.10, 3.11, 3.12 y os ubuntu, macos. Excluye la combinación (3.10, macos) porque en tu proyecto esa combinación tiene problemas conocidos.

Ver solución
strategy:
  matrix:
    python-version: ["3.10", "3.11", "3.12"]
    os: [ubuntu-latest, macos-latest]
    exclude:
      - python-version: "3.10"
        os: macos-latest

jobs:
  test:
    runs-on: ${{ matrix.os }}
    # ...

Así tendrás 5 jobs en lugar de 6: (3.10, ubuntu), (3.11, ubuntu), (3.11, macos), (3.12, ubuntu), (3.12, macos).


Troubleshooting

Cache siempre miss

Causa: La clave cambia en cada ejecución (por ejemplo, incluye un timestamp) o el path es incorrecto.

Solución: Usa una clave estable: hashFiles('requirements.txt') es determinista. Si requirements no cambia, la clave es la misma. Verifica que path coincida con donde pip guarda el cache. En Ubuntu es ~/.cache/pip.

Cache corrupto o instalación incorrecta

Causa: A veces un cache restaurado puede causar instalaciones inconsistentes (paquete actualizado en PyPI, cache viejo).

Solución: Puedes borrar el cache desde la UI de GitHub: Settings → Actions → Caches → Delete. O añadir un step que fuerce reinstall cuando lo necesites. La mayoría del tiempo el cache es fiable; si ves fallos extraños, prueba sin cache una vez.

Matrix job falla en una versión

Causa: Código o dependencia incompatible con esa versión de Python.

Solución: Revisa el error en el log de ese job. Puede ser sintaxis no soportada (ej. match en 3.9), una dependencia que no soporta esa versión, o un test que asume comportamiento de una versión nueva. Corrige el código o excluye esa combinación del matrix si no la soportas.

Tiempo de CI no baja con cache

Causa: El cache no se está usando (miss), o requirements.txt es pequeño y pip ya era rápido.

Solución: Verifica que el step de cache muestre "Cache hit" en la segunda ejecución. Si siempre es miss, revisa la clave. Con pocas dependencias (2-3), el ahorro puede ser mínimo (10-20s). Con muchas (20+), el ahorro suele ser significativo (1-2 min).

Diferentes versiones de Python comparten cache

Causa: La clave del cache no incluye la versión de Python.

Solución: Incluye matrix.python-version en la clave cuando uses matrix: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('requirements.txt') }}. Cada versión de Python tiene su propio cache.

El matrix genera jobs con nombres largos

Causa: Con matrix, GitHub muestra "test (3.10)", "test (3.11)", etc. En branch protection, el check puede llamarse "test (3.10)" o simplemente "test" dependiendo de la configuración.

Solución: Para branch protection con matrix, normalmente necesitas que todos los jobs del matrix pasen. Configura "Require status checks" y selecciona todos los checks del job (o el check padre si existe). GitHub suele exponer un check por combinación de matrix.


Práctica guiada extendida: medir el impacto del cache

Para ver el efecto real del cache en tu proyecto:

1. Desactiva el cache temporalmente

Comenta el cache y cache-dependency-path en setup-python. Haz push. Anota el tiempo del step "Install dependencies" en los logs (ej. 78s).

2. Reactiva el cache

Vuelve a añadir cache: "pip" y cache-dependency-path. Haz push de nuevo (sin cambiar requirements). La primera vez será miss. Haz un segundo push trivial (ej. cambio en README). La segunda vez debería ser hit.

3. Compara

En la segunda ejecución, "Install dependencies" debería bajar a 10-25s. La diferencia es el ahorro. En proyectos con muchas dependencias, puede ser de 1+ minuto por ejecución.


Conexión con Proyecto

Matrix y caching son componentes clave del proyecto del Módulo 7. Un CI pipeline profesional valida en múltiples versiones de Python y usa cache para mantener builds rápidos. En la cápsula 05 añadirás coverage en CI y branch protection. El workflow que construiste aquí — matrix + cache — será la base a la que sumarás coverage y umbrales.


Resumen

  • Matrix strategy ejecuta un job en múltiples configuraciones (p. ej. Python 3.10, 3.11, 3.12) en paralelo
  • fail-fast: true cancela el resto cuando uno falla; fail-fast: false deja que todos terminen
  • Caching de pip reduce el tiempo de install reutilizando paquetes entre ejecuciones
  • La clave del cache debe incluir hash de requirements.txt (o pyproject.toml) y, con matrix, la versión de Python
  • actions/setup-python con cache: "pip" simplifica la configuración
  • Antes del cache: install puede ser 60-90s; después (cache hit): 10-25s es común

Próxima cápsula: Coverage en CI y Branch Protection — reportes de coverage, comentarios en PRs, y protecciones de branch.


Recursos Adicionales

  1. GitHub Actions: Workflow syntax - matrix - Documentación oficial del matrix
  2. actions/setup-python - caching - Cache integrado de pip
  3. actions/cache - Acción de cache manual
  4. Caching dependencies to speed up workflows - Guía de caching
  5. hashFiles - Función para generar claves de cache
  6. Python versión support - Ciclo de vida de versiones de Python

Módulo 7, Cápsula 04 — Testing with Claude Code Guide