Reservalia tiene cinco workflows, dieciséis jobs y un problema que ninguna lección anterior ha tocado: el bloque de checkout + setup-node + npm ci aparece copiado seis veces. Cuando en la 04-04 se mejoró la caché hubo que aplicar el cambio seis veces, y el día que alguien toque una copia y olvide las otras cinco, el pipeline tendrá comportamientos distintos según el workflow sin que nadie sepa por qué. Y viene lo peor: el módulo 5 va a añadir una aplicación móvil y unos microservicios que necesitan exactamente lo mismo. La idea central de esta lección es fácil de enunciar y difícil de aplicar: el pipeline es código de producción y merece el mismo trato —revisión, versionado, refactorización y, sobre todo, pruebas—. Veremos los tres mecanismos de reutilización de GitHub Actions y cuándo usar cada uno, extraeremos de verdad una composite action y un workflow reutilizable, reescribiremos el ci.yml encima, discutiremos el repositorio central de plantillas y sus riesgos, fijaremos convenciones de legibilidad y terminaremos con lo que casi nadie hace: probar el pipeline antes de fusionarlo.

Contenido

  1. El pipeline es código, y hoy no se trata como tal
  2. El problema real de Reservalia, contado con números
  3. Los tres mecanismos de reutilización comparados
  4. Extraer una composite action: preparar-node
  5. Extraer un workflow reutilizable: reusable-build-publicar.yml
  6. El ci.yml de Reservalia: antes y después
  7. Un repositorio central de workflows y sus riesgos
  8. Convenciones para que un pipeline sea legible
  9. Probar el pipeline
  10. Migración progresiva sin congelar al equipo
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. El pipeline es código, y hoy no se trata como tal

Compara cómo trata Reservalia su código de aplicación y cómo trata su YAML:

Práctica apps/api .github/workflows/
Control de versiones y revisión en PR Sí (CODEOWNERS)
Formato y lint automáticos Prettier + ESLint No
Pruebas automatizadas Unitarias e integración No
Refactorización cuando duele Habitual Nunca
Reutilización de lo repetido Funciones y paquetes Copiar y pegar
Se despliega tras verificar Se fusiona y se mira qué pasa

Las cuatro filas en negrita son la lección. Y no es una cuestión de pulcritud: el pipeline es el sistema que decide qué llega a producción, así que un fallo en él tiene el mismo alcance que un fallo en la aplicación, con el agravante de que el pipeline no tiene quien lo verifique. Cuando la 04-01 diagnosticó "la lógica de negocio escondida en YAML" y "el pipeline que solo Nuria entiende" describía los síntomas; esta lección da el tratamiento.

  1. El problema real de Reservalia, contado con números

Bloque duplicado Dónde aparece Veces
checkout + setup-node + npm ci ci.yml (×4 jobs), nocturno.yml, cd.yml (job web) 6
Credenciales OIDC + login en ECR ci.yml (publicar), cd.yml (×3), rollback.yml 5
docker build con caché de registro ci.yml (×2), nocturno.yml 3
Smoke test contra /version cd.yml (×3), rollback.yml 4

Cuatro consecuencias concretas, ninguna teórica. Toda mejora cuesta ×6: la caché de capas de la 04-04 se aplicó bien en ci.yml y se olvidó en nocturno.yml, que sigue tardando dos minutos de más. Las copias divergen: dos jobs ya usan actions/checkout@v4 y uno se quedó en @v3. La revisión se vuelve inútil, porque un diff de 200 líneas de YAML repetido no se lee, se aprueba. Y añadir un servicio nuevo es copiar 150 líneas y confiar en no haber olvidado nada. La regla que se aplica al código sirve igual aquí: a la tercera repetición, extrae. Con dos copias, la abstracción prematura suele salir mal; con seis, el coste ya se está pagando todos los días.

  1. Los tres mecanismos de reutilización comparados

Composite action Reusable workflow Matrix
Qué encapsula Una secuencia de steps Uno o varios jobs completos El mismo job con datos distintos
Dónde vive .github/actions/<nombre>/action.yml o un repositorio propio .github/workflows/<nombre>.yml Dentro del job
Se invoca con uses: dentro de un job uses: a nivel de job strategy: matrix
¿Define runs-on? No: hereda el del job que la llama : define sus propios jobs No
Recibe / devuelve inputs / outputs inputs, secrets / outputs Valores de la matriz / nada
Acceso a secretos Solo los que le pases como input Explícitos o con secrets: inherit
Cuándo usarlo Repetir pasos dentro de jobs distintos Repetir jobs enteros o pipelines completos Repetir lo mismo con datos distintos

La regla de elección, en una frase: si lo repetido son pasos dentro de un job, composite action; si es el job entero o un pipeline completo, reusable workflow; si es el mismo trabajo con parámetros distintos, matrix. Y una limitación que sorprende y conviene conocer antes de diseñar nada: una composite action no puede definir services:, así que el PostgreSQL de las pruebas no se puede encapsular ahí; tiene que quedarse en el job o subir a un reusable workflow.

  1. Extraer una composite action: preparar-node

Empezamos por lo más repetido. El bloque que aparece seis veces es este:

      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci

Y se convierte en una acción local:

# .github/actions/preparar-node/action.yml
name: Preparar Node
description: Checkout, Node desde .nvmrc con caché de npm, e instalación reproducible.

inputs:
  fetch-depth:
    description: Profundidad del historial. 0 para el historial completo.
    required: false
    default: '1'                       # 1

runs:
  using: composite                     # 2
  steps:
    - uses: actions/checkout@v4
      with: { fetch-depth: '${{ inputs.fetch-depth }}' }
    - uses: actions/setup-node@v4
      with: { node-version-file: .nvmrc, cache: npm }
    - name: Instalar dependencias
      shell: bash                      # 3
      run: npm ci
  1. Los inputs con default son lo que hace usable la acción. El caso normal no necesita configurar nada; el job seguridad, que necesita el historial completo para gitleaks, pasa fetch-depth: 0. Sin ese input habría que elegir entre duplicar la acción o traer siempre el historial entero, que es lento.
  2. using: composite distingue una acción de pasos de una acción de JavaScript o de contenedor. Y shell: bash es obligatorio (3) en cada run de una composite action: su ausencia es el error número uno al escribir la primera, y el mensaje resultante no es especialmente claro.

El uso queda así en cualquier job:

      - uses: ./.github/actions/preparar-node                       # 1 · caso normal
      - uses: ./.github/actions/preparar-node                       # en el job seguridad
        with: { fetch-depth: '0' }
  1. La ruta empieza por ./ porque es una acción local, del propio repositorio: GitHub descarga el repositorio para resolverla, de modo que el patrón funciona aunque el checkout lo haga la propia acción. Si viviera en otro repositorio se referenciaría como reservalia/acciones/preparar-node@v1 y se le aplicaría la regla de fijar por SHA de la 04-03.

  1. Extraer un workflow reutilizable: reusable-build-publicar.yml

La composite action resuelve los steps repetidos, pero build y publicar son jobs enteros que se repiten con variaciones entre ci.yml y nocturno.yml, y que el módulo 5 querrá reutilizar para el móvil y los microservicios. Eso es un reusable workflow:

# .github/workflows/reusable-build-publicar.yml
name: Reusable · build y publicar

on:
  workflow_call:                       # 1 · lo que lo hace invocable
    inputs:
      dockerfile:    { type: string,  required: true }
      repositorio:   { type: string,  required: true }    # p. ej. reservalia/api
      publicar:      { type: boolean, required: false, default: false }
    secrets:
      AWS_ROLE:      { required: true }                    # 2 · explícito, no inherit
    outputs:                                               # 3
      digest: { description: Digest de la imagen, value: '${{ jobs.construir.outputs.digest }}' }

jobs:
  construir:
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    permissions: { contents: read, id-token: write }
    outputs:
      digest: ${{ steps.imagen.outputs.digest }}
    steps:
      - uses: ./.github/actions/preparar-node
      - run: npm run build

      - uses: aws-actions/configure-aws-credentials@v4
        if: inputs.publicar                                # 4
        with: { role-to-assume: '${{ secrets.AWS_ROLE }}', aws-region: eu-west-1 }
      - uses: aws-actions/amazon-ecr-login@v2
        if: inputs.publicar

      - uses: docker/setup-buildx-action@v3
      - id: imagen
        uses: docker/build-push-action@v5
        with:
          context: .
          file: ${{ inputs.dockerfile }}
          push: ${{ inputs.publicar }}
          tags: ${{ vars.ECR_REGISTRY }}/${{ inputs.repositorio }}:${{ github.sha }}
          cache-from: type=registry,ref=${{ vars.ECR_REGISTRY }}/${{ inputs.repositorio }}:cache
          cache-to:   type=registry,ref=${{ vars.ECR_REGISTRY }}/${{ inputs.repositorio }}:cache,mode=max
  1. on: workflow_call convierte el fichero en algo invocable desde otro workflow, y los inputs con tipo son un contrato verificado: pasar una cadena donde se espera un booleano falla al analizar el fichero, antes de ejecutar nada.
  2. Los secretos se declaran uno a uno. Existe secrets: inherit, que pasa todos los del llamador, y es cómodo y mala idea: rompe el mínimo privilegio de la 04-03 y hace imposible saber a qué tiene acceso el workflow leyendo su cabecera.
  3. Los outputs del workflow reutilizable se alimentan de los outputs de sus jobs, y son lo que permite que el llamador reciba el digest y lo promocione: el mecanismo de la 04-01, un nivel más arriba. if: inputs.publicar (4) permite además un solo workflow para dos comportamientos: en un pull request se construye sin publicar y sin pedir credenciales; en main se publica. Un parámetro en vez de dos ficheros casi iguales.

  1. El ci.yml de Reservalia: antes y después

Antes, los jobs build y publicar ocupaban unas 45 líneas con el bloque de preparación repetido en cada uno:

  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci
      - run: npm run build
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: false, cache-from: '…' }

  publicar:
    needs: [calidad, test, build]
    if: github.ref == 'refs/heads/main'
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@v4          # ← otra vez
      - uses: actions/setup-node@v4        # ← otra vez
        with: { node-version-file: .nvmrc, cache: npm }
      - run: npm ci                        # ← y credenciales, login en ECR,
      # … build y push duplicados respecto al job anterior

Después, los dos jobs se convierten en dos llamadas:

jobs:
  calidad:
    runs-on: ubuntu-22.04
    steps:
      - uses: ./.github/actions/preparar-node          # 1
      - run: npx prettier --check .
      - run: npm run lint
      - run: npm run typecheck

  build:                                                # 2
    uses: ./.github/workflows/reusable-build-publicar.yml
    with: { dockerfile: apps/api/Dockerfile, repositorio: reservalia/api, publicar: false }
    secrets: { AWS_ROLE: '${{ secrets.AWS_ROLE_CI }}' }

  publicar:
    needs: [calidad, test, build, seguridad]
    if: github.ref == 'refs/heads/main'
    uses: ./.github/workflows/reusable-build-publicar.yml
    with: { dockerfile: apps/api/Dockerfile, repositorio: reservalia/api, publicar: true }
    secrets: { AWS_ROLE: '${{ secrets.AWS_ROLE_CI }}' }
  1. Un uses: sustituye a tres steps, en los seis sitios donde estaban.
  2. Un job que llama a un reusable workflow no tiene steps ni runs-on: los define el workflow llamado. Tiene, eso sí, needs, if, with y secrets. Es lo que más confunde al principio, porque parece un job y se comporta como una llamada a función.

Balance: ci.yml pasa de unas 180 líneas a unas 95, la caché se configura en un solo sitio, y añadir el microservicio del módulo 5 será añadir un job de cuatro líneas con otro dockerfile y otro repositorio. Con un aviso honesto: la indirección tiene un coste real de legibilidad —para saber qué hace build ahora hay que abrir otro fichero, y en la interfaz los jobs aparecen anidados—. Extraer algo que solo se usa una vez empeora el pipeline; la regla de la tercera repetición evita justamente eso.

  1. Un repositorio central de workflows y sus riesgos

Cuando el módulo 5 añada el móvil y los microservicios, la pregunta siguiente es si esas plantillas deben vivir en cada repositorio o en uno central, tipo reservalia/acciones.

Plantillas en cada repositorio Repositorio central
Duplicación Alta entre repositorios Ninguna
Autonomía de cada equipo Total Limitada
Propagar una mejora Repositorio a repositorio Una vez, para todos
Radio de un error Un repositorio Todos a la vez
Quién lo mantiene Cada equipo Hace falta un dueño con nombre

El riesgo está en la fila del radio de error: un cambio en la plantilla central llega a todos los equipos a la vez y, si rompe algo, rompe todos los pipelines de la empresa un martes por la mañana. La mitigación es la misma que para cualquier dependencia compartida, como vimos en la 04-02: versionar por etiqueta y no consumir main.

  build:
    uses: reservalia/acciones/.github/workflows/build-node.yml@v2   # 1 · etiqueta, no main
  1. Con @v2, un cambio en la plantilla no llega hasta que cada equipo sube su referencia. Consumir @main significa que cualquier commit del repositorio central se despliega instantáneamente en todos los pipelines, sin revisión ni pruebas por parte de quien lo sufre. La convención práctica es una etiqueta mayor móvil (v2 apuntando siempre a la última 2.x) para recibir correcciones compatibles, y un cambio explícito de etiqueta para las mayores. Y el repositorio central necesita su propio pipeline: pruebas, revisión y un CHANGELOG que explique qué cambia en cada versión, porque es una librería aunque no lo parezca.

  1. Convenciones para que un pipeline sea legible

El antipatrón "el pipeline que solo Nuria entiende" de la 04-01 se combate con cuatro convenciones baratas. Nombres explícitos, en jobs y en steps: name: Publicar imagen a ECR en lugar de publicar-2; los nombres son lo que se ve en la interfaz cuando algo falla y en los checks obligatorios, así que un nombre malo cuesta un clic cada vez, para siempre. env centralizado en la cabecera: región, zona horaria, versiones de herramientas y nombres de recursos en un único bloque a nivel de workflow, en vez de repetidos en cada step; cambiar de región debe ser cambiar una línea. Ficheros cortos con una responsabilidad: si un workflow supera las 150 líneas o mezcla dos propósitos, probablemente sean dos workflows, que es la misma heurística que aplicarías a un módulo de código. Y comentarios que expliquen el porqué, nunca el qué: # instala dependencias sobre un npm ci es ruido; lo que hay que escribir es la razón no evidente:

      # cancel-in-progress: false en main a propósito: cada commit de main produce
      # un artefacto publicable y cancelar dejaría commits sin imagen (ver 02-07).
      concurrency: { group: cd-main, cancel-in-progress: false }

  1. Probar el pipeline

Aquí está el hueco más grande, y el que da título a la lección. Hoy, la única forma que tiene Reservalia de saber si un cambio en el pipeline funciona es fusionarlo y mirar. Eso es probar en producción, con la particularidad de que la producción del pipeline es la capacidad del equipo de entregar software: si se rompe, nadie despliega.

Nivel Qué comprueba Coste Cuándo se ejecuta
Lint y validación de esquema (actionlint) Sintaxis, expresiones, referencias a jobs inexistentes Segundos En cada PR
Ejecución local (act) Que los steps hacen lo que crees Minutos En el portátil
Rama de prueba El flujo completo con disparadores reales Una ejecución Antes de fusionar
Servicio de bajo riesgo primero El cambio contra algo real que no es crítico Un despliegue Al desplegar el cambio

actionlint es la herramienta de mayor rendimiento y se añade al job calidad en dos líneas: descargar el binario con el script oficial y ejecutar ./actionlint -color.

Detecta lo que el editor no ve: una expresión ${{ }} mal formada, un needs: [buidl] con una errata que dejaría el job sin ejecutarse en silencio, un runs-on inexistente, un shell que falta en una composite action, e incluso errores de shell dentro de los bloques run, porque incorpora shellcheck. Es la diferencia entre enterarse en veinte segundos y enterarse cuando el workflow ya está en main.

act ejecuta los workflows localmente en contenedores. Sirve para iterar sobre la lógica de un job sin gastar veinte minutos por intento, y tiene límites que conviene conocer para no confiarse: no reproduce los entornos de GitHub, ni las reglas de protección, ni el token OIDC, ni los services con exactitud. La regla práctica: act sirve para depurar, no para validar. La rama de prueba cubre lo que act no puede: se crea prueba/pipeline-plantillas, se apuntan los disparadores a esa rama y se ejecuta el flujo real, con sus entornos y sus permisos, contra recursos de dev. Es la única forma de verificar de verdad un cambio en cd.yml. Y por último, el despliegue del cambio a un servicio de bajo riesgo primero. Cuando las plantillas sean centrales, la versión nueva se aplica antes a un servicio interno cuya caída no note ningún cliente, se deja una semana y solo entonces se propaga. Es exactamente el canary de la 03-04 aplicado al pipeline, y por la misma razón: es el único despliegue progresivo posible cuando el radio de impacto es toda la organización.

  1. Migración progresiva sin congelar al equipo

La tentación de reescribir los cinco workflows en un pull request gigante hay que resistirla: sería un PR de 800 líneas imposible de revisar que, si sale mal, deja al equipo sin pipeline y sin saber qué parte falló. El plan de Reservalia son cinco pasos, ninguno bloqueante:

  1. Añadir actionlint primero, sobre los workflows actuales. Es la red que hará seguros los cuatro pasos siguientes, y encuentra tres erratas el primer día.
  2. Extraer preparar-node y usarla en un solo job, calidad. Un PR de veinte líneas, revisable, con riesgo acotado a un job.
  3. Propagarla a los cinco jobs restantes, uno o dos por PR, verificando cada vez. Aquí es donde se corrigen las divergencias acumuladas, como el checkout@v3 olvidado.
  4. Extraer reusable-build-publicar.yml y usarlo primero en nocturno.yml, el workflow de menor riesgo: si falla no bloquea a nadie y se arregla por la mañana.
  5. Migrar ci.yml y cd.yml, solo cuando los pasos anteriores lleven un par de semanas funcionando.

Dos principios sostienen el plan. Cada paso deja el sistema funcionando: en ningún momento hay un estado intermedio roto, y se puede parar en el paso 3 durante un mes si surge algo urgente. Y se empieza por lo de menor riesgo y mayor repetición, que es donde el beneficio llega antes y el error cuesta menos. Es la misma estrategia que aplicarías a refactorizar un módulo de producción, porque el problema es exactamente el mismo.

Errores Comunes y Consejos

Error 1: extraer a la primera repetición. Una abstracción construida sobre un solo caso acaba llena de parámetros para acomodar el segundo; espera a la tercera. Error 2: olvidar shell: bash en los run de una composite action, con un mensaje de error poco claro.

Error 3: usar secrets: inherit por comodidad. Pasa todos los secretos del llamador y rompe el mínimo privilegio; declara los que hagan falta. Error 4: consumir plantillas centrales desde main, con lo que un commit ajeno cambia el pipeline de todos sin aviso. Error 5: poner steps en un job que llama a un reusable workflow: no es un job normal, es una llamada, y falla al analizar el fichero.

Error 6: sobreabstraer hasta que entender por qué falló el pipeline exija abrir cuatro ficheros; a veces diez líneas duplicadas y claras son mejores que una plantilla con nueve parámetros. Error 7: fusionar un cambio de pipeline sin haberlo ejecutado nunca, que es probar en producción con el equipo entero de cobaya. Error 8: reescribir los cinco workflows en un PR, imposible de revisar y de diagnosticar si algo va mal. Consejo 1: añade actionlint hoy, aunque no vayas a refactorizar nada; es la mejor relación beneficio/esfuerzo de toda la lección. Consejo 2: versiona las plantillas por etiqueta y publica un CHANGELOG. Consejo 3: documenta en el README de las plantillas cómo se usan y cómo se prueban, o volverás a tener un sistema que solo entiende una persona. Consejo 4: aplica al pipeline las mismas revisiones que al código, empezando por CODEOWNERS sobre .github/.

Ejercicios

Ejercicio 1

En estos tres casos, decide qué mecanismo de reutilización usarías y justifícalo: (a) cuatro jobs repiten los mismos cinco steps de preparación; (b) tres repositorios distintos necesitan el mismo pipeline completo de construcción y publicación de imágenes; (c) hay que ejecutar la misma suite de pruebas contra Node 18, 20 y 22.

Ejercicio 2

Un equipo extrae sus plantillas a empresa/workflows y todos los repositorios las consumen con @main. Un martes por la mañana alguien fusiona un cambio en la plantilla y todos los pipelines de la empresa fallan. Explica qué falló en el diseño, qué prácticas de esta lección lo habrían evitado y cómo lo arreglarías, distinguiendo la respuesta inmediata de la estructural.

Ejercicio 3

Diseña la estrategia de pruebas para un cambio en cd.yml que añade una verificación de firma con cosign antes de desplegar a prod. Indica qué comprobarías en cada nivel, en qué orden y qué harías si algo falla en el último.

Soluciones

Solución 1. (a) Composite action. Lo repetido son steps dentro de jobs que siguen siendo distintos entre sí; una composite action se inserta con un uses: y hereda el runs-on del job que la llama, que es justo lo que se quiere. Un reusable workflow sería excesivo, porque obligaría a convertir cada job en una llamada y a redefinir el resto de su contenido.

(b) Reusable workflow, alojado en un repositorio central y consumido por etiqueta. Lo repetido es un pipeline entero con sus jobs, sus runs-on y sus permisos, y eso una composite action no puede encapsularlo. Los inputs parametrizan lo que cambia —dockerfile, repositorio de imágenes, si publica— y los secretos se declaran explícitamente. (c) Matrix. No hay nada que extraer: es el mismo job ejecutado tres veces con un dato distinto. strategy: matrix: { node: [18, 20, 22] } con fail-fast: false para ver los tres resultados. Conviene además el job agregador de la 04-04, porque los checks obligatorios se configuran por nombre y los nombres incluyen el valor de la matriz.

Solución 2. Qué falló: consumir una dependencia compartida desde su rama de desarrollo. @main significa que cualquier commit del repositorio central entra en producción —la producción de todos los pipelines— sin revisión, sin pruebas y sin que los consumidores puedan elegir cuándo. Es exactamente lo que en la 04-02 sería no tener lockfile: todas las dependencias en latest. Qué lo habría evitado: (1) versionar por etiqueta (@v2), de modo que un cambio en el central no llegue hasta que cada equipo suba su referencia; (2) que el repositorio central tenga su propio pipeline con actionlint, validación de esquema y pruebas, más revisión obligatoria con CODEOWNERS; (3) probar el cambio en un repositorio de bajo riesgo antes de publicar la etiqueta, que es el canary del apartado 9; y (4) un CHANGELOG que permita a los consumidores saber qué cambia antes de subir de versión.

Respuesta inmediata: revertir el commit en el repositorio central, que restablece todos los pipelines a la vez, y solo después investigar. La tentación de arreglar hacia delante con el equipo entero bloqueado es el error clásico de gestión de incidentes, y aquí aplica igual que en producción: revertir primero, diagnosticar después. Respuesta estructural: publicar una etiqueta v1 con el estado bueno conocido, migrar todos los repositorios de @main a @v1 y, a partir de ahí, adoptar el ciclo de versionado con pruebas.

Solución 3. Cinco niveles, en orden de coste creciente:

  1. actionlint en local y en el PR. Detecta expresiones mal formadas y errores de shell en el bloque run de cosign. Segundos, y descarta la mitad de los errores posibles.
  2. Verificación manual del comando fuera del pipeline. Ejecutar cosign verify desde un portátil contra una imagen real ya publicada, comprobando que falla con una imagen no firmada. Esto es clave: una verificación que siempre pasa no verifica nada, así que hay que probar el caso negativo antes que el positivo.
  3. act para iterar sobre la lógica del step, sabiendo que no reproducirá el OIDC ni los entornos: sirve para afinar el script, no para dar el visto bueno.
  4. Rama de prueba con despliegue real a dev, el primer nivel que ejercita el flujo completo con permisos y entornos reales. Se comprueban los dos caminos: imagen firmada, que despliega, e imagen sin firmar, que se detiene; sin el caso negativo, la puerta no está probada.
  5. Aplicar el cambio primero a dev y staging durante una semana, y solo después a prod. La verificación de firma es una puerta nueva, y su modo de fallo más probable no es dejar pasar algo malo, sino bloquear un despliegue legítimo un día que haya que desplegar deprisa.

Si algo falla en el último nivel —la verificación bloquea un despliegue legítimo en prod—, hay dos respuestas y solo una es correcta. La incorrecta es desactivar la verificación "temporalmente", porque esa desactivación no se revierte nunca. La correcta es tratarlo como un incidente: usar rollback.yml para volver a la versión anterior si urge, y diagnosticar por qué la firma no era válida —el rol OIDC cambió, la identidad del firmante no coincide con la expresión regular, la imagen se republicó sin firmar— para arreglar la causa. Y prepararlo de antemano: documentar en SECURITY.md quién puede autorizar una excepción, con qué justificación y con qué fecha de revisión, para que esa decisión no se improvise a las once de la noche.

Conclusión

El pipeline de Reservalia ha dejado de ser seis copias del mismo YAML. Una composite action, .github/actions/preparar-node, concentra el checkout, la instalación de Node desde .nvmrc y el npm ci con caché, parametrizada con fetch-depth para que el job seguridad obtenga el historial completo sin duplicar nada. Un reusable workflow, .github/workflows/reusable-build-publicar.yml, encapsula la construcción y la publicación con inputs tipados, secrets declarados uno a uno —nunca inherit— y un output con el digest, de modo que un único fichero cubre el caso "construir sin publicar" de los pull requests y el caso "publicar" de main. El ci.yml ha pasado de 180 líneas a 95, la caché se configura en un solo sitio, y el móvil y los microservicios del módulo 5 serán cuatro líneas cada uno. Con la regla que evita el exceso: a la tercera repetición se extrae, y una abstracción que solo se usa una vez empeora el pipeline. Y sabemos elegir el mecanismo —composite action para steps, reusable workflow para jobs o pipelines completos, matrix para el mismo trabajo con datos distintos—, cuándo compensa un repositorio central y cuál es su riesgo real: que su radio de impacto es toda la organización, motivo por el que se consume por etiqueta y jamás desde main, con su propio pipeline, su revisión y su CHANGELOG. Y tenemos convenciones que impiden volver al pipeline que solo entiende una persona: nombres explícitos, env centralizado, ficheros cortos con una responsabilidad y comentarios que expliquen el porqué.

Lo que más cambia, sin embargo, es lo último. Reservalia ya no fusiona cambios de pipeline para ver qué pasa: los pasa por actionlint en cada pull request —que encontró tres erratas el primer día, entre ellas un needs mal escrito que dejaba un job sin ejecutarse en silencio—, los itera en local con act sabiendo que sirve para depurar y no para validar, los ejecuta en una rama de prueba con disparadores y entornos reales, y los aplica primero al servicio de menor riesgo. Todo ello por migración progresiva, en cinco pasos que dejan el sistema funcionando en cada uno, sin congelar el trabajo de nadie. Queda el cuarto frente, y es el que más despliegues rompe. Todo lo construido en tres módulos se apoya en una propiedad que el esquema de la base de datos no tiene: un artefacto es inmutable y se sustituye por el anterior en cuatro minutos, pero una columna borrada no vuelve, y el rollback.yml de la 03-05 no sabe deshacer un DROP COLUMN. La siguiente lección, Bases de Datos en el Pipeline: Migraciones Seguras, aborda el estado compartido: migraciones versionadas ejecutadas desde el pipeline y nunca desde un portátil, el patrón expand and contract desarrollado paso a paso sobre un caso real de Reservalia, la clasificación de los cambios de esquema en seguros, peligrosos y prohibidos en caliente, los bloqueos que un ALTER TABLE puede provocar en producción, y por qué los datos de clientes nunca deben acabar en staging.

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