Al cerrar el módulo 3 quedó escrito que esta lección disecciona la anatomía completa de un pipeline —sus etapas, cómo se orquestan, qué se paraleliza, qué se puede saltar y cómo se diseña uno que siga siendo rápido cuando el equipo haya triplicado su tamaño. Ese es exactamente el trabajo de hoy, y llega en el momento adecuado: Reservalia tiene cuatro workflows —ci.yml, cd.yml, infra.yml y rollback.yml— construidos pieza a pieza a lo largo de dos módulos, sin que nadie se haya sentado nunca a mirarlos como un solo sistema. Fijaremos el vocabulario preciso, recorreremos las etapas canónicas y el invariante que cada una garantiza, veremos por qué su orden no es arbitrario, dibujaremos el grafo real de Reservalia de punta a punta, entenderemos por qué cada ejecución parte de una máquina limpia y cómo se pasa información entre jobs, diseñaremos la puerta de calidad y terminaremos con los antipatrones que convierten un pipeline en un problema. Lo que no haremos aquí es optimizar tiempos —eso es la 04-04— ni extraer plantillas reutilizables —eso es la 04-05—: hoy toca entender la forma antes de tocarla.

Contenido

  1. Vocabulario preciso: qué es exactamente cada pieza
  2. Las etapas canónicas y el invariante de cada una
  3. Por qué el orden no es arbitrario: fail fast
  4. Los modelos de orquestación: secuencial, grafo y fan-out/fan-in
  5. El grafo completo de Reservalia
  6. Pipelines efímeros frente a pipelines de larga vida
  7. Paso de información entre jobs
  8. La puerta de calidad: bloquear, informar o esperar
  9. Cuatro pipelines, no uno
  10. Antipatrones de diseño
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. Vocabulario preciso: qué es exactamente cada pieza

Hasta ahora hemos usado estas palabras con cierta laxitud porque el contexto las aclaraba. A partir de este módulo la precisión importa: cuando en la 04-05 extraigamos plantillas o en la 04-04 midamos tiempos, "job" y "step" dejarán de ser intercambiables.

Término Definición En Reservalia
Pipeline El recorrido completo de un cambio desde el commit hasta producción ci.ymlcd.yml, encadenados
Workflow Un fichero YAML con su disparador y su propio grafo de jobs ci.yml, cd.yml, infra.yml, rollback.yml
Job Unidad de ejecución aislada: su propia máquina, su propio disco calidad, test, build, publicar
Step Un comando o acción dentro de un job; comparte disco con sus hermanos npm ci, npm run lint
Etapa (stage) Concepto lógico: un grupo de jobs con el mismo propósito "verificación", "empaquetado", "despliegue"
Disparador (trigger) El evento que arranca un workflow pull_request, push, workflow_run, workflow_dispatch
Puerta (gate) Punto donde el pipeline se detiene hasta cumplirse una condición Aprobación del entorno prod
Artefacto Salida que sobrevive a la máquina que la produjo reservalia/api:a3f9c21 en ECR
Runner La máquina que ejecuta un job ubuntu-22.04

Dos distinciones causan más confusión de la que parece. Job y step no son lo mismo, y la diferencia es física. Dos steps del mismo job corren en la misma máquina y comparten el sistema de ficheros: por eso npm ci en un step deja node_modules/ disponible para el siguiente. Dos jobs corren en máquinas distintas y limpias, posiblemente a la vez, y no comparten nada salvo lo que se transfiera explícitamente. Esa es la razón de que en ci.yml cada uno de los cuatro jobs repita su propio npm ci: no es un descuido, es una consecuencia del aislamiento.

"Etapa" es un concepto prestado. GitLab CI y Jenkins tienen stages como palabra clave, con la semántica "todos los jobs de la etapa N terminan antes de que empiece la N+1". GitHub Actions no tiene stages: tiene un grafo declarado con needs. Cuando digamos "etapa" nos referiremos al agrupamiento lógico, no a una sintaxis; la equivalencia exacta se ve en el módulo 6.

  1. Las etapas canónicas y el invariante de cada una

Casi cualquier pipeline del mundo, en cualquier herramienta, es una variación de la misma secuencia de siete etapas. Lo importante no es memorizarlas, sino entender qué afirma cada una: una etapa que pasa es una promesa concreta sobre el cambio, y si no puedes escribir esa promesa en una frase, esa etapa probablemente sobra.

Etapa Pregunta que responde Invariante que garantiza al pasar En Reservalia
Validar ¿Está bien escrito? Cumple el formato, no tiene errores de lint ni de tipos Job calidad
Probar ¿Hace lo que debe? El comportamiento observable es el esperado Job test
Construir ¿Se puede compilar? Existe un dist/ reproducible desde este commit npm run build en build
Empaquetar ¿Es transportable? Hay una imagen autocontenida que arranca sin la máquina que la creó docker build con apps/api/Dockerfile
Publicar ¿Existe fuera del runner? El artefacto es inmutable y direccionable por digest Job publicar → ECR
Desplegar ¿Está corriendo? El entorno declara ese digest y no otro cd.yml sobre ECS
Verificar ¿Funciona de verdad? El servicio responde y sirve la versión esperada Smoke test contra /version

Tres observaciones sobre la tabla. Construir y empaquetar suelen fundirse en un solo job —en Reservalia ambos viven en build— pero son invariantes distintos: el primero dice que el código compila, el segundo que el resultado corre en una máquina que no es la tuya. Publicar es la frontera de la inmutabilidad: antes de esa etapa todo es reproducible pero volátil; después existe un objeto con nombre criptográfico al que apuntarán todos los entornos. Y verificar no es opcional: sin ella, "desplegar" solo afirma que un comando de AWS no dio error, que es exactamente el diagnóstico de la lección 03-02.

  1. Por qué el orden no es arbitrario: fail fast

La pregunta que ordena un pipeline es una sola: si este cambio va a fallar, ¿cómo hago que falle lo antes posible? De ahí sale el principio de fail fast, con dos criterios que a veces entran en conflicto. Criterio 1: primero lo barato. Un tsc --noEmit de 25 segundos antes que una suite de integración de tres minutos, y esta antes que un despliegue completo: cuesta menos descartar el cambio. Criterio 2: primero lo que más falla. Si la mitad de los PR rotos lo están por una prueba de negocio, esa prueba debería ejecutarse pronto aunque sea cara; una etapa barata que nunca detecta nada solo añade latencia.

En un grafo el conflicto casi desaparece, porque todo lo que puede correr a la vez corre a la vez y los criterios solo ordenan dentro de cada job. Este es el orden de los steps de calidad, y no es casual:

    steps:
      - run: npx prettier --check .    # 1 · ~5 s  · formato
      - run: npm run lint              # 2 · ~20 s · errores reales
      - run: npm run typecheck         # 3 · ~25 s · el más caro de los tres

Formato primero porque es instantáneo y su fallo se repara con un comando (npx prettier --write .); tipos al final porque es el más lento. Si un PR trae los tres problemas, Diego ve el más barato en cinco segundos. Y hay una asimetría de coste que justifica todo el diseño: el precio de detectar un fallo se multiplica por diez en cada etapa que avanza. Un error de tipos detectado en calidad cuesta un minuto; el mismo error detectado en prod cuesta un incidente, un rollback, un negocio que no pudo aceptar reservas y una fila en la tabla incidentes.

La regla de los 10 minutos que el equipo fijó en la 02-01 merece una formulación exacta: el tiempo desde que empujas hasta que sabes si tu cambio es aceptable no debe superar los 10 minutos. No es un número mágico, es el umbral por encima del cual la gente hace otra cosa mientras espera, pierde el contexto y vuelve media hora después. Todo diseño de pipeline es, en el fondo, la gestión de ese presupuesto, y hoy Reservalia lo cumple con holgura: 50 s de validación, 3 min de pruebas, 4 min de construcción —el único que empieza a apretar— y 2 min de publicación, en paralelo salvo el último.

  1. Los modelos de orquestación: secuencial, grafo y fan-out/fan-in

Cómo se ordenan los jobs entre sí determina el tiempo total y la calidad de la señal.

Secuencial. Cada job espera al anterior. Es lo que sale por defecto cuando alguien encadena needs sin pensar:

jobs:
  calidad: { runs-on: ubuntu-22.04 }
  test:    { needs: calidad, runs-on: ubuntu-22.04 }
  build:   { needs: test,    runs-on: ubuntu-22.04 }

Tiempo total = 50 s + 3 min + 4 min = 7 min 50 s. Ventaja real: si calidad falla, no se gastan minutos de runner en test ni en build. Inconveniente: si build está roto, el desarrollador tarda casi ocho minutos en enterarse.

Grafo de dependencias (DAG). Cada job declara solo de quién depende de verdad, y el motor ejecuta en paralelo todo lo que puede. Es lo que hace ci.yml:

jobs:
  calidad:  { runs-on: ubuntu-22.04 }                 # sin needs → arranca ya
  test:     { runs-on: ubuntu-22.04 }                 # sin needs → arranca ya
  build:    { runs-on: ubuntu-22.04 }                 # sin needs → arranca ya
  publicar: { needs: [calidad, test, build] }         # espera a los tres

Tiempo total = max(50 s, 3 min, 4 min) + 2 min = 6 min. Y algo más valioso que el minuto y medio ahorrado: si el PR tiene un fallo de lint y una prueba rota, se ven los dos en la misma ejecución, en lugar de arreglar uno, esperar y descubrir el otro.

Fan-out / fan-in. Es un patrón dentro del grafo, no un modelo aparte: fan-out es un punto que se abre en varios jobs paralelos, fan-in el punto donde convergen. En ci.yml el fan-out ocurre en el disparador y el fan-in en publicar. La forma automática de fan-out es matrix:

  test:
    strategy:
      matrix:
        particion: [1, 2, 3]      # 1 · tres jobs reales, no tres steps
      fail-fast: false             # 2 · que un fallo no cancele a los hermanos
    steps:
      - run: npm run test:unidad -- --shard=${{ matrix.particion }}/3
  1. matrix genera jobs, con todo lo que implica: máquinas distintas, npm ci repetido en cada una y ningún fichero compartido.
  2. fail-fast: false es casi siempre lo que quieres en pruebas: prefieres saber que fallan las particiones 1 y 3 a que se cancele todo al primer fallo.

Jobs condicionales. El tercer mecanismo de forma es if:, que decide si un job existe en esta ejecución. publicar lleva if: github.ref == 'refs/heads/main'; los jobs de cd.yml llevan if: github.event.workflow_run.conclusion == 'success'. Un job saltado por if no cuenta como fallo, y esa propiedad es la que sostiene la ejecución selectiva de la 04-04.

Modelo Tiempo total Coste en minutos Calidad de la señal Cuándo usarlo
Secuencial Suma de todos Mínimo Parcial: solo el primer fallo Jobs muy caros y con dependencia real
Grafo (DAG) El camino más largo Mayor: se gasta en jobs que fallarán Completa Por defecto
Fan-out/fan-in El más lento del abanico El mayor de todos Completa y granular Suites largas y divisibles

La conclusión práctica: paraleliza salvo que exista una dependencia real de datos. Un needs de más es una decisión que cuesta minutos cada día a cada persona del equipo.

  1. El grafo completo de Reservalia

Los cuatro workflows nunca se han dibujado juntos. Este es el pipeline de verdad, del commit a producción:

flowchart TD
    subgraph CI["ci.yml · pull_request y push a main"]
        Q["calidad ~50 s"] --> P["publicar ~2 min<br/>solo main"]
        T["test ~3 min"] --> P
        B["build ~4 min"] --> P
    end
    subgraph CD["cd.yml · workflow_run si conclusion == success"]
        D1["desplegar dev"] --> S1["smoke /version"]
        S1 --> D2["desplegar staging"]
        D2 --> G["puerta: aprobación<br/>entorno prod"]
        G --> C["canary 10 → 50 → 100 %"]
        C --> R["registrar en despliegues"]
    end
    subgraph OTROS["Bajo demanda"]
        I["infra.yml<br/>plan → aplicar"]
        RB["rollback.yml<br/>workflow_dispatch"]
    end
    P ==>|"digest de a3f9c21"| D1
    R -.->|"métricas DORA"| Q
    C -.->|"circuit breaker"| RB

Tres propiedades de este grafo son decisiones de diseño, no accidentes. El enlace entre ci.yml y cd.yml es un workflow_run, no un needs. needs solo funciona dentro de un workflow. Separarlos en dos ficheros tiene una razón concreta: ci.yml corre en cada pull request y no debe tener permisos sobre AWS; cd.yml corre solo sobre main y sí los tiene. La frontera entre los dos ficheros es una frontera de privilegios, y eso es material de la 04-03.

Lo que viaja por la flecha gruesa es un digest, no código fuente. El principio de "construir una vez, promocionar el mismo digest" se materializa aquí: cd.yml nunca reconstruye nada. infra.yml y rollback.yml cuelgan fuera de la cadena a propósito. No forman parte del camino de un cambio de código: uno se dispara cuando cambia infra/, el otro cuando una persona decide volver atrás. Un pipeline no es un único hilo, es un grafo con entradas por varios sitios.

  1. Pipelines efímeros frente a pipelines de larga vida

Una decisión que casi nadie toma conscientemente porque las herramientas modernas ya la traen tomada: cada ejecución parte de una máquina limpia que se destruye al terminar.

Efímero (Actions, GitLab SaaS) Larga vida (Jenkins clásico)
Estado inicial Máquina nueva, imagen conocida Lo que dejó la ejecución anterior
Dependencias del sistema Se declaran o se instalan Instaladas a mano hace años
Reproducibilidad Alta: dos ejecuciones iguales dan lo mismo Baja: depende del historial del agente
Fallo típico "Falta instalar X" en frío "En el agente 3 falla y en el 4 no"

Los tres argumentos a favor de lo efímero son contundentes. Un verde solo vale si se puede repetir: si un job pasa porque en ese agente alguien instaló una herramienta en 2021, no has verificado tu código, has verificado ese agente. Un contenedor que quedó vivo, un fichero temporal o una variable exportada contaminan la ejecución siguiente, y producen el peor fallo posible: intermitente y dependiente de en qué máquina caiga. Y en seguridad, un runner reutilizado que ejecutó código de un PR de un fork puede haber dejado algo esperando al job siguiente, que quizá sí tiene credenciales. El precio es el arranque en frío: instalar dependencias en cada ejecución. La respuesta correcta no es conservar estado, sino cachear entradas verificables —dependencias identificadas por el hash del lockfile, capas de Docker por su digest—, que es reconstruir a partir de algo cuya identidad se comprueba. Ese matiz es la diferencia entre una caché y un agente sucio, y se desarrolla en la 04-04.

  1. Paso de información entre jobs

Como los jobs no comparten disco, todo dato que cruce la frontera debe transferirse explícitamente. Hay cuatro mecanismos, y elegir mal produce pipelines lentos o rotos.

Mecanismo Qué transporta Tamaño Persistencia Uso típico
outputs Cadenas cortas Bytes La ejecución Un SHA, un digest, un booleano
Artefactos de ejecución Ficheros Hasta GB Días (retención) dist/, informes, capturas
Registro externo Imágenes, paquetes GB Meses o años ECR, npm
Variables y secretos Configuración Bytes Permanente AWS_REGION, ARN de roles
  build:
    outputs:
      digest: ${{ steps.imagen.outputs.digest }}     # 1 · se expone al grafo
    steps:
      - id: imagen
        uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: true }
      - uses: actions/upload-artifact@v4               # 2 · ficheros, no cadenas
        with: { name: web-dist, path: apps/web/dist, retention-days: 7 }

  publicar:
    needs: [build]
    steps:
      - run: echo "Promociono ${{ needs.build.outputs.digest }}"   # 1
  1. Un output de job se declara a nivel de job y se alimenta del output de un step con id: es el canal correcto para el digest, un dato pequeño y de identidad. Se lee con needs.<job>.outputs.<nombre>, lo que implica una dependencia real: solo puedes leer el output de un job que esté en tu needs.
  2. Los artefactos son para ficheros, y retention-days importa: por defecto son 90 días, y en un repositorio activo eso son gigabytes facturables de dist/ que nadie volverá a abrir.

La regla práctica: cadenas cortas por outputs, ficheros por artefactos, y cualquier cosa que deba sobrevivir al workflow, a un registro externo. Subir una imagen Docker como artefacto de ejecución para "pasarla" al job siguiente es un antipatrón caro: cientos de megas de subida y bajada frente a un docker pull desde ECR, que además deja el artefacto donde debe estar.

  1. La puerta de calidad: bloquear, informar o esperar

No todo lo que un pipeline ejecuta debe tener derecho de veto. Confundir estas categorías produce, o bien un main frágil, o bien un equipo que aprende a saltarse las reglas. Y hay dos puertas distintas, con criterios distintos: la que bloquea el merge y la que bloquea el despliegue.

Comprobación ¿Bloquea el merge? ¿Bloquea el despliegue? Efecto si falla
calidad (formato, lint, tipos) Sí, indirectamente Botón de fusionar desactivado
test (unitarias + integración) Sí, indirectamente Botón de fusionar desactivado
build (compilar + imagen) Sí, indirectamente Botón de fusionar desactivado
Quality gate de SonarQube (código nuevo) No Botón de fusionar desactivado
Cobertura o tamaño de la imagen fuera de umbral No No Comentario en el PR
Smoke test contra /version Despliegue rojo, rollback
Métricas del canary fuera de umbral Se detiene la promoción
Aprobación del entorno prod Espera Pausa hasta que Marta o Nuria aprueben
Suite E2E completa (nocturna) No No Ticket a la mañana siguiente

El criterio para clasificar es una sola pregunta: ¿un fallo aquí significa siempre que el código está mal? Si la respuesta admite un "depende", no es bloqueante. Una prueba E2E que falla el 3 % de las veces por un problema de red no cumple el criterio, y convertirla en bloqueante enseña al equipo que "el rojo a veces no importa", lo que destruye el valor de todos los demás checks. Y un job puede ser informativo de dos maneras, que no son equivalentes:

  cobertura:
    continue-on-error: true          # el job queda en amarillo, el workflow sigue verde
    steps:
      - run: npm run test -- --coverage --coverage.thresholds.lines=70

continue-on-error: true deja el job marcado pero no tumba el workflow. La alternativa —simplemente no incluirlo en los checks obligatorios de la regla de protección de rama— es más limpia, porque el rojo sigue siendo rojo y solo deja de bloquear. Prefiere la segunda: continue-on-error sobre un check que sí debería bloquear es una forma silenciosa de desactivar una regla sin que nadie lo note.

  1. Cuatro pipelines, no uno

Un error frecuente es meter todo en un único workflow que se dispara siempre. El resultado son PR de veinte minutos con verificaciones que solo tienen sentido una vez al día.

Pull request main Nocturno Bajo demanda
Disparador pull_request push a main schedule: cron workflow_dispatch
Objetivo ¿Es seguro fusionar? Producir y desplegar Lo lento y lo que no bloquea Operaciones puntuales
Presupuesto < 10 min < 20 min Sin límite práctico El que haga falta
E2E Solo los 5 críticos Los 5 críticos Todos
Imagen y despliegue Se construye, no se publica Se publica y se despliega hasta prod rollback.yml, infra.yml
Escaneo de seguridad Rápido Rápido Profundo (04-03)

La lógica de fondo es un intercambio entre latencia y cobertura. En el PR se paga por latencia: cada minuto lo sufre una persona esperando, así que solo entra lo que responde a "¿es seguro fusionar?". En el nocturno la latencia es gratis —nadie espera— y caben una suite E2E de cuarenta minutos y un escaneo completo.

# .github/workflows/nocturno.yml — el quinto workflow de Reservalia
name: Nocturno
on:
  schedule: [{ cron: '0 2 * * 1-5' }]   # el cron se interpreta en UTC, no en la zona del repo
  workflow_dispatch:                     # y a mano, para reproducir sin esperar al día siguiente

El nocturno tiene una trampa conocida: si nadie mira sus resultados, no existe. Reservalia lo resuelve con una regla que Marta escribió en el README.md: el nocturno en rojo se revisa a primera hora y, si no se arregla el mismo día, se abre un ticket con dueño.

  1. Antipatrones de diseño

El pipeline monolítico de un solo job. Todo en steps seguidos: lint, pruebas, build, push, despliegue. Sale de forma natural al empezar, porque evita el problema de compartir ficheros entre jobs. Y funciona… hasta que tarda dieciocho minutos, no hay nada paralelizable, un fallo de formato en el segundo minuto impide saber si las pruebas pasan y —lo más grave— el job que ejecuta npm ci es el mismo que tiene las credenciales de producción. La cura es partirlo por responsabilidades y aceptar el coste de transferir artefactos.

El job que hace de todo. Variante más sutil del anterior: el grafo existe, pero uno de los jobs acumula responsabilidades porque "ya que está la máquina caliente". Un build que además despliega, notifica y actualiza un ticket es imposible de reintentar por partes: cuando falla la notificación, hay que repetir la construcción entera. Un job debería poder relanzarse solo, y eso exige que tenga un único invariante.

La lógica de negocio escondida en el YAML. Un run: | de setenta líneas de Bash que decide a qué entorno se despliega según la rama, calcula el número de versión y aplica reglas de negocio. No se puede probar, no se puede ejecutar en local, no tiene control de tipos, no lo revisa nadie porque está dentro de un fichero de configuración, y el día que se migre de herramienta hay que reescribirlo entero. La regla: el YAML orquesta, los scripts hacen. Por eso Reservalia tiene infra/scripts/desplegar.sh, peso-canary.sh y comprobar-metricas.sh como ficheros versionados y ejecutables desde un portátil, y no como bloques incrustados.

El pipeline que solo Nuria entiende. Cuatrocientas líneas con condiciones anidadas, expresiones ilegibles y nombres como job2. Funciona, pero es un punto único de fallo humano: cuando Nuria está de vacaciones y el pipeline se rompe, el equipo se queda parado. Los síntomas son fáciles de detectar: nadie más ha tocado .github/ en seis meses y, cuando alguien lo intenta, prueba a base de commits con mensaje "fix ci" repetidos. La cura tiene tres partes —nombres explícitos, comentarios que expliquen el porqué y no el qué, y un CODEOWNERS que reparta el conocimiento— y se desarrolla en la 04-05. Un último antipatrón cierra la lista: el pipeline que termina en un portátil, en el que el workflow construye y publica pero el último paso lo hace una persona en su máquina. Es el ritual del viernes de la 01-04, y reaparece con disfraces —el arreglo rápido subido a mano, la migración que Diego lanza desde psql—. Si un camino a producción no pasa por el pipeline, el pipeline no es la fuente de verdad.

Errores Comunes y Consejos

Error 1: encadenar needs por costumbre. Cada needs innecesario convierte un grafo en una fila india: declara solo las dependencias de datos reales. Error 2: confundir job con step. Esperar que un job vea el node_modules/ del anterior produce un fallo desconcertante —"pero si acabo de instalarlo"—; son máquinas distintas.

Error 3: un único workflow para todo. Mete verificaciones nocturnas en cada PR y permisos de producción en el pipeline de los PR; sepáralos por disparador y por privilegio. Error 4: continue-on-error sobre un check que sí debe bloquear, que desactiva una regla de protección sin que nadie lo note.

Error 5: pasar imágenes de contenedor como artefactos de ejecución. Cientos de megas de subida y bajada donde bastaba un docker pull. Error 6: dar por bueno un verde que depende del estado del runner: si el job pasa por algo que alguien instaló a mano en el agente, no has verificado tu código. Consejo 1: dibuja tu pipeline; un diagrama de veinte líneas revela dependencias absurdas que en YAML pasan desapercibidas durante meses. Consejo 2: pon timeout-minutes en todos los jobs, cercano al doble de lo habitual: sin él, un job colgado consume seis horas de minutos facturables antes de rendirse. Consejo 3: escribe al lado de cada job cuánto tarda, que es la línea base sin la cual la 04-04 no puede empezar.

Ejercicios

Ejercicio 1

Un equipo tiene un ci.yml con cinco jobs encadenados en secuencia: install (1 min) → lint (40 s) → test (4 min) → build (3 min) → e2e (7 min). Cada job repite el npm ci porque el anterior no le deja nada. Calcula el tiempo total, propón un rediseño con el modelo adecuado indicando los needs de cada job y estima el nuevo tiempo. Justifica qué haces con e2e.

Ejercicio 2

En Reservalia, alguien propone añadir al job build un step que ejecute pruebas de carga contra staging "ya que la imagen está construida". Argumenta si es buena idea usando los conceptos del tema, y di dónde colocarías esa verificación y por qué.

Ejercicio 3

Un pipeline publica la imagen en el job build, y el job desplegar la reconstruye desde el código fuente porque "así es más simple que pasar el digest". Explica qué invariante se rompe, qué puede salir mal en concreto y reescribe el paso de información con el mecanismo correcto.

Soluciones

Solución 1. Tiempo actual: 1 + 0,67 + 4 + 3 + 7 = 15 min 40 s, más el npm ci repetido cinco veces.

Rediseño con un grafo. install desaparece como job: instalar en una máquina no sirve de nada en otra, así que cada job hace su propio npm ci con caché de dependencias (04-04). Quedan:

  lint:  { }                       # sin needs
  test:  { }                       # sin needs
  build: { }                       # sin needs
  e2e:   { needs: [build] }        # necesita la imagen: dependencia real de datos

Los tres primeros arrancan a la vez. El camino crítico es build (3 min) + e2e (7 min) = 10 minutos, frente a 15:40. Pero 10 minutos siguen estando en el límite del presupuesto de un PR, y el culpable es e2e. La decisión correcta es sacar la suite E2E completa del pipeline de PR: dejar los cinco escenarios críticos (≈ 90 s) y mover el resto al pipeline nocturno del apartado 9. El PR baja entonces a unos 4 min 30 s y la cobertura completa se conserva, solo que con la latencia desplazada a donde no la sufre nadie.

Solución 2. No es buena idea, por tres razones que se apoyan en tres conceptos distintos. (1) Confunde categorías: una prueba de carga no responde a "¿es seguro fusionar este cambio?" sino a "¿el sistema aguanta?", y su resultado depende del entorno y del tráfico del momento; es informativa, no bloqueante, así que no pertenece al camino que bloquea el merge. (2) Rompe el presupuesto de tiempo: una prueba de carga con sentido dura entre diez y treinta minutos, multiplica por cinco el tiempo del PR y hace que el equipo deje de mirar el pipeline. (3) Rompe el invariante del job y la frontera de privilegios: build tiene un único invariante —"existe una imagen que arranca"— y apuntar contra staging le añade otro; además build corre en cada PR, así que doce personas podrían estar cargando staging a la vez, ninguna medida sería fiable y el entorno quedaría inservible para validar despliegues. Y build no tiene, ni debe tener, credenciales sobre entornos desplegados.

Dónde va: en el pipeline nocturno, contra staging en reposo, comparando con una línea base histórica y abriendo ticket si se degrada. Cómo se diseña esa prueba es materia de la 04-04.

Solución 3. Se rompe el invariante de la etapa publicar: "el artefacto es inmutable y es el mismo en todos los entornos", el principio de construir una vez y promocionar el digest de la 02-06. Reconstruir produce un binario distinto aunque el código fuente sea idéntico: puede entrar una versión nueva de una dependencia transitiva no fijada, la imagen base node:20.11.0-bookworm-slim puede haber recibido parches con la misma etiqueta, o puede cambiar cualquier detalle del entorno de construcción. Consecuencia concreta: lo que se verificó en build no es lo que se despliega, y /version puede decir la verdad sobre el commit mientras miente sobre el contenido. Si el fallo aparece solo en producción, no hay forma de reproducirlo, porque el artefacto verificado ya no existe.

El paso correcto es propagar el digest inmutable por outputs:

  build:
    outputs: { digest: '${{ steps.imagen.outputs.digest }}' }
    steps:
      - id: imagen
        uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: true,
                tags: '${{ env.ECR }}/reservalia/api:${{ github.sha }}' }
  desplegar:
    needs: [build]
    steps:
      - run: ./infra/scripts/desplegar.sh dev "${{ needs.build.outputs.digest }}"

El digest (sha256:…) es preferible a la etiqueta incluso cuando la etiqueta es el SHA del commit: una etiqueta se puede reescribir apuntando a otra imagen, un digest no. Es la diferencia entre un nombre y una huella.

Conclusión

Ahora hay una visión de conjunto donde antes había cuatro ficheros YAML. Tenemos el vocabulario preciso —workflow, job, step, etapa, puerta, disparador, artefacto— y sabemos que la separación entre jobs es física y por eso cuesta atravesarla. Conocemos las siete etapas canónicas y, más importante, el invariante que cada una promete al pasar; sabemos por qué el orden obedece al fail fast y a una asimetría de coste que multiplica por diez el precio del fallo en cada etapa que avanza. Conocemos los modelos de orquestación y por qué el grafo es el que se usa por defecto: no solo ahorra tiempo, sino que entrega toda la señal de una vez en lugar de por goteo. Hemos dibujado el grafo real de Reservalia y hemos visto que la frontera entre ci.yml y cd.yml es una frontera de privilegios y que lo que la cruza es un digest. Sabemos por qué cada ejecución parte de una máquina limpia, cómo mover información entre jobs con el mecanismo correcto, y cómo clasificar cada comprobación en bloqueante, informativa o de espera, con dos puertas distintas: la del merge y la del despliegue.

Queda también un inventario de lo que un pipeline no debe ser: un solo job que lo hace todo con las credenciales de producción en la mano, un job que acumula responsabilidades y no se puede reintentar por partes, setenta líneas de Bash con lógica de negocio dentro de un fichero de configuración, cuatrocientas líneas que solo entiende una persona, o una cadena que termina en el portátil de alguien. Con la forma del pipeline ya clara, el módulo ataca los cuatro frentes de uno en uno, y el siguiente es el que más silenciosamente rompe la reproducibilidad que tanto trabajo costó conseguir. La lección Gestión de Dependencias entra en el package-lock.json de Reservalia para responder a la pregunta que Marta no supo contestar al cerrar el módulo 3: cuántos paquetes entran de verdad en reservalia/api:a3f9c21, quién los mantiene, qué ocurre cuando uno de ellos publica una versión menor con un cambio de comportamiento, y cómo se mantiene todo eso al día sin que el equipo se pase el lunes revisando cuarenta pull requests de actualización.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados