DevOps, CI/CD y contenedores

Integración continua: prácticas, pipeline de build y una configuración que funciona

Principiante10 min de lecturaActualizado

Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés

La integración continua significa que cada desarrollador integra en la rama principal compartida al menos una vez al día y que un build automatizado demuestra que cada merge compila y pasa sus tests. Esta guía cubre las prácticas en las que se basa la CI, la anatomía de un pipeline de build, un workflow de CI completo para un proyecto Java, cómo mantener los builds rápidos y fiables, y los hábitos que distinguen a un equipo que hace CI de uno que simplemente tiene un servidor de CI.

Lo que aprenderás

  • Enumerar las prácticas que definen la integración continua y por qué existe cada una
  • Diseñar las etapas de un build de CI: compilación, tests unitarios, análisis estático, empaquetado
  • Escribir un workflow de CI que lance el build en cada push y en cada pull request
  • Mantener el build rápido y fiable con caché, paralelismo y buena higiene frente a los tests flaky
  • Elegir una política de merge —trunk-based o ramas de corta duración— que permita integrar a diario

La integración solía ser una fase. Los equipos trabajaban durante semanas en ramas separadas y luego pasaban días —a veces semanas— haciendo merges, descubriendo que dos personas habían cambiado la misma interfaz de formas incompatibles y persiguiendo bugs que solo aparecían al juntarlo todo. La integración continua (CI) disuelve esa fase haciendo que la integración sea tan frecuente que nunca resulte dolorosa: todo el mundo integra cambios pequeños en una rama principal compartida cada día, y un build automatizado demuestra después de cada merge que el software sigue compilando y que sus tests siguen pasando. La práctica es sencilla de describir y sorprendentemente exigente de cumplir. Esta guía explica qué requiere, cómo se monta un pipeline de build y cómo mantener todo lo bastante rápido para que el equipo confíe en él.

Qué exige la integración continua

La CI es una práctica, y el servidor de CI es solo uno de sus ingredientes. La lista completa:

Un único repositorio de código que contenga todo lo necesario para construir el sistema: código, tests, scripts de build, migraciones de base de datos, plantillas de configuración. Alguien con una máquina recién instalada y un checkout debería poder hacer el build sin preguntar a un compañero qué más tiene que instalar.

Un build automatizado que se lance con un solo comando. mvn verify, ./gradlew build, npm ci && npm test: la herramienta concreta importa menos que la garantía de que no hay ningún paso manual. El build tiene que poder ejecutarse en cualquier máquina, no solo en la que se configuró por primera vez.

Un build autoverificable. El build ejecuta los tests automatizados y falla si falla cualquiera de ellos. Un build que solo compila demuestra muy poco; son los tests los que dan sentido a un resultado en verde.

Todo el mundo hace commit en la rama principal cada día. Es la regla que los equipos se saltan con más frecuencia. Una rama que vive dos semanas son dos semanas de trabajo sin integrar, por muy verde que esté su propio build. Merges pequeños y diarios mantienen la divergencia entre desarrolladores lo bastante pequeña como para resolverla en minutos.

Cada commit dispara un build en una máquina de integración. Nada de «en mi portátil funciona»: un entorno limpio, un checkout nuevo, los mismos pasos cada vez. Ese es el trabajo del servidor de CI.

Los builds rotos se arreglan de inmediato. La rama principal es un activo compartido del equipo; cuando está en rojo nadie puede integrar con seguridad, así que arreglarla (o revertir) es la máxima prioridad.

El build es rápido. Un feedback que llega una hora después no es feedback; es una interrupción. Diez minutos es el límite que suele citarse para el build principal.

Todo el mundo puede ver el resultado. Un dashboard, un mensaje de Slack, un badge de estado en la pull request. Esconder el estado del build en un servidor que nadie visita es la receta para que los builds en rojo pasen desapercibidos.

Anatomía de un build de CI

El build de un servicio típico avanza por etapas de coste creciente y frecuencia decreciente. Cada etapa actúa como filtro: si falla, las siguientes no se ejecutan, lo que mantiene el feedback rápido para los fallos más habituales.

EtapaQué haceTiempo típicoFalla por
CheckoutClon limpio del commit que se pruebasegundosRepositorio inaccesible
CompilaciónCompila el código principal y el de testsde segundos a un minutoErrores de sintaxis y de tipos
Tests unitariosTests rápidos y aislados, sin I/Ode uno a cinco minutosRegresiones de lógica
Análisis estáticoLinters, comprobaciones de estilo, escaneo de vulnerabilidades de dependenciasde uno a tres minutosCode smells, CVE conocidas
EmpaquetadoGenera el jar o la imagen de contenedorun minutoEmpaquetado mal configurado
Tests de integraciónTests contra una base de datos o un broker reales (a menudo en un contenedor)de cinco a quince minutosBugs de cableado, SQL o serialización
PublicaciónSube el artefacto a un repositorio, etiquetado con la versiónsegundosPermisos del repositorio

Maven codifica casi todo esto en su ciclo de vida: compile, test (tests unitarios con Surefire), package, verify (tests de integración con Failsafe, además de comprobaciones como los umbrales de cobertura), install y deploy. Por eso, ejecutar mvn verify desde la CI lanza compilación, tests unitarios, empaquetado y tests de integración en el orden correcto y se detiene en el primer fallo.

<!-- pom.xml: run *IT classes as integration tests during "verify" -->
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.5.2</version>
  <executions>
    <execution>
      <goals>
        <goal>integration-test</goal>
        <goal>verify</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Separar los tests unitarios (Surefire, *Test) de los de integración (Failsafe, *IT) es clave para la velocidad: los desarrolladores pueden ejecutar mvn test en local en segundos, mientras el servidor de CI ejecuta el verify completo.

Un workflow de CI completo

Este es un workflow de GitHub Actions para un proyecto Maven con Java 21. La misma estructura se traslada directamente a un pipeline declarativo de Jenkins (lo cubre la guía del Jenkinsfile) o a GitLab CI.

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

on:
  push:
    branches: [main]
  pull_request:

# Cancel an older run of the same branch when a new push arrives
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      postgres:                     # real database for the integration tests
        image: postgres:16
        env:
          POSTGRES_PASSWORD: ci
        ports: ["5432:5432"]
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s --health-timeout 5s --health-retries 10
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
          cache: maven              # caches ~/.m2 keyed on pom.xml hashes

      - name: Build and test
        run: mvn -B --no-transfer-progress verify
        env:
          DB_URL: jdbc:postgresql://localhost:5432/postgres
          DB_PASSWORD: ci

      - name: Publish test report
        if: always()                # upload even when tests failed
        uses: actions/upload-artifact@v4
        with:
          name: surefire-reports
          path: |
            target/surefire-reports
            target/failsafe-reports

Algunos detalles en los que conviene fijarse. -B (modo batch) evita que Maven imprima barras de progreso y códigos de color que ensucian los logs. concurrency con cancel-in-progress evita encolar cinco builds por cinco pushes seguidos a la misma pull request. timeout-minutes impide que un test colgado ocupe un runner durante seis horas. El if: always() en la subida de informes hace que un build fallido te siga dando los informes que necesitas para diagnosticarlo. Cachear el repositorio de Maven con una clave basada en el contenido de pom.xml convierte cinco minutos de descarga de dependencias en unos segundos cuando estas no han cambiado.

Mantener el build rápido

Un build lento es la razón más común por la que la CI se degrada. Estrategias, más o menos ordenadas por rentabilidad:

Cachea las dependencias. El ~/.m2 de Maven, la caché de Gradle, el node_modules de npm restaurado desde una caché con clave en el lock file. Es la mayor mejora individual en runners alojados, que arrancan desde una máquina limpia.

Separa lo rápido de lo lento. Tests unitarios en el job principal; tests de integración y end-to-end en un segundo job que se ejecuta después, o en un pipeline nocturno si tardan una hora. Los desarrolladores reciben primero el veredicto rápido.

Ejecuta los tests en paralelo. Surefire y JUnit 5 pueden ejecutar clases de test en paralelo en varios núcleos (<parallel> / junit.jupiter.execution.parallel.enabled), y una suite grande puede repartirse entre varios runners.

Construye solo lo que ha cambiado. En un monorepo, herramientas como la caché de build de Gradle, Nx o Bazel se saltan los módulos cuyas entradas no han cambiado.

No pruebes a través de la interfaz lo que puede cubrir un test unitario. Una pirámide de tests con muchos tests unitarios rápidos, menos tests de integración y un puñado de tests end-to-end es más rápida y menos flaky que la inversa. La guía de estrategia de automatización de pruebas explica cómo darle esa forma.

Vigila la tendencia. Registra la duración de los builds y represéntala en una gráfica. Los builds se vuelven lentos test a test; nadie se da cuenta hasta que el build de diez minutos tarda cuarenta.

Tests flaky y confianza

Un test flaky pasa y falla con el mismo código. Su coste es mucho mayor que los minutos que hace perder: en cuanto los desarrolladores aprenden que un build en rojo puede ser «solo ese test flaky», dejan de investigar los builds en rojo, y los fallos reales se cuelan junto con los falsos.

Las causas típicas son tests dependientes del orden que comparten estado estático, tests que dependen de la hora del reloj o de la zona horaria, race conditions en código asíncrono probado con Thread.sleep, colisiones de puertos cuando los tests se ejecutan en paralelo y dependencia de servicios externos. La solución nunca es envolver todo el build en un bucle de reintentos; hay que poner el test en cuarentena (moverlo a un job aparte que no bloquee), abrir una incidencia y arreglarlo o borrarlo en pocos días. Una suite de tests solo sirve si un resultado en rojo significa algo.

Estrategia de merge: trunk frente a ramas

La exigencia de integrar a diario que impone la CI condiciona cómo usas las ramas.

El trunk-based development —todo el mundo hace commit en main, directamente o mediante pull requests que viven unas horas— es el enfoque más alineado con la CI. Las funcionalidades a medio hacer se ocultan tras feature flags (un interruptor de configuración que mantiene esa ruta de código desactivada en producción) en lugar de quedarse en una rama, de modo que el código se integra y compila cada día mientras la funcionalidad sigue siendo invisible para los usuarios.

Las ramas de funcionalidad de corta duración con pull requests funcionan bien cuando cada rama dura uno o dos días y su build tiene que estar en verde antes del merge. Exige que la rama esté al día con main antes de integrarla (o usa una merge queue) para que el resultado del build refleje de verdad la combinación.

Las ramas de larga duración —una rama develop que va semanas por detrás de main, ramas de release que reciben correcciones en paralelo, ramas por desarrollador que se integran una vez al mes— son el punto donde vuelve el dolor de la integración. A veces son necesarias (para mantener una release antigua), pero deberían ser la excepción. Los compromisos entre estos modelos se analizan en flujos de trabajo con ramas en Git.

Comprobaciones en las pull requests

La CI moderna se ejecuta en las pull requests, no solo en los merges, y las plataformas permiten que el botón de merge dependa de comprobaciones. Comprobaciones obligatorias razonables para un servicio:

  • El build y los tests unitarios pasan.
  • El análisis estático no detecta nuevos problemas críticos (quality gate de SonarQube, ESLint, Checkstyle).
  • El escaneo de dependencias no detecta nuevas vulnerabilidades de gravedad alta (OWASP Dependency-Check, Dependabot, Trivy).
  • La cobertura de código en las líneas modificadas no ha bajado del umbral acordado: una regla sobre el código nuevo, porque imponer una cifra global a una base de código legacy lleva a hacer trampas con la métrica.
  • Al menos un revisor ha dado su aprobación.

Mantén la lista lo bastante corta para que un cambio rutinario pase en minutos; cada comprobación que añades es una comprobación que la gente intentará esquivar si es lenta o ruidosa.

Errores comunes

  • Hacer commit en main una vez por semana desde una rama larga. El servidor de CI se ejecuta, pero la integración no es continua. Integra a diario; usa feature flags para el trabajo sin terminar.
  • Hacer el build solo en el servidor de CI. Si mvn verify no funciona en local, los desarrolladores hacen push para averiguar si su cambio compila y el pipeline se convierte en un compilador lento. El build tiene que funcionar en un portátil.
  • Dejar el build en rojo. Pasado un día, todo el mundo da por hecho que otra persona se está encargando. Asígnalo explícitamente o revierte.
  • Tests que atacan entornos compartidos. Una suite de tests que necesita la base de datos de staging solo admite una ejecución a la vez y se rompe en cuanto otra persona usa staging. En su lugar, arranca una base de datos en un contenedor por cada build.
  • Secretos en el fichero del pipeline. Una contraseña de base de datos en ci.yml queda para siempre en el historial de Git. Usa el almacén de secretos del sistema de CI e inyéctala como variable de entorno en tiempo de ejecución.
  • Ignorar los warnings. Un build que imprime trescientos warnings de deprecación esconde el que importa. Haz fallar el build con los warnings nuevos o, al menos, sigue su número.
  • Construir un artefacto distinto para cada entorno. El build de CI debe producir un único artefacto que las etapas posteriores promocionan; reconstruirlo por entorno significa que lo que probaste no es lo que desplegaste. Este es el puente hacia la entrega continua.

Ideas clave

  • La integración continua es una práctica —merges diarios en una rama principal compartida, verificados por un build automatizado y autoverificable— y un servidor de CI por sí solo no la proporciona.
  • Estructura el build en etapas de coste creciente para que los fallos habituales (errores de compilación, regresiones en tests unitarios) se notifiquen en minutos.
  • Mantén el build principal por debajo de diez minutos con caché de dependencias, tests en paralelo y una pirámide de tests sensata; lleva las suites lentas a una etapa posterior.
  • Los tests flaky destruyen la confianza más deprisa que los builds lentos; ponlos en cuarentena y arréglalos en lugar de reintentar.
  • El trunk-based development o las ramas cortas con comprobaciones obligatorias permiten integrar a diario; las ramas de larga duración recrean la fase de integración que la CI existe para eliminar.
  • Una rama principal en rojo es la máxima prioridad del equipo: arréglala en minutos o revierte.

Fuentes y lecturas recomendadas

Preguntas frecuentes

Tenemos un servidor Jenkins que hace un build cada noche. ¿Eso es integración continua?
No del todo. Un build nocturno te avisa a la mañana siguiente de que algo que se integró ayer rompió el código, cuando todo el mundo ya se había ido a casa y quizá con más cambios apilados encima. La CI significa que el build se ejecuta en cada push, termina en minutos y quien lo rompió se entera mientras el cambio todavía está fresco. El servidor es necesario, pero lo que lo convierte en CI son la frecuencia y la reacción.
¿Cuánto debería tardar un build de CI?
Apunta a menos de diez minutos desde el push hasta el resultado en el ciclo de feedback principal. A partir de ahí, los desarrolladores dejan de esperar el resultado, empiezan la siguiente tarea y viven los fallos como interrupciones en lugar de como feedback. Las suites más largas —integración, end-to-end, rendimiento— van en una etapa posterior del pipeline, que se ejecuta cuando la rápida ya ha pasado.
¿La CI debe ejecutarse en las ramas de funcionalidad o solo en la rama principal?
En ambas. Ejecutarla en cada rama y en cada pull request detecta los problemas antes del merge y mantiene main en verde. Pero los builds de rama no sustituyen a integrar a menudo: una rama que lleva tres semanas en verde puede seguir teniendo conflictos serios con main. Mantén las ramas cortas y haz rebase o merge desde main a diario.
¿Qué hacemos cuando el build está en rojo?
Arreglarlo pasa a ser la máxima prioridad de quien lo rompió, y nadie más integra encima de un build en rojo. Si la solución no es evidente en diez minutos, revierte el commit culpable e investiga en una rama. Un equipo que tolera una rama principal en rojo durante días ha perdido por completo el beneficio de la CI.