Reservalia ya tiene cuatro jobs, pruebas de verdad, análisis estático y un artefacto publicado en ECR. Y sin embargo, todo eso puede seguir sin funcionar por una razón que no tiene nada que ver con el YAML: si las ramas viven tres semanas, no hay Integración Continua por muy bueno que sea el pipeline. Esta última lección del módulo cierra el círculo conectando la herramienta con la forma de trabajar del equipo. Compararemos los tres modelos de ramas más extendidos y veremos cuál encaja con CI/CD y cuál no; configuraremos en GitHub las reglas que convierten los acuerdos de la lección 02-01 en algo que no depende de la buena voluntad; aprenderemos a evitar ejecuciones inútiles con paths y concurrency; analizaremos qué efecto tiene cada estrategia de merge sobre la trazabilidad del artefacto y sobre el cálculo del lead time; y terminaremos con el ci.yml completo de Reservalia, con sus cuatro jobs y sus dependencias, y con el balance de lo conseguido en el módulo.

Contenido

  1. Por qué el modelo de ramas decide el destino de tu CI
  2. Trunk-based, GitHub Flow y Git Flow cara a cara
  3. Ramas cortas y tamaño del pull request
  4. Proteger main: checks obligatorios, CODEOWNERS y merge queue
  5. Evitar ejecuciones inútiles: paths y concurrency
  6. Estrategias de merge, trazabilidad y lead time
  7. Entornos efímeros de previsualización por PR
  8. El ci.yml completo de Reservalia
  9. Errores Comunes y Consejos
  10. Ejercicios
  11. Conclusión

  1. Por qué el modelo de ramas decide el destino de tu CI

Vuelve al integration hell de la lección 01-01: el coste de fusionar crece de forma no lineal con el tiempo que las ramas viven separadas. Ese hecho, por sí solo, ya determina qué modelos de ramas son compatibles con la CI.

Una rama de vida larga es un lote grande disfrazado. Mientras vive, no se está integrando nada —el pipeline verifica esa rama contra un main que se aleja cada día—; el verde es engañoso, porque "mi rama pasa" no significa "pasará cuando se fusione con lo que ha entrado mientras tanto"; y el riesgo se acumula: diez cambios pequeños fusionados a lo largo de dos semanas se diagnostican uno a uno, mientras que los mismos diez fusionados de golpe forman un único fallo con diez causas posibles.

La regla que resume el apartado. La frecuencia de integración es una variable del proceso, no de la herramienta. Ningún pipeline puede compensar ramas de tres semanas.

  1. Trunk-based, GitHub Flow y Git Flow cara a cara

Trunk-based GitHub Flow Git Flow
Ramas permanentes main main main + develop
Ramas temporales Muy cortas o ninguna Una por funcionalidad feature/, release/, hotfix/
Vida de la rama Horas, máx. 1 día 1-3 días Semanas
Frecuencia de integración Varias veces al día Diaria Semanal o menos
Coste de merge Casi nulo Bajo Alto
Encaje con CI/CD Excelente Muy bueno Malo
Complejidad mental Baja Baja Alta
Requiere feature flags Sí, a menudo A veces Rara vez
Contexto donde brilla Equipos con CI madura y despliegue continuo La mayoría de equipos de producto Software con versiones mantenidas en paralelo

Tres precisiones que evitan malentendidos:

Git Flow no es "malo", está diseñado para otro problema. Nació en 2010 para software que se distribuye en versiones y mantiene varias a la vez —piensa en una aplicación de escritorio que da soporte a la 3.x mientras desarrolla la 4.0—. En ese contexto, las ramas release/ y hotfix/ tienen sentido. Aplicarlo a un SaaS con un único despliegue, como Reservalia, añade una rama develop que solo sirve para retrasar la integración.

GitHub Flow es el punto dulce para la mayoría. Una rama corta por cambio, un pull request, verificación automática, revisión y merge a main. Es lo que hace Reservalia, y es lo que hemos supuesto durante todo el módulo.

Trunk-based no significa "sin ramas ni revisión". Significa ramas de horas y merge diario. Cuando un cambio no se puede terminar en un día, se integra incompleto pero inactivo, escondido tras un feature flag —la técnica de la lección 03-05—. Esa es la habilidad que hay que aprender, y no una cuestión de disciplina.

  1. Ramas cortas y tamaño del pull request

Hay una variable que predice el tiempo de revisión mejor que ninguna otra: el número de líneas cambiadas.

Tamaño del PR Tiempo típico hasta el merge Calidad de la revisión
< 100 líneas Horas Alta: se lee de verdad
100-400 líneas 1 día Aceptable
400-1.000 líneas 2-4 días Baja: se revisa en diagonal
> 1.000 líneas Una semana o más Casi nula: "LGTM"

Y funciona como un bucle que se retroalimenta: un PR grande tarda en revisarse, y mientras espera acumula conflictos con main, lo que obliga a rehacer partes, lo que lo hace más grande. La salida del bucle es partir el trabajo.

Cómo se parte en la práctica, con el ejemplo de las reservas recurrentes de Reservalia:

  1. PR 1: migración que añade la columna recurrencia a citas, sin usarla. Cero riesgo.
  2. PR 2: el tipo Recurrencia en tipos-compartidos y la lógica de cálculo, con sus pruebas unitarias. Nadie la llama todavía.
  3. PR 3: el endpoint que la usa, detrás de un flag desactivado.
  4. PR 4: la interfaz en apps/web, también tras el flag.
  5. PR 5: activación del flag.

Cinco PR de 100-200 líneas, cada uno revisable en un rato, cada uno integrado el mismo día. Y una propiedad valiosa: si algo falla, se sabe exactamente cuál de los cinco lo rompió.

  1. Proteger main: checks obligatorios, CODEOWNERS y merge queue

Los acuerdos de la lección 02-01 —"nadie empuja a main", "no se fusiona en rojo"— siguen dependiendo de que todo el mundo se acuerde. Las reglas de protección de rama los convierten en algo que el sistema hace cumplir.

Configuración de Reservalia para main:

Regla Qué impide Por qué
Require a pull request before merging git push directo a main Todo cambio pasa por verificación y revisión
Require status checks to pass: calidad, test, build Fusionar en rojo Es la regla 3 del acuerdo, ya no negociable
Require branches to be up to date Fusionar sobre una base antigua Evita el "verde que se rompe al fusionar"
Require 1 approval Fusionar tu propio código sin revisión Segunda mirada sobre el diseño
Dismiss stale approvals Aprobar y luego cambiar el código La aprobación se refiere a un diff concreto
Require linear history Historial en forma de espagueti Un main lineal es legible y bisecable
Block force pushes Reescribir el historial de main Un force-push a main es irreversible en la práctica
Include administrators Que Marta se salte sus propias reglas Una excepción convierte la regla en sugerencia

CODEOWNERS declara quién debe revisar según la ruta tocada. Se guarda en .github/CODEOWNERS:

# Por defecto, cualquier cambio lo revisa el equipo
*                               @reservalia/equipo

# Las migraciones y el pipeline necesitan mirada experta
apps/api/src/db/migraciones/    @reservalia/marta @reservalia/nuria
.github/workflows/              @reservalia/nuria
infra/                          @reservalia/nuria

Combinado con "require review from Code Owners", garantiza que un cambio en una migración no se fusione sin que lo vea alguien que entiende las consecuencias.

La merge queue resuelve un problema real y poco conocido. Escenario: los PR A y B están ambos verdes, ambos partiendo del mismo main. A se fusiona. B sigue diciendo verde, pero nunca se probó junto a A. Si A renombró una función que B usa, main se pone roja después del merge, sin que ningún PR estuviera en rojo.

flowchart LR
    A["PR A verde<br/>base: main@X"] --> Q["merge queue"]
    B["PR B verde<br/>base: main@X"] --> Q
    Q --> C1["prueba A sobre main@X"] --> C2["prueba B sobre main@X+A"]
    C2 -- verde --> M["merge de A y B"]
    C2 -- rojo --> R["B sale de la cola<br/>main sigue sana"]

La cola construye una rama temporal con los cambios ya encolados y verifica cada PR contra el resultado de los anteriores. Con dos o tres PR al día no compensa; a partir de una decena diaria sobre el mismo repositorio, evita que main se rompa varias veces por semana. Reservalia no la necesita hoy; sí la necesitaría con quince desarrolladores.

  1. Evitar ejecuciones inútiles: paths y concurrency

Cada ejecución cuesta minutos y, sobre todo, cuesta atención. Dos mecanismos evitan gastarlos en balde.

concurrency: cancelar ejecuciones obsoletas. Diego empuja tres veces seguidas a su rama en diez minutos. Sin configuración, tendrás tres pipelines completos corriendo a la vez, y solo el último importa:

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}   # una cola por rama
  cancel-in-progress: true                             # cancela la anterior

github.ref identifica la rama, así que cada rama tiene su propia cola y los PR no se cancelan entre sí. Aviso importante: en main conviene no cancelar (cancel-in-progress: false o una condición sobre la rama), porque cada commit de main produce un artefacto publicable y no quieres perder ninguno.

paths: no ejecutar lo que no aplica. Un cambio en un .md no necesita construir imágenes. La forma robusta no es filtrar el evento entero —ya vimos en la 02-02 que eso deja los checks obligatorios esperando para siempre—, sino detectar qué ha cambiado y saltarse el trabajo caro, manteniendo el check verde:

      - uses: dorny/paths-filter@v3
        id: cambios
        with:
          filters: |
            codigo:
              - 'apps/**'
              - 'packages/**'
              - 'package-lock.json'

      - name: Construir la imagen
        if: steps.cambios.outputs.codigo == 'true'     # ← el step se salta, el job sale verde
        run: docker build -f apps/api/Dockerfile .

Así el check build siempre reporta un resultado, y el PR de documentación se fusiona sin esperar tres minutos por nada.

  1. Estrategias de merge, trazabilidad y lead time

Cómo se integra un PR en main tiene consecuencias que van más allá del estilo.

Estrategia Qué deja en main Efecto en la trazabilidad Efecto en el lead time
Merge commit Todos los commits del PR + un commit de fusión Historial completo pero ramificado; git bisect se complica El primer commit puede ser muy anterior al merge
Squash Un solo commit por PR Un commit = un cambio = un artefacto; ideal para bisect Se pierden las fechas intermedias
Rebase Los commits del PR, reescritos sobre main Historial lineal y detallado Cada commit conserva su fecha original

Reservalia usa squash, y las razones son concretas:

  • Un commit de main = un artefacto. Como el job publicar etiqueta la imagen con el SHA, un main con un commit por PR hace que reservalia/api:a3f9c21 se corresponda exactamente con un pull request revisable. Con merge commits, un solo merge introduce varios SHA y la correspondencia se difumina.
  • git bisect funciona bien. Cada commit de main es un estado completo y verificado. Con merge commits te tropiezas con commits intermedios rotos ("wip", "arreglando el test").
  • Los commits de trabajo dejan de importar. Diego puede hacer quince commits con mensajes de andar por casa; lo que queda escrito es el título del PR, que es lo que valida commitlint (lección 02-05).

Ahora el efecto sobre el lead time for changes de la lección 01-05, que se calcula desde la fecha del commit hasta el despliegue:

  • Con squash, el commit resultante nace en el momento del merge, así que el lead time medido cubre solo merge → despliegue y subestima el tiempo real: no cuenta lo que el cambio pasó esperando revisión.
  • Con merge commit o rebase, se conserva la fecha del primer commit y el lead time incluye toda la espera, con lo que refleja mejor la realidad.

Si usas squash —como Reservalia—, la medición honesta consiste en tomar como origen la fecha del primer commit del PR (dato que la API de GitHub proporciona) en lugar de la del commit fusionado. No es un detalle menor: es la diferencia entre medir 40 minutos y medir los 6,2 días reales de la línea base.

  1. Entornos efímeros de previsualización por PR

Un entorno de previsualización es un despliegue temporal y aislado del código de un pull request, con su propia URL: pr-482.dev.reservalia.com. Nace al abrir el PR y se destruye al cerrarlo.

Para qué sirve, en orden de valor real: Marta abre el enlace y ve funcionar el cambio en lugar de imaginárselo leyendo el diff; se pueden ejecutar contra él las pruebas E2E críticas de la 02-04; y alguien de negocio valida el cambio sin instalar nada.

Y lo que hay que tener en cuenta antes de montarlo: cuesta dinero (cada PR abierto consume recursos, y sin destrucción automática se acumulan), necesita datos (una base de datos por entorno, poblada con datos ficticios: nunca una copia de producción con datos de clientes reales) y no todo lo merece: en Reservalia tiene mucho más sentido para apps/web, que es visual y barata de desplegar en S3, que para apps/api.

La mecánica de despliegue y destrucción es materia del módulo 3; aquí basta con conocer el concepto y saber que el artefacto ya está listo para alimentarlo.

  1. El ci.yml completo de Reservalia

Este es el resultado del módulo entero, con las partes ya explicadas resumidas para que se vea la estructura:

# .github/workflows/ci.yml
name: CI

on:
  pull_request: { branches: [main] }
  push:         { branches: [main] }

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}   # nunca cancelar main

env:
  TZ: Europe/Madrid

jobs:
  calidad:                                   # ~50 s
    name: Calidad
    runs-on: ubuntu-22.04
    timeout-minutes: 10
    steps:
      # checkout · setup-node (.nvmrc + cache) · npm ci
      - run: npx prettier --check .
      - run: npm run lint                    # --max-warnings 0
      - run: npm run typecheck               # tsc --noEmit

  test:                                      # ~3 min
    name: Pruebas
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    services:
      postgres:
        image: postgres:16.3
        env: { POSTGRES_USER: reservalia, POSTGRES_PASSWORD: ci, POSTGRES_DB: reservalia_test }
        ports: ['5432:5432']
        options: --health-cmd "pg_isready -U reservalia -d reservalia_test" --health-retries 10
    env:
      DATABASE_URL: postgres://reservalia:ci@localhost:5432/reservalia_test
    steps:
      # checkout · setup-node · npm ci
      - run: npm run test:unidad --workspaces --if-present
      - run: npm run migrate --workspace apps/api
      - run: npm run test:integracion --workspace apps/api
      - run: npm run test --workspace apps/api -- --coverage

  build:                                     # ~4 min
    name: Construcción
    runs-on: ubuntu-22.04
    timeout-minutes: 15
    steps:
      # checkout · setup-node · npm ci
      - run: npm run build
      - run: test -f apps/api/dist/index.js && test -d apps/web/dist
      - uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: false,
                cache-from: 'type=gha', cache-to: 'type=gha,mode=max' }

  publicar:                                  # ~2 min · solo en main
    name: Publicar artefacto
    runs-on: ubuntu-22.04
    needs: [calidad, test, build]
    if: github.ref == 'refs/heads/main'
    permissions: { id-token: write, contents: read }
    steps:
      # checkout · credenciales OIDC de AWS · login en ECR
      - uses: docker/build-push-action@v5
        with: { context: ., file: apps/api/Dockerfile, push: true,
                tags: '${{ steps.ecr.outputs.registry }}/reservalia/api:${{ steps.meta.outputs.sha_corto }}' }
flowchart LR
    E["pull_request / push a main"] --> Q["calidad ~50 s"]
    E --> T["test ~3 min"]
    E --> B["build ~4 min"]
    Q --> P["publicar ~2 min<br/>solo en main"]
    T --> P
    B --> P

Tres propiedades de este diseño que conviene subrayar:

  1. calidad, test y build corren en paralelo. El tiempo total de un PR es el del job más lento —unos 4 minutos—, no la suma de los tres. Muy por debajo del límite de 10 minutos que fijó el equipo.
  2. publicar depende de los tres y solo se ejecuta en main. Un PR nunca publica; un main verde siempre deja artefacto.
  3. Los tres primeros son los checks obligatorios de la regla de protección de rama del apartado 4. Sin ellos en verde, el botón de fusionar está desactivado.

Errores Comunes y Consejos

Error 1: ramas de vida larga con un pipeline excelente. Es el error que resume el módulo. Ningún YAML compensa integrar cada tres semanas.

Error 2: adoptar Git Flow por costumbre. Si tu producto tiene un único despliegue y no mantienes versiones antiguas, la rama develop solo sirve para retrasar la integración. Error 3: no marcar "include administrators"; el día que alguien con permisos se salta el check "porque corre prisa", la regla deja de existir para todos.

Error 4: filtrar con paths un evento cuyo check es obligatorio. El PR queda esperando eternamente un resultado que nunca llegará. Filtra a nivel de step, no de evento.

Error 5: cancelar ejecuciones en main. cancel-in-progress: true sin excepción para main te deja commits fusionados sin artefacto publicado.

Consejo 1: mide el tamaño medio de tus PR. Si supera las 400 líneas, el problema del equipo no es el pipeline: es cómo se parte el trabajo.

Consejo 2: revisa los tiempos del pipeline cada mes. Es la métrica que se degrada más silenciosamente. Trátala como un presupuesto: para añadir tres minutos, busca de dónde quitarlos.

Consejo 3: escribe las reglas de protección en el README.md; que el equipo sepa qué se exige y por qué reduce la fricción y las peticiones de excepción.

Ejercicios

Ejercicio 1

Un equipo de 6 personas desarrolla un SaaS con despliegue único. Usa Git Flow: ramas feature/ de 2-3 semanas, develop, ramas release/ semanales y hotfix/. Tienen un pipeline completo y bien hecho. Explica tres razones por las que no practican Integración Continua y propón la transición, indicando qué cambiar primero.

Ejercicio 2

Los PR A y B están verdes sobre main@X. A renombra calcularHuecos a calcularDisponibilidad y actualiza sus llamadas. B añade una llamada nueva a calcularHuecos. Se fusionan ambos. Describe qué ocurre, por qué ninguna regla de protección lo evitó y qué dos mecanismos lo habrían impedido.

Ejercicio 3

Reservalia mide un lead time de 41 minutos, pero Marta sabe que un cambio tarda días desde que se empieza. Explica de dónde viene la discrepancia y cómo corregir la medición.

Soluciones

Solución 1. Tres razones: (1) las ramas feature/ de 2-3 semanas violan la práctica central de integrar al menos a diario, así que el pipeline verifica ramas cada vez más divergentes de develop; (2) develop actúa como un almacén intermedio: el código está "integrado" ahí, pero main —lo que de verdad se despliega— solo lo recibe semanalmente, con lo que la integración real es semanal; (3) las ramas release/ implican que el software se estabiliza después de desarrollarse, lo contrario de "la rama principal siempre desplegable".

Transición, por orden: primero acortar las ramas (partir el trabajo en PR de menos de 400 líneas, con feature flags para lo incompleto), porque es el cambio que produce el beneficio y el más difícil; segundo, eliminar develop y hacer que los PR vayan directos a main; tercero, suprimir las ramas release/ desplegando desde main cuando corresponda; cuarto, activar las reglas de protección con los checks obligatorios. Empezar por la configuración sin acortar las ramas no cambiaría nada.

Solución 2. Al fusionar A, main deja de tener calcularHuecos. B sigue verde porque se verificó contra main@X, donde la función todavía existía; al fusionarse, main se rompe: fallará el typecheck del job calidad y probablemente las pruebas. Ningún PR estuvo nunca en rojo.

No lo evitó ninguna regla porque las reglas comprueban el estado del PR, no el resultado de combinarlo con lo que se ha fusionado entre medias. Los dos mecanismos que lo impiden: (a) require branches to be up to date before merging, que obliga a B a actualizarse con main —y entonces su pipeline falla antes del merge, como debe—; y (b) la merge queue, que prueba B sobre el resultado de A antes de fusionarlo y lo saca de la cola si falla. La primera es gratis y basta para equipos pequeños; la segunda escala mejor con volumen alto de PR.

Solución 3. La discrepancia viene de la estrategia de squash. Al fusionar, git crea un commit nuevo cuya fecha es la del merge, de modo que el git show -s --format=%cI que usábamos en la 01-05 mide únicamente el tramo merge → despliegue, que en Reservalia son unos 41 minutos: el tiempo del pipeline y del despliegue. Todo lo anterior —desarrollo, espera de revisión, correcciones— desaparece del cálculo.

Corrección: tomar como origen la fecha del primer commit del pull request, disponible en la API de GitHub, y guardarla en la tabla despliegues junto al SHA. El lead time pasa a medirse desde que el trabajo empezó de verdad. Como validación cruzada, conviene comparar esa cifra con el tiempo medio entre la apertura del PR y su merge; si difieren mucho, el cuello de botella está antes de abrir el PR.

Conclusión

Con esta lección se cierra el módulo 2, y conviene mirar de dónde venimos. Al empezar, Reservalia no tenía nada: .github/workflows/ estaba vacío, Diego compilaba en su portátil y las pruebas se ejecutaban "cuando alguien se acordaba". Ahora:

  • El equipo tiene seis reglas escritas antes que una sola línea de YAML, y las prácticas que constituyen la CI de verdad: integración diaria, build automática, main siempre desplegable, stop the line y feedback rápido.
  • Un ci.yml que se dispara en cada pull request y en cada push a main, sobre runners fijados, con Node leído del .nvmrc y un PostgreSQL 16.3 con healthcheck para las pruebas.
  • Una build reproducible con npm ci y el lockfile, y un Dockerfile multi-etapa con imagen base fijada, usuario no root y .dockerignore.
  • Pruebas unitarias y de integración con criterio sobre qué bloquea el merge, cobertura tratada como señal y una política de cuarentena para las pruebas inestables.
  • Un job calidad con Prettier, ESLint a --max-warnings 0 y tsc --noEmit, y un plan realista para reducir la deuda sin parar el equipo.
  • Un job publicar que sube a ECR un artefacto inmutable identificado por el SHA, listo para promocionarse sin reconstruirse, y un endpoint /version para saber siempre qué está corriendo.
  • Y las reglas de protección de main que convierten los acuerdos en algo que el sistema hace cumplir, junto con CODEOWNERS, concurrency y una estrategia de merge —squash— elegida por sus consecuencias sobre la trazabilidad, no por gusto.

En cuanto a las métricas DORA de la lección 01-05, el módulo ha atacado sobre todo el change failure rate: cada cambio se verifica en una máquina limpia antes de fusionarse. El lead time también ha mejorado, aunque de forma parcial, porque los PR son más pequeños. Pero las otras dos métricas siguen intactas, y la razón es simple: el artefacto existe, está publicado y verificado… y sigue siendo Diego quien lo despliega a mano. El ritual del viernes continúa. Hemos automatizado la mitad izquierda del diagrama que dibujamos en la 01-01 y no hemos tocado la derecha.

Eso es exactamente lo que arranca en el módulo 3, Despliegue Continuo (CD). La primera lección, Introducción al Despliegue Continuo, retoma la distinción entre Entrega Continua y Despliegue Continuo —ahora con un pipeline real delante— y define qué necesita un equipo antes de dejar que una máquina toque producción: entornos reproducibles, migraciones seguras, capacidad de volver atrás y observabilidad suficiente para enterarse antes que el cliente. A partir de ahí, el artefacto reservalia/api:a3f9c21 que hoy espera en ECR empezará a viajar solo hasta dev, staging y prod, y el viernes por la tarde volverá a ser, sencillamente, un viernes por la tarde.

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