Módulo 2: Anatomy Of A Github Actions Workflow

2. Anatomía completa de un bloque de workflow

Descripción

Esta lección diseca un workflow completo, campo por campo, corriéndolo con act mientras lo lees — no un fragmento aislado de documentación. Vas a ver on, jobs, steps, runs-on, uses, with y env (a los tres niveles donde puede aparecer: workflow, job y step) en un único archivo YAML real, y vas a confirmar con salida literal, no solo con prosa, qué hace cada uno.

Conexión con el módulo

Esta es la lección más densa del módulo, y la base de todo lo que sigue. La lección 3 profundiza específicamente en on (qué eventos existen, cómo filtrarlos); la lección 4 hace lo mismo con el caso particular de schedule; la lección 5 profundiza en uses/with (qué es exactamente una Action). Sin la anatomía completa de esta lección, esas tres lecciones estarían explicando partes sueltas de algo que nunca viste entero.


Analogía: el plano de una casa, no un ladrillo suelto

Mirar un step aislado —"esto corre terraform plan"— es como mirar un ladrillo suelto de una casa: es información real, pero no te dice nada sobre dónde entra esa habitación, ni qué la conecta con el resto. Un workflow completo es más parecido al plano completo de una casa: el on es la dirección postal y las condiciones bajo las que alguien puede entrar (¿es de día? ¿tiene llave?); jobs son las habitaciones —cada una con su propósito, algunas conectadas entre sí, otras completamente independientes—; steps es el recorrido ordenado dentro de una habitación específica, puerta por puerta. Leer un workflow ladrillo por ladrillo (un step aislado, copiado de Stack Overflow) es exactamente como intentar entender una casa mirando un solo ladrillo: técnicamente no estás equivocado sobre ese ladrillo, pero no sabes nada de la casa.


El workflow completo, sin recortar

Este es el archivo real de esta lección — vive en un laboratorio desechable, fuera de andes-cargo-infra/ (igual que el "hola mundo" del Módulo 1; el primer workflow real de Andes Cargo llega en el proyecto de este módulo, la lección 8):

name: andes-cargo-ci-demo

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

env:
  TF_VERSION: "1.9.5"

jobs:
  inspect-environment:
    runs-on: ubuntu-latest
    env:
      REGION: us-east-1
    steps:
      - name: Check out the repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Show workflow-level and job-level context
        run: |
          echo "Terraform version pinned at workflow level: ${TF_VERSION}"
          echo "AWS region pinned at job level: ${REGION}"
          echo "Event that triggered this run: ${{ github.event_name }}"
          echo "Run id: ${{ github.run_id }}"

      - name: Show a step-level env var, scoped to this step alone
        env:
          STEP_ONLY: "visible-here-only"
        run: |
          echo "STEP_ONLY inside this step: ${STEP_ONLY}"

      - name: Confirm the step-level var does not leak into this step
        run: |
          echo "STEP_ONLY outside that step: '${STEP_ONLY}'"

Este único archivo tiene todo lo que esta lección necesita diseccionar: tres eventos distintos en on, env en los tres niveles posibles (workflow, job, step), un uses con with, y cuatro steps en secuencia. Vamos campo por campo.


on — bajo qué condiciones corre este workflow

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

on es, siempre, el primer campo que GitHub Actions —y act— evalúan. Nada del resto del archivo importa si el evento actual no aparece aquí. Este workflow escucha tres eventos distintos: un push a main, un pull_request contra main, y workflow_dispatch (el botón de "Run workflow" manual, sin ningún filtro adicional). La lección 3 de este módulo diseca cada uno de estos tres a fondo, incluido qué hace exactamente branches: [main]; por ahora, quédate con la idea central: on es una lista de puertas, y el evento que dispara la corrida tiene que coincidir con al menos una para que el resto del archivo se ejecute.


env a nivel de workflow — variables para todos los jobs

env:
  TF_VERSION: "1.9.5"

Un bloque env en la raíz del archivo (al mismo nivel que on y jobs, no dentro de ningún job específico) define variables de entorno disponibles para todos los jobs de este workflow. Es el lugar correcto para algo que de verdad es constante en todo el pipeline —como la versión de Terraform que vas a fijar en el Módulo 3—, no para algo que solo un job necesita.


jobs — la unidad de trabajo

jobs:
  inspect-environment:
    runs-on: ubuntu-latest
    ...

jobs es un mapa (no una lista): cada clave —aquí, inspect-environment— es el ID de un job, el identificador corto que usas para referenciarlo desde la terminal (act -j inspect-environment) o desde otro job con needs: (Módulo 5). Este workflow tiene un solo job, pero un archivo real puede tener varios, cada uno corriendo en paralelo por defecto —a menos que uno declare needs: sobre otro, forzando un orden—. Ese encadenamiento es exactamente lo que vas a construir en el Módulo 5, cuando apply.yml necesite esperar a que plan termine con éxito antes de arrancar.

runs-on — en qué imagen corre el job

    runs-on: ubuntu-latest

runs-on le dice al motor —GitHub real, o act en tu máquina— en qué sistema operativo/imagen correr este job específico. ubuntu-latest es, con diferencia, la opción más común en workflows reales; bajo act, tu .actrc resuelve esa etiqueta genérica a la imagen concreta que pineaste en el Módulo 1: catthehacker/ubuntu:act-latest. Un job puede declarar runs-on: windows-latest o runs-on: macos-latest si necesita ese sistema operativo específico —act también soporta simularlos, con imágenes distintas, aunque esta guía no los usa: todo el trabajo de Andes Cargo corre sobre Linux.

env a nivel de job — variables solo para este job

    env:
      REGION: us-east-1

Un bloque env dentro de un job específico (con la misma indentación que runs-on y steps, no la de la raíz del archivo) solo aplica a ese job — si el workflow tuviera un segundo job, REGION no existiría ahí, a menos que ese segundo job declare su propio env. Es el nivel correcto para algo específico de una tarea, como la región de AWS que un job de despliegue necesita y otro job de solo-lectura tal vez no.


steps — la secuencia ordenada dentro de un job

    steps:
      - name: Check out the repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

steps es una lista (no un mapa, fíjate en el guion - antes de cada name), y el orden en el que aparecen es el orden exacto en que corren — de arriba hacia abajo, uno después del otro, nunca en paralelo dentro del mismo job. Cada step puede tener un name (opcional pero fuertemente recomendado — es lo que ves en la salida de act, en el 🐳 de cada línea) y exactamente una de estas dos formas de hacer trabajo: run (un comando de shell) o uses (una Action reutilizada). Nunca las dos a la vez en el mismo step.

uses y with — reutilizar código, con parámetros

uses: actions/checkout@v4 no ejecuta un comando de shell — descarga y ejecuta el código de otra persona (en este caso, el equipo de GitHub), publicado como una Action reutilizable en el Marketplace. actions/checkout es, con diferencia, la Action más usada en todo GitHub Actions: pone el contenido de tu repositorio dentro del workspace del job — sin ella, cada step arrancaría en un directorio vacío, sin ver una sola línea de tu código.

with: le pasa parámetros de entrada a esa Action, de la misma forma que le pasarías argumentos a una función. fetch-depth: 0 es un parámetro específico de actions/checkout que le dice que traiga el historial completo de Git, no solo el último commit (el valor por defecto, 1, es más rápido pero no sirve si tu workflow necesita, por ejemplo, comparar contra un commit anterior). La lección 5 de este módulo profundiza en uses/with con más ejemplos, incluida la Action de Terraform que vas a usar desde el Módulo 3.

env a nivel de step — variables solo para ese step

      - name: Show a step-level env var, scoped to this step alone
        env:
          STEP_ONLY: "visible-here-only"
        run: |
          echo "STEP_ONLY inside this step: ${STEP_ONLY}"

Este es el nivel más acotado de los tres: una variable declarada en el env de un step específico solo existe durante ese step — ni antes, ni en el step siguiente. Es la jerarquía completa de env en GitHub Actions: workflow (todos los jobs) → job (todos los steps de ese job) → step (solo ese step) — cada nivel más específico puede agregar variables nuevas, sin afectar a los niveles más amplios.


Ejecutándolo: act -l primero, sin correr nada

act -l

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

Stage  Job ID               Job name             Workflow name        Workflow file     Events
0      inspect-environment  inspect-environment  andes-cargo-ci-demo  anatomy-demo.yml  push,pull_request,workflow_dispatch

Fíjate en la columna Events: los tres eventos del bloque on aparecen juntos, separados por coma — este único job va a correr para cualquiera de los tres, no necesita tres jobs distintos.

Ejecutándolo: act push, con la salida completa

act push

Qué esperar (salida literal, ejecutada para escribir esta lección; se omite, por brevedad, la advertencia de arquitectura Apple Silicon del Módulo 1, ya la conoces):

[andes-cargo-ci-demo/inspect-environment] ⭐ Run Set up job
[andes-cargo-ci-demo/inspect-environment] 🚀  Start image=catthehacker/ubuntu:act-latest
[andes-cargo-ci-demo/inspect-environment]   🐳  docker pull image=catthehacker/ubuntu:act-latest platform= username= forcePull=true
[andes-cargo-ci-demo/inspect-environment]   🐳  docker create image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[andes-cargo-ci-demo/inspect-environment]   🐳  docker run image=catthehacker/ubuntu:act-latest platform= entrypoint=["tail" "-f" "/dev/null"] cmd=[] network="host"
[andes-cargo-ci-demo/inspect-environment]   ✅  Success - Set up job
[andes-cargo-ci-demo/inspect-environment] ⭐ Run Main Check out the repository
[andes-cargo-ci-demo/inspect-environment]   🐳  docker cp src=/ruta/a/tu/laboratorio/. dst=/ruta/a/tu/laboratorio
[andes-cargo-ci-demo/inspect-environment]   ✅  Success - Main Check out the repository [26.732ms]
[andes-cargo-ci-demo/inspect-environment] ⭐ Run Main Show workflow-level and job-level context
[andes-cargo-ci-demo/inspect-environment]   🐳  docker exec cmd=[bash -e /var/run/act/workflow/1] user= workdir=
[andes-cargo-ci-demo/inspect-environment]   | Terraform version pinned at workflow level: 1.9.5
[andes-cargo-ci-demo/inspect-environment]   | AWS region pinned at job level: us-east-1
[andes-cargo-ci-demo/inspect-environment]   | Event that triggered this run: push
[andes-cargo-ci-demo/inspect-environment]   | Run id: 1
[andes-cargo-ci-demo/inspect-environment]   ✅  Success - Main Show workflow-level and job-level context [62.416167ms]
[andes-cargo-ci-demo/inspect-environment] ⭐ Run Main Show a step-level env var, scoped to this step alone
[andes-cargo-ci-demo/inspect-environment]   🐳  docker exec cmd=[bash -e /var/run/act/workflow/2] user= workdir=
[andes-cargo-ci-demo/inspect-environment]   | STEP_ONLY inside this step: visible-here-only
[andes-cargo-ci-demo/inspect-environment]   ✅  Success - Main Show a step-level env var, scoped to this step alone [63.100167ms]
[andes-cargo-ci-demo/inspect-environment] ⭐ Run Main Confirm the step-level var does not leak into this step
[andes-cargo-ci-demo/inspect-environment]   🐳  docker exec cmd=[bash -e /var/run/act/workflow/3] user= workdir=
[andes-cargo-ci-demo/inspect-environment]   | STEP_ONLY outside that step: ''
[andes-cargo-ci-demo/inspect-environment]   ✅  Success - Main Confirm the step-level var does not leak into this step [59.551333ms]
[andes-cargo-ci-demo/inspect-environment] ⭐ Run Complete job
[andes-cargo-ci-demo/inspect-environment]   ✅  Success - Complete job
[andes-cargo-ci-demo/inspect-environment] 🏁  Job succeeded

(github.run_id, en la salida, es 1fijo bajo act, como confirmaste en el Módulo 1. La ruta que reemplaza docker cp src=... es la de tu propio laboratorio en tu máquina — variable, depende de dónde clonaste el proyecto.)

Léela con atención a los tres puntos exactos que esta lección quería demostrar:

  1. Terraform version pinned at workflow level: 1.9.5 — confirma que TF_VERSION, declarado en el env de la raíz, llegó hasta el step sin que el job tuviera que redeclararlo.
  2. AWS region pinned at job level: us-east-1 — confirma que REGION, declarado en el env del job, también llegó, sin estar en el env de la raíz.
  3. STEP_ONLY inside this step: visible-here-only seguido de STEP_ONLY outside that step: '' — la prueba directa de que una variable de env a nivel de step no sobrevive al step siguiente. Está vacía —no es un error, es exactamente el comportamiento esperado del alcance más acotado de los tres.

También fíjate en un detalle técnico que vale la pena entender ahora, no descubrirlo por accidente: la línea docker cp src=... dst=... del step Check out the repository — bajo act, actions/checkout no hace un git clone real contra ningún servidor remoto. Copia el contenido de tu carpeta local dentro del contenedor del job, con docker cp. Tiene sentido: no hay ningún "GitHub remoto" en esta simulación, así que act usa lo único que sí existe, tu working directory local. Es la misma Action real, el mismo uses: actions/checkout@v4 que correría en un repositorio real de GitHub.com —donde sí haría un git clone de verdad—, resolviendo el mismo problema (poner el código del repo en el workspace del job) con el mecanismo que tiene disponible en cada caso.


Profundización: por qué cada nivel de env existe

No es capricho de diseño que GitHub Actions tenga tres niveles de env en vez de uno solo. Cada nivel resuelve un problema real de organización:

  • Workflow-level: para algo verdaderamente constante en todo el pipeline —la versión de una herramienta, una bandera de comportamiento global—. Cambiarlo una vez cambia el comportamiento de todos los jobs.
  • Job-level: para algo específico de una tarea, pero que ese job necesita en varios de sus steps —la región de AWS, el nombre de un ambiente—. Evita repetir el mismo valor en cada step.
  • Step-level: para algo que de verdad solo un step necesita, típicamente un valor sensible o temporal que no tiene sentido que "contamine" el resto del job. Vas a usar exactamente este patrón en el Módulo 4, cuando un secreto entre al ambiente de un step específico, no de todo el job.

Errores comunes

Confundir run con uses y poner ambos en el mismo step (de sintaxis). Qué pasa: alguien escribe un step con uses: actions/checkout@v4 y, en la misma indentación, agrega run: echo "hola", esperando que ambos corran. Por qué pasa: parece razonable que un step pueda "primero reutilizar código, después correr un comando extra". Cómo detectarlo: GitHub Actions (y act) van a fallar el parseo del workflow, o —dependiendo de la versión— van a ignorar uno de los dos silenciosamente. Cómo corregirlo: un step es uno u otro, nunca los dos. Si necesitas reutilizar una Action y después correr un comando, son dos steps distintos, uno después del otro en la misma lista de steps.

Escribir un valor con dos puntos dentro de un run: de una sola línea, sin comillas que YAML entienda (de sintaxis, reproducido en esta misma lección). Qué pasa: un step como run: echo "texto: valor" —en una sola línea, sin |— puede fallar el parseo si YAML interpreta el : seguido de espacio como el inicio de una nueva clave dentro de un mapeo, incluso estando técnicamente dentro de comillas dobles del lado derecho. Reproducido para esta lección, al escribir el step de STEP_ONLY con una sola línea en vez del bloque | que ves arriba, act respondió: Error: workflow is not valid. 'anatomy-demo.yml': yaml: line 34: mapping values are not allowed in this context. Cómo detectarlo: el mensaje exacto mapping values are not allowed in this context, apuntando a una línea que sí tiene una cadena entre comillas con un : adentro. Cómo corregirlo: para cualquier run que vaya a imprimir texto con dos puntos, usa el bloque literal run: | (como en todos los steps de esta lección) — dentro de un bloque |, cada línea es texto literal, sin ninguna de las reglas de escaneo de un mapeo YAML.

Creer que jobs corre siempre en el orden en que están escritos en el archivo (conceptual). Qué pasa: alguien con dos jobs en el mismo workflow asume que el segundo espera a que termine el primero, simplemente por estar escrito después. Por qué pasa: se parece, superficialmente, a steps, donde el orden sí importa. Cómo detectarlo: si esperas que un segundo job "espere su turno" sin haber declarado needs: explícitamente. Cómo corregirlo: por defecto, todos los jobs de un workflow corren en paralelo, sin ningún orden garantizado entre ellos — el orden solo existe si lo declaras explícitamente con needs: (Módulo 5). El orden secuencial es una propiedad de steps dentro de un job, no de jobs dentro de un workflow.


Ejercicios

Ejercicio 1 — Predice el resultado de un cuarto nivel imposible. Un compañero te pregunta: "¿existe un nivel de env todavía más específico que el de step, por ejemplo a nivel de un solo comando dentro de un run: | de varias líneas?". Respóndele con precisión, según lo que viste en esta lección.

Ver solución

No — el nivel de step es el más específico que existe en GitHub Actions. Dentro de un bloque run: | de varias líneas, todas las líneas comparten exactamente el mismo entorno del step completo; no hay forma de declarar una variable "solo para la tercera línea de este script". Si necesitas ese nivel de aislamiento, la única opción real es dividir ese run en steps separados, cada uno con su propio bloque env.

Ejercicio 2 — Explica el docker cp de actions/checkout bajo act. Sin mirar esta lección, explica a un colega por qué la salida de act push muestra docker cp en vez de git clone para el step que usa actions/checkout@v4, y por qué esto no significa que act esté usando una versión distinta de la Action.

Ver solución

Una respuesta completa suena, más o menos, así: "actions/checkout normalmente clona tu repositorio desde el servidor de GitHub hacia el workspace del runner — pero bajo act, no hay ningún servidor de GitHub involucrado, todo corre en tu máquina. act resuelve ese mismo problema —poner tu código en el workspace del job— copiando directamente tu carpeta local con docker cp, en vez de hacer un git clone remoto. Es exactamente la misma Action, la misma versión (v4), el mismo uses: en el YAML; lo que cambia es el mecanismo de bajo nivel que usa para lograr el mismo resultado, porque el entorno de ejecución es distinto."

Ejercicio 3 — Diagnostica el error de mapping values. Un colega te muestra este error: Error: workflow is not valid. 'ci.yml': yaml: line 12: mapping values are not allowed in this context, y jura que su YAML "se ve perfecto". Sin ver su archivo, ¿qué le preguntarías primero, según lo que aprendiste en "Errores comunes"?

Ver solución

Le preguntarías primero: "¿tienes algún run: de una sola línea (sin el bloque |) que imprima texto con dos puntos adentro, como run: echo "resultado: ok"?". Es, con diferencia, la causa más común de este error exacto: YAML interpreta el : seguido de espacio dentro de un valor de una sola línea como el inicio de un nuevo par clave-valor, incluso si técnicamente está dentro de comillas del lado derecho de otro campo. La solución casi siempre es cambiar ese run: de una línea a un bloque run: |.


Resumen y siguiente paso

En esta lección diseccionaste un workflow real completo, campo por campo, corriéndolo con act mientras leías cada pieza: on como la condición de disparo, jobs como la unidad de trabajo con su propio runs-on, steps como la secuencia ordenada dentro de un job, uses/with como la forma de reutilizar código con parámetros, y env en sus tres niveles —workflow, job, step—, confirmado con salida literal de que cada nivel tiene exactamente el alcance que promete. También reprodujiste, en vivo, el error de YAML más común de esta capa: un run: de una sola línea con dos puntos adentro.

Antes de avanzar deberías poder: explicar de memoria qué hace cada uno de los siete campos principales; predecir en qué step una variable de env va a estar disponible según en qué nivel se declaró; y reconocer el error mapping values are not allowed sin tener que buscarlo.

Tienes la anatomía completa. La lección 3 profundiza en el campo que abre todo el archivo: on, con los tres eventos más comunes de un pipeline de infraestructura.

Recursos

  1. GitHub Docs — Workflow syntax for GitHub Actions — la referencia oficial completa de cada campo de esta lección.
  2. nektosact.com — User Guide — documentación oficial de act -l y act push, usadas para ejecutar el workflow de esta lección.
  3. GitHub — actions/checkout — el repositorio oficial de la Action usada en esta lección, con su documentación completa de parámetros (with:).