Saltar al contenido

Pipelines de CI/CD que no te estorban

Cómo diseñar pipelines de CI/CD rápidos, fiables y en los que tu equipo confíe: caché, paralelismo, paridad de entornos y estrategias de despliegue.

3 min de lectura
Cinta transportadora isométrica que lleva un push de código a través de un paso de caché y puertas de lint, verificación de tipos y pruebas verdes hacia staging y producción.

El pipeline que mata la productividad

Un pipeline de CI/CD que tarda 25 minutos en ejecutarse no es una red de seguridad — es un impuesto sobre cada desarrollador, cada commit, cada día. Los pipelines lentos se ignoran. Los checks en verde se convierten en sellos automáticos. El pipeline deja de protegerte.

Así es como pienso la construcción de sistemas de CI/CD en los que los equipos realmente confían y usan.

Principio 1: La velocidad es una funcionalidad

Apunta a menos de 5 minutos para el ciclo de feedback desde el push hasta los resultados de los tests. Así se consigue:

Cachea todo lo que puedas

ymlyaml
# GitHub Actions — aggressive dependency caching
- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      node_modules
      .next/cache
    key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-
 
# Docker layer caching
- name: Build image
  uses: docker/build-push-action@v5
  with:
    context: .
    cache-from: type=gha
    cache-to: type=gha,mode=max

Con una caché bien configurada, npm install baja de 3 minutos a 8 segundos cuando hay cache hit.

Paraleliza los jobs independientes

ymlyaml
jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [checkout, setup-node, run-lint]
 
  type-check:
    runs-on: ubuntu-latest
    steps: [checkout, setup-node, run-tsc]
 
  unit-tests:
    runs-on: ubuntu-latest
    steps: [checkout, setup-node, run-jest]
 
  # Only run after all checks pass
  deploy:
    needs: [lint, type-check, unit-tests]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps: [deploy-to-staging]

Ejecutar lint, type-check y tests en paralelo reduce el tiempo del pipeline un 60%.

Principio 2: Paridad de entornos

«En mi máquina funciona» es un síntoma de desajuste entre entornos. Trata la paridad de entornos como un requisito innegociable.

ymlyaml
# Pin exact tool versions everywhere
- uses: actions/setup-node@v4
  with:
    node-version-file: ".nvmrc" # Read from project file, not hardcoded
 
- uses: actions/setup-python@v5
  with:
    python-version-file: ".python-version"
dockerfiledockerfile
# Development and CI use the same base image
FROM node:22.11.0-alpine AS base
 
# Deterministic installs
RUN npm ci --frozen-lockfile
 
# Match production exactly
ENV NODE_ENV=production

Los lock files, .nvmrc, .tool-versions y los digests fijados de las imágenes Docker son la infraestructura de los builds reproducibles.

Principio 3: Falla rápido, falla con claridad

Los desarrolladores deberían saber en menos de 60 segundos si su push tiene un error evidente.

ymlyaml
jobs:
  quick-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      # These run in seconds and catch 80% of failures
      - name: Check formatting
        run: npx prettier --check .
 
      - name: Type check
        run: npx tsc --noEmit
 
      - name: Lint
        run: npx eslint . --max-warnings 0
 
  # Run the slow tests only after quick checks pass
  integration-tests:
    needs: quick-checks
    runs-on: ubuntu-latest
    steps:
      - name: Run integration tests
        run: npm run test:integration

Ordena los jobs por velocidad y probabilidad de fallo. El check más rápido e informativo debe ejecutarse primero.

Principio 4: Estrategias de despliegue

Nunca despliegues directamente a producción. Usa despliegues por etapas.

Despliegues blue-green

shbash
#!/bin/bash
# Deploy to inactive slot, then swap
CURRENT=$(get_active_slot)  # "blue" or "green"
NEXT=$([ "$CURRENT" = "blue" ] && echo "green" || echo "blue")
 
# Deploy new version to inactive slot
deploy_to_slot $NEXT $IMAGE_TAG
 
# Run smoke tests against new slot
run_smoke_tests $NEXT
 
# Swap traffic
swap_traffic $CURRENT $NEXT
 
# Keep old slot warm for rollback
echo "Old slot ($CURRENT) ready for rollback"

Esto te da rollback instantáneo: devuelve el tráfico al slot anterior.

Feature flags para cambios arriesgados

tstypescript
// Deploy code to production before enabling it
import { createClient } from "@vercel/edge-config";
 
const config = createClient(process.env.EDGE_CONFIG);
 
export async function getFeatureFlag(
  flag: string,
  userId: string,
): Promise<boolean> {
  const flags =
    await config.get<Record<string, { enabled: boolean; rollout: number }>>(
      "features",
    );
  const feature = flags?.[flag];
 
  if (!feature?.enabled) return false;
 
  // Gradual rollout — deterministic per userId
  const hash = parseInt(userId.slice(-2), 16);
  return (hash / 255) * 100 < feature.rollout;
}

Publica el código. Actívalo para el 1% de los usuarios. Verifica las métricas. Aumenta gradualmente. Así se desacopla el despliegue del release.

Principio 5: Observabilidad en el pipeline

Tu pipeline debería ser tan observable como tu aplicación.

ymlyaml
- name: Upload test results
  if: always() # Upload even if tests fail
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: |
      coverage/
      test-results.xml
 
- name: Annotate PR with test failures
  if: failure()
  uses: actions/github-script@v7
  with:
    script: |
      const results = require('./test-results.json');
      const failures = results.testResults
        .flatMap(r => r.testResults.filter(t => t.status === 'failed'))
        .map(t => `- ${t.ancestorTitles.join(' > ')} > ${t.title}`)
        .join('\n');
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `## Test Failures\n${failures}`,
      });

Los checks de PR fallidos deberían decirte exactamente qué falló y por qué, sin necesidad de entrar a revisar los logs.

Checklist de salud del pipeline

Ejecuta esta auditoría sobre tu pipeline actual:

  • El tiempo p95 del pipeline es inferior a 8 minutos
  • La tasa de cache hits supera el 80% en la instalación de dependencias
  • Los errores de lint y de tipos se reportan en menos de 90 segundos
  • Los despliegues son por etapas (staging → canary → producción)
  • El rollback tarda menos de 2 minutos
  • Los fallos del pipeline crean anotaciones accionables en los PRs
  • Los tests inestables (flaky) se registran y se ponen en cuarentena

Un pipeline bien diseñado es invisible — corre rápido, reporta con claridad y no estorba. Cuando tu equipo deja de quejarse del CI, lo has hecho bien.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX