Módulo 2: Anatomy Of A Github Actions Workflow
4. El disparador `schedule` y sintaxis cron
Descripción
Los tres disparadores de la lección 3 —push, pull_request, workflow_dispatch— tienen algo en común: todos dependen de que alguien haga algo (un commit, un PR, un clic). schedule es distinto: corre por el simple paso del tiempo, sin que ninguna persona lo dispare. Esta lección te enseña la sintaxis cron: completa, con el caso de uso exacto que vas a construir en el Módulo 5 —detección de drift, corrida periódicamente para descubrir si algo cambió la infraestructura por fuera de Terraform— y una honestidad específica sobre qué parte de este disparador se puede ejecutar en una lección escrita, y qué parte no.
Conexión con el módulo
Esta lección cierra el trío conceptual de disparadores (junto con push/pull_request/workflow_dispatch de la lección 3) antes de pasar, en la lección 5, a uses/with — el otro pilar de la anatomía de un workflow. El Módulo 5 (lecciones 6 y 7) retoma schedule con el drift.yml completo de Andes Cargo, construido sobre exactamente la sintaxis que ves aquí.
Analogía: la alarma del despertador, no el timbre de la puerta
Los disparadores de la lección 3 son, todos, un timbre: alguien lo toca, y algo responde. schedule es una alarma de despertador: suena a la hora que configuraste, sin que nadie tenga que tocar nada — y, como cualquier alarma real, no puedes "probarla" adelantando el reloj de tu casa; solo puedes confirmar que la hora está bien configurada, y esperar (o disparar el mismo timbre que sonaría, a mano, para confirmar que la lógica de adentro funciona).
La sintaxis cron:
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
schedule toma una lista de expresiones cron (fíjate el guion antes de cron: — puedes declarar varios horarios distintos para el mismo workflow, cada uno con su propia línea). Cada expresión cron tiene cinco campos, separados por espacio, siempre en este orden:
┌───────────── minuto (0 - 59)
│ ┌───────────── hora (0 - 23)
│ │ ┌───────────── día del mes (1 - 31)
│ │ │ ┌───────────── mes (1 - 12)
│ │ │ │ ┌───────────── día de la semana (0 - 6, domingo = 0)
│ │ │ │ │
* * * * *
"0 6 * * *" se lee: minuto 0, hora 6, cualquier día del mes (*), cualquier mes (*), cualquier día de la semana (*) — es decir, todos los días a las 6:00 AM UTC. El asterisco (*) significa "cualquier valor válido en esta posición"; es el mismo símbolo, con el mismo significado, que usarías en un crontab de Linux — GitHub Actions no inventó una sintaxis propia, adoptó el estándar POSIX que ya conocías si alguna vez programaste una tarea con cron en un servidor.
Dos detalles que vale la pena fijar de memoria:
- La hora siempre es UTC, sin importar en qué zona horaria estés tú o el servidor donde corre el runner.
"0 6 * * *"es las 6:00 AM en Londres en horario de invierno, no en tu zona horaria local — si necesitas que corra a una hora local específica, tienes que calcular el offset a UTC tú mismo. - El intervalo mínimo es de 5 minutos. GitHub Actions no permite un
cron:que dispare con más frecuencia que eso — no vas a poder simular, por ejemplo, "cada 30 segundos" con este disparador.
El caso de Andes Cargo: detección de drift
El Módulo 5 (lección 6) construye el drift.yml completo de Andes Cargo, pero la sintaxis del disparador ya la puedes leer ahora:
name: drift-detection
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
jobs:
check-drift:
runs-on: ubuntu-latest
steps:
- run: echo "Running the drift check job, triggered by ${{ github.event_name }}"
La idea, adelantada aquí y construida a fondo en el Módulo 5: cada día, a las 6:00 AM UTC, este job va a correr un terraform plan de solo-lectura sobre andes-cargo-infra/ — si ese plan detecta una diferencia entre lo que Terraform cree que existe y lo que existe de verdad en la nube (alguien cambió algo a mano, por fuera del pipeline), el job lo reporta. workflow_dispatch está ahí a propósito, junto a schedule — es la puerta de escape para correr exactamente el mismo chequeo, a mano, sin esperar al horario programado; es lo que vas a usar en esta misma lección, más abajo.
Ejecutándolo: lo que SÍ se puede correr hoy, y lo que no
Acá está la honestidad exacta de esta lección. Hay dos cosas distintas cuando hablamos de "correr" un workflow con schedule:
1. El temporizador en sí — REPRESENTATIVO, y por una razón simple. No existe forma de que una lección escrita, leída en cualquier momento del día, "espere" hasta que sean las 6:00 AM UTC para mostrarte una corrida disparada genuinamente por el paso del tiempo. Ni siquiera GitHub real te deja simular esto bajo demanda —el disparo automático de un cron en GitHub Actions, además, puede demorarse varios minutos respecto a la hora exacta declarada, bajo carga alta de la plataforma, algo que la propia documentación de GitHub advierte—. Esta parte queda, necesariamente, en la categoría de "así es como funciona", sin una salida de terminal que mostrar.
2. El job que ese temporizador dispararía — EJECUTADO, de verdad, ahora mismo. Esto es lo que sí puedes correr: act te deja simular el evento schedule directamente, sin esperar ningún reloj — y también, como alternativa más simple, correr el mismo job vía workflow_dispatch.
act schedule
Qué esperar (salida literal, ejecutada para escribir esta lección):
[drift-detection-demo/check-drift] ⭐ Run Set up job
[drift-detection-demo/check-drift] 🚀 Start image=catthehacker/ubuntu:act-latest
[drift-detection-demo/check-drift] ✅ Success - Set up job
[drift-detection-demo/check-drift] ⭐ Run Main echo "Running the drift check job, triggered by schedule"
[drift-detection-demo/check-drift] | Running the drift check job, triggered by schedule
[drift-detection-demo/check-drift] ✅ Success - Main echo "Running the drift check job, triggered by schedule" [63.788917ms]
[drift-detection-demo/check-drift] 🏁 Job succeeded
Fíjate en la línea impresa: github.event_name vale schedule, exactamente como si el cron: real lo hubiera disparado — act genera un evento sintético de tipo schedule para que el job corra con el mismo contexto que tendría en una corrida programada real. Lo único que no puedes probar es que ese evento sintético haya llegado a las 6:00 AM, porque no llegó por ningún reloj — llegó porque tú tecleaste el comando.
act workflow_dispatch -j check-drift
Qué esperar (salida literal, ejecutada para escribir esta lección):
[drift-detection-demo/check-drift] ⭐ Run Set up job
[drift-detection-demo/check-drift] 🚀 Start image=catthehacker/ubuntu:act-latest
[drift-detection-demo/check-drift] ✅ Success - Set up job
[drift-detection-demo/check-drift] ⭐ Run Main echo "Running the drift check job, triggered by workflow_dispatch"
[drift-detection-demo/check-drift] | Running the drift check job, triggered by workflow_dispatch
[drift-detection-demo/check-drift] ✅ Success - Main echo "Running the drift check job, triggered by workflow_dispatch" [59.139125ms]
[drift-detection-demo/check-drift] 🏁 Job succeeded
Misma lógica de negocio, event_name distinto — esta es la corrida "a mano" que un operador de Andes Cargo usaría en el Módulo 5 para confirmar el chequeo de drift fuera de su horario, sin tener que esperar ni fingir un evento schedule.
Profundización: por qué workflow_dispatch acompaña a casi todo schedule real
Fíjate que el drift-detection.yml de esta lección declara ambos disparadores, schedule y workflow_dispatch, no uno solo. Es un patrón deliberado y extremadamente común en workflows reales de producción: schedule te da la cadencia automática, pero workflow_dispatch te da la capacidad de correr exactamente el mismo chequeo bajo demanda —después de un cambio sospechoso, durante un incidente, o simplemente para confirmar que la lógica sigue funcionando sin esperar hasta la próxima corrida programada—. Vas a ver este mismo par en drift.yml cuando lo completes en el Módulo 5.
Errores comunes
Esperar que el cron: corra exactamente a la hora declarada, al segundo (de expectativa). Qué pasa: alguien configura cron: "0 6 * * *" y se sorprende cuando la corrida real aparece a las 6:11 o 6:23 AM UTC en vez de exactamente las 6:00. Por qué pasa: GitHub Actions ejecuta cron sobre una infraestructura compartida entre millones de repositorios; la documentación oficial es explícita en que la hora programada es un mínimo, no una garantía exacta, y que la demora puede crecer durante períodos de alta demanda de la plataforma. Cómo detectarlo: si tu monitoreo asume que un job de schedule corrió "tarde" por estar unos minutos después de la hora declarada. Cómo corregirlo: diseña cualquier lógica que dependa de schedule asumiendo una ventana de tolerancia, no un instante exacto — y, si de verdad necesitas precisión al segundo, schedule de GitHub Actions no es la herramienta correcta.
Escribir una hora local en el cron:, olvidando que siempre es UTC (de sintaxis, muy común). Qué pasa: alguien en una zona horaria de UTC-5 quiere que un job corra "a las 6 AM, hora local" y escribe cron: "0 6 * * *", sin ajustar — el job termina corriendo a las 6 AM UTC, que es la 1 AM en su zona horaria. Cómo detectarlo: si un drift.yml corre a una hora que "no tiene sentido" según tu reloj local. Cómo corregirlo: siempre calcula el offset a UTC antes de escribir el cron: — "0 6 * * *" para las 6 AM en Bogotá (UTC-5) tendría que escribirse como "0 11 * * *".
Confundir act schedule con una prueba de que el cron: está bien escrito (conceptual). Qué pasa: alguien corre act schedule con éxito y concluye que la sintaxis cron: completa —el patrón de cinco campos— está validada. Por qué pasa: parece razonable que simular el evento incluya validar la expresión que lo dispararía en la vida real. Cómo detectarlo: si crees que act schedule "revisó" tu cadena cron: de alguna forma. Cómo corregirlo: act schedule simula el tipo de evento, sin evaluar en absoluto el contenido de la cadena cron: que declaraste —el mismo tipo de límite que viste en la lección 3 con branches:/paths:—. Para validar que una expresión cron dice lo que crees que dice, usa una herramienta dedicada como crontab.guru (Recursos, abajo), no act.
Ejercicios
Ejercicio 1 — Traduce tres expresiones cron. Sin usar ninguna herramienta externa todavía, traduce a español estas tres expresiones: (a) "0 0 * * *"; (b) "*/15 * * * *"; (c) "0 9 * * 1".
Ver solución
(a) "0 0 * * *" — todos los días, a la medianoche UTC (00:00). (b) "*/15 * * * *" — cada 15 minutos, todo el día, todos los días (el */15 es la sintaxis de "cada N unidades" en el campo de minutos). (c) "0 9 * * 1" — todos los lunes, a las 9:00 AM UTC (el 1 en el último campo es lunes, con domingo como 0).
Ejercicio 2 — Explica por qué esta lección no puede "esperar" a que corra el cron. Un colega, después de leer esta lección, te pregunta: "¿por qué no corrieron el workflow real a las 6 AM y pegaron esa salida, en vez de usar act schedule?". Respóndele con precisión técnica, no con una excusa vaga.
Ver solución
Una respuesta completa suena, más o menos, así: "Una lección escrita se lee en cualquier momento del día, en cualquier zona horaria, potencialmente años después de escrita — no hay forma de sincronizar su contenido con un instante específico del reloj UTC. Lo que sí se puede hacer, y es justamente lo que hace esta lección, es simular el mismo tipo de evento (schedule) que ese cron generaría, sin depender de ningún reloj — act schedule corre el job con el mismo contexto (github.event_name == 'schedule') que tendría en una corrida real, la única diferencia es que lo disparaste tú, tecleando el comando, en vez del scheduler de GitHub a las 6 AM."
Ejercicio 3 — Justifica por qué drift.yml necesita workflow_dispatch además de schedule. Sin mirar esta lección, explica en dos frases por qué el drift.yml que vas a construir en el Módulo 5 declara ambos disparadores, en vez de solo schedule.
Ver solución
Una respuesta completa suena, más o menos, así: "schedule te da la cadencia automática —el chequeo corre solo, todos los días, sin que nadie tenga que acordarse—, pero si sospechas que algo cambió fuera de Terraform ahora mismo, no quieres esperar hasta la próxima corrida programada. workflow_dispatch te da esa salida de emergencia: corre exactamente el mismo job, bajo demanda, apretando un botón (o con act workflow_dispatch en esta guía), sin tocar ni esperar el cron."
Resumen y siguiente paso
En esta lección aprendiste la sintaxis cron: completa —cinco campos, siempre en UTC, con un mínimo de 5 minutos entre corridas— y corriste, de verdad, el job que ese cron dispararía, con act schedule y con act workflow_dispatch como alternativa manual. Quedó claro, con la honestidad explícita que sostiene esta guía, qué parte de schedule es literalmente imposible de "esperar" en una lección escrita (el paso real del tiempo) y qué parte sí se ejecuta de verdad hoy (el job, con el contexto de evento correcto).
Antes de avanzar deberías poder: leer cualquier expresión cron: de cinco campos sin ayuda externa; explicar por qué siempre está en UTC; y explicar la diferencia exacta entre "simular el evento schedule" y "esperar a que el reloj dispare el cron: real".
Con los cuatro disparadores cubiertos (push, pull_request, workflow_dispatch, schedule), la lección 5 pasa al otro pilar de un workflow: qué es exactamente una Action reutilizable, y por qué versionarla por SHA en vez de por tag es una práctica real de seguridad, no un capricho.
Recursos
- GitHub Docs — Events that trigger workflows:
schedule— documentación oficial del disparadorschedule, incluida la advertencia sobre demoras bajo alta demanda. - crontab.guru — traductor interactivo de expresiones cron a lenguaje natural, útil para verificar cualquier
cron:antes de escribirlo en un workflow real. - nektosact.com — User Guide — documentación oficial de
act scheduley los demás nombres de evento que acepta la línea de comandos.