Las tres lecciones anteriores describieron tres flujos de trabajo distintos, y las tres acabaron señalando la misma dependencia: comprobaciones automáticas en las que se pueda confiar. Git Flow las necesita menos porque tiene una fase de QA manual en la rama de versión. GitHub Flow las exige para poder afirmar que main es desplegable. Trunk Based Development las exige rápidas y perfectas, porque el tronco recibe cambios cada hora.

Esta lección construye esa pieza. Y cierra dos promesas pendientes del curso: la de la lección 06-01, donde vimos que los hooks de cliente no son un control de seguridad porque cualquiera los esquiva con --no-verify, y dejamos anunciada la familia de hooks que se ejecutan en el servidor; y la de la 07-04, donde apareció un protected branch hook declined sin explicar del todo qué era.

También conviene delimitar el alcance desde el principio. Aquí hablamos de integración continua: comprobar automáticamente que lo que se integra funciona. No de entrega ni de despliegue continuos —cómo ese código llega a producción, con qué entornos, qué estrategia de promoción y cómo se revierte—, que son el contenido de la lección 10-05: Git en DevOps. La frontera está justo donde el código se declara apto para integrar.

Contenido

  1. Qué es la integración continua y qué no es
  2. Cómo se engancha a Git: los disparadores
  3. Qué commit se comprueba realmente en una pull request
  4. Una tubería de ejemplo, explicada línea a línea
  5. Comprobaciones de estado y ramas protegidas
  6. Los hooks de servidor: pre-receive, update y post-receive
  7. Cliente, servidor y CI: quién comprueba qué
  8. Mantener el CI rápido
  9. Merge queue: integrar sin carreras
  10. Qué queda fuera: el despliegue

  1. Qué es la integración continua y qué no es

Empecemos por el malentendido, porque es casi universal.

"Nosotros ya hacemos integración continua: tenemos un servidor que ejecuta las pruebas."

Eso no es integración continua. Es automatización de pruebas, que está muy bien y es un requisito, pero no es lo mismo.

La definición original, de Martin Fowler y del movimiento de la programación extrema, es esta:

Integración continua es la práctica por la que los miembros de un equipo integran su trabajo con frecuencia, al menos a diario, y cada integración se verifica mediante una compilación y una batería de pruebas automáticas para detectar los errores de integración lo antes posible.

La palabra que carga con todo el significado es integran. Un equipo donde cada persona trabaja tres semanas en su rama, con un servidor de CI que prueba esas ramas aisladas religiosamente, no está haciendo integración continua. Está probando de forma continua tres versiones divergentes del proyecto que aún no se han encontrado. El día que se encuentren, aparecerán todos los problemas de golpe: es exactamente lo que la práctica pretendía evitar.

Por eso esta lección viene después de la 07-05 y no antes. La política de ramificación es la parte difícil; el servidor de comprobaciones es la parte fácil.

Es integración continua No lo es
Todo el equipo integra en la línea principal a diario Tener un servidor de CI
Cada integración se verifica automáticamente Ejecutar las pruebas antes de una publicación mensual
La línea principal está siempre sana Que las pruebas pasen en la rama de cada uno
Un fallo se arregla de inmediato, con prioridad Acumular un tablero de comprobaciones en rojo
La respuesta llega en minutos Un ciclo de comprobación de dos horas

Y las cuatro reglas prácticas que la sostienen:

  1. Integrar a menudo, al menos una vez al día por persona (lección 07-05).
  2. Cada integración dispara la verificación, sin excepciones ni exclusiones manuales.
  3. Arreglar la línea principal es la máxima prioridad. Si main está en rojo, se para todo. Si no se arregla en minutos, se revierte (lección 05-06).
  4. La respuesta debe ser rápida, o el bucle deja de cerrarse (apartado 8).

  1. Cómo se engancha a Git: los disparadores

El sistema de CI no adivina cuándo trabajar: reacciona a eventos de Git. Esos eventos son los disparadores (triggers), y son la superficie de contacto entre las dos cosas.

Los tres fundamentales:

Disparador por push

El más básico. Cada vez que alguien actualiza una referencia en el servidor, se lanzan las comprobaciones sobre ese commit.

# Disparar en cada envío a cualquier rama
on:
  push:
    branches:
      - '**'

Y las variantes habituales, que en un proyecto real se combinan:

on:
  push:
    # Solo en la línea principal y en las ramas de trabajo
    branches:
      - main
      - 'funcionalidad/**'
      - 'correccion/**'
    # Ignorar cambios que no pueden romper nada
    paths-ignore:
      - '**.md'
      - 'docs/**'

El paths-ignore merece un comentario: filtrar por rutas ahorra tiempo de máquina, pero úsalo con cuidado. Si el filtro excluye un fichero que afecta al resultado (una configuración, un fichero de datos), tendrás integraciones sin comprobar y no lo sabrás. Y si esa comprobación es obligatoria en la rama protegida, la PR puede quedarse esperando eternamente una comprobación que nunca se lanzará. Es un problema clásico y desconcertante.

Disparador por pull request

Se lanza al abrir una PR y cada vez que se le añaden commits. Es el disparador que sostiene la revisión: quien revisa quiere ver el resultado antes de aprobar.

on:
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened, ready_for_review]

Los tipos de evento importan:

Evento Cuándo ocurre
opened Se abre la PR
synchronize Llegan commits nuevos a la rama: el caso más frecuente
reopened Se reabre una PR cerrada
ready_for_review Deja de ser borrador (lección 07-01)

Disparador por etiqueta

Las etiquetas de la lección 05-05 son el disparador natural de todo lo relacionado con publicar una versión:

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'      # v2.4.0, pero no v2.4.0-beta

Recuerda de la 05-05 que las etiquetas no viajan solas: git push origin main no las envía. Si tu publicación depende de una etiqueta, el comando es git push origin main --follow-tags o git push origin v2.4.0. Es una causa muy común de "he etiquetado y no ha pasado nada".

Otros disparadores

Disparador Uso típico
Programado (cron) Batería completa nocturna, auditoría de dependencias
Manual Relanzar una comprobación, ejecutar algo bajo demanda
Por otra tubería Encadenar fases
Webhook externo Reaccionar a eventos de otros sistemas

Un patrón muy extendido y muy recomendable: batería rápida en cada envío y en cada PR, batería completa por la noche. Lo veremos en el apartado 8.

  1. Qué commit se comprueba realmente en una pull request

Este apartado explica una de las sorpresas más frecuentes de todo el módulo.

Ana abre una PR desde funcionalidad/filtro-por-etiqueta hacia main. El CI se ejecuta y sale en verde. Ana integra… y main se pone en rojo. ¿Cómo es posible, si acababa de pasar?

Y a veces ocurre lo contrario: Ana no ha tocado nada, pero al volver de comer el CI de su PR ha pasado de verde a rojo sin que ella haya hecho ningún commit.

La explicación es la misma en los dos casos, y es esta:

En una pull request, el CI normalmente no comprueba tu rama. Comprueba el resultado de fusionar tu rama con la rama destino.

Es decir: no se prueba D2 (la punta de tu rama), se prueba un commit de fusión efímero entre D2 y la punta actual de main.

gitGraph
   commit id: "C1"
   commit id: "C3 (base de la rama)"
   branch funcionalidad/filtro
   checkout funcionalidad/filtro
   commit id: "D1"
   commit id: "D2 (tu punta)"
   checkout main
   commit id: "C4"
   commit id: "C5 (punta actual)"
   merge funcionalidad/filtro id: "M (lo que prueba el CI)"

En GitHub, ese commit es exactamente la referencia refs/pull/<n>/merge que vimos en la lección 07-01: el servidor la calcula y la mantiene actualizada cada vez que cambia cualquiera de los dos lados.

# Traerte exactamente lo que el CI está probando
git fetch origin pull/42/merge:lo-que-prueba-el-ci
git switch lo-que-prueba-el-ci

Ese comando es el mejor diagnóstico cuando el CI falla y tú no reproduces el fallo en local: probablemente estás ejecutando pull/42/head mientras el CI ejecuta pull/42/merge.

Por qué se hace así, y es lo correcto: lo que importa no es si tu rama funciona aislada, sino si funcionará una vez integrada. Comprobar la fusión detecta los conflictos semánticos de los que hablamos en la lección 07-05: Carla renombró una función en main, tú añadiste una llamada con el nombre viejo en tu rama, no hay conflicto textual, y el resultado está roto. Probar solo tu rama no lo vería jamás.

Las tres consecuencias prácticas, que explican los dos misterios del principio:

  1. El resultado de tu PR puede cambiar sin que tú hagas nada, porque main ha avanzado. No es un fallo del sistema: es información valiosa que acabas de recibir gratis.
  2. Verde en la PR no garantiza verde en main al integrar si main ha avanzado entre la última comprobación y la fusión. Es la carrera que resuelve la merge queue del apartado 9.
  3. Si hay conflictos, el commit de fusión no se puede calcular y el CI no se ejecuta en absoluto. Primero se resuelven los conflictos (lección 03-05), después vuelve a haber señal.

Y un matiz importante para la lección 07-01: en GitLab esto se llama merged results pipelines y es configurable; algunas plataformas prueban por defecto solo la punta de la rama. Averigua cuál hace la tuya, porque cambia por completo cómo interpretas un rojo.

Restricción de seguridad, retomando la 07-01. Cuando la PR viene de un fork, el código lo ha escrito alguien de fuera. Si esa ejecución tuviera acceso a las credenciales del proyecto, cualquiera podría robarlas abriendo una PR que imprima las variables de entorno. Por eso las plataformas ejecutan las PRs de forks en modo restringido, sin secretos y con permisos de solo lectura, y a menudo exigen que un miembro del equipo autorice cada ejecución. Es incómodo y es imprescindible.

  1. Una tubería de ejemplo, explicada línea a línea

Vamos a construir la tubería de gestor-tareas. El fichero usa la sintaxis y los nombres de eventos más habituales, pero los conceptos son idénticos en cualquier plataforma: lo que cambia son las palabras, no las ideas.

# .ci/tuberia.yml — Integración continua de gestor-tareas
name: Integracion continua

# 1. DISPARADORES: cuándo se ejecuta esto
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# 2. CONCURRENCIA: cancelar ejecuciones obsoletas de la misma rama
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 3. PRIMER TRABAJO: comprobaciones rápidas
  comprobaciones-rapidas:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      # 3.1 Traer el código
      - name: Obtener el codigo
        uses: actions/checkout@v4
        with:
          fetch-depth: 0        # historial completo: hace falta para el paso 3.5

      # 3.2 Preparar el entorno
      - name: Preparar Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'          # cachear ~/.npm entre ejecuciones

      # 3.3 Instalar dependencias de forma reproducible
      - name: Instalar dependencias
        run: npm ci

      # 3.4 Análisis estático
      - name: Ejecutar el linter
        run: npm run lint

      # 3.5 Comprobar el formato de los mensajes de commit
      - name: Validar los mensajes de confirmacion
        if: github.event_name == 'pull_request'
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          git log --format=%s "$BASE..HEAD" | while read -r asunto; do
            if ! echo "$asunto" | grep -qE '^(GT-[0-9]+|Fusionar) '; then
              echo "Mensaje sin referencia de ticket: $asunto"
              exit 1
            fi
          done

  # 4. SEGUNDO TRABAJO: pruebas en varios sistemas, en paralelo
  pruebas:
    runs-on: ${{ matrix.so }}
    timeout-minutes: 20
    strategy:
      fail-fast: false          # que un sistema no cancele a los demás
      matrix:
        so: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - name: Ejecutar la bateria de pruebas
        run: npm test -- --coverage

      # 4.1 Guardar resultados aunque las pruebas fallen
      - name: Publicar el informe de cobertura
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: cobertura-${{ matrix.so }}
          path: cobertura/

  # 5. TERCER TRABAJO: comprobar los submódulos
  submodulos:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive    # traer componentes-ui
      - name: Comprobar que el submodulo apunta a un commit publicado
        run: |
          cd componentes-ui
          git fetch origin main
          git merge-base --is-ancestor HEAD origin/main \
            || { echo "componentes-ui apunta a un commit que no esta en main"; exit 1; }

  # 6. PUERTA FINAL: un único trabajo que resume el resultado
  todo-correcto:
    runs-on: ubuntu-latest
    needs: [comprobaciones-rapidas, pruebas, submodulos]
    if: always()
    steps:
      - name: Verificar que no ha fallado nada
        run: |
          [ "${{ contains(needs.*.result, 'failure') }}" = "false" ] || exit 1
          echo "Todas las comprobaciones han pasado."

Ahora, por qué cada pieza está donde está.

1. Disparadores. Se ejecuta en cada envío a main y en cada PR contra main. No en cualquier rama: la PR ya cubre las ramas de trabajo, y ejecutar las dos cosas duplica el gasto sin aportar señal.

2. Concurrencia. Si Ana envía tres commits en cinco minutos, sin esto se lanzan tres ejecuciones completas y solo importa la última. cancel-in-progress cancela las obsoletas. En un equipo activo esto solo ya reduce el gasto de máquina a la mitad. Cuidado: no lo apliques a main si tu tubería tiene efectos (publicar un artefacto, por ejemplo); cancelar a medias puede dejar cosas a medio hacer.

3.1 fetch-depth: 0. Por defecto, los sistemas de CI hacen un clon superficial (git clone --depth 1, lección 02-02): traen solo el último commit. Es mucho más rápido, pero rompe todo lo que necesite historial: git log entre dos referencias, git describe --tags, git merge-base, git blame. Como el paso 3.5 recorre los commits de la PR, aquí hace falta el historial completo. Este es probablemente el fallo de configuración más común de todo el CI: un comando de Git que funciona en local y en el CI dice fatal: bad revision.

3.3 npm ci y no npm install. ci instala exactamente lo que dice el fichero de bloqueo y falla si no coincide con el manifiesto. install puede resolver versiones nuevas y hacer que la ejecución de hoy difiera de la de ayer sin que nadie haya cambiado nada. Reproducibilidad: la misma entrada debe dar el mismo resultado.

3.5 Validación de los mensajes de commit. Aquí está la conexión con la lección 06-01: el hook commit-msg ya valida el formato GT-NNN en el portátil, pero se salta con --no-verify. Esta comprobación lo verifica en el servidor, donde nadie puede esquivarla. Fíjate en el rango: $BASE..HEAD, es decir, solo los commits que la PR aporta (lección 07-02). Las reglas concretas del formato son el tema de la lección 08-01; aquí solo montamos el mecanismo que las hace cumplir.

4. Matriz. Ana usa Ubuntu, Bruno macOS y Carla Windows 11. Comprobar los tres sistemas en paralelo detecta el clásico problema de rutas con \ frente a / o de mayúsculas en los nombres de fichero antes de que llegue a main. fail-fast: false es importante: sin él, el primer sistema que falle cancela los demás y pierdes información útil.

4.1 if: always(). Sin esto, el informe de cobertura no se guarda cuando las pruebas fallan, que es justo cuando más lo necesitas para diagnosticar.

5. Submódulos. Retoma la lección 06-05. gestor-tareas usa componentes-ui como submódulo, y el error clásico es confirmar un puntero a un commit que solo existe en el portátil de quien lo hizo. Cualquier otra persona clonará y no podrá inicializar el submódulo. git merge-base --is-ancestor comprueba que el commit apuntado sea alcanzable desde la rama principal del submódulo. Es una comprobación de tres líneas que ahorra tardes enteras.

6. La puerta final. Un único trabajo que depende de todos los demás. Su utilidad es práctica: en la rama protegida se exige una sola comprobación obligatoria, todo-correcto, en lugar de mantener una lista de cinco nombres que hay que actualizar cada vez que se añade un trabajo. Con la matriz es aún más valioso, porque los nombres generados (pruebas (ubuntu-latest), etc.) cambian al tocar la matriz y las reglas de protección se quedan esperando comprobaciones que ya no existen.

  1. Comprobaciones de estado y ramas protegidas

Tenemos las comprobaciones. Ahora hay que convertirlas en un requisito, y esta es la parte que responde definitivamente a la pregunta que dejó abierta la lección 06-01.

La comprobación de estado

Cuando la tubería termina, publica su resultado asociado al commit, no a la rama ni a la PR. Es un dato del tipo: "el commit a7c2e91 ha superado la comprobación todo-correcto". Eso es una comprobación de estado (status check).

Consultable desde la propia plataforma, y visible en la interfaz como el tic verde o la cruz roja junto al commit.

La rama protegida

Por sí sola, una comprobación de estado es informativa. Lo que la convierte en obligación es la configuración de la rama protegida, que ya presentamos en la lección 07-04:

Regla Efecto sobre main
Prohibir envío directo Todo entra por PR
Exigir N aprobaciones Nada entra sin revisión
Exigir comprobaciones en verde Nada entra con el CI en rojo
Exigir la rama actualizada con main Se prueba la combinación real
Descartar aprobaciones al llegar commits No se aprueba una versión y se integra otra
Prohibir el envío forzado Nadie reescribe el historial publicado (05-06)
Prohibir el borrado de la rama main no desaparece
Exigir firma de los commits Solo entra código con autoría verificada

Cuando alguien intenta saltarse esto, el rechazo llega del servidor:

git push origin main
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Required status check "todo-correcto" is expected.
To [email protected]:equipo/gestor-tareas.git
 ! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'git.ejemplo.es:equipo/gestor-tareas.git'

Por qué esto sí es un control efectivo

Aquí está la respuesta que la lección 06-01 dejó pendiente, y conviene enunciarla con precisión:

Un hook de cliente vive en el disco de quien lo ejecuta, en .git/hooks/, que no se versiona. Cualquiera puede borrarlo, modificarlo o esquivarlo con --no-verify. Es una ayuda contra el despiste.

Una comprobación de estado se ejecuta en la infraestructura del proyecto y la decisión de aceptar el envío la toma el servidor. --no-verify es una opción del cliente: le dice a tu Git que no ejecute tus hooks. No viaja en el protocolo de red y el servidor no se entera de que existe. No hay nada que saltarse.

La misma diferencia, en una frase: el hook de cliente te avisa; el servidor decide.

Un git push --force sobre una rama protegida se rechaza igual, y ahí está la garantía técnica de la regla de oro de la lección 05-06: no reescribir historial publicado deja de ser una recomendación y pasa a ser una imposibilidad.

Un aviso sobre las excepciones

Casi todas las plataformas permiten que ciertos roles se salten las protecciones. Es tentador dejar esa puerta abierta "por si acaso". Pero piensa cuándo se usa en la práctica: durante un incidente en producción, con prisa, a las once de la noche, por alguien cansado. Es decir, exactamente cuando más falta hacen las comprobaciones. Si de verdad hace falta una vía de emergencia, que sea explícita, que deje registro y que se revise después.

  1. Los hooks de servidor: pre-receive, update y post-receive

Y ahora, por fin, la familia completa que la lección 06-01 dejó anunciada.

Cuando alguien hace git push, el servidor recibe los objetos y, antes de actualizar ninguna referencia, ejecuta sus propios hooks. Viven en el directorio hooks/ del repositorio bare (lección 04-01), no en un .git/hooks/ de ningún portátil.

Hook Cuándo Entrada Efecto del código de salida
pre-receive Una vez, antes de aceptar nada Por stdin: <sha-viejo> <sha-nuevo> <ref>, una línea por referencia Salida distinta de 0 rechaza el push entero
update Una vez por cada referencia Como argumentos: <ref> <sha-viejo> <sha-nuevo> Rechaza solo esa referencia; las demás pueden pasar
post-receive Después de aceptar, con todo actualizado Igual que pre-receive, por stdin Se ignora: ya no se puede rechazar nada

pre-receive: la puerta

Ejemplo real para gestor-tareas: rechazar envíos que introduzcan ficheros de más de cinco megas, que es la causa habitual de repositorios que se vuelven inmanejables (y el motivo de existir de Git LFS, lección 10-03).

#!/bin/bash
# hooks/pre-receive en el repositorio bare del servidor
LIMITE=$((5 * 1024 * 1024))

while read -r viejo nuevo ref; do
    # Rama nueva: comparar contra el árbol vacío
    if [ "$viejo" = "0000000000000000000000000000000000000000" ]; then
        rango="$nuevo"
    else
        rango="$viejo..$nuevo"
    fi

    # Recorrer los objetos nuevos que llegan en este push
    while read -r modo tipo sha tam ruta; do
        [ "$tipo" = "blob" ] || continue
        if [ "$tam" -gt "$LIMITE" ]; then
            echo "RECHAZADO: '$ruta' ocupa $tam bytes (limite: $LIMITE)."
            echo "Usa Git LFS para ficheros grandes."
            exit 1
        fi
    done < <(git rev-list --objects "$rango" |
             git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
             awk '{print "-", $1, $2, $3, substr($0, index($0,$4))}')
done
exit 0

Fíjate en dos detalles. El primero: la comprobación del hash de ceros, que es como Git indica "esta referencia no existía antes" (una rama nueva). El segundo: exit 1 en cualquier punto rechaza el push completo, incluidas las demás ramas que vinieran en el mismo envío. Esa atomicidad es deliberada: o entra todo o no entra nada.

Otros usos típicos de pre-receive:

  • Rechazar force pushes sobre ramas concretas.
  • Rechazar commits cuyo mensaje no siga la convención GT-NNN.
  • Rechazar commits cuyo autor no tenga un correo del dominio de la empresa.
  • Detectar secretos (claves de API, credenciales) en el contenido que llega. Esto es materia de la lección 08-05, pero conviene saber que el sitio correcto para esa barrera es aquí.

update: control por referencia

Se ejecuta una vez por cada referencia y puede rechazar solo esa. Es el sitio natural para reglas específicas de una rama o de las etiquetas:

#!/bin/bash
# hooks/update — solo el equipo de publicación puede crear etiquetas de version
ref="$1"; viejo="$2"; nuevo="$3"

case "$ref" in
  refs/tags/v*)
    if [ "$viejo" != "0000000000000000000000000000000000000000" ]; then
        echo "RECHAZADO: las etiquetas de version no se modifican ni se mueven."
        exit 1
    fi
    if ! id -nG "$USER" 2>/dev/null | grep -qw publicacion; then
        echo "RECHAZADO: solo el grupo 'publicacion' puede crear etiquetas v*."
        exit 1
    fi
    ;;
esac
exit 0

Este ejemplo protege algo importante que vimos en la lección 05-05: una etiqueta publicada no se mueve. Si alguien mueve v2.0.0 a otro commit, quien ya la había descargado tiene una versión distinta de la de quien la descargue mañana, y eso es una fuente de fallos imposibles de diagnosticar.

post-receive: notificar

Se ejecuta con todo ya aceptado, así que no puede rechazar nada. Su papel es avisar al resto del mundo.

#!/bin/bash
# hooks/post-receive
while read -r viejo nuevo ref; do
    rama="${ref#refs/heads/}"
    [ "$rama" = "$ref" ] && continue      # ignorar etiquetas

    autor=$(git log -1 --format='%an' "$nuevo")
    asunto=$(git log -1 --format='%s' "$nuevo")

    # Avisar al chat del equipo
    curl -sS -X POST "https://chat.ejemplo.es/hooks/gestor-tareas" \
         -H 'Content-Type: application/json' \
         -d "{\"texto\": \"$autor ha enviado a $rama: $asunto\"}" >/dev/null

    # Y disparar la tubería de integración continua
    curl -sS -X POST "https://ci.ejemplo.es/api/ejecutar" \
         -H "Authorization: Bearer $TOKEN_CI" \
         -d "{\"rama\": \"$rama\", \"commit\": \"$nuevo\"}" >/dev/null
done

Ese segundo curl es literalmente el disparador del apartado 2. Cuando usas una plataforma alojada, esto ocurre por debajo: el post-receive del servidor emite un webhook que el sistema de CI recibe. Es también el hook que genera el mensaje remote: Crea una pull request para... que vimos en la lección 07-01.

La limitación de siempre

Ya lo apuntamos en la 06-01 y hay que repetirlo: en una plataforma alojada no tienes acceso al directorio hooks/. GitHub, GitLab o Bitbucket no te dejan poner un script en su servidor. Lo que ofrecen son sus equivalentes gestionados:

Concepto Servidor propio Plataforma alojada
Rechazar un push por su contenido pre-receive Push rules (en planes de pago) o comprobación en CI
Controlar quién actualiza qué referencia update Ramas protegidas, reglas de etiqueta, CODEOWNERS
Notificar y disparar post-receive Webhooks, integraciones nativas
Exigir que las pruebas pasen Difícil: el push es síncrono Comprobaciones de estado obligatorias

La última fila es interesante y explica el reparto real. Un pre-receive se ejecuta durante el push, con el usuario esperando en la terminal: no puede lanzar una batería de veinte minutos. Por eso las comprobaciones lentas no viven en un hook, sino en el CI, y su obligatoriedad se impone en el momento de fusionar la PR, no en el del push. Los hooks de servidor son para reglas rápidas y estructurales; el CI, para verificaciones lentas y sustantivas.

  1. Cliente, servidor y CI: quién comprueba qué

La foto completa de las tres barreras, que es el resumen práctico de todo el módulo 6 y este:

Hook de cliente Hook de servidor CI / comprobación de estado
Dónde se ejecuta Portátil de cada persona Servidor Git Infraestructura de CI
Cuándo Antes de commit / push Durante el push Tras el push, en paralelo
¿Se puede esquivar? : --no-verify, o borrando el fichero No No
¿Se versiona? No (salvo core.hooksPath + Husky) No, es del servidor : la tubería está en el repositorio
Velocidad exigida Segundos Segundos Minutos
Bloquea al usuario Sí, mientras se ejecuta Sí, mientras se ejecuta No: es asíncrono
Qué poner aquí Formateo, lint de lo modificado, formato del mensaje Tamaño de ficheros, permisos sobre refs, secretos, force push Pruebas, compilación, matriz de sistemas, cobertura, seguridad
Papel Ahorrar el viaje al servidor Reglas estructurales innegociables La verificación real

La estrategia sana no es elegir una, sino encadenar las tres, cada vez más lentas y más completas:

flowchart LR
    A["pre-commit<br/>2 segundos<br/>formato y lint"] --> B["pre-push<br/>30 segundos<br/>pruebas rápidas"]
    B --> C["pre-receive<br/>1 segundo<br/>reglas estructurales"]
    C --> D["CI en la PR<br/>8 minutos<br/>batería completa"]
    D --> E["Rama protegida<br/>decide si se integra"]

Cada eslabón detecta lo suyo lo antes posible. El hook local te ahorra un viaje al servidor; el servidor rechaza lo estructuralmente inaceptable; el CI verifica de verdad; y la rama protegida convierte el resultado del CI en la decisión final.

Y la regla que resume la relación entre las tres: lo obligatorio se comprueba donde el usuario no manda.

  1. Mantener el CI rápido

Un CI lento no es un inconveniente menor: destruye la práctica que pretende sostener.

La cadena causal es directa. Si la comprobación tarda cuarenta minutos, la gente deja de esperar el resultado y cambia de tarea. Al volver, hay que reconstruir el contexto. Como esperar es caro, se acumulan cambios para amortizar la espera: PRs más grandes, ramas más largas, integraciones menos frecuentes. Es decir, exactamente lo contrario de la integración continua. Y peor: un rojo tarda cuarenta minutos en detectarse, durante los cuales otras cinco personas han construido encima.

Referencias prácticas ampliamente aceptadas:

Duración Efecto sobre el equipo
< 5 min Se espera el resultado sin cambiar de tarea. Ideal
5–10 min Aceptable. El límite práctico para TBD
10–20 min Se cambia de contexto. Empieza a doler
20–60 min Se acumulan cambios. La integración continua se degrada
> 60 min La gente ignora el CI. Ha dejado de servir

Las cinco técnicas que más rinden:

  1. Caché

Lo más rentable, con diferencia. Instalar dependencias suele ser el paso más lento y el más repetitivo.

- name: Cachear las dependencias
  uses: actions/cache@v4
  with:
    path: ~/.npm
    # La clave incluye el hash del fichero de bloqueo:
    # si no cambian las dependencias, se reutiliza la caché
    key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      npm-${{ runner.os }}-

La clave del asunto es la clave: debe cambiar exactamente cuando cambie lo cacheado. Un restore-keys como red de seguridad permite reutilizar una caché parcial cuando la exacta no existe.

  1. Paralelismo

Trabajos independientes se ejecutan a la vez. Con la matriz del apartado 4, tres sistemas operativos cuestan lo mismo en tiempo de reloj que uno. También se pueden partir las pruebas en fragmentos:

strategy:
  matrix:
    fragmento: [1, 2, 3, 4]
steps:
  - run: npm test -- --shard=${{ matrix.fragmento }}/4

Cuatro máquinas, un cuarto del tiempo. Ojo: hay un coste fijo por trabajo (arrancar la máquina, traer el código, instalar) que pone un suelo. Partir en veinte fragmentos de treinta segundos cada uno no acelera nada si arrancar cuesta un minuto.

  1. Ejecutar solo lo afectado

Si la PR solo toca README.md, no hace falta la batería completa. Git da la información necesaria:

- name: Detectar que ha cambiado
  id: cambios
  run: |
    BASE="${{ github.event.pull_request.base.sha }}"
    FICHEROS=$(git diff --name-only "$BASE...HEAD")
    echo "$FICHEROS"
    if echo "$FICHEROS" | grep -qvE '\.(md|txt)$'; then
      echo "codigo=true" >> "$GITHUB_OUTPUT"
    else
      echo "codigo=false" >> "$GITHUB_OUTPUT"
    fi

- name: Ejecutar las pruebas
  if: steps.cambios.outputs.codigo == 'true'
  run: npm test

Fíjate en el "$BASE...HEAD" con tres puntos: es el diff desde el ancestro común, exactamente por la razón que explicamos en la lección 07-02. Con dos puntos aparecerían como cambiados los ficheros que ha tocado main, y la detección sería incorrecta.

Aviso importante: si esta comprobación es obligatoria en la rama protegida y decides saltarla, la PR se quedará esperando para siempre una comprobación que nunca llega. El patrón correcto es que el trabajo se ejecute siempre y termine rápido cuando no hay nada que hacer, no que no se ejecute.

  1. Escalonar: rápido siempre, completo por la noche

No todo tiene que ejecutarse en cada envío.

Batería Cuándo Duración Contenido
Rápida Cada envío y cada PR < 10 min Lint, pruebas unitarias, compilación
Media Al integrar en main < 30 min Pruebas de integración, matriz completa
Completa Cada noche Sin límite Extremo a extremo, rendimiento, seguridad, navegadores
on:
  schedule:
    - cron: '0 3 * * *'      # cada día a las 03:00 UTC

El compromiso: un fallo detectado solo por la nocturna tarda hasta un día en aparecer. Es aceptable si la batería rápida cubre lo que rompe con frecuencia.

  1. Clon superficial cuando se pueda

El fetch-depth: 0 del apartado 4 es necesario para los pasos que consultan el historial, pero es lento en repositorios grandes. En los trabajos que no lo necesiten, deja el clon superficial por defecto. Y para casos extremos existen los clones parciales (--filter=blob:none), que veremos en la lección 10-04.

Y una última recomendación que no es técnica: mide. Casi todas las plataformas muestran la duración por trabajo y por paso. Diez minutos mirando ese desglose suelen revelar que el 70% del tiempo se va en un paso concreto que se puede cachear. Optimizar a ciegas es tirar el tiempo.

  1. Merge queue: integrar sin carreras

Recuerda la consecuencia 2 del apartado 3: verde en tu PR no garantiza verde en main, porque entre la última comprobación y la fusión main puede haber avanzado.

Con tres personas y cuatro integraciones al día, la probabilidad es baja y se asume. Con treinta personas y cincuenta integraciones al día, ocurre a diario, y la solución obvia —exigir que la rama esté actualizada con main antes de integrar— produce un efecto perverso: cada vez que alguien integra, todas las demás PRs quedan desactualizadas y hay que reactualizarlas y volver a probarlas. Se convierte en una carrera donde solo gana quien pulsa el botón más rápido.

La cola de integración (merge queue, merge train en GitLab) resuelve esto. En lugar de fusionar al pulsar el botón, la PR entra en una cola, y el sistema:

  1. Toma la primera PR de la cola.
  2. Construye un commit especulativo: main + esa PR.
  3. Ejecuta las comprobaciones sobre esa combinación.
  4. Si pasa, la fusiona en main de verdad.
  5. Si falla, la expulsa de la cola y avisa a su autor, sin bloquear a las demás.

Y la optimización que la hace viable: se procesan varias en paralelo de forma especulativa. Con tres PRs en cola se prueban simultáneamente main+A, main+A+B y main+A+B+C. Si las tres pasan, se integran las tres de golpe. Si B falla, se descarta B y se reaprovecha el resultado de main+A, reprocesando solo lo posterior.

flowchart TD
    A["PR A aprobada"] --> Q["Cola de integración"]
    B["PR B aprobada"] --> Q
    C["PR C aprobada"] --> Q
    Q --> S1["Probar main+A"]
    Q --> S2["Probar main+A+B"]
    Q --> S3["Probar main+A+B+C"]
    S1 --> R["Verde: integrar"]
    S2 --> R
    S3 --> X["Rojo: expulsar C<br/>e integrar solo A y B"]

Ventajas: main nunca se rompe por una carrera, nadie tiene que reactualizar su rama a mano, y el rendimiento del equipo deja de degradarse con su tamaño.

Coste: consume bastante más tiempo de máquina (cada combinación especulativa es una ejecución completa) y añade latencia entre aprobar e integrar.

Cuándo hace falta: cuando el equipo integra tantas veces al día que las carreras son habituales, normalmente a partir de diez o quince personas trabajando sobre el mismo repositorio. Por debajo de eso es complejidad innecesaria: con exigir la rama actualizada basta.

  1. Qué queda fuera: el despliegue

Hemos llegado al límite de esta lección, y conviene marcarlo bien.

Lo que hemos construido responde a: "¿es correcto este cambio y puede integrarse?". Eso es integración continua.

Lo que viene después responde a: "¿cómo llega este cambio a las manos de los usuarios?". Eso es entrega y despliegue continuos, e incluye entornos de preproducción, estrategias de promoción, despliegue progresivo, reversión, infraestructura como código, observabilidad y todo lo que ocurre después de que el commit entre en main.

Esa mitad es el contenido de la lección 10-05: Git en DevOps. Desde el punto de vista de Git, lo único que hay que retener aquí es la frontera:

Integración continua (esta lección) Entrega y despliegue (10-05)
Pregunta ¿Es correcto? ¿Puede integrarse? ¿Cómo llega al usuario?
Disparadores en Git push, pull request Fusión en main, etiqueta
Resultado Verde o rojo, integrar o no Versión en funcionamiento
Si falla No se integra Se revierte el despliegue

Fíjate en la fila de los disparadores: las etiquetas de la lección 05-05 son la bisagra entre las dos mitades. Es lo que hace que git tag -a v2.4.0 && git push --follow-tags sea, en muchos proyectos, el acto que inicia una publicación.

Errores Comunes y Consejos

Error 1: creer que "tener CI" es hacer integración continua. Si cada persona integra cada tres semanas, un servidor de pruebas no arregla nada. La práctica es integrar a menudo; el servidor solo la verifica.

Error 2: no saber que el CI prueba la fusión, no tu rama. Es la causa del "en local me funciona" más frecuente en las PRs. Trae pull/<n>/merge y reproduce sobre eso.

Error 3: olvidar fetch-depth: 0 cuando la tubería usa el historial. El clon superficial rompe git log entre referencias, git describe, git merge-base y git blame con errores confusos.

Error 4: npm install en lugar de npm ci. Las ejecuciones dejan de ser reproducibles y aparecen fallos que nadie ha causado.

Error 5: exigir como obligatorias comprobaciones cuyos nombres cambian. Con matrices, los nombres se generan. Usa un único trabajo resumen (todo-correcto) como comprobación obligatoria.

Error 6: filtrar por rutas una comprobación obligatoria. Si no se lanza, la PR espera para siempre. El trabajo debe ejecutarse siempre y terminar rápido cuando no hay nada que hacer.

Error 7: convivir con pruebas inestables. Un rojo aleatorio enseña al equipo a relanzar sin mirar, y con eso muere toda la señal. Arréglala, aíslala o bórrala, pero no la ignores.

Error 8: tolerar un CI de cuarenta minutos. Degrada el flujo entero hacia PRs grandes y ramas largas. Es una inversión con retorno inmediato.

Error 9: confiar en los hooks de cliente para lo obligatorio. --no-verify y ya está. Lo innegociable va en el servidor o en una comprobación de estado.

Error 10: dejar abierta la excepción de las ramas protegidas. Se usará durante un incidente, con prisa, que es cuando peor idea es.

Error 11: no enviar las etiquetas y esperar que se dispare la publicación. git push --follow-tags.

Consejo 1: concurrency con cancel-in-progress desde el primer día. Ahorro inmediato de tiempo de máquina, coste cero.

Consejo 2: un trabajo resumen del que dependan todos los demás. Simplifica muchísimo la configuración de la rama protegida.

Consejo 3: mide antes de optimizar. El desglose por pasos casi siempre revela un único culpable cacheable.

Consejo 4: escalona las baterías. Rápida en cada envío, media al integrar, completa por la noche.

Consejo 5: valida en el CI lo que el hook commit-msg valida en local. El hook avisa pronto; el CI obliga.

Consejo 6: comprueba los punteros de los submódulos en el CI. Tres líneas que evitan clones rotos para todo el equipo (lección 06-05).

Consejo 7: guarda los artefactos con if: always(). Los informes de un fallo son justamente los que más necesitas.

Consejo 8: --filter y clones parciales en repositorios grandes. Y si el problema son ficheros binarios pesados, la respuesta es Git LFS (lección 10-03).

Ejercicios

Ejercicio 1: comprobar lo que comprueba el CI

Sin plataforma, reproduciendo el mecanismo con Git puro:

  1. Crea un repositorio con app.js y main en tres commits.
  2. Crea funcionalidad/filtros desde el segundo commit y haz dos commits que renombren una función.
  3. Vuelve a main y añade un commit que llame a esa función con el nombre antiguo.
  4. Calcula a mano el commit que probaría el CI: crea una rama temporal desde main y fusiona la rama de la funcionalidad.
  5. Comprueba que git diff main...funcionalidad/filtros no revela el problema, y que el resultado de la fusión sí lo tiene.
  6. Escribe un script de tres líneas que, dadas dos ramas, produzca el commit de fusión sin dejar rastro (pista: git merge-tree).

Ejercicio 2: un hook de servidor efectivo

  1. Crea /tmp/servidor/gestor.git como repositorio bare y clónalo en /tmp/ana.
  2. Escribe un hooks/pre-receive en el bare que rechace cualquier push que contenga un commit cuyo asunto no empiece por GT-NNN o por Fusionar .
  3. Comprueba que un commit con mensaje correcto pasa y uno incorrecto se rechaza, y que en el segundo caso ninguna referencia del push se actualiza.
  4. Instala en /tmp/ana un hook de cliente commit-msg con la misma regla, y demuestra que git commit --no-verify lo esquiva pero que el pre-receive sigue rechazando el push.
  5. Escribe un hooks/update que impida modificar o borrar cualquier etiqueta ya existente, y compruébalo.

Ejercicio 3: una tubería mínima y su optimización

  1. Escribe un fichero ci/tuberia.yml para gestor-tareas con: disparadores por envío a main y por PR, un trabajo de lint, un trabajo de pruebas en matriz de tres sistemas y un trabajo resumen del que dependan los dos.
  2. Añade caché de dependencias con una clave basada en el hash del fichero de bloqueo.
  3. Añade un paso que valide los mensajes de commit solo de la PR, usando el rango correcto.
  4. Añade una detección de cambios que evite ejecutar las pruebas si solo se han tocado ficheros .md, sin dejar de publicar el resultado de la comprobación.
  5. Explica, para cada elección, qué problema previene.

Soluciones

Solución 1:

mkdir /tmp/ci-fusion && cd /tmp/ci-fusion
git init -qb main
printf 'function guardarTareas(t) { return t; }\n' > app.js
git add . && git commit -q -m "Anadir guardarTareas"
echo 'const tareas = [];' >> app.js
git add . && git commit -q -m "Anadir el estado inicial"
BASE=$(git rev-parse HEAD)
echo 'guardarTareas(tareas);' >> app.js
git commit -qam "Guardar el estado al arrancar"
# 2. La rama renombra
git switch -qc funcionalidad/filtros "$BASE"
sed -i 's/function guardarTareas/function persistirTareas/' app.js
git commit -qam "Renombrar guardarTareas a persistirTareas"
echo 'function filtrar(f) { return tareas.filter(f); }' >> app.js
git commit -qam "Anadir el filtrado"
# 3. main ya tenía la llamada antigua (commit del paso 1)
# 4. El commit que probaría el CI
git switch -q main
git switch -qc simulacion-merge-del-ci
git merge -q funcionalidad/filtros
cat app.js
function persistirTareas(t) { return t; }
const tareas = [];
guardarTareas(tareas);
function filtrar(f) { return tareas.filter(f); }

Fusiona sin conflicto y el resultado llama a guardarTareas, que ya no existe: conflicto semántico.

# 5. El diff de la PR no lo revela
git switch -q main
git diff main...funcionalidad/filtros

El diff muestra el renombrado y el filtrado, y nada más: la llamada rota está en main, no en la rama, así que no aparece. Solo probando la fusión se detecta.

# 6. El commit de fusión sin tocar el árbol de trabajo
git merge-tree --write-tree main funcionalidad/filtros
4f8b1d3a9c2e7f0b5d8a1c4e7f0b3d6a9c2e5f81

git merge-tree (Git 2.38 o superior) devuelve el árbol resultante sin modificar el directorio de trabajo ni crear ningún commit. Es, esencialmente, lo que hacen las plataformas para calcular pull/<n>/merge.

Solución 2:

mkdir -p /tmp/servidor && git init -q --bare /tmp/servidor/gestor.git
git clone -q /tmp/servidor/gestor.git /tmp/ana
cd /tmp/ana && git switch -qc main
echo "inicial" > app.js && git add . && git commit -q -m "GT-001 Estado inicial"
git push -qu origin main
# 2. El hook de servidor
cat > /tmp/servidor/gestor.git/hooks/pre-receive <<'EOF'
#!/bin/bash
VACIO=0000000000000000000000000000000000000000
while read -r viejo nuevo ref; do
    [ "$nuevo" = "$VACIO" ] && continue          # borrado de rama
    if [ "$viejo" = "$VACIO" ]; then
        rango="$nuevo"
    else
        rango="$viejo..$nuevo"
    fi
    while read -r asunto; do
        if ! echo "$asunto" | grep -qE '^(GT-[0-9]{1,5} |Fusionar )'; then
            echo "RECHAZADO: '$asunto' no sigue la convencion GT-NNN."
            exit 1
        fi
    done < <(git log --format=%s "$rango")
done
exit 0
EOF
chmod +x /tmp/servidor/gestor.git/hooks/pre-receive
# 3. Mensaje correcto: pasa
cd /tmp/ana
echo "ok" >> app.js && git commit -qam "GT-002 Anadir el filtrado por etiqueta"
git push origin main
# Mensaje incorrecto: se rechaza
echo "mal" >> app.js && git commit -qam "arreglos varios"
git push origin main
remote: RECHAZADO: 'arreglos varios' no sigue la convencion GT-NNN.
To /tmp/servidor/gestor.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to '/tmp/servidor/gestor.git'
# Y ninguna referencia se ha movido
git --git-dir=/tmp/servidor/gestor.git log --oneline -1 main
# 4. El hook de cliente y --no-verify
cat > /tmp/ana/.git/hooks/commit-msg <<'EOF'
#!/bin/bash
grep -qE '^(GT-[0-9]{1,5} |Fusionar )' "$1" || {
  echo "El mensaje debe empezar por GT-NNN."; exit 1; }
EOF
chmod +x /tmp/ana/.git/hooks/commit-msg

cd /tmp/ana
git reset -q --hard HEAD~1
echo "mal" >> app.js
git commit -qam "otra vez arreglos"            # el hook lo bloquea
git commit -qam "otra vez arreglos" --no-verify # lo esquiva
git push origin main                            # el servidor NO se esquiva

--no-verify desactiva el hook local porque el fichero está en el disco de Ana. El pre-receive se ejecuta en el servidor y no hay opción del cliente que lo alcance: eso es lo que lo convierte en un control efectivo.

# 5. Proteger las etiquetas
git reset -q --hard HEAD~1
cat > /tmp/servidor/gestor.git/hooks/update <<'EOF'
#!/bin/bash
ref="$1"; viejo="$2"
VACIO=0000000000000000000000000000000000000000
case "$ref" in
  refs/tags/*)
    if [ "$viejo" != "$VACIO" ]; then
        echo "RECHAZADO: las etiquetas no se modifican ni se mueven."
        exit 1
    fi ;;
esac
exit 0
EOF
chmod +x /tmp/servidor/gestor.git/hooks/update

cd /tmp/ana
git tag -a v1.0.0 -m "Version 1.0.0" && git push -q origin v1.0.0
git tag -f -a v1.0.0 -m "Movida" && git push --force origin v1.0.0
remote: RECHAZADO: las etiquetas no se modifican ni se mueven.
 ! [remote rejected] v1.0.0 -> v1.0.0 (hook declined)

Solución 3:

# ci/tuberia.yml
name: Integracion continua

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

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      # 2. Caché con clave basada en el fichero de bloqueo
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: npm-${{ runner.os }}-
      - run: npm ci
      - run: npm run lint

      # 3. Mensajes de commit: solo los que aporta la PR
      - name: Validar los mensajes de confirmacion
        if: github.event_name == 'pull_request'
        run: |
          BASE="${{ github.event.pull_request.base.sha }}"
          git log --format=%s "$BASE..HEAD" | while read -r a; do
            echo "$a" | grep -qE '^(GT-[0-9]+ |Fusionar )' || {
              echo "Mensaje invalido: $a"; exit 1; }
          done

  pruebas:
    runs-on: ${{ matrix.so }}
    strategy:
      fail-fast: false
      matrix:
        so: [ubuntu-latest, macos-latest, windows-latest]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # 4. Detección de cambios: el trabajo SIEMPRE se ejecuta,
      #    pero se salta el paso caro si solo hay documentación.
      - name: Detectar cambios de codigo
        id: cambios
        shell: bash
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
            FICHEROS=$(git diff --name-only "$BASE...HEAD")
          else
            FICHEROS=$(git diff --name-only HEAD~1 HEAD)
          fi
          if echo "$FICHEROS" | grep -qvE '\.(md|txt)$'; then
            echo "codigo=true" >> "$GITHUB_OUTPUT"
          else
            echo "codigo=false" >> "$GITHUB_OUTPUT"
          fi

      - uses: actions/setup-node@v4
        if: steps.cambios.outputs.codigo == 'true'
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
        if: steps.cambios.outputs.codigo == 'true'
      - run: npm test
        if: steps.cambios.outputs.codigo == 'true'
      - name: Sin cambios de codigo
        if: steps.cambios.outputs.codigo == 'false'
        run: echo "Solo documentacion: no se ejecutan las pruebas."

  todo-correcto:
    runs-on: ubuntu-latest
    needs: [lint, pruebas]
    if: always()
    steps:
      - run: |
          [ "${{ contains(needs.*.result, 'failure') }}" = "false" ] || exit 1
          echo "Todas las comprobaciones han pasado."

5. Qué previene cada elección:

Elección Problema que previene
concurrency + cancel-in-progress Malgastar máquinas en ejecuciones que ya no importan
fetch-depth: 0 fatal: bad revision al recorrer los commits de la PR
Caché con hash del bloqueo Reinstalar dependencias idénticas en cada ejecución
npm ci Ejecuciones no reproducibles por resolución de versiones
fail-fast: false Perder la información de los otros sistemas al fallar uno
Rango $BASE..HEAD Validar commits de main que no son de esta PR
Diff con tres puntos en la detección Contar como cambiados ficheros que solo tocó main
El trabajo se ejecuta siempre, se salta el paso caro Que una comprobación obligatoria no se lance y la PR espere para siempre
Trabajo todo-correcto Tener que listar nombres de matriz en la rama protegida

Conclusión

Con esta lección se cierra el módulo 7. Lo esencial:

  • La integración continua no es tener un servidor de CI: es integrar de verdad y a menudo, al menos a diario, con cada integración verificada automáticamente. Sin integración frecuente, un servidor de pruebas solo verifica versiones divergentes que aún no se han encontrado.
  • Se engancha a Git mediante disparadores: por push, por pull request (incluido synchronize, cuando llegan commits nuevos) y por etiqueta, que es la bisagra hacia la publicación. Y recuerda que las etiquetas no viajan con git push a secas.
  • En una pull request, el CI comprueba el resultado de la fusión, no tu rama. Es la referencia pull/<n>/merge. Por eso tu PR puede ponerse en rojo sin que hayas hecho nada, por eso detecta conflictos semánticos, y por eso verde en la PR no garantiza verde en main.
  • Una tubería bien construida trae el historial que necesita (fetch-depth: 0), instala de forma reproducible (npm ci), paraleliza en matriz de sistemas, comprueba también los punteros de los submódulos, y termina en un trabajo resumen del que dependan los demás.
  • Una comprobación de estado se asocia a un commit; una rama protegida la convierte en requisito. Y esto es lo que 06-01 dejó pendiente: --no-verify es una opción del cliente, no viaja por la red y el servidor no se entera de que existe. El hook local te avisa; el servidor decide.
  • Los hooks de servidor cierran el círculo: pre-receive (una vez, rechaza el push entero), update (una vez por referencia, rechaza solo esa) y post-receive (después, solo notifica y dispara). Son efectivos porque se ejecutan donde el usuario no manda. En plataformas alojadas no tienes acceso a hooks/, y sus equivalentes son las reglas de push, las ramas protegidas y los webhooks.
  • El reparto sano encadena las tres barreras: hook de cliente para lo rápido y local, hook de servidor para lo estructural e innegociable, CI para la verificación lenta y real. Lo obligatorio se comprueba donde el usuario no manda.
  • Un CI lento destruye el flujo: empuja hacia PRs grandes, ramas largas e integración tardía. Menos de diez minutos como objetivo, con caché, paralelismo, ejecución de solo lo afectado y baterías escalonadas.
  • La merge queue elimina la carrera entre PRs aprobadas probando combinaciones especulativas antes de fusionar. Merece la pena a partir de diez o quince personas sobre el mismo repositorio.
  • Y la frontera: aquí acaba la integración. Cómo ese código llega a los usuarios —entornos, promoción, despliegue progresivo, reversión— es la lección 10-05.

El módulo, en una idea

El equipo de gestor-tareas empezó este módulo con una herramienta dominada y sin acuerdos. Ahora tiene un proceso completo: Diego, sin permiso de escritura, propone cambios desde su fork mediante pull requests; Ana los revisa con git diff main...rama y git range-diff; el equipo ha elegido su flujo de ramas según el producto —Git Flow para la versión instalable, algo cercano a GitHub Flow o TBD para la nube—; y unas comprobaciones automáticas que nadie puede esquivar deciden qué entra en la línea principal.

Tienen proceso. Lo que les falta ahora son hábitos.

Porque un proceso impecable convive perfectamente con un historial ilegible. Se puede tener ramas protegidas, revisión obligatoria y CI en verde, y aun así acumular cien commits llamados "cambios" que nadie podrá interpretar dentro de un año; versionar la carpeta node_modules y un fichero de configuración con la contraseña de la base de datos; sufrir conflictos absurdos porque un compañero de Windows guarda los finales de línea de otra manera; y descubrir un día que una clave de API lleva ocho meses en el historial público.

Eso es el módulo 8. Cómo escribir mensajes de confirmación que sirvan de documentación (08-01, donde por fin desarrollaremos la convención que aquí solo hemos validado); cómo mantener un historial legible y qué política de integración elegir (08-02, que cierra la decisión que dejamos abierta en la 07-04); qué no versionar nunca (08-03 y 08-04); cómo no filtrar un secreto y qué hacer si ya ha pasado (08-05); y cómo mantener el repositorio rápido cuando crece (08-06).

Empezamos por lo más cotidiano y lo más desatendido: la lección 08-01: Escribiendo Buenos Mensajes de Confirmación.

Dominando Git: De Principiante a Avanzado

Módulo 1: Introducción a Git

Módulo 2: Operaciones Básicas de Git

Módulo 3: Ramas y Fusión

Módulo 4: Trabajando con Repositorios Remotos

Módulo 5: Operaciones Avanzadas de Git

Módulo 6: Herramientas y Técnicas de Git

Módulo 7: Estrategias de Colaboración y Flujo de Trabajo

Módulo 8: Mejores Prácticas y Consejos de Git

Módulo 9: Solución de Problemas y Depuración

Módulo 10: Git en el Mundo Real

© Copyright 2026. Todos los derechos reservados