Módulo 8: Proyecto — un pipeline de CI para Reservo
8. Proyecto: un pipeline de CI para Reservo
Descripción
Llegó el concierto. Durante ocho módulos ensayaste cada sección por separado; en este módulo las juntaste, capa por capa, sobre la suite de Reservo. Ahora las tocas todas a la vez en un solo entregable: el pipeline de CI completo de Reservo, tejido de las seis capas que armaste —el workflow base (lección 2), la reproducibilidad (lección 3), la matriz (lección 4), la velocidad (lección 5), la puerta de cobertura (lección 6) y la política de flaky (lección 7)— más la prueba de que hace lo que dice.
Este es el proyecto formal del capstone, y su evaluación es distinta de todo lo anterior: se juzga por el método, no por el número de tests. No importa si tu suite tiene catorce tests o doscientos; importa que cada decisión del pipeline esté justificada —por qué esta matriz, por qué este umbral, por qué -n auto o por qué no, qué haces con un flaky—. Un pipeline con seis tests y seis decisiones bien defendidas vale más que uno con doscientos tests y ninguna. Vas a producir dos entregables: el tests.yml completo, comentado decisión por decisión, y el setup de paridad local (check.sh), un script que corre en tu máquina exactamente lo que el CI corre en el runner. Y te vas a evaluar contra una rúbrica de método.
Esta lección, además, cierra la guía. Después del proyecto viene el repaso de los ocho módulos —el mapa completo de lo que aprendiste— y el camino hacia dónde seguir, con las guías hermanas del ecosistema de testing. El ensayo terminó; toca el concierto y luego el mapa de lo que sigue.
Conexión con el módulo: esta lección integra las siete anteriores. La 1 te dio el plano y la tabla etapa↔problema; las 2 a 7 armaron cada capa en su lugar; el proyecto las teje en un solo archivo y te pide defenderlas. Es el examen práctico, no el escrito: nadie te dice qué capa toca en cada momento; lo decides tú, con criterio, y lo justificas. Y como cierre de la guía, mira hacia afuera: a las guías hermanas que enseñan lo que esta, a propósito, no enseñó —cómo escribir los tests, cómo diagnosticar un fallo, cómo pensar la estrategia—.
El examen práctico de manejo, con el instructor al lado
Piensa en sacar la licencia de conducir. El examen escrito prueba que sabes las reglas: qué significa una señal, a qué distancia se frena. El examen práctico prueba algo distinto: que puedes aplicarlas todas a la vez, en tráfico real, sin que nadie te diga cuál regla toca en cada momento. Te subes al carro, el instructor se sienta al lado, y manejas —coordinando el volante, los pedales, los espejos, las señales— en una sola tarea integrada donde todo importa simultáneamente.
Las lecciones 1 a 7 fueron el examen escrito: cada una te enseñó una capa y te la tomó por separado. Este proyecto es el práctico. Te subes al carro y armas un pipeline de verdad, tomando tú las decisiones que antes te venían dadas. ¿Qué versiones en la matriz? ¿Qué umbral de cobertura? ¿-n auto sí o no? ¿Qué política de flaky? Nadie te lo dicta; lo decides con lo que aprendiste, y lo defiendes. Y como en el examen práctico, no se evalúa la perfección teórica sino la competencia integrada: al final, un tests.yml que protege a Reservo, que se lee bien, cuyas decisiones puedes justificar una por una, y que —comprobado con la paridad local— hace exactamente lo que dice.
El proyecto es el examen práctico: aplicar las seis capas a la vez, tomando tú las decisiones, y defender cada una. No se evalúa el número de tests, sino el método —por qué cada pieza del pipeline es como es—.
El encargo
Eres responsable del CI de Reservo como librería —la publicas para que otros equipos la instalen—. Su pyproject.toml declara requires-python = ">=3.11" y prometes soportar 3.11, 3.12 y 3.13. Reservo es lógica pura (aritmética de enteros, fechas; sin red, sin disco, sin BD), con una suite de catorce tests en tests/ y la feature dependiente de versión report_pages. Tu trabajo, los dos entregables:
Entregable 1 — el tests.yml completo. Un solo workflow que teje las seis capas: dispara en push y PR (lección 2), corre en la matriz de tres versiones (lección 4) con instalación reproducible desde requirements-dev.txt (lección 3), cachea las dependencias (lección 5), aplica la puerta de cobertura (lección 6), y tiene una política de flaky coherente con la suite (lección 7). Comentado decisión por decisión.
Entregable 2 — la paridad local (check.sh). Un script corto que corre en tu máquina la misma instalación, la misma suite y la misma puerta que el CI, con su salida real como evidencia. Su valor: convierte el CI de "donde descubro que algo estaba mal" en "la confirmación de algo que ya sé que está bien".
Y una nota de decisión por cada capa: por qué esta matriz y no otra, por qué este umbral, por qué -n auto o por qué no, qué política de flaky. Esa nota es lo que la rúbrica evalúa. Intenta cada entregable por tu cuenta antes de mirar la solución de referencia; el aprendizaje está en construirlo tú.
La rúbrica: se evalúa el método
Antes de la solución, la rúbrica, porque define qué es un buen pipeline aquí. No hay puntos por "muchos tests" ni por "muchas etapas encendidas". Los hay por decisiones justificadas:
| Criterio | Qué se evalúa | Señal de que está bien |
|---|---|---|
| Cobertura de capas | ¿Están las seis capas presentes y en su lugar? | Push+PR, matriz, deps pinneadas, caché, puerta, política de flaky — ninguna falta, ninguna sobra sin razón. |
| Justificación de la matriz | ¿El tamaño de la matriz responde a un riesgo real? | Tres versiones = la promesa del README; una sola fila de SO = lógica pura. No 3×3 por reflejo. |
| Umbral defendible | ¿La puerta está donde muerde sin exigir lo imposible? | Umbral bajo la cobertura real, con colchón; ni 100% (fetiche) ni 50% (decorativo). |
| Velocidad con criterio | ¿Cachea siempre, paraleliza solo si paga? | Caché sí; -n auto decidido con la medición de la suite, no por reflejo. |
| Política de flaky honesta | ¿Hay una política, y no es "retry global"? | Parche por test + ticket + raíz; nunca --reruns global permanente. |
| Paridad local ejecutada | ¿Corriste en tu máquina lo que el CI corre? | check.sh con salida real verde; el YAML es contenido, la corrida es evidencia. |
| Honestidad de alcance | ¿Dices qué protege el pipeline y qué no? | Nota de alcance que reconoce lo que queda fuera, sin venderlo como "completo". |
Fíjate en lo que no está en la rúbrica: el número de tests, las líneas de YAML, la cantidad de celdas. Un pipeline se evalúa por si sus decisiones protegen lo que Reservo de verdad arriesga, con el mínimo de costo y ruido. Ese es el oficio.
Los cinco pasos del proyecto
Paso 1 — Teje el tests.yml. Parte del workflow base de la lección 2 y apila: la matriz de la 4, la instalación pinneada de la 3, el caché de la 5, la puerta de la 6. Comenta cada bloque con la lección de la que viene.
Paso 2 — Escribe check.sh. El script de paridad: actualizar pip, instalar desde requirements-dev.txt, correr la suite con la puerta de cobertura. Los mismos comandos que el CI, en tu terminal.
Paso 3 — Corre la paridad local. Ejecuta check.sh (o sus comandos) de verdad y captura la salida. Esta es la evidencia real; el YAML es contenido.
Paso 4 — Decide la política de flaky. Reservo hoy no tiene flaky (es determinista). Escribe la política para cuando aparezcan, coherente con lo que la suite es.
Paso 5 — Escribe las notas de decisión. Una por capa, justificando el porqué. Es lo que la rúbrica pesa.
Solución de referencia
Entregable 1 — El tests.yml completo, comentado
# .github/workflows/tests.yml
name: tests
# Capa 1 (leccion 2): dispara en cada push y en cada pull request.
# push = feedback en cada cambio; pull_request = guardian del merge a main.
on: [push, pull_request]
jobs:
test:
# Capa 3 (leccion 4): una sola fila de sistema operativo.
# Reservo es logica pura -> da identico en Linux/macOS/Windows,
# asi que probar en 3 SO seria costo y ruido sin cazar un bug extra.
runs-on: ubuntu-latest
strategy:
# Siendo libreria, queremos el MAPA COMPLETO de versiones, no cancelar
# al primer rojo: fail-fast apagado.
fail-fast: false
matrix:
# Capa 3 (leccion 4): exactamente lo que promete el README.
# Ni una version de mas (reflejo), ni una de menos (promesa sin probar).
python-version: ["3.11", "3.12", "3.13"]
steps:
# Capa 1 (leccion 2): primero traer el codigo, o nada funciona.
- name: Check out the code
uses: actions/checkout@v5
# Capa 1 + 4: instala la version de ESTA celda de la matriz.
# Capa 4 (leccion 5): cache: pip evita re-descargar deps en cada corrida;
# la clave se ata a requirements-dev.txt, asi que se reconstruye solo
# cuando las dependencias cambian (cache honesto).
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: requirements-dev.txt
# Capa 2 (leccion 3): instalacion reproducible desde una lista pinneada.
# requirements-dev.txt trae pytest + las herramientas de CI (cov, xdist,
# rerunfailures), todas con == para que el runner instale identico a ti.
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
# Capa 5 (leccion 6): la puerta de cobertura ROMPE el build si baja de 85%.
# --cov-branch mide tambien las ramas (if/else), no solo las lineas: es lo
# que llena las columnas Branch/BrPart y hace comparable el 88% real.
# Capa 4 (leccion 5): -n auto queda como forma lista-para-escalar; hoy la
# suite es de 0.02s y el paralelismo solo paga cuando crezca (ver nota).
# Sin --reruns global: la politica de flaky es por-test (leccion 7).
- name: Run the suite with the coverage gate
run: |
python -m pytest -n auto \
--cov=reservo --cov-branch --cov-report=term-missing \
--cov-fail-under=85
Y las dos listas de dependencias que el workflow usa:
# requirements.txt (produccion: Reservo es stdlib pura)
pytest==9.1.1
# requirements-dev.txt (desarrollo + CI: produccion + herramientas)
pytest==9.1.1
pytest-cov==7.1.0
pytest-xdist==3.8.0
pytest-rerunfailures==16.4
Cada línea del YAML apunta a su lección. El workflow entero es la tabla etapa↔problema de la lección 1, hecha archivo: on resuelve "se me olvidó correr los tests" (M2); requirements-dev.txt pinneado resuelve "en mi máquina funciona" (M3); la matriz resuelve "funciona en mi versión" (M4); cache: pip resuelve "el CI tarda" (M5); --cov-fail-under resuelve "llegó código sin tests" (M6); la ausencia de --reruns global es la política de flaky honesta (M7).
Entregable 2 — El script de paridad local (check.sh)
#!/usr/bin/env bash
# check.sh — corre en tu maquina LO MISMO que el CI, para no tener sorpresas.
# Paridad local: misma instalacion, misma suite, misma puerta de cobertura.
set -e # aborta al primer comando que falle, como haria el runner
# Mismos comandos que el step "Install dependencies" del workflow.
python -m pip install --upgrade pip
pip install -r requirements-dev.txt
# Mismo step "Run the suite with the coverage gate", en SERIE:
# la suite de Reservo es de 0.02s, y -n auto ahi solo anade sobrecarga
# (leccion 5). El pipeline mantiene -n auto como forma lista-para-escalar;
# aqui medimos la puerta, que es lo que decide verde/rojo.
python -m pytest --cov=reservo --cov-branch --cov-report=term-missing --cov-fail-under=85
La decisión de correr check.sh en serie (sin -n auto) mientras el pipeline lo tiene puesto es exactamente la lección 5 aplicada con honestidad: en una suite de 0.02s, -n auto la haría más lenta, así que localmente no lo usas; el -n auto del YAML es la forma lista para cuando la suite crezca, cuyo efecto real ya mediste (4.07s → 1.19s sobre una suite lenta). La paridad que importa —la que decide verde o rojo— es la instalación y la puerta de cobertura, y esas sí son idénticas.
Entregable 3 — La paridad local, ejecutada de verdad
./check.sh
Qué esperar (salida real en Python 3.14.0 con pytest 9.1.1; el tramo de la puerta de cobertura):
================================ tests coverage ================================
Name Stmts Miss Branch BrPart Cover Missing
-----------------------------------------------------------------
reservo/__init__.py 0 0 0 0 100%
reservo/calendar.py 9 1 0 0 89% 15
reservo/models.py 9 0 0 0 100%
reservo/pricing.py 5 0 2 0 100%
reservo/refunds.py 7 0 4 0 100%
reservo/reports.py 7 2 2 1 67% 14-16
reservo/schedule.py 20 3 8 2 82% 15, 16->13, 40-41
-----------------------------------------------------------------
TOTAL 57 6 16 3 88%
Required test coverage of 85% reached. Total coverage: 87.67%
========================= 13 passed, 1 skipped in 0.03s =========================
echo "exit code: $?"
exit code: 0
La evidencia, señalada:
13 passed, 1 skipped— la suite completa, con el skip esperado del fallback dereport_pages(en 3.14, ≥ 3.12). Los trece verdes incluyen los seis números-ancla (7500, 6000, 2500 y los reembolsos 6000, 3000, 0).TOTAL ... 88%yRequired test coverage of 85% reached— la puerta pasó: 88% ≥ 85%. El hueco del 67% enreports.pyes la rama de otra versión (cubierta por la celda de 3.11), no un defecto.exit code: 0— verde. En el runner, este cero pintaría cada celda de la matriz de verde; con protección de rama, el merge se habilita. Un exit distinto de cero (por un test roto o por la cobertura bajo el umbral) lo bloquearía.
Esta corrida es la paridad hecha evidencia: acabas de ejecutar, con tus manos, lo que las tres celdas del pipeline ejecutarían. La única diferencia con el runner es la línea platform (darwin local vs. linux en el runner) y que aquí es una versión (3.14) donde el runner correría tres (3.11/3.12/3.13).
Entregable 4 — La política de flaky
# Politica de tests flaky (README de Reservo)
Reservo hoy es determinista (logica pura, sin red/disco/tiempo/concurrencia),
asi que no tiene flaky y el pipeline NO usa --reruns global. Cuando se integren
dependencias externas (p. ej. una API de pagos que a veces tarde) y aparezca un
flaky, se trata asi: (1) contener con @pytest.mark.flaky(reruns=2) POR TEST, o
cuarentena con -m "not flaky" si es muy molesto; nunca --reruns global.
(2) abrir ticket con dueño y fecha. (3) arreglar de raiz (aislar el recurso con
un doble de test, para no depender de la latencia real de un tercero); la
diagnosis sigue la guia test-failure-diagnosis. (4) al arreglarlo, quitar el
parche. Un flaky en cuarentena que empieza a fallar SIEMPRE = bug real.
La política es coherente con lo que Reservo es: como no hay flaky hoy, el pipeline no anticipa con retry (eso solo escondería bugs futuros); define el procedimiento para cuando la superficie del proyecto crezca. Prohíbe explícitamente el --reruns global —el error más común— y ata el retry a un ticket, para que la contención sea temporal y no un silenciador permanente.
Entregable 5 — Las notas de decisión (lo que la rúbrica pesa)
- Matriz: tres versiones, una fila de SO. Reservo es librería y su README promete 3.11/3.12/3.13 — cada versión es una promesa a usuarios que no controlo, y
report_pagestiene ramas por versión que solo la matriz ejercita en su celda. La dimensión de SO no paga: la lógica pura da idéntico en todo sistema; agregarla sería nueve celdas para seis veces el mismo verde. Disparador para sumar SO: cuando Reservo escriba a disco (rutas, saltos de línea). - Umbral: 85%. Bajo el 88% real (colchón de ~3 puntos para no romper por ruido), pero alto para morder: una función de 40 líneas sin tests hundiría la cobertura y dispararía la puerta. Ni 100% (fetiche imposible por la rama de versión de
reports.py) ni un número decorativo muy por debajo del real. - Velocidad: caché sí,
-n autocomo inversión. Cacheo porque la matriz repite la instalación tres veces y el caché lo elimina sin desventaja.-n autono paga hoy (suite de 0.02s; la sobrecarga de workers la haría más lenta), pero lo dejo puesto, documentado, listo para cuando la suite crezca — su efecto real lo medí en la lección 5 (4.07s → 1.19s sobre una suite lenta). - Flaky: política por-test, sin retry global. Reservo es determinista; el pipeline no reintenta nada. La política queda escrita para el futuro, con la prohibición explícita del
--rerunsglobal. - Alcance, con honestidad: el pipeline protege los 14 tests en cada push/PR, en tres versiones, con puerta de cobertura al 85%. No cubre otros SO (innecesario para lógica pura hoy), ni paraleliza de hecho (suite instantánea), ni despliega a producción (esta guía es CI de tests, no CD). Es el pipeline que Reservo de verdad merece — ni inflado por reflejo, ni por debajo de sus promesas.
Errores comunes
Entregar el YAML sin la paridad local ejecutada. Qué pasa: alguien escribe un tests.yml impecable y lo da por terminado, sin correr nunca los comandos en su máquina. Sube, y el CI real revela en minutos lo que la terminal habría revelado en segundos. Por qué pasa: el YAML "se ve bien" y da sensación de trabajo terminado. Cómo detectarlo: si no tienes una salida real de pytest/coverage de tu propia terminal, no verificaste nada. Cómo corregirlo: la paridad local (check.sh con su salida verde) es la mitad del entregable, no un extra; el YAML es contenido, la corrida es evidencia.
Inflar el pipeline para que "se vea completo". Qué pasa: alguien agrega la matriz 3×3, -n auto, y una puerta al 100% para que el pipeline luzca robusto, sin preguntarse si Reservo lo necesita. Termina con nueve celdas para lógica pura, paralelismo que frena una suite de 0.02s, y una puerta imposible por la rama de versión de reports.py. Por qué pasa: "más etapas encendidas" se siente más profesional. Cómo detectarlo: por cada etapa, pregúntate "¿qué problema real de Reservo quita?"; si la respuesta es "ninguno", es costo y ruido. Cómo corregirlo: la rúbrica premia el método, no el tamaño — justifica cada capa por el riesgo real, y ten el criterio de dejar -n auto como inversión documentada, no como paralelismo activo que estorba.
Presentar el pipeline como "CI completo" ocultando lo que no hace. Qué pasa: alguien monta el pipeline y anuncia "listo, CI completo", sin mencionar que no prueba otros SO, no despliega, y su -n auto no acelera nada hoy. Por qué pasa: terminar el capstone se siente como cubrirlo todo. Cómo detectarlo: intenta listar lo que tu pipeline no hace; si te salen varias cosas, no es "completo". Cómo corregirlo: escribe la nota de alcance del entregable 5 — nombrar lo que queda fuera no es admitir una falla, es honestidad de ingeniería, y es justo lo que la rúbrica valora.
Ejercicios
Ejercicio 1 — El pipeline con tres defectos de método. Un compañero entrega este tests.yml "completo". Tiene tres defectos de método (no de sintaxis). Encuéntralos y corrígelos.
name: tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- run: pip install -r requirements-dev.txt
- run: pytest --reruns 3 --cov=reservo --cov-fail-under=100
Ver solución
Los tres defectos de método:
-
La matriz 3×3 (nueve celdas) para lógica pura.
os: [ubuntu, macos, windows]× tres versiones = nueve celdas, pero Reservo es aritmética de enteros y fechas: da idéntico en todo SO. Ocho de las nueve celdas prueban entornos donde el código no se comporta distinto — costo y ruido. Corrección: una sola fila de SO (runs-on: ubuntu-latest, sin dimensiónos), tres celdas de versión. La dimensión de SO se agrega el día que Reservo toque el disco. -
La puerta al 100%.
--cov-fail-under=100es imposible de satisfacer por celda: la ramaelsedereport_pageses inalcanzable en 3.12+ (y la delif, en 3.11), así que la puerta fallaría siempre, no por código mal probado, sino por ramas de otra versión. Corrección: un umbral defendible bajo la cobertura real (85%), que muerde sin exigir lo imposible. -
El
--reruns 3global. Reintenta todos los tests, escondiendo no solo flaky sino bugs reales — y Reservo ni siquiera tiene flaky (es determinista), así que el retry global es puro riesgo sin beneficio. Corrección: quitar--rerunsglobal; la política de flaky es por-test, y hoy no hace falta ninguno.
Un cuarto punto defendible: on: [push] sin pull_request pierde el guardián del merge; conviene on: [push, pull_request]. Y faltan el caché y las versiones pinneadas del entregable. Corregido, el pipeline se parece a la solución de referencia: tres celdas de versión en Linux, caché, instalación pinneada, puerta al 85%, sin retry global. La lección: los defectos no eran de sintaxis (el YAML es válido), sino de método — encender etapas por reflejo en vez de por el riesgo real de Reservo.
Ejercicio 2 — Defiende una decisión en el review. Un revisor te dice: "Tu pipeline tiene -n auto pero tu check.sh corre en serie. Eso es inconsistente, elige uno." Respóndele defendiendo la decisión con lo que sabes de la lección 5.
Ver solución
No es inconsistencia; es la lección 5 aplicada con criterio. -n auto paga cuando la suite es lo bastante lenta para amortizar la sobrecarga fija de arrancar workers (≈1s); estorba cuando la suite ya es instantánea. La suite de Reservo corre en 0.02 segundos, así que hoy -n auto la haría más lenta, no más rápida — lo medí. Por eso check.sh, que corre en tu máquina donde el objetivo es feedback inmediato, va en serie: es objetivamente más rápido para esta suite.
¿Por qué entonces el pipeline mantiene -n auto? Como inversión lista-para-escalar y documentada. El pipeline es infraestructura que vive años; el día que Reservo crezca a cientos de tests o a tests lentos (cuando integre esa API de pagos, digamos), -n auto empezará a pagar sin que nadie tenga que tocar el YAML en medio de una urgencia. Dejarlo puesto con un comentario que dice "no paga hoy, listo para cuando la suite crezca" es previsión, no contradicción. Y la parte que decide verde o rojo —la instalación pinneada y la puerta de cobertura— es idéntica entre check.sh y el pipeline; la única diferencia es el -n auto, que no cambia el resultado, solo (eventualmente) el tiempo. La paridad que importa es la del veredicto, y esa es exacta.
Si el revisor prefiriera consistencia estricta, la alternativa igual de defendible es quitar -n auto de ambos hasta que la suite lo pida — también correcto. Lo indefendible sería lo contrario: poner -n auto en check.sh "por consistencia" y hacer tu feedback local más lento por una simetría cosmética. La decisión se toma por la medición, no por la simetría.
Ejercicio 3 — Amplía el pipeline para una nueva capacidad. Reservo agrega export_report(path), que escribe el reporte diario a un archivo de texto. De golpe, tres capas del pipeline podrían necesitar cambios. Di cuáles y cómo, aplicando el criterio del capstone.
Ver solución
Escribir a un archivo de texto hace que Reservo toque el terreno del sistema operativo (separador de ruta, saltos de línea \n vs \r\n, encoding) y el disco (E/S real, recursos, posible lentitud), lo que activa tres capas:
-
La matriz (lección 4) gana la dimensión de SO. Hasta ahora una sola fila de Linux bastaba porque la lógica era pura; ahora
export_reportpuede comportarse distinto en Windows (rutas con\, saltos\r\n) que en POSIX. La matriz crece aos: [ubuntu-latest, windows-latest]× tres versiones = seis celdas (incluyo Windows por la diferencia real POSIX/Windows; macOS es casi redundante con Linux para esto). El disparador que la lección 4 anticipó se cumplió. -
La política de flaky (lección 7) se vuelve relevante. La E/S a disco introduce recursos compartidos (dos tests escribiendo al mismo archivo) y timing (el disco a veces tarda), los disparadores clásicos de flaky — sobre todo bajo el
-n autodel pipeline, que reparte los tests y puede hacer que dos choquen sobre el mismo archivo. La política pasa de "escrita para el futuro" a "activa": cada test deexport_reportdebe usar un archivo temporal único por test (eltmp_pathde pytest), para no depender del archivo de otro. Si aparece un flaky, se contiene por-test con ticket. -
La velocidad (lección 5) puede empezar a justificar
-n autode verdad. Los tests de E/S son más lentos que la aritmética pura; si la suite crece con varios de ellos y empieza a tardar segundos, el-n autoque hoy es inversión-a-futuro empieza a pagar. Se re-mide: si la suite en serie ya duele, el paralelismo deja de ser sobrecarga y se vuelve aceleración real.
(La puerta de cobertura de la lección 6 también notaría el cambio: export_report es código nuevo, y sin tests que lo ejerciten la cobertura caería y la puerta dispararía — exactamente su trabajo, empujándote a probar la función nueva.) La lección del capstone: un pipeline no es estático; evoluciona con lo que el código arriesga. Cuando Reservo pasó de lógica pura a tocar el disco, tres capas que estaban dormidas por buena razón despertaron, cada una por un riesgo nuevo y concreto — no por reflejo, sino porque el terreno cambió.
Resumen y siguiente paso
En este proyecto integraste el módulo entero produciendo el entregable real del capstone: el pipeline de CI completo de Reservo. Tejiste las seis capas en un solo tests.yml —push+PR (M2), matriz de tres versiones (M4), instalación pinneada (M3), caché (M5), puerta de cobertura al 85% (M6), política de flaky honesta (M7)—, cada bloque comentado con la lección de la que viene. Escribiste check.sh, la paridad local, y la corriste de verdad: 13 passed, 1 skipped, cobertura 88%, puerta al 85% satisfecha, exit code 0 — la evidencia de que el pipeline hará lo correcto. Y escribiste las notas de decisión que la rúbrica pesa, justificando cada capa por el riesgo real de Reservo, no por reflejo.
Sobre todo, practicaste el examen práctico: tomar todas las decisiones a la vez —qué matriz, qué umbral, -n auto o no, qué política de flaky— y defender cada una. Eso es lo que separa a quien copia un pipeline de internet de quien diseña el que su proyecto merece. Un pipeline se evalúa por su método, no por su tamaño.
Cierre de la guía: los ocho módulos
Llegaste al final. Mira todo el camino, porque cada módulo fue una pieza de una sola habilidad: correr tu suite automáticamente en cada cambio, y hacerlo bien.
- Módulo 1 — De tu máquina al pipeline. Por qué existe el CI: "en mi máquina funciona" es una afirmación sobre un entorno; un pipeline corre tus tests en cada push, sin depender de que alguien se acuerde. El bucle de feedback y las etapas de una tubería.
- Módulo 2 — Tu primer pipeline. El workflow de GitHub Actions que corre pytest en cada push: la anatomía del YAML (
on,jobs,steps), el checkout,setup-python, instalar,pytest, y cómo el exit code pinta el job. - Módulo 3 — Reproducir un fallo de CI localmente. La brecha de entorno: CI en rojo, local en verde. Dependencias pinneadas, instalaciones deterministas, y cómo reproducir en tu máquina lo que solo veías en CI.
- Módulo 4 — La matriz. Correr la suite en varias versiones de Python (y SO) a la vez con
strategy.matrix; un job por combinación; y cuándo la matriz paga y cuándo es ruido. - Módulo 5 — CI rápido. Cachear dependencias y paralelizar con
pytest-xdist; el trade-off velocidad/costo; y la disciplina de medir antes de optimizar. - Módulo 6 — Puertas de calidad. El umbral de cobertura que rompe el build (
--cov-fail-under); dónde ponerlo; y por qué el 100% es un fetiche. - Módulo 7 — Flaky en CI. El test que a veces pasa y a veces falla; el debate del retry; la cuarentena; y por qué un flaky erosiona la confianza en todo el pipeline.
- Módulo 8 — El capstone. Tejer las seis capas en un pipeline completo para Reservo, con paridad local, evaluado por el método de sus decisiones.
Sales sabiendo montar un pipeline que protege la rama principal sin volverse un cuello de botella: automático, reproducible, multi-versión, rápido cuando importa, exigente en calidad, y honesto con los flaky. Esa es la habilidad completa.
Hacia dónde seguir
Esta guía enseñó a correr tus tests en CI, y a propósito dejó fuera otras habilidades del ecosistema de testing, cada una con su guía hermana. Aquí el mapa de a dónde ir según lo que quieras aprender:
- Cómo escribir y organizar los tests que este pipeline corre →
testing-fundamentals-and-tdd(el ABC de escribir un buen test, TDD, la cobertura como herramienta local) ytest-automation-framework-architecture(cómo estructurar una suite grande: fixtures, capas, mantenibilidad). Aquí los tests ya existían; allá aprendes a crearlos. - Cómo diagnosticar un fallo —sobre todo un flaky— hasta su causa raíz →
test-failure-diagnosis. El módulo 7 te enseñó a contener un flaky en CI; esta guía te enseña a cazarlo: reproducir, aislar el disparador, corregir la fixture. La frontera que marcamos apunta aquí. - Cómo pensar la estrategia de calidad —qué probar, cuánto, dónde poner el esfuerzo →
test-strategy-and-quality-engineering. Sube un nivel: de las herramientas al criterio de qué merece un test y qué no, la pirámide de tests, el riesgo. - Cómo probar una app web de verdad —no lógica pura como Reservo, sino endpoints, base de datos, autenticación →
testing-backend-applications-guide. Roza el "CD a producción" que esta guía dejó fuera; ahí los tests tocan el mundo (red, BD) y el pipeline que montaste aquí se vuelve el que protege ese despliegue.
El pipeline que armaste no es el final de nada; es la infraestructura sobre la que todo lo demás corre. Cada test que escribas (fundamentos), cada suite que estructures (arquitectura), cada fallo que diagnostiques (diagnóstico), cada app que pruebes (backend) va a correr en un pipeline como el de Reservo. Aprendiste a construir la máquina que protege la rama principal; ahora tienes el ecosistema entero para llenarla de tests que valgan la pena. Ese era el objetivo de toda la guía: saber cuáles tests sirven — y correrlos, automáticamente, en cada cambio.
Recursos
- Building and testing Python — GitHub Actions — la guía oficial que recorre matriz, caché y cobertura en un solo workflow de Python, el pipeline completo que armaste. La referencia para montarlo en tu propio proyecto.
- Workflow syntax for GitHub Actions — la sintaxis completa del YAML (
on,jobs,strategy,steps), para consultar cualquier detalle deltests.ymlque tejiste. El diccionario del pipeline. - pytest-xdist, pytest-cov y pytest-rerunfailures — las tres herramientas que las capas 5, 6 y 7 activaron, juntas en el pipeline final. Sus documentaciones, para afinar cada capa en tu proyecto.
- About protected branches — GitHub — cómo hacer que los checks del pipeline sean requeridos para mergear, el eslabón que convierte "el CI está rojo" en "el merge está bloqueado". El paso que le da dientes a todo lo que montaste.