Módulo 2: Tu primer pipeline — correr pytest en CI
3. `on:` los disparadores: push y pull request
Descripción
De las cuatro claves que desarmaste en la lección 2, la que hace que un workflow sea automático es on:. Sin ella, tu tests.yml sería un archivo perfectamente escrito que no se ejecuta jamás. Con ella, tu suite se corre sola en los momentos que tú declaras. Esta lección se dedica entera a esa clave: qué significa exactamente push, qué agrega pull_request, por qué el par [push, pull_request] es el estándar que usa casi todo el mundo, y —cuando lo necesites— cómo acotar el disparador a ciertas ramas.
Es una lección corta en cantidad de sintaxis y larga en criterio, porque elegir cuándo corre tu CI es una de las pocas decisiones de diseño reales de este módulo. Correr de más gasta tiempo y recursos; correr de menos deja pasar errores hasta un momento más caro. El punto dulce —cada push y cada pull request— no es un accidente: es el equilibrio que cierra el bucle de feedback lo más temprano posible sin volverse ruido. Al terminar vas a entender por qué ese par es el estándar y a poder justificarlo, no solo copiarlo.
Conexión con el módulo: en la lección 2 viste on: [push, pull_request] de pasada, como una de las cuatro claves. Aquí la abrimos. Las lecciones 4, 5 y 6 se ocupan del qué corre (los steps); esta es la única del módulo dedicada al cuándo. Mantente en la frontera: filtrar por rama lo tocamos con moderación (branches:), pero la orquestación avanzada de eventos y las condiciones complejas quedan fuera del alcance del módulo, que es "el primer workflow que corre la suite". Aquí basta con entender a fondo el par que resuelve el 95% de los casos.
El timbre de la casa y el timbre del portón
Imagina que administras un edificio y quieres saber cada vez que alguien intenta entrar. Tienes dos puntos donde poner un timbre, y ponerlos en los dos no es redundante: cubren momentos distintos.
El primero es el portón de la calle, la reja exterior. Suena cuando alguien llega a la propiedad, antes de acercarse a las puertas de los departamentos. Te avisa temprano: "viene alguien". El segundo es la puerta de tu departamento. Suena cuando ese alguien ya subió y está a punto de entrar a tu espacio. Te avisa tarde, pero en el momento crítico: "esta persona va a entrar aquí, ahora".
Un buen sistema tiene los dos. El del portón te da tiempo de reacción —puedes ver quién es antes de que llegue arriba—; el de la puerta es la última línea, la que suena justo antes de que el visitante cruce el umbral de lo que proteges. Si solo tuvieras el del portón, alguien podría subir y entrar a tu departamento en un descuido. Si solo tuvieras el de la puerta, te enterarías demasiado tarde, con la persona ya frente a ti.
En un workflow, push es el timbre del portón y pull_request es el timbre de la puerta. push suena temprano, cada vez que subes commits a cualquier rama —te avisa pronto si algo se rompió—. pull_request suena en el momento crítico, cuando tu código está a punto de fusionarse con la rama principal, la que protege a todo el equipo. Los dos juntos te dan feedback temprano y un guardián en el umbral. Por eso el par [push, pull_request] es el estándar: cubre los dos timbres.
push: el disparador del feedback temprano
on: [push]
push dispara el workflow cada vez que subes commits a una rama del repositorio. Trabajas en tu máquina, haces git commit, haces git push, y en ese instante GitHub ve el push y arranca tu workflow: levanta el runner, trae el código en el estado que acabas de subir, y corre tu suite. En cuestión de un par de minutos sabes si lo que subiste pasa los tests.
Ese es el bucle de feedback del módulo 1, ahora automático y pegado a tu ritmo de trabajo. No esperas al final del día ni al momento de compartir; cada vez que subes, el CI opina. Y opina sobre la rama exacta donde estás trabajando, no solo sobre la principal. Si estás en una rama llamada add-cancel-feature y subes un commit que rompe un test, el push a esa rama dispara el workflow y se pinta de rojo ahí mismo, en tu rama, antes de que nadie más vea tu código. Atrapas el error mientras el contexto está fresco en tu cabeza —justo lo que la guía busca—.
El costo de push es que corre seguido: cada commit subido es una corrida. Para un proyecto como Reservo, cuya suite tarda centésimas de segundo, eso no cuesta prácticamente nada. Para suites lentas empieza a importar, y ahí entran las técnicas de velocidad del módulo 5 (caché, paralelismo) y el criterio de acotar por rama que vemos más abajo. Pero como punto de partida, correr en cada push es lo correcto: el feedback temprano vale más que los segundos de cómputo.
pull_request: el guardián de la rama principal
on: [pull_request]
pull_request dispara el workflow cuando se abre una solicitud de fusión (un pull request) o cuando se le suben commits nuevos. Un pull request es la propuesta formal de "quiero meter los cambios de esta rama en la rama principal (main)". Es el momento de la revisión, donde un compañero mira tu código antes de aceptarlo, y es exactamente el punto donde quieres que los tests corran como condición de entrada.
Aquí está la diferencia sutil pero importante con push. Cuando abres un pull request de tu rama hacia main, pull_request no corre los tests sobre tu rama tal cual: los corre sobre el resultado de fusionar tu rama con main, es decir, sobre cómo quedaría main si aceptaran tu cambio. Eso responde la pregunta que de verdad importa antes de fusionar: "¿la rama principal seguirá sana después de meter esto?". Un cambio puede pasar los tests aislado en tu rama y aun así romper algo al combinarse con lo que otros metieron a main mientras tanto; pull_request cataliza justo ese escenario.
Y es el disparador sobre el que se construyen las protecciones de rama: GitHub te deja configurar que un pull request no se pueda fusionar mientras su workflow esté en rojo. Ese es el mecanismo real por el que "el CI protege main": no es que el CI impida físicamente un mal merge, es que el pull request queda bloqueado —el botón de fusionar se desactiva— hasta que la corrida esté verde. Configurar esa regla es un ajuste del repositorio, no del YAML, y queda fuera del alcance de este módulo; pero conviene saber que pull_request es la pieza del workflow sobre la que esa protección se apoya. Sin un workflow disparado por pull_request, no hay nada verde o rojo que la regla de protección pueda exigir.
Ejemplo trabajado: el par estándar y qué dispara cada evento
El workflow del módulo usa los dos juntos:
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
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
Recuerda la regla del módulo: este archivo es contenido que explicamos, no un CI que ejecutamos. Pero lo que dispara cada evento sí lo podemos trazar con precisión. Sigamos una historia típica de trabajo en Reservo y veamos qué corrida provoca cada acción:
Qué esperar (la secuencia de disparos):
1. Creas la rama add-cancel-feature y subes un commit.
→ push a add-cancel-feature ⇒ corre el workflow (sobre tu rama)
2. Subes dos commits más mientras trabajas.
→ push × 2 ⇒ corre el workflow en cada uno
3. Abres un pull request de add-cancel-feature hacia main.
→ pull_request (opened) ⇒ corre el workflow (sobre el merge simulado)
4. Un compañero te pide un cambio; subes un commit a tu rama.
→ push a add-cancel-feature ⇒ corre el workflow (por el push)
→ pull_request (synchronize) ⇒ corre el workflow (el PR se actualizó)
5. Se aprueba y se fusiona a main.
→ push a main ⇒ corre el workflow (sobre main ya fusionada)
Un par de observaciones sobre esta secuencia, porque revelan cómo se comporta el par en la práctica:
- En el paso 4 notarás que un solo commit disparó dos corridas: una por
push(subiste a tu rama) y otra porpull_request(el pull request abierto se actualizó, lo que GitHub llama el eventosynchronize). Es la duplicación más comentada del par estándar: mientras un pull request está abierto, cada commit que subas a esa rama dispara ambos eventos. Para Reservo, con su suite instantánea, no importa; en proyectos grandes es una de las razones por las que a veces se acotapusha ramas concretas (lo vemos enseguida). No es un error: es el precio de tener los dos timbres. - En el paso 5, al fusionar, el
pushamaincorre la suite una vez más, ahora sobre la rama principal ya con tu cambio dentro. Es la confirmación final de quemainquedó sana. Ese es el timbre del portón sonando en la casa que más te importa proteger.
La suite que cada una de esas corridas ejecutaría es la misma que corres en local. En la lección 6 la vas a correr de verdad y ver su salida; aquí lo que importa es cuándo se dispara, y la respuesta es: en cada push y en cada actualización de pull request, que es tan seguido como tiene sentido.
Acotar por rama, sin perdernos
A veces push en toda rama es más de lo que quieres —por ejemplo, si tienes ramas experimentales de borradores que no necesitan CI en cada commit—. La clave on: admite una forma más detallada para acotar. En vez de la lista corta, se escribe como un mapa con el evento y sus filtros:
on:
push:
branches: [main]
pull_request:
branches: [main]
Léelo con lo que sabes de YAML (lección 2): on ya no es una lista corta sino un mapa con dos claves, push y pull_request, y cada una tiene dentro un branches: que la filtra. Esto dice: "corre en los push a main, y en los pull request dirigidos a main". Los push a otras ramas ya no disparan el workflow por sí solos; pero —y esto es lo que salva el feedback temprano— los pull request hacia main sí siguen corriendo el CI, así que tu código igual se prueba antes de fusionarse. El efecto neto es: menos corridas redundantes en ramas de trabajo, sin perder el guardián del umbral.
No te obsesiones con esto todavía. El par simple [push, pull_request] sin filtros es un default perfectamente bueno y el que usa el módulo; la forma con branches: es la herramienta para cuando el volumen de corridas empiece a molestar. Menciónala en tu cabeza como "existe y así se ve", y vuelve a ella cuando la necesites. Lo importante hoy es entender los dos eventos, no dominar todos sus filtros.
Un tercer disparador útil: correr a mano con workflow_dispatch
push y pull_request cubren el "automático", que es el 95% de lo que quieres. Pero hay un tercer disparador que conviene conocer desde ya porque resuelve un caso concreto y frecuente: querer correr el workflow tú mismo, a mano, sin tener que hacer un push. Se llama workflow_dispatch:
on:
push:
pull_request:
workflow_dispatch:
(Fíjate en la forma: on: como mapa, con cada evento como clave y sin valor —los dos puntos solos significan "este evento, con su comportamiento por defecto"—. Es otra forma válida de escribir on:, útil cuando mezclas eventos automáticos con workflow_dispatch.)
workflow_dispatch agrega un botón "Run workflow" en la pestaña Actions del repositorio. Al pulsarlo, el workflow corre sobre la rama que elijas, en ese momento, sin necesidad de un commit nuevo. ¿Para qué sirve eso? Casos típicos: quieres re-correr la suite después de arreglar algo externo (un servicio que estaba caído), quieres verificar que una rama vieja sigue verde sin tocarla, o simplemente quieres disparar el pipeline bajo demanda mientras experimentas. Es el "botón manual" que complementa los disparadores automáticos.
No lo necesitas para el workflow del módulo —con [push, pull_request] basta—, pero es bueno saber que existe, porque tarde o temprano vas a querer correr el CI sin hacer un push "de mentira" solo para dispararlo. workflow_dispatch es la forma limpia de hacerlo. (Hay más eventos —correr por horario con schedule, reaccionar a otros workflows—, pero esos ya son terreno más avanzado; el trío push + pull_request + workflow_dispatch cubre casi todo lo que un pipeline de tests necesita.)
Errores comunes
Escribir un workflow sin on: y esperar que corra (de disparador ausente). Qué pasa: alguien define jobs y steps impecables pero olvida la clave on:, hace push, y el workflow nunca se ejecuta. Por qué pasa: es fácil concentrarse en el qué (los pasos) y olvidar el cuándo, que es lo único que conecta el archivo con un evento real. Cómo detectarlo: si la pestaña Actions no muestra ninguna corrida y la ubicación del archivo es correcta, revisa que exista on: con al menos un evento. Cómo corregirlo: todo workflow necesita un on:; sin él, GitHub no tiene ningún momento en el cual dispararlo. Añade on: [push, pull_request] y el archivo cobra vida.
Poner solo push en un flujo de pull requests y perder el guardián del umbral (de cobertura incompleta). Qué pasa: un equipo configura on: [push] a secas, y como acostumbra revisar todo vía pull requests hacia main, cree que está cubierto. Funciona hasta que un cambio pasa los tests aislado en su rama pero rompería main al fusionarse —el escenario que solo pull_request cataliza, porque corre sobre el merge simulado, no sobre la rama sola—. Por qué pasa: uno asume que "si probé mi rama, probé el merge", y no siempre es cierto. Cómo detectarlo: si tu flujo de trabajo pasa por pull requests pero tu on: no incluye pull_request, te falta el timbre de la puerta. Cómo corregirlo: incluye ambos eventos. push te da feedback temprano en tu rama; pull_request prueba cómo quedaría main con tu cambio dentro. Son complementarios, no redundantes.
Sorprenderse por las corridas dobles y "arreglarlas" quitando un evento (de duplicación mal entendida). Qué pasa: alguien ve que cada commit a una rama con pull request abierto dispara dos corridas (una por push, otra por pull_request synchronize), lo interpreta como un bug, y quita pull_request para "arreglarlo" —perdiendo justo el guardián del umbral—. Por qué pasa: la duplicación se ve como desperdicio sin entender que cada evento cubre un momento distinto. Cómo detectarlo: dos corridas casi idénticas por commit mientras un PR está abierto es el comportamiento normal del par estándar, no una falla. Cómo corregirlo: si la duplicación de verdad te molesta (suites lentas, muchos contribuyentes), la solución correcta es acotar push por rama con branches: [main] —así los push a ramas de trabajo dejan de duplicar, pero el pull_request sigue protegiendo el merge—, no eliminar el evento que protege la rama principal.
Ejercicios
Ejercicio 1 — Predice los disparos. Con on: [push, pull_request], di cuántas corridas del workflow se disparan en esta secuencia y por qué evento cada una: (a) subes un commit a tu rama fix-refund; (b) abres un pull request de fix-refund hacia main; (c) subes un commit más a fix-refund con el pull request ya abierto; (d) se fusiona el pull request a main.
Ver solución
- (a) subir un commit a
fix-refund→ 1 corrida, porpush(a tu rama). - (b) abrir el pull request → 1 corrida, por
pull_request(eventoopened), sobre el merge simulado conmain. - (c) subir un commit con el PR abierto → 2 corridas: una por
push(subiste a la rama) y otra porpull_request(eventosynchronize, el PR se actualizó). Esta es la duplicación del par estándar. - (d) fusionar a
main→ 1 corrida, porpush(ahora amain, sobre la rama principal ya con el cambio).
Total: 5 corridas. El punto de aprendizaje es el (c): mientras un pull request está abierto, cada commit dispara ambos eventos. Es normal y esperado; solo se vuelve algo a optimizar cuando la suite es lenta o hay mucho tráfico, y la herramienta para eso es acotar push por rama, no quitar pull_request.
Ejercicio 2 — Elige el on: correcto. Un equipo trabaja así: nadie hace commits directos a main; todo cambio va por una rama y se fusiona vía pull request hacia main tras revisión. Quieren (i) que el CI corra cuando alguien propone un cambio a main, y (ii) que corra sobre main cuando un merge ya entró, para confirmar que quedó sana. No les interesa gastar corridas en cada commit de las ramas de borrador. Escribe el on: que cumple eso y explica por qué.
Ver solución
on:
push:
branches: [main]
pull_request:
branches: [main]
Por qué cumple lo pedido:
pull_request:conbranches: [main]cubre el punto (i): corre el CI cuando se propone o actualiza un cambio dirigido amain, probándolo sobre el merge simulado. Es el guardián del umbral.push:conbranches: [main]cubre el punto (ii): corre sobremainjusto cuando un merge entra (fusionar es un push amain), confirmando que la rama principal quedó verde.- Al acotar
pushabranches: [main], los push a las ramas de borrador no disparan el workflow por sí solos, que es lo que el equipo quería evitar. Pero no pierden nada de protección: cuando esas ramas se propongan amainvía pull request, el CI correrá igual (por elpull_request).
Este es exactamente el patrón para el que existe branches:: reducir corridas redundantes en ramas de trabajo sin perder el guardián del merge. Fíjate que si el equipo hiciera commits directos a main (que dijeron que no hacen), este on: seguiría protegiéndolos, porque push a main sí dispara.
Ejercicio 3 — Diagnostica el silencio. Un compañero jura que su workflow está bien escrito, el archivo está en .github/workflows/tests.yml, y aun así "nunca corre nada" tras sus push. Te muestra el inicio del archivo. ¿Cuál es el problema y cómo se arregla?
name: tests
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: pytest
Ver solución
El problema es que falta por completo la clave on:. El workflow tiene nombre, job, máquina y steps —todo el qué y el dónde— pero no tiene cuándo. Sin un on:, GitHub no tiene ningún evento que dispare el workflow, así que el archivo se queda ahí, válido pero inerte, y nunca corre. Por eso el síntoma es "silencio total": ni verde ni rojo, ninguna corrida.
La corrección es añadir el disparador:
name: tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: pytest
Este caso contrasta con el error de ubicación de la lección 2 (archivo fuera de .github/workflows/), que produce el mismo síntoma —silencio—. Cuando un workflow no corre y el archivo está bien ubicado, las dos primeras cosas que revisas son: ¿existe on:?, y ¿el evento que esperas está listado? Aquí, la ausencia del on: era todo.
Resumen y siguiente paso
En esta lección abriste la clave que hace automático a un workflow: on:. Entendiste los dos eventos del par estándar como dos timbres que cubren momentos distintos. push suena temprano —cada vez que subes commits a una rama— y te da feedback pegado a tu ritmo de trabajo, sobre la rama exacta donde estás. pull_request suena en el umbral —cuando propones fusionar hacia main— y prueba tu cambio sobre el merge simulado, respondiendo la pregunta que de verdad importa: "¿main seguirá sana con esto dentro?". Juntos, [push, pull_request], cubren feedback temprano y guardián del merge, por eso son el estándar. Viste también la duplicación normal (un commit con un PR abierto dispara ambos eventos) y cómo, cuando el volumen molesta, se acota push por rama con branches: sin perder la protección del pull request.
Antes de avanzar deberías poder: explicar la diferencia entre lo que prueba push (tu rama) y lo que prueba pull_request (el merge con main); justificar por qué se usan los dos juntos; reconocer la corrida doble como comportamiento esperado; y escribir un on: acotado a main con branches: cuando haga falta.
Ya tienes el cuándo y el dónde. Lo que falta es el qué: los pasos que preparan la máquina y corren la suite. En la lección 4 empezamos por los dos steps que ponen el terreno antes de que nada se pueda probar: actions/checkout, que trae tu código al runner vacío, y actions/setup-python, que instala el Python exacto que pediste. Vas a entender por qué el runner arranca sin nada, por qué el orden de estos pasos importa, y qué es ese with: que le pasa python-version a la acción. Son los cimientos sobre los que, dos lecciones después, correrá pytest.
Recursos
- Eventos que disparan workflows — el catálogo oficial de todos los eventos que pueden ir en
on:, no solopushypull_request. La referencia para cuando necesites disparar por otra cosa (una etiqueta, un horario, manualmente). Densa; úsala como diccionario. - Disparar un workflow (documentación de GitHub Actions) — la guía práctica de cómo elegir cuándo corre un workflow, con los filtros de rama (
branches:) que vimos de pasada. El siguiente paso si quieres afinar el cuándo más allá del par estándar. - Sobre los pull requests (documentación de GitHub) — qué es un pull request y cómo funciona la revisión y la fusión hacia
main. Útil si el concepto de pull request todavía no está firme; es el terreno sobre el que se apoya el disparadorpull_request.