Módulo 5: Apply On Merge The Cd Half

2. El disparador de fusión: `push` a `main`

Descripción

Esta lección diseca el primer campo que vas a escribir en apply.yml: on: push: branches: [main]. Ya conoces push desde el Módulo 2 (lección 3) — lo nuevo aquí no es la sintaxis, es la decisión que representa: por qué este disparador específico, con este filtro específico, es la única puerta que apply.yml puede tener, y por qué el hallazgo del Módulo 2 —act no evalúa branches:— importa mucho más en este archivo que en cualquier otro que hayas escrito hasta ahora.

Conexión con el módulo

La lección 1 te dio el mapa completo. Esta lección construye la primera línea real de apply.yml: su bloque on. La lección 3 sigue con jobs y needs; la lección 4 junta todo en el archivo completo, corrido con act push. Todo lo que leas aquí se vuelve, literalmente, las tres primeras líneas del archivo que vas a construir.


Analogía: la puerta que solo se abre desde adentro

Un pull_request es alguien tocando timbre desde afuera —cualquiera puede proponer un cambio, sin que eso signifique que entró—. Un push a main, en cambio, es alguien que ya está adentro del edificio, caminando hacia la puerta de la bóveda: ya pasó control de acceso (la revisión del Pull Request, Módulo 3), ya fue autorizado a estar en esa zona (la fusión). apply.yml, al escuchar únicamente push a main, es la puerta de la bóveda que solo responde a alguien que ya cruzó todos los controles anteriores — nunca a alguien que solo tocó timbre.


El bloque on de apply.yml

on:
  push:
    branches: [main]

Tres piezas, cada una con un rol preciso:

  • push — el disparador correcto para "algo ya llegó a una rama", que ya viste en el Módulo 2 (lección 3). A diferencia de pull_request, no hay ninguna ambigüedad sobre si el código "ya está ahí" — un evento push solo existe después de que los commits llegaron de verdad a la rama.
  • branches: [main] — el filtro que reduce ese disparador de "cualquier rama" a, exclusivamente, main. Sin este filtro, un push a feature/lo-que-sea dispararía apply.yml con la misma fuerza que un push a main — algo que este archivo, por diseño, nunca debe permitir.
  • La ausencia de pull_request — tan importante como lo que sí está escrito. apply.yml no tiene, ni va a tener nunca, un bloque pull_request: en su on. Es la separación estructural exacta que el Módulo 3 (lección 2) explicó con la analogía de las dos puertas: ci.yml escucha pull_request y nunca aplica; apply.yml escucha push a main y nunca corre sobre una propuesta sin revisar. No hay ninguna condición if: en el medio decidiendo esto — es una propiedad del archivo mismo.

Por qué main, no cualquier otra rama

El nombre de la rama importa por una razón muy concreta de esta guía: git init -b main (Módulo 1, lección 8) fijó main como la rama por defecto exactamente para que este filtro tuviera sentido. Si tu repositorio usara master, o cualquier otro nombre, este branches: [main] simplemente nunca dispararía — no fallaría con un error, solo se quedaría esperando un evento que nunca llega. Vale la pena confirmarlo, no asumirlo: la rama de tu repositorio real tiene que llamarse exactamente main para que todo lo que sigue en esta lección tenga sentido.

cd andes-cargo-infra
git branch --show-current

Qué esperar (literal, si seguiste el Módulo 1 sin desviarte):

main

El hallazgo del Módulo 2, ahora con consecuencias reales

En el Módulo 2 (lección 3) descubriste, con una prueba real, que act no evalúa branches:/paths: antes de correr un job — ese filtro es un mecanismo del lado del servidor de GitHub, y act, al no tener ningún servidor involucrado, simplemente lo ignora. En aquel momento, la consecuencia era abstracta: "esto va a importar para apply.yml". Ahora que estás escribiendo ese archivo, la consecuencia es directa.

Confírmalo de nuevo, esta vez sobre el disparador real de apply.yml. Un workflow mínimo, con el mismo on exacto que vas a usar en la lección 4:

name: apply-trigger-test

on:
  push:
    branches: [main]

jobs:
  would-apply:
    runs-on: ubuntu-latest
    steps:
      - run: echo "This step would run terraform apply, on ref ${{ github.ref }}"

Un evento push escrito a mano, describiendo un push a una rama de feature — exactamente el mismo patrón de nombre que ya usas en pr-event.json desde el Módulo 2:

{
  "ref": "refs/heads/feature/add-shipment-tags",
  "repository": { "default_branch": "main" }
}
act push -e push-feature-event.json -j would-apply

Qué esperar (salida literal, ejecutada para escribir esta lección):

[apply-trigger-test/would-apply] ⭐ Run Main echo "This step would run terraform apply, on ref refs/heads/feature/add-shipment-tags"
[apply-trigger-test/would-apply]   🐳  docker exec cmd=[bash -e /var/run/act/workflow/0] user= workdir=
[apply-trigger-test/would-apply]   | This step would run terraform apply, on ref refs/heads/feature/add-shipment-tags
[apply-trigger-test/would-apply]   ✅  Success - Main echo "This step would run terraform apply, on ref refs/heads/feature/add-shipment-tags" [89.622542ms]
[apply-trigger-test/would-apply] ⭐ Run Complete job
[apply-trigger-test/would-apply] Cleaning up container for job would-apply
[apply-trigger-test/would-apply]   ✅  Success - Complete job
[apply-trigger-test/would-apply] 🏁  Job succeeded

El job corrió. github.ref vale refs/heads/feature/add-shipment-tags — exactamente la rama que branches: [main] debería haber bloqueado. Si este fuera el apply.yml real, con un step de terraform apply en vez de un echo, este comando acabaría de aplicar infraestructura a partir de una rama de feature sin revisar, bajo act, sin que nada te avisara del problema.

Contrástalo con el evento por defecto de act, que —como confirmaste en el Módulo 2 (lección 2)— deriva el ref de tu rama de Git real:

act push -j would-apply

Qué esperar (literal, corrido dentro de un repositorio cuya rama actual es main):

[apply-trigger-test/would-apply] ⭐ Run Main echo "This step would run terraform apply, on ref refs/heads/main"
[apply-trigger-test/would-apply]   | This step would run terraform apply, on ref refs/heads/main
[apply-trigger-test/would-apply]   ✅  Success - Main echo "This step would run terraform apply, on ref refs/heads/main" [102.859958ms]
[apply-trigger-test/would-apply] 🏁  Job succeeded

Aquí no hay ningún misterio: act push, sin -e, arma un evento sintético a partir de tu rama local actual —si estás parado en main, el evento dice main—. La diferencia entre esta corrida y la anterior no es que act haya "aprendido" a respetar el filtro; es que, esta vez, el evento sintético coincidía por casualidad con lo que el filtro permite. El filtro sigue sin evaluarse en ningún caso — solo que, cuando trabajas desde main (que es exactamente cómo vas a correr apply.yml en la lección 4), la diferencia no se nota.


La consecuencia práctica, sin exagerar ni minimizar

branches: [main] en apply.yml sigue siendo la protección correcta y obligatoria en un repositorio real de GitHub — funciona exactamente como esperas ahí, bloqueando cualquier push que no sea a main antes de que el job siquiera arranque. Lo que esta lección confirma, otra vez, es que act no puede demostrarte que ese filtro funciona — puede confirmarte que el YAML es válido, que el job hace lo que el resto del archivo dice que hace, pero no puede simular el rechazo del lado del servidor. Es la misma distinción exacta que el Módulo 2 ya estableció, ahora aplicada al archivo donde más importa: si algún día necesitas verificar de verdad que apply.yml rechaza un push a una rama de feature, la única forma confiable es un repositorio real de GitHub — tema que retoma el Módulo 8 (lección 5).


Errores comunes

Confiar en act para validar que apply.yml está bien protegido (el error central de esta lección). Qué pasa: alguien corre act push sobre apply.yml desde una rama de feature, ve que el job corre, y concluye que el filtro branches: [main] "no sirve" o que hay que agregar una condición if: adicional. Por qué pasa: es razonable, aunque incorrecto, esperar que una herramienta que simula GitHub Actions replique cada mecanismo de GitHub Actions. Cómo detectarlo: si tu conclusión después de correr act sobre una rama de feature es "el YAML está mal", en vez de "esto es exactamente el límite de simulación que ya conozco del Módulo 2". Cómo corregirlo: el filtro está bien escrito. Confía en branches: [main] en apply.yml de la misma forma que confías en cualquier pieza de GitHub Actions que act no puede ejecutar completa —documentada, correcta, simplemente no demostrable localmente—.

Olvidar que main tiene que coincidir exactamente con el nombre real de tu rama (de configuración). Qué pasa: alguien clona o crea un repositorio donde la rama principal se llama master (el valor por defecto de versiones viejas de Git, o de algunas configuraciones globales), y apply.yml simplemente nunca corre, sin ningún mensaje de error. Cómo detectarlo: git branch --show-current muestra algo distinto a main, y ningún push real dispara apply.yml en un repositorio de GitHub real. Cómo corregirlo: revisa el Módulo 1 (lección 8) — git init -b main fija esto desde el origen; si tu repositorio ya existe con otro nombre, renombra la rama (git branch -m master main) antes de continuar, o ajusta branches: para que coincida con el nombre real, aunque esta guía asume main en todos sus ejemplos.

Agregar pull_request al on de apply.yml "por si acaso" (de diseño, revisita el Módulo 3). Qué pasa: alguien, pensando que más disparadores dan más flexibilidad, agrega pull_request: junto a push: en el on de apply.yml. Por qué pasa: se siente conveniente poder "probar" el apply desde un Pull Request antes de fusionar. Cómo corregirlo: esto rompe exactamente la separación estructural que el Módulo 3 (lección 2) explicó como decisión de seguridad, no de conveniencia — reintroduce el riesgo de que un Pull Request sin revisar dispare un apply real. Si necesitas "probar" el apply antes de fusionar, la herramienta correcta es revisar el plan que ci.yml ya publica, no darle a apply.yml la capacidad de correr sobre una propuesta.


Ejercicios

Ejercicio 1 — Predice el resultado de tres eventos distintos. Sin correr nada todavía, para un apply.yml con on: push: branches: [main], predice si el job correría bajo act (no en GitHub real) para: (a) act push desde la rama main local; (b) act push -e evento.json con "ref": "refs/heads/main"; (c) act push -e evento.json con "ref": "refs/heads/hotfix/urgent".

Ver solución

Las tres correrían bajo act — en los tres casos, el tipo de evento (push) coincide con lo declarado en on, y act no evalúa el contenido de branches: en ninguno de los tres. La diferencia entre (a)/(b) y (c) solo importaría en GitHub real, donde (c) sería rechazado antes de asignar un runner.

Ejercicio 2 — Explica la ausencia de pull_request a un colega. Un colega, mirando apply.yml, te pregunta por qué no tiene un bloque pull_request: como ci.yml, ya que "sería útil para probar el apply antes de fusionar". Respóndele con la razón de seguridad exacta.

Ver solución

Una respuesta completa suena, más o menos, así: "Si apply.yml escuchara pull_request, cualquier Pull Request sin revisar podría disparar un terraform apply real, exactamente el escenario de 'pwn request' que el Módulo 3 explicó — la separación entre 'calcular y mostrar' (ci.yml, disparado por pull_request) y 'aplicar' (apply.yml, disparado únicamente por push a main) es lo que garantiza que ningún cambio sin revisar pueda tocar infraestructura real. No es una limitación técnica, es la decisión de seguridad central de todo este módulo."

Ejercicio 3 — Diagnostica un apply.yml que nunca corre en GitHub real. Un colega te dice: "en mi repositorio de GitHub real, hago push a mi rama principal y apply.yml nunca se dispara, aunque el archivo existe y el YAML es válido". ¿Cuál es la primera pregunta que le harías, según esta lección?

Ver solución

La primera pregunta sería: "¿tu rama principal se llama exactamente main?". Si el repositorio usa master (o cualquier otro nombre) como rama por defecto, branches: [main] nunca va a coincidir, y GitHub simplemente no dispara el workflow — sin ningún error visible, porque desde la perspectiva de GitHub, el evento real (push a master) nunca cumplió la condición del filtro. La solución es renombrar la rama a main o ajustar el filtro para que coincida con el nombre real.


Resumen y siguiente paso

En esta lección escribiste el bloque on de apply.ymlpush a main, sin pull_request— y confirmaste, con una prueba real, que act sigue sin evaluar branches: incluso en este archivo específico: un push sintético a una rama de feature corrió el job igual, exactamente como predijo el Módulo 2. Viste también que el evento por defecto de act push deriva su ref de tu rama de Git local, así que trabajar desde main —como vas a hacer en la lección 4— hace que esta limitación no se note en la práctica, aunque siga existiendo.

Antes de avanzar deberías poder: escribir on: push: branches: [main] de memoria; explicar por qué apply.yml nunca debería tener pull_request en su on; y decir, sin dudar, que verificar ese filtro de verdad requiere un repositorio real de GitHub, no act.

La lección 3 sigue con la segunda pieza de apply.yml: cómo estructurar sus jobs para que el apply dependa del éxito de un paso anterior, y cómo pasarle el plan exacto que ci.yml ya calculó.

Recursos

  1. GitHub Docs — Events that trigger workflows: push — documentación oficial del disparador push y el filtro branches:.
  2. nektosact.com — User Guide — documentación oficial de act -e y del evento sintético por defecto.
  3. Módulo 2 de esta guía (03-triggers-push-pull-request-and-workflow-dispatch.md) — el hallazgo original sobre branches:/paths:, retomado aquí con consecuencias directas.
  4. Módulo 3 de esta guía (02-the-hashicorp-github-pattern.md) — la razón de seguridad completa detrás de separar ci.yml de apply.yml en dos archivos.