Módulo 1: De tu máquina al pipeline: por qué CI

5. Las etapas de un pipeline

Descripción

Al terminar esta lección vas a saber qué es un pipeline y a poder nombrar sus etapas: checkout → instalar → testear → reportar. Hasta ahora hablamos del CI como una caja —"corre tu suite en una máquina limpia"—; esta lección abre la caja y te muestra la maquinaria por dentro. Y la buena noticia, que vas a comprobar, es que dentro no hay magia: cada etapa del pipeline es un comando que ya conoces —clonar el repo, instalar dependencias, correr pytest— puesto en fila, uno tras otro, ejecutado por una máquina en vez de por ti. Un pipeline no es más que tu flujo manual de testing, escrito para que una máquina lo repita igual cada vez.

Vas a entender tres ideas que hacen que un pipeline funcione. Primero, que las etapas van en orden y dependen unas de otras: no puedes testear lo que no instalaste, no puedes instalar lo que no clonaste. Segundo, que un pipeline falla rápido (fail fast): si una etapa se cae, las siguientes ni se intentan, porque no tendría sentido. Y tercero, la más importante de todas: cuál es la señal con la que el pipeline decide si el conjunto pasó o falló —el código de salida del proceso de tests—, un número que ya estaba ahí en tu máquina y que nunca miraste, pero del que depende todo el veredicto del CI. Ese número lo vamos a medir de verdad.

Conexión con el módulo: esta lección es la anatomía. La 3 definió el CI, la 4 explicó por qué conviene; esta muestra las piezas concretas que lo hacen. Lo que sigue se apoya en ella: la lección 6 explica cómo el veredicto de estas etapas protege la rama principal; la 7 ubica el CD como etapas adicionales que vienen después de estas. Y una frontera importante: aquí verás las etapas como concepto y como comandos; el archivo YAML que las declara en GitHub Actions —con steps:, uses:, run:— es el tema del módulo 2. Aquí entiendes qué hace cada etapa; allá aprendes a escribirla.

La cadena de montaje

Piénsalo así. En una fábrica de coches, un coche no se arma en un solo puesto donde una persona hace todo. Se arma en una cadena de montaje: una fila de estaciones, cada una con una tarea, por la que el coche avanza en orden. La primera estación pone el chasis. La segunda, que recibe el chasis de la primera, monta el motor. La tercera, que recibe eso, pone las ruedas. La cuarta inspecciona y aprueba. Cada estación depende de que la anterior haya terminado bien: no puedes montar el motor si no llegó el chasis, no puedes poner ruedas si no hay coche.

Y hay una regla de oro en la cadena: si una estación detecta un defecto grave —el chasis viene torcido—, para la línea. No tiene sentido montarle un motor de miles de dólares a un chasis torcido que va a ir a la basura. Mejor detenerse en la estación 1, tirar el chasis malo, y no gastar el motor. Parar temprano ahorra todo el trabajo de las estaciones siguientes.

Un pipeline de CI es esa cadena de montaje, pero lo que avanza por la fila no es un coche: es tu cambio de código. Cada estación es una etapa, con una tarea, y cada una depende de la anterior. Y rige la misma regla de oro: si una etapa se cae, la línea se para —las siguientes ni se intentan—, porque no tiene sentido seguir. La palabra "pipeline" (tubería, o cadena) viene justo de esta imagen: algo entra por un extremo, pasa por una serie de estaciones en orden, y sale por el otro con un veredicto —aprobado o rechazado—.

Un pipeline es una secuencia ordenada de etapas automáticas, donde cada una depende de la anterior y una falla detiene la cadena. Para un pipeline de tests, las etapas son: traer el código, instalar el entorno, correr la suite, y reportar el veredicto.

Las cuatro etapas, mapeadas a comandos que ya conoces

Aquí está lo desmitificador: cada etapa de un pipeline de tests es un comando que ya usas —o usarías— cuando pruebas Reservo a mano. El pipeline solo los pone en fila para que una máquina los ejecute. Veámoslas una por una.

Etapa 1 — Checkout: traer el código

Qué hace: la máquina limpia del CI arranca vacía; lo primero es traer tu código a esa máquina. Es el equivalente a que tú, en una máquina nueva, clonaras el repositorio.

El comando que ya conoces: git clone (o, dentro de un repo, traer la rama y el commit exactos del cambio que disparó el pipeline). En Reservo, es el paso de tener la carpeta reservo/ con sus modelos y funciones, y los test_*.py junto a ella, presentes en la máquina.

Por qué es la etapa 1: sin código, no hay nada que instalar ni que testear. Todo lo demás depende de esta.

Etapa 2 — Instalar: construir el entorno

Qué hace: sobre la máquina limpia con el código ya presente, instala lo que el proyecto necesita para correr: la versión de Python declarada y las dependencias. Esta es la etapa que materializa el "entorno limpio y reproducible" de la lección 3: se construye desde cero, a partir de lo que el proyecto declara, no de lo que "ya estaba instalado".

El comando que ya conoces: preparar la versión de Python y luego pip install -r requirements.txt (o el gestor de dependencias que uses). Reservo es stdlib pura, así que sus dependencias son mínimas —básicamente pytest—, pero la etapa existe igual: instala pytest en la máquina limpia.

Por qué depende de la 1: para instalar las dependencias que el proyecto declara, primero necesitas el archivo que las declara (requirements.txt), que vino con el código en la etapa 1.

Etapa 3 — Testear: correr la suite

Qué hace: con el código presente y el entorno construido, corre tu suite. Este es el corazón del pipeline, la razón de ser de todo lo demás. Y aquí está la clave que repetimos desde la lección 3: el CI corre exactamente el mismo comando que tú corres en tu máquina.

El comando que ya conoces: python3 -m pytest. El mismo, tal cual. En Reservo, esto ejecuta los test_pricing.py, test_scheduling.py y test_refunds.py y produce la salida que ya viste —12 passed—, solo que en la máquina limpia del runner.

Por qué depende de la 2: no puedes correr pytest si pytest no está instalado, y no puedes probar import reservo si el código no está. Las etapas 1 y 2 existen para que esta pueda ocurrir.

Etapa 4 — Reportar: entregar el veredicto

Qué hace: toma el resultado de la etapa 3 y lo convierte en un veredicto visible: un check verde o rojo en el commit o el PR, con el log completo disponible para inspección, y a veces artefactos (un reporte de cobertura, por ejemplo —eso es el módulo 6—). Esta es la etapa que cierra el bucle de feedback de la lección 4: es la que te avisa.

El comando que ya conoces: aquí no tecleas un comando nuevo; el "reporte" nace de algo que ya estaba en la etapa 3 y que nunca miraste: el código de salida de pytest. De eso trata la sección que sigue, porque es el mecanismo central de todo el CI.

El mecanismo central: el código de salida

Aquí llegamos a la pieza más importante y peor entendida del CI. ¿Cómo sabe la máquina si tu suite pasó o falló? No "lee" el 12 passed como texto ni entiende de descuentos. La máquina sabe una sola cosa, la única que necesita: el código de salida que el proceso de pytest le entrega al terminar.

Todo programa de línea de comandos, al terminar, devuelve al sistema un número entero llamado código de salida (o exit code). La convención universal —no solo de pytest, de todo Unix— es simple: 0 significa "todo bien"; cualquier número distinto de 0 significa "algo falló". pytest respeta esta convención al pie de la letra: si todos los tests pasan, sale con 0; si al menos uno falla, sale con 1. Ese número es la señal que el CI lee para decidir verde o rojo. Lo demás —el log bonito, los puntos, el resumen— es para los humanos; para la máquina, todo el veredicto cabe en un número.

Este número siempre estuvo ahí, en tu máquina, cada vez que corriste pytest —solo que tu shell no te lo mostraba—. Vamos a medirlo de verdad, porque verlo con tus ojos desmitifica el CI entero.

Qué esperar (suite verde). Corremos la suite completa de Reservo y, justo después, preguntamos por el código de salida con echo $? (una variable especial del shell que guarda el código de salida del último comando):

$ python3 -m pytest test_pricing.py test_scheduling.py test_refunds.py
============================== 12 passed in 0.02s ==============================
$ echo $?
0

Cero. Los doce tests pasaron, pytest salió con 0, y ese 0 es lo que el CI interpreta como "verde, deja pasar el cambio". Ahora rompamos algo —corremos el test frágil de la lección 2 en un entorno limpio, donde falla—:

$ env -u RESERVO_PRO_DISCOUNT python3 -m pytest test_env_pricing.py
============================== 1 failed in 0.03s ==============================
$ echo $?
1

Uno. El test falló, pytest salió con 1, y ese 1 es lo que el CI interpreta como "rojo, bloquea el cambio". Esto es, literalmente, todo lo que el CI necesita saber. No entiende de Reservo, ni de precios, ni de por qué falló: leyó un 1 y pintó el check de rojo. En la lección 3 lo viste desde el otro lado, en el log del CI: la línea Error: Process completed with exit code 1 es el runner reaccionando a este mismísimo número.

Detente en lo que esto significa, porque es liberador: el CI no es inteligente, y no necesita serlo. Toda la maquinaria del pipeline se reduce a "corre estos comandos en orden; si alguno sale con un código distinto de cero, para la cadena y reporta rojo". La misma regla de la cadena de montaje —para la línea ante un defecto— es, en código, "si el código de salida no es cero, detente". Entender esto te quita el miedo al CI: es una tubería que ejecuta tus comandos y mira un número.

Fail fast: por qué una etapa caída detiene la cadena

Vuelve a la regla de oro de la cadena de montaje: si el chasis viene torcido, no le montes el motor. En el pipeline es igual, y se llama fail fast (fallar rápido). Si una etapa temprana se cae, las siguientes no se ejecutan, porque no tendría sentido:

  • Si la etapa 1 (checkout) falla —no se pudo traer el código—, no hay nada que instalar ni testear. La cadena se detiene ahí.
  • Si la etapa 2 (instalar) falla —una dependencia no se pudo instalar, por ejemplo un requirements.txt roto—, la etapa 3 ni se intenta: no puedes correr una suite en un entorno que no se pudo construir. El pipeline reporta rojo en la etapa de instalación, y el log te dice que el fallo fue ahí, no en tus tests.
  • Si la etapa 3 (testear) falla —un test rojo—, la etapa de reporte recibe el código de salida 1 y pinta el check de rojo.

Esto tiene una consecuencia práctica muy útil para diagnosticar: el pipeline te dice en qué etapa se cayó, y eso ya es media diagnosis. Un rojo en la etapa 2 (instalar) es un problema de entorno o dependencias —nada que ver con tu lógica—; un rojo en la etapa 3 (testear) es un test que falló. Saber distinguirlos te ahorra buscar el bug en el lugar equivocado. (Cuando el rojo de la etapa 3 aparece solo en el CI y no en tu local, estás ante el "en mi máquina funciona" de la lección 2, y reproducirlo es el módulo 3.)

Errores comunes

Pensar que el CI "entiende" tus tests. Qué pasa: alguien imagina que el CI analiza inteligentemente el código y decide si está bien, y por eso lo trata como un oráculo o le tiene un respeto reverencial. Por qué pasa: el resultado —un veredicto verde o rojo— parece un juicio inteligente. Cómo detectarlo: si crees que el CI hace algo más que correr comandos y mirar códigos de salida, le atribuyes inteligencia que no tiene. Cómo corregirlo: interioriza que el CI corre pytest (el mismo que tú) y lee un número: 0 o distinto de 0. Toda su "inteligencia" es esa convención. Esto no lo hace menos útil —lo hace predecible, que es mejor—: sabes exactamente qué mira, así que sabes exactamente cómo darle verde (haz que tus comandos salgan con 0).

Confundir un fallo de instalación con un fallo de test. Qué pasa: el CI está en rojo, alguien asume que un test falló, y se pone a revisar la lógica de sus tests… cuando en realidad la etapa 2 (instalar) se cayó y la etapa 3 ni corrió. Pierde una hora buscando un bug que no existe. Por qué pasa: "el CI está rojo" se lee como "un test falló", sin mirar en qué etapa. Cómo detectarlo: si no revisaste en qué etapa se detuvo la cadena antes de empezar a depurar, vas a ciegas. Cómo corregirlo: lee el log de arriba abajo y ubica la primera etapa que se cayó. Un rojo en instalar es un problema de dependencias o entorno; un rojo en testear es un test. El fail fast te regala esa pista —úsala—.

Ignorar el código de salida al correr en local. Qué pasa: alguien encadena comandos en un script local —correr tests y después desplegar— sin comprobar el código de salida de los tests, así que el despliegue ocurre aunque los tests hayan fallado. El script "corrió bien" a sus ojos porque no explotó, pero desplegó código roto. Por qué pasa: en la terminal, el código de salida es invisible si no lo miras con echo $?; es fácil olvidar que existe. Cómo detectarlo: si tus scripts encadenan pasos sin verificar que el anterior salió con 0, tienes esta bomba puesta. Cómo corregirlo: respeta el código de salida como lo respeta el CI. En un script, cmd_a && cmd_b solo corre cmd_b si cmd_a salió con 0 —esa es la regla de fail fast a mano—. El CI hace esto por ti entre etapas; en local, hazlo tú.

Ejercicios

Ejercicio 1 — Mapea el flujo manual a las etapas. Aquí está lo que haces a mano cuando pruebas Reservo en una máquina nueva, en desorden. Asócialo a las cuatro etapas del pipeline (checkout / instalar / testear / reportar) y ponlo en el orden correcto. (a) Corres python3 -m pytest y miras si dice passed o failed. (b) Ejecutas pip install -r requirements.txt. (c) Clonas el repositorio de Reservo. (d) Lees el resumen y decides si el código está listo para mergear.

Ver solución

El orden y el mapeo:

  1. (c) Clonas el repositorio → etapa 1, checkout. Traer el código a la máquina. Sin esto, no hay nada que hacer.
  2. (b) pip install -r requirements.txt → etapa 2, instalar. Construir el entorno con las dependencias declaradas (para Reservo, esencialmente pytest).
  3. (a) python3 -m pytest → etapa 3, testear. Correr la suite. El corazón del pipeline; el mismo comando que corre el CI.
  4. (d) Lees el resumen y decides → etapa 4, reportar. Convertir el resultado en un veredicto. En el CI, esta etapa es automática y nace del código de salida de (a).

La lección: un pipeline no inventa pasos nuevos; toma los que ya haces a mano y los pone en fila para que una máquina los repita igual cada vez. Reconocer que "el CI es mi flujo manual automatizado" le quita todo el misterio.

Ejercicio 2 — Predice el código de salida. Para cada situación, di con qué código de salida termina pytest (0 o distinto de 0) y, por tanto, qué check pinta el CI (verde o rojo). (a) Los 12 tests de Reservo pasan. (b) 11 pasan y 1 falla. (c) Los 12 "pasan" pero uno estaba mal escrito y en realidad no probaba nada (aserción que siempre es verdadera). (d) La etapa de instalar falló y pytest ni llegó a correr.

Ver solución
  • (a) Código 0 → verde. Todos pasaron; pytest sale con 0. Es el caso que medimos: 12 passed, echo $?0.
  • (b) Código 1 → rojo. Al menos un test falló; pytest sale con 1 (sin importar cuántos pasaron). Lo medimos: 1 failed, echo $?1.
  • (c) Código 0 → verde. ¡Ojo con este! pytest solo mira si los tests pasan, no si son buenos. Un test que siempre da verde (una aserción débil) pasa, así que pytest sale con 0 y el CI pinta verde —aunque no esté probando nada—. El CI hereda la calidad de tu suite; no la juzga. (Esto es tema de fundamentos y del módulo 6: la cobertura ayuda a detectar estos huecos.)
  • (d) Código distinto de 0 → rojo, pero en otra etapa. Si instalar falló, la cadena se detiene ahí (fail fast) y pytest nunca corre. El pipeline reporta rojo, pero el log muestra el fallo en la etapa 2, no en tus tests. Distinguirlo es clave para no depurar en el lugar equivocado.

La lección: el CI decide por el código de salida, y ese número solo dice "pasó / no pasó", no "es bueno / es malo". Un verde del CI vale exactamente lo que vale tu suite.

Ejercicio 3 — El fail fast como diagnóstico. El CI de un compañero está en rojo. Sin ver su código, le haces una sola pregunta que te dice si el problema es de entorno o de lógica. (a) ¿Cuál es la pregunta? (b) Si te responde "se cayó en la etapa de instalar", ¿dónde NO deberías buscar el bug? (c) Si te responde "se cayó en la etapa de testear, con un test en rojo, pero en mi máquina pasa", ¿qué fenómeno de la guía es y a qué módulo lo mandas?

Ver solución
  • (a) La pregunta es: "¿en qué etapa se detuvo la cadena?" (o "¿cuál es la primera etapa que aparece en rojo en el log?"). Gracias al fail fast, la etapa donde se cayó ya te dice de qué tipo es el problema.
  • (b) Si se cayó en instalar, NO busques en la lógica de tus tests ni de Reservo. El problema es de entorno o dependencias: un requirements.txt roto, una versión que no se pudo instalar, un paquete que falta. La suite ni corrió, así que el bug no está en ella. Buscar ahí sería perder el tiempo.
  • (c) Es "en mi máquina funciona" (lección 2): un test que pasa en tu entorno y falla en el limpio del CI. Es casi seguro una fuga de entorno —variable, versión, dependencia, ruta, reloj—. Lo mandas al módulo 3, que se dedica a reproducir localmente un fallo que solo aparece en el CI.

La lección: el fail fast no es solo eficiencia (no gastar etapas de más); es diagnóstico. Saber en qué etapa se cayó la cadena reduce a la mitad el espacio donde buscar el problema, antes de leer una sola línea de código.

Resumen y siguiente paso

En esta lección abriste el pipeline por dentro y viste que no hay magia: es una secuencia ordenada de etapascheckout → instalar → testear → reportar— donde cada una depende de la anterior y una falla detiene la cadena (fail fast). Cada etapa es un comando que ya conoces —git clone, pip install, python3 -m pytest— puesto en fila para que una máquina lo repita igual cada vez. Un pipeline es tu flujo manual de testing, automatizado.

Lo más importante que te llevas es el mecanismo central: el CI decide verde o rojo leyendo el código de salida del proceso de tests. Lo mediste de verdad: la suite verde de Reservo sale con 0 (echo $?0), y un test rojo sale con 1 (echo $?1). El CI no entiende de precios ni de tests; lee ese número y pinta el check. Toda su maquinaria se reduce a "corre los comandos en orden; si uno sale con código distinto de cero, para y reporta rojo".

Antes de avanzar deberías poder: nombrar las cuatro etapas en orden y el comando de cada una; explicar qué es el código de salida y qué significan 0 y 1; explicar el fail fast y por qué la etapa donde se cae la cadena es una pista de diagnóstico.

Lo que sigue es entender qué protege todo este mecanismo. En la lección 6 vas a ver que el veredicto de estas etapas —ese verde o rojo nacido de un código de salida— tiene un trabajo concreto en un equipo: mantener la rama principal siempre verde, de modo que nadie mergee código roto y quien clone main reciba siempre algo que funciona.

Recursos