Por qué tu pipeline tarda 40 minutos y las cuatro correcciones que funcionan

La duración del pipeline es la palanca de productividad más barata que casi nadie usa. Dónde se va el tiempo de verdad y cómo bajar de 40 minutos a menos de 10 sin cambiar de proveedor de CI.

Un pipeline de 40 minutos cuesta más que minutos de compilación. Cuesta el cambio de contexto: la persona abre otra cosa, vuelve una hora después, se encuentra un fallo, y el cambio que debía haber entrado antes de comer entra mañana. Diez ingenieros que integran tres veces al día convierten un ahorro de 30 minutos en aproximadamente una persona entera de capacidad recuperada.

La solución casi nunca es el proveedor de CI. Son estas cuatro cosas, en este orden.

1. Mide las etapas antes de optimizar nada

Todo sistema de CI reporta la duración por etapa. Saca las últimas 200 ejecuciones de tu pipeline principal y calcula la mediana de cada etapa. La distribución es casi siempre una de estas:

  • Una etapa dominante (normalmente los tests, a veces la construcción de la imagen). Ve a la sección 2 o a la 3.
  • Plana entre muchas etapas: suele ser coste de preparación repetido por job (instalar dependencias, descargar imágenes, clonar todo el histórico). Ve a la sección 4.
  • Mucho hueco entre encolado y arranque. No es un problema de compilación: te faltan ejecutores. Arregla capacidad o concurrencia primero, porque ningún otro trabajo se notará en los números mientras los jobs esperan en cola.

Saca también el p95, no solo la mediana. Un p95 al triple del p50 significa tests inestables y reintentos, y los reintentos son desperdicio puro: una suite con un 2 por ciento de inestabilidad repartida en 40 jobs tumba un tercio de los pipelines sin motivo.

2. Paraleliza la suite de tests bien

Repartir los tests en N jobs es la mayor victoria individual y la que peor suele hacerse. Repartir por número de ficheros te deja un job de 12 minutos y siete de 90 segundos, porque las duraciones son muy desiguales.

Reparte por duración registrada:

# guarda los tiempos de la ejecución anterior y particiona con ellos
pytest --splits 8 --group "$CI_NODE_INDEX" --durations-path .test_durations

Casi todos los ecosistemas tienen equivalente (knapsack en Ruby, --shard con fichero de tiempos en Jest, la distribución de tests de Gradle). El objetivo es que cada fragmento quede dentro del 20 por ciento de la media.

Después, evita que los fragmentos repitan la preparación: la imagen del contenedor, la instalación de dependencias y el esquema de la base de datos deberían construirse una vez en un job previo y reutilizarse, no repetirse ocho veces.

3. Cachea la compilación, no solo las dependencias

Cachear dependencias es lo mínimo. La victoria grande está en cachear la compilación.

Para imágenes de contenedor, usa BuildKit con caché en el registro para que las capas sobrevivan a los runners efímeros:

docker buildx build \
  --cache-from type=registry,ref=ghcr.io/acme/orders:buildcache \
  --cache-to   type=registry,ref=ghcr.io/acme/orders:buildcache,mode=max \
  --tag ghcr.io/acme/orders:${GIT_SHA} --push .

mode=max cachea las capas intermedias, no solo la final, que es la diferencia entre reconstruir en 30 segundos o en 6 minutos tras un cambio de código.

Ordena el Dockerfile para que la capa que más cambia quede la última. Copiar todo el árbol de fuentes antes de npm ci invalida la capa de dependencias en cada commit: ese único error explica una parte enorme de las construcciones lentas de contenedores.

En lenguajes compilados, una caché remota compartida (caché de compilación de Gradle, sccache, caché remota de Bazel) convierte una reconstrucción completa en una descarga. Este es el paso donde los agentes efímeros se pagan solos en vez de costarte.

4. Haz menos trabajo por commit

La mayoría de pipelines ejecutan todo en cada commit porque era lo más sencillo de escribir.

  • Filtros por ruta. En un monorepo, un cambio en docs/ no debería lanzar la suite de integración. GitLab (rules: changes:) y Actions (paths:) lo soportan en dos líneas.
  • Clonado superficial. fetch-depth: 1 en vez de clonar todo el histórico ahorra de 30 a 90 segundos por job en un repositorio antiguo, multiplicado por cada job paralelo.
  • Separa la puerta rápida de la lenta. Lint, tests unitarios y compilación en cada push: menos de 10 minutos, bloqueante. Integración, extremo a extremo, escaneos de seguridad y rendimiento al integrar en main o de forma programada, con los fallos dirigidos a su responsable. Un escaneo de contenedores que añade cuatro minutos a cada push y bloquea por cosas no accionables es puro impuesto.

Cómo se ve lo bueno

De push a verde en el pipeline bloqueante en menos de 10 minutos, p95 por debajo de 15. Verificación completa dentro de la hora siguiente a la integración. Inestabilidad por debajo del 0,5 por ciento, con una lista de cuarentena que alguien trabaja de verdad.

Si ahora estás en 40 minutos, la medición de la sección 1 suele llevar una tarde y te dice cuál de las otras tres secciones merece una semana. Rara vez es más de una.

ConsultorIA

¿Quieres esto en tu nube?

El diagnóstico de diez días en solo lectura es gratuito, y con Skyline puedes ver tu entorno en un mapa antes de escribirnos.

Artículos relacionados

Módulos que la gente reutiliza en vez de copiar

Los dos fracasos son un módulo que envuelve un recurso y no aporta nada, y un módulo que lo hace todo y que nadie se atreve a cambiar. Una interfaz mínima, valores por defecto seguros y un versionado honesto son lo que los separa.