Módulo 2: Tu primer pipeline — correr pytest en CI
2. Anatomía de un workflow de GitHub Actions
Descripción
En la lección 1 viste el tests.yml entero de un vistazo y aprendiste a leerlo como tres preguntas —cuándo, dónde, qué—. Esta lección lo desarma tornillo por tornillo. Al terminar vas a saber exactamente dónde vive el archivo en tu repositorio, cómo funciona el YAML como formato (y por qué la indentación no es decoración sino sintaxis), y qué hace cada una de las cuatro claves que estructuran todo workflow: name, on, jobs, y dentro de un job, runs-on y steps.
No vamos a correr nada en esta lección —recuerda la regla del módulo: el workflow es contenido que se explica—. Pero sí vamos a leerlo con la misma seriedad con que leerías el código de una función que vas a mantener, porque eso es lo que es: un archivo de configuración que otras personas de tu equipo van a leer, copiar y modificar. Un workflow que no entiendes es un workflow que no puedes arreglar cuando falle, y va a fallar; el objetivo de hoy es que este archivo deje de ser una caja negra y se vuelva algo que lees de corrido.
Conexión con el módulo: esta lección arma el esqueleto del workflow; las que siguen rellenan cada hueco. Aquí ves las cuatro claves y el workflow mínimo completo, pero tratamos on: y los steps de forma superficial a propósito: el on: a fondo es la lección 3, los steps de preparación (checkout, setup-python) son la 4, instalar dependencias es la 5, y el step que corre pytest es la 6. Piensa en esta lección como el plano de la casa: hoy ves las habitaciones y cómo se conectan; el mobiliario de cada una llega después.
Un workflow es una receta de cocina
Imagina la receta de un pastel escrita en una tarjeta. Tiene una estructura muy reconocible, y esa estructura es casi idéntica a la de un workflow.
Arriba, el nombre: "Pastel de zanahoria". No cambia nada de cómo se hace el pastel; sirve para que, cuando tengas un fichero de recetas, sepas cuál es cuál de un vistazo. Ese es el name del workflow.
Después, cuándo se hace: "para cumpleaños". La ocasión que dispara que alguien saque esta receta y se ponga a cocinar. Ese es el on.
Luego, el trabajo en sí, que a su vez tiene dos partes. Primero, dónde y con qué se cocina: "en la cocina, con horno". El entorno. Ese es el runs-on. Y segundo, los pasos en orden: precalentar el horno, mezclar los secos, batir los húmedos, combinar, hornear 40 minutos. Esa lista numerada, que hay que seguir de arriba a abajo porque el paso 4 no tiene sentido sin el 3, son los steps.
Un workflow es exactamente eso: un nombre, una ocasión, un lugar donde trabajar, y una lista ordenada de pasos. Si sabes leer una receta, ya sabes leer la forma de un workflow; solo falta aprender cómo se escribe cada parte en el formato que GitHub entiende. Y ese formato tiene una peculiaridad que conviene atender antes que nada: se llama YAML, y le importa mucho cómo alineas las cosas.
Dónde vive el archivo (y por qué ahí)
Antes del contenido, la ubicación, porque si el archivo no está en el lugar exacto, GitHub no lo mira y nada de esto ocurre.
Los workflows viven en una carpeta muy específica dentro de tu repositorio: .github/workflows/. Fíjate en los detalles, porque cada uno importa:
- El nombre de la carpeta externa empieza con un punto:
.github. En Unix, un nombre que empieza con punto es un archivo o carpeta "oculto" (no aparece en unlsnormal), y es la convención para cosas de configuración. GitHub busca su configuración justo ahí. - Dentro va otra carpeta,
workflows, en plural. - Y adentro, tus archivos
.yml(o.yaml, las dos extensiones sirven). El nombre del archivo lo eliges tú:tests.yml,ci.yml, lo que sea descriptivo. Puedes tener varios; GitHub corre todos los que encuentre ahí.
La ruta completa de nuestro archivo, entonces, es:
mi-repositorio/
├── .github/
│ └── workflows/
│ └── tests.yml ← el workflow vive aquí
├── reservo/
│ ├── models.py
│ ├── pricing.py
│ └── ...
├── test_pricing.py
├── test_refunds.py
├── requirements.txt
└── README.md
GitHub revisa esa carpeta automáticamente: cada vez que haces push, mira qué workflows hay en .github/workflows/ y evalúa si alguno debe dispararse. No hay que "registrar" el workflow en ningún panel ni activar nada; poner el archivo en esa carpeta y hacer push es todo lo que se necesita para que exista. Por eso el error número uno de principiante con Actions es poner el archivo en el lugar equivocado —en la raíz, o en workflows/ sin el .github, o con una falta de ortografía en la ruta— y luego preguntarse por qué "no pasa nada" en cada push. Si tu workflow parece ignorado, lo primero que revisas es la ruta exacta.
El YAML: un formato donde la indentación es la sintaxis
El archivo está escrito en YAML, un formato de texto para datos estructurados. Su gracia es que se lee casi como una lista con viñetas hecha a mano, sin las llaves y comillas de otros formatos. Su trampa es que la indentación —los espacios al inicio de cada línea— no es estética: es parte del significado. Dos líneas con la misma indentación están "al mismo nivel"; una línea más indentada está "dentro de" la de arriba. Cambiar la indentación cambia la estructura, igual que en Python.
YAML tiene solo tres construcciones que necesitas reconocer, y ya las viste todas en el workflow:
Uno: pares clave-valor (un "mapa" o diccionario). Una clave, dos puntos, un valor:
name: tests
runs-on: ubuntu-latest
name es la clave, tests el valor. Léelo como "el nombre es tests". El espacio después de los dos puntos es obligatorio.
Dos: listas. Cada elemento empieza con un guion y un espacio:
on:
- push
- pull_request
Eso es "una lista de dos cosas: push y pull_request". Hay una forma corta, en una línea, entre corchetes, que significa exactamente lo mismo:
on: [push, pull_request]
Las dos son idénticas; usamos la corta cuando la lista es breve y la larga cuando cada elemento tiene detalles adentro. Lo verás con los steps, que son una lista donde cada elemento es a su vez un mapa.
Tres: anidamiento por indentación. Aquí está el corazón. Cuando una clave contiene más estructura, esa estructura va indentada debajo:
jobs:
test:
runs-on: ubuntu-latest
Se lee de dentro hacia afuera: runs-on: ubuntu-latest está dentro de test, que está dentro de jobs. Es "jobs contiene un job llamado test, y ese job corre en ubuntu-latest". La indentación (aquí, dos espacios por nivel) es lo único que expresa ese "dentro de". Si runs-on estuviera al mismo nivel que test, significaría otra cosa completamente distinta —o, más probablemente, sería un error de sintaxis y GitHub rechazaría el workflow—.
Dos reglas prácticas que te ahorran el 90% de los dolores con YAML:
- Usa espacios, nunca tabuladores. YAML prohíbe el tabulador para indentar. Configura tu editor para que la tecla Tab inserte espacios en archivos
.yml. Un tabulador escondido es la causa clásica de un "workflow inválido" que no encuentras a simple vista. - Sé consistente con la cantidad de espacios. Dos espacios por nivel es la convención. Lo importante no es el número exacto, sino no mezclar: si un nivel usa dos espacios y otro usa cuatro sin razón, tarde o temprano te confundes.
Ejemplo trabajado: el workflow entero, comentado línea por línea
Aquí está el workflow mínimo que corre la suite de Reservo. Es el mismo de la lección 1, ahora anotado. Léelo despacio; después lo recorremos clave por clave.
# .github/workflows/tests.yml
# El nombre del workflow, como aparece en la pestaña "Actions" de GitHub.
name: tests
# CUÁNDO correr: en cada push y en cada pull request. (Lección 3.)
on: [push, pull_request]
# QUÉ trabajos correr. Un workflow tiene uno o más "jobs".
jobs:
# Definimos un job y lo llamamos "test" (el nombre lo eliges tú).
test:
# DÓNDE correr este job: una máquina Ubuntu limpia y actualizada.
runs-on: ubuntu-latest
# Los PASOS del job, en orden, de arriba a abajo.
steps:
# Paso 1: traer tu código al runner. (Lección 4.)
- uses: actions/checkout@v5
# Paso 2: instalar el Python que pediste. (Lección 4.)
- uses: actions/setup-python@v5
with:
python-version: "3.14"
# Paso 3: actualizar pip. (Lección 5.)
- run: python -m pip install --upgrade pip
# Paso 4: instalar las dependencias del proyecto. (Lección 5.)
- run: pip install -r requirements.txt
# Paso 5: correr la suite de tests. (Lección 6.)
- run: pytest
Las líneas que empiezan con # son comentarios: YAML las ignora por completo, existen solo para el humano que lee. Puedes ponerlos donde quieras para explicarte a ti mismo o a tu equipo qué hace cada parte.
Ahora, las cuatro claves que estructuran todo, de arriba a abajo.
name — cómo se llama el workflow
name: tests
Es la etiqueta con la que este workflow aparece en la interfaz de GitHub, en la pestaña "Actions" donde se ven todas las corridas. Es puramente cosmético: si lo borras, el workflow sigue funcionando igual (GitHub usaría el nombre del archivo como etiqueta). Pero ponlo, porque en cuanto tengas más de un workflow, un nombre claro es la diferencia entre encontrar la corrida que buscas y adivinar. tests dice lo que hace.
on — el disparador
on: [push, pull_request]
El cuándo. Le dice a GitHub qué eventos hacen que este workflow se ejecute. Aquí, dos: cada push (cada vez que subes commits) y cada pull_request (cada vez que se abre o actualiza una solicitud de fusión). Esta única línea es lo que convierte el workflow en algo automático: sin ella, el archivo estaría ahí sin dispararse nunca. La lección 3 es entera sobre esta clave —qué eventos existen, por qué push + pull_request es el par estándar, y cómo acotar a ramas específicas—; por ahora quédate con que on responde "cuándo".
jobs — el trabajo (o los trabajos)
jobs:
test:
runs-on: ubuntu-latest
steps:
...
Aquí vive el músculo del workflow. jobs es un mapa de uno o más trabajos, y cada trabajo es una unidad de ejecución independiente que corre en su propia máquina. Le pusimos un solo job y lo llamamos test —ese nombre lo eliges tú; podría ser build, lint, unit-tests—. Un workflow puede tener varios jobs (por ejemplo, uno que corre los tests y otro que revisa el estilo del código) y, por defecto, corren en paralelo, cada uno en su propia máquina limpia. En este módulo nos basta con uno.
Dentro del job hay dos claves que lo definen: runs-on (dónde) y steps (qué pasos). Veámoslas.
runs-on — la máquina donde corre el job
runs-on: ubuntu-latest
El dónde. Especifica el sistema operativo y la versión de la máquina virtual —el runner— donde este job se ejecutará. ubuntu-latest significa "la versión estable más reciente de Ubuntu Linux que GitHub ofrece". Es la opción más común porque es rápida, barata (los runners de Linux consumen menos que los de Windows o macOS) y suficiente para correr tests de Python. Hay otras opciones —windows-latest, macos-latest— y correr en varias a la vez es justamente el tema de la matriz del módulo 4; aquí, con Ubuntu nos sobra. Lo esencial: esta máquina arranca limpia, sin tu código y sin tus dependencias. Todo lo que necesite para correr tu suite, se lo dan los steps.
steps — la lista ordenada de pasos
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
with:
python-version: "3.14"
- run: python -m pip install --upgrade pip
- run: pip install -r requirements.txt
- run: pytest
El qué, y la parte donde ocurre el trabajo de verdad. steps es una lista —fíjate en el guion al inicio de cada elemento— y GitHub la ejecuta en orden, de arriba a abajo, en la misma máquina. Ese orden es sagrado: no puedes correr pytest (paso 5) antes de instalar las dependencias (paso 4), ni instalar dependencias antes de traer el código (paso 1). Es la lista de la receta, y saltarse el orden la rompe.
Hay dos tipos de step, y ya aparecen los dos aquí:
- Un step con
uses:ejecuta una acción reutilizable —un bloque de trabajo empaquetado que alguien más (a menudo el propio GitHub) escribió y publicó—.actions/checkout@v5es "usa la acción oficial de checkout, versión 5". No reinventas cómo traer el código; usas la pieza probada que ya existe. El@v5fija la versión, un detalle que la lección 4 explica con cuidado. - Un step con
run:ejecuta un comando de terminal literal, tal como lo escribirías tú en tu shell.run: pytestes, palabra por palabra, correrpytesten la máquina.run: pip install -r requirements.txtes ese mismo comando de pip.
La regla mnemotécnica: uses es "trae una herramienta ya hecha", run es "escribe un comando yo mismo". Traer el código y poner Python son tareas comunes que ya tienen acción oficial (uses); instalar tus dependencias y correr tus tests son comandos tuyos, específicos de tu proyecto (run). La lección 4 desmenuza los dos steps de uses, y la 6 el run: pytest que es el corazón de todo.
Errores comunes
Poner el archivo fuera de .github/workflows/ y creer que el CI está roto (de ubicación). Qué pasa: alguien crea tests.yml en la raíz del repo, o en una carpeta workflows/ sin el .github, hace push, y no ocurre absolutamente nada —ninguna corrida, ningún error visible—. Por qué pasa: GitHub solo mira esa ruta exacta; un archivo de workflow en cualquier otro lado es, para GitHub, un archivo de texto cualquiera. Cómo detectarlo: si tras un push la pestaña "Actions" no muestra ninguna corrida nueva, sospecha de la ubicación antes que del contenido. Cómo corregirlo: confirma la ruta carácter por carácter —.github/workflows/tests.yml, con el punto inicial y workflows en plural—. El silencio total (ni verde ni rojo) casi siempre es un problema de dónde está el archivo, no de qué dice.
Romper la estructura con indentación inconsistente o un tabulador (de sintaxis YAML). Qué pasa: alguien alinea steps con cuatro espacios en un lado y dos en otro, o su editor mete un tabulador, y GitHub reporta "Invalid workflow file" con un error de sintaxis que no se ve a simple vista. Por qué pasa: en YAML la indentación es la estructura, y un tabulador —invisible— no es lo mismo que espacios. Cómo detectarlo: GitHub marca el workflow como inválido y suele señalar la línea; muchos editores muestran los tabuladores si activas "ver caracteres invisibles". Cómo corregirlo: usa siempre espacios (nunca Tab) y sé consistente con la cantidad por nivel. Configura tu editor para que la tecla Tab inserte espacios en archivos .yml, y el problema desaparece de raíz.
Confundir uses con run (de tipo de step). Qué pasa: alguien escribe run: actions/checkout@v5 (como si fuera un comando) o uses: pytest (como si fuera una acción), y el step falla. Por qué pasa: los dos son "pasos" y es fácil mezclar cuál va con qué. Cómo detectarlo: si un step que debería traer el código o poner Python falla con "command not found", probablemente pusiste una acción dentro de un run; si un pytest no corre, quizá lo pusiste como uses. Cómo corregirlo: recuerda la regla —uses para acciones reutilizables (checkout, setup-python), run para comandos de terminal (pip, pytest)—. Si es algo que tú escribirías en tu shell, es run; si es una pieza empaquetada con un nombre tipo owner/name@version, es uses.
Ejercicios
Ejercicio 1 — Identifica las cuatro claves. Mira este fragmento de workflow y responde: ¿cuál es el nombre, cuándo se dispara, en qué máquina corre, y cuántos steps tiene? Además, di cuál de los steps es de tipo uses y cuál de tipo run.
name: checks
on: [push]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: pytest -q
Ver solución
- Nombre:
checks(lo que dirá la etiqueta en la pestaña Actions). - Cuándo: en cada
push(solo push; este no corre en pull requests, a diferencia del nuestro). - Máquina:
ubuntu-latest, una Ubuntu limpia. - Steps: dos. El primero,
uses: actions/checkout@v5, es de tipouses(trae una acción reutilizable para traer el código). El segundo,run: pytest -q, es de tiporun(un comando de terminal literal, aquí pytest en modo silencioso con-q).
Fíjate que este workflow le falta algo para funcionar de verdad con un proyecto que tiene dependencias: no hay setup-python ni pip install. Correría pytest en la máquina con el Python que Ubuntu trae de fábrica y sin instalar nada del proyecto —tema de las lecciones 4 y 5—. Estructuralmente es un workflow válido; funcionalmente, incompleto.
Ejercicio 2 — Arregla la indentación. Este workflow está mal indentado y GitHub lo rechazaría. Sin cambiar ninguna palabra, corrige la estructura para que runs-on y steps queden dentro del job test, y el step dentro de steps.
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: pytest
Ver solución
El problema es que runs-on, steps y el step están todos al mismo nivel que test, cuando deberían estar dentro de él. Corregido con dos espacios adicionales por nivel:
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: pytest
Lo que cambió: runs-on y steps ahora están indentados debajo y dentro de test (cuatro espacios, dos más que test), y el - run: pytest está dentro de steps (seis espacios). La indentación es lo único que expresa "esto pertenece a aquello", y sin ella GitHub no sabe que runs-on es una propiedad del job test en vez de otra cosa suelta. Este es exactamente el tipo de error que produce un "Invalid workflow file", y por eso la consistencia en la indentación es la primera regla de higiene del YAML.
Ejercicio 3 — Traduce la receta a las cuatro claves. Un compañero describe lo que quiere en prosa: "Un workflow que se llame ci, que corra cuando alguien hace push, en una máquina Ubuntu, y que tenga tres pasos: traer el código con la acción de checkout, y luego dos comandos míos, pip install -r requirements.txt y pytest." Escribe el YAML correspondiente. (No te preocupes por setup-python; solo traduce lo que pidió.)
Ver solución
name: ci
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: pip install -r requirements.txt
- run: pytest
Repasa las decisiones:
name: ci— el nombre que pidió.on: [push]— solo push (no mencionó pull requests, así que no los ponemos; podríamos, y sería mejor práctica, pero el ejercicio es traducir lo pedido).jobs:con un jobtest— el nombre del job lo elegimos nosotros;testes descriptivo.runs-on: ubuntu-latest— la máquina Ubuntu.- Tres steps en orden:
checkoutconuses(es una acción reutilizable), y luego los dos comandos conrun(son comandos de terminal suyos). El orden importa: primero traer el código, luego instalar, luego correr.
Este workflow es válido y casi completo; lo único que lo haría robusto es un setup-python entre el checkout y el pip install, para fijar la versión de Python en vez de depender de la que traiga el runner. Eso es exactamente lo que agrega la lección 4.
Resumen y siguiente paso
En esta lección desarmaste el tests.yml pieza por pieza. Aprendiste dónde vive —en .github/workflows/, con el punto inicial y el plural, una ruta exacta cuyo error más común es ponerla mal y creer que "el CI no funciona"—. Entendiste el YAML como formato: pares clave-valor, listas con guiones, y anidamiento por indentación, donde los espacios son la sintaxis (nunca tabuladores, siempre consistente). Y recorriste las cuatro claves que estructuran todo workflow: name (cómo se llama, cosmético), on (cuándo se dispara), jobs (los trabajos), y dentro de un job runs-on (la máquina limpia donde corre) y steps (la lista ordenada de pasos). Viste también la distinción que vas a usar en cada workflow: un step con uses trae una acción reutilizable ya hecha (checkout, setup-python); uno con run ejecuta un comando de terminal tuyo (pip, pytest).
Antes de avanzar deberías poder: escribir de memoria la ruta donde vive un workflow; explicar por qué la indentación importa en YAML y qué la rompe; nombrar las cuatro claves y qué responde cada una; y clasificar un step como uses o run con solo mirarlo.
Tienes el esqueleto. Lo que sigue es rellenar cada hueco, empezando por el que hace todo automático. En la lección 3 nos enfocamos en la clave on: —los disparadores—: qué significa exactamente push, qué agrega pull_request, por qué ese par es el estándar que protege la rama principal de un equipo, y cómo, si lo necesitas, acotar el workflow a ciertas ramas. Es la línea que convierte tu archivo de "catorce líneas de configuración" en "un guardián que se despierta solo en cada cambio".
Recursos
- Sintaxis de workflows para GitHub Actions — la referencia oficial y completa de todas las claves de un workflow (
name,on,jobs,runs-on,steps, y muchas más). Es densa; consúltala como diccionario cuando quieras el detalle exacto de una clave, no de corrido. - Sobre YAML para GitHub Actions — la sección específica sobre cómo GitHub usa YAML, con ejemplos de mapas, listas e indentación. El complemento exacto de la parte de esta lección sobre el formato.
- Especificación de YAML (yaml.org) — la referencia del formato en sí, más allá de GitHub. Útil si quieres entender YAML como herramienta general (lo vas a encontrar en muchísimos otros lugares de la vida de un desarrollador, no solo en Actions).