Escena Viva está en internet. Pero desplegar sigue siendo un ritual: alguien ejecuta las pruebas en su portátil (si se acuerda), construye la imagen, la publica, lanza las migraciones, escala los procesos y comprueba a mano que la venta funciona. Seis pasos en el orden correcto, con una persona concreta que sabe hacerlos. Si esa persona está de vacaciones el día del estreno del Festival de Jazz de Primavera, el equipo tiene un problema.

Esta lección cierra el módulo automatizando la cadena entera. El objetivo no es la elegancia técnica: es que subir una versión deje de ser un acontecimiento. Que sea aburrido. Que se haga cinco veces al día sin que nadie contenga la respiración. Un despliegue aburrido se hace a menudo, y desplegar a menudo es lo que hace que cada despliegue sea pequeño y, por tanto, seguro.

Contenido

  1. Integración, entrega y despliegue continuos: tres cosas distintas
  2. Anatomía de la tubería de Escena Viva
  3. GitHub Actions: el fichero de CI, bloque a bloque
  4. Secretos en CI
  5. Qué hace que una tubería sea útil
  6. Construir una vez y promover el artefacto
  7. Migraciones, pruebas de humo y reversión automática
  8. Versionado y notas de la versión
  9. Qué no debe estar en la tubería
  10. DORA: saber si estás mejorando

  1. Integración, entrega y despliegue continuos: tres cosas distintas

Las siglas CI/CD se usan como una sola palabra y esconden tres conceptos distintos. La integración continua (CI) consiste en integrar cada cambio en la rama principal con frecuencia —al menos a diario— y, en cada integración, dejar que un sistema automático construya el proyecto y ejecute las pruebas. Resuelve el infierno de la integración: dos personas trabajando dos semanas por separado y descubriendo al final que sus cambios son incompatibles. Con CI, la incompatibilidad aparece a las horas, cuando arreglarla cuesta minutos.

La entrega continua (CD) va un paso más allá: cada cambio que pasa la CI queda listo para desplegarse, con el artefacto construido, probado y almacenado. Desplegar es apretar un botón. Lo que se automatiza no es el despliegue, sino la capacidad de desplegar en cualquier momento. Y el despliegue continuo elimina el botón: cada cambio que pasa todas las etapas llega a producción solo.

Integración continua Entrega continua Despliegue continuo
Se automatiza Construir y probar Todo hasta preproducción Todo, hasta producción
A producción llega A mano Apretando un botón Solo
Requiere Pruebas fiables Lo anterior + entornos Lo anterior + humo y reversión
Riesgo por despliegue El habitual Menor El mínimo, porque son diminutos

Escena Viva implementará integración continua completa y entrega continua, con despliegue automático a preproducción y aprobación manual para producción. Es la elección sensata para un equipo pequeño con un negocio donde una caída durante una venta cuesta dinero directo. El despliegue continuo puro es un objetivo legítimo, pero exige una confianza en las pruebas que se gana con el tiempo, no que se decreta.

  1. Anatomía de la tubería de Escena Viva

flowchart TD
    A[Push o PR] --> B[npm ci con cache] --> C[Linter y formato]
    B --> D[Unitarias]
    C --> E[Integracion: Postgres, Mongo, Redis]
    D --> E
    E --> F[Cobertura con umbral] --> G[Auditoria] --> H{Rama principal?}
    H -->|No| I[Fin: informe en el PR]
    H -->|Si| J[Construir imagen y publicar] --> K[Migrar y desplegar en preproduccion]
    K --> L[Pruebas de humo]
    L -->|Fallan| M[Reversion automatica]
    L -->|Pasan| N{Aprobacion manual} -->|Aprobado| O[Promover la MISMA imagen]
    O --> P[Humo en produccion] -->|Fallan| Q[Reversion automatica]
    P -->|Pasan| R[Notas de la version]

Dos propiedades del diseño, antes de escribir código. La primera: las etapas van de rápida a lenta y de barata a cara; el linter tarda 15 segundos, y si falla no tiene sentido gastar cuatro minutos en pruebas de integración — es el mismo fallar rápido que aplicamos a la validación de configuración en 11-01. La segunda: la imagen se construye una sola vez y después se promueve, de modo que el artefacto que pasó las pruebas y se desplegó en preproducción es exactamente el que llega a producción. El apartado 6 explica por qué esto no es negociable.

  1. GitHub Actions: el fichero de CI, bloque a bloque

# .github/workflows/ci.yml
name: Integracion continua

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

# Cancela ejecuciones anteriores de la misma rama: no gastes minutos
# probando un commit que ya ha sido reemplazado.
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # 1. Calidad estatica: rapida y barata, va primero.
  calidad:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24.x', cache: 'npm' }
      - run: npm ci
      - run: npm run lint
      - run: npm run format -- --check
      # Prohibe console en src: hay que usar el logger de pino (11-02).
      - run: '! grep -rn "console\." src/ --include="*.js"'

  # 2. Unitarias en los dos extremos del rango de engines.
  unidad:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix: { node: ['24.5', '24.x'] }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '${{ matrix.node }}', cache: 'npm' }
      - run: npm ci
      - run: npm run test:unidad

  # 3. Integracion: necesita las tres bases de datos del M9.
  integracion:
    runs-on: ubuntu-latest
    needs: [calidad, unidad]

    services:
      postgres:
        image: postgres:17-alpine
        env: { POSTGRES_USER: escena, POSTGRES_PASSWORD: escena, POSTGRES_DB: escena_viva_test }
        ports: ['5432:5432']
        options: >-
          --health-cmd "pg_isready -U escena -d escena_viva_test"
          --health-interval 5s --health-timeout 3s --health-retries 10
      mongo:
        image: mongo:8
        ports: ['27017:27017']
        options: >-
          --health-cmd "mongosh --quiet --eval 'db.adminCommand(\"ping\")'"
          --health-interval 5s --health-retries 10
      redis:
        image: redis:7-alpine
        ports: ['6379:6379']
        options: '--health-cmd "redis-cli ping" --health-interval 5s --health-retries 10'

    env:
      NODE_ENV: test
      NIVEL_REGISTRO: error
      URL_POSTGRES: postgres://escena:escena@localhost:5432/escena_viva_test
      URL_MONGO: mongodb://localhost:27017/escena_viva_test
      URL_REDIS: redis://localhost:6379
      # Secretos de PRUEBA: no valen nada, pero cumplen el esquema de 11-01.
      JWT_SECRETO: '0123456789abcdef0123456789abcdef'
      SESION_SECRETO: 'fedcba9876543210fedcba9876543210'
      CSRF_SECRETO: 'aaaabbbbccccddddeeeeffff00001111'

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24.x', cache: 'npm' }
      - run: npm ci
      - run: npm run migrar
      - name: Integracion con cobertura y umbral
        run: npm run test:ci

  # 4. Cadena de suministro (M5): checkout + setup-node + npm ci y, al final:
  #      - run: npm audit --audit-level=high --omit=dev

on, concurrency, jobs y runs-on. Los dos disparadores tienen propósitos distintos: en pull request se ejecuta antes de fusionar, como barrera que impide que entre código roto; en push a master se ejecuta después, porque el resultado de fusionar dos ramas que pasaban por separado puede fallar — la fusión semántica rota, más frecuente de lo que parece. concurrency cancela la ejecución anterior de la misma rama cuando llega un commit nuevo, así que si empujas tres commits seguidos solo se prueba el último. Cada job corre en una máquina virtual limpia y aislada, por defecto en paralelo; needs: [calidad, unidad] establece el orden y así se implementa el «rápido y barato primero» del diagrama. Y runs-on: ubuntu-latest debe coincidir con producción: probar en Windows y desplegar en Alpine es pedir sorpresas con rutas, permisos y módulos nativos.

checkout, setup-node y la caché. actions/checkout@v4 clona el repositorio; fíjate en el @v4, que fija la versión mayor de la acción, porque usar una acción sin versión —o de un tercero desconocido— es ejecutar código ajeno con acceso a tu repositorio, y para acciones no oficiales conviene fijar el hash del commit. actions/setup-node@v4 instala Node, y cache: 'npm' es la línea que más tiempo ahorra de todo el fichero: guarda la caché de npm entre ejecuciones usando el hash de package-lock.json como clave, de modo que npm ci pasa de unos 45 segundos con caché fría a unos 8 con caché caliente.

npm ci: el pago definitivo del módulo 5. Instala exactamente lo que dice package-lock.json sin resolver rangos, así que lo probado en CI es idéntico versión por versión a lo que corre en producción; borra node_modules antes de instalar, de modo que no hay estado heredado de otra ejecución; falla si package.json y el lock no son coherentes, atrapando el error de editar uno sin el otro; y es más rápido, porque se salta la resolución. npm install en CI es un antipatrón: puede resolver una versión nueva de una dependencia transitiva y provocar el caso peor —que CI pase y producción falle, o al revés—, y además modifica el lock sin que nadie lo revise.

La matriz de versiones. engines dice >=24.5.0 <25, así que se prueban los dos extremos del rango soportado: la versión mínima declarada y la última del mayor. Si algo usa una API que solo existe desde 24.8, la 24.5 lo detecta — y esa es justo la que podría instalar la PaaS de 11-05. fail-fast: false evita que el fallo de una combinación cancele las demás, porque saber si falla en las dos o solo en una cambia el diagnóstico.

Servicios auxiliares. El bloque services da vida a las pruebas de integración del módulo 9. Tres cosas importantes:

  1. localhost, no el nombre del servicio. A diferencia de docker compose (11-04), el trabajo corre en la máquina anfitriona y los contenedores exponen puertos en ella. Es la confusión número uno al migrar de compose a Actions.
  2. Las comprobaciones de salud no son opcionales. Sin --health-cmd, el trabajo arranca en cuanto el contenedor existe, no cuando el servicio acepta conexiones. PostgreSQL tarda unos segundos, y el resultado es una prueba que falla una de cada diez veces: una prueba intermitente creada por la propia configuración de CI.
  3. Las versiones coinciden con producción. Probar contra PostgreSQL 15 y desplegar sobre 17 es probar otra cosa.

Los env reproducen .env.test del módulo 9, con secretos de mentira que cumplen el mínimo de 32 caracteres que impone el esquema de 11-01. Otro dividendo de validar la configuración: si el esquema y el entorno de CI se desincronizan, el fallo es inmediato y explícito. Sobre cobertura y auditoría, npm run test:ci ejecuta las pruebas bajo c8, y el umbral se declara en la configuración de c8, no en el YAML:

{
  "c8": {
    "all": true, "include": ["src/**/*.js"],
    "lines": 80, "functions": 80, "branches": 70,
    "check-coverage": true
  }
}

Con check-coverage, c8 sale con código distinto de cero si no se alcanza el umbral y el trabajo falla. Sobre los números: fija el umbral en el nivel actual y súbelo poco a poco, porque poner 90 % cuando estás en 62 % garantiza que alguien desactive la comprobación en dos semanas; y recuerda del módulo 9 que la cobertura mide qué líneas se ejecutan, no si las pruebas comprueban algo útil. Por su parte, npm audit --audit-level=high --omit=dev cierra el módulo 5: solo falla ante vulnerabilidades altas o críticas, para que la tubería no se rompa cada semana por un aviso menor, e ignora las dependencias de desarrollo, que no llegan a producción. Como complemento, un escaneo de la imagen con Trivy detecta además las del sistema base.

  1. Secretos en CI

Los secretos se guardan en la configuración del repositorio o de la organización y se inyectan como variables (env: { TOKEN: '${{ secrets.REGISTRO_TOKEN }}' }). Las reglas que hay que respetar:

  • Mínimo privilegio. El token que publica imágenes solo debe poder escribir en el registro. Nada de tokens personales con acceso completo a la cuenta.
  • Nunca en el run directamente. docker login -p ${{ secrets.TOKEN }} deja el valor en la línea de comandos, visible en la tabla de procesos. Pásalo por env y léelo con --password-stdin.
  • Los secretos no se exponen a los PR de forks. GitHub lo hace por diseño, y es esencial: sin ello, cualquiera podría abrir un PR con un flujo modificado que imprima tus credenciales. La consecuencia práctica es que los trabajos que necesitan secretos —construir y desplegar— solo se ejecutan en pushes a master.
  • Rotación periódica, con fecha de caducidad si el proveedor la ofrece.
  • Si se filtra uno, se aplica el procedimiento de 11-01: revocar primero, investigar después. Los registros de CI son públicos en repositorios públicos y, aunque GitHub enmascara los valores conocidos, un secreto construido a trozos o codificado en base64 escapa a ese filtro.

Un patrón moderno que conviene conocer es OIDC: en lugar de guardar credenciales de larga vida, el flujo obtiene un token temporal del proveedor de nube demostrando su identidad, eliminando el secreto persistente por completo.

  1. Qué hace que una tubería sea útil

Una tubería que existe pero nadie mira es peor que no tener ninguna: da una falsa sensación de seguridad. Tres propiedades la hacen útil. Rápida. Objetivo: menos de 10 minutos desde el push hasta el resultado. Por encima de eso la gente cambia de tarea, pierde el contexto y deja de esperar. Las técnicas que más rinden son cachear npm (30-40 s por trabajo), paralelizar trabajos independientes (pagas el más largo, no la suma), poner lo barato antes que lo caro con needs, cancelar ejecuciones obsoletas con concurrency y cachear las capas de Docker (1-3 min por construcción).

Fiable. Aquí está la causa número uno de que un equipo abandone su CI: las pruebas intermitentes. Una prueba intermitente falla a veces sin que el código haya cambiado. El daño no es el fallo, es lo que provoca en las personas: la primera vez se investiga; la tercera, alguien dice «vuelve a lanzarla, es la intermitente esa». A partir de ahí cualquier fallo rojo se atribuye a la intermitencia, incluidos los reales, y la CI ha dejado de aportar información para aportar solo retraso. En el módulo 9 vimos las causas, y aquí reaparecen agravadas porque las máquinas de CI son más lentas y variables que tu portátil:

Causa Síntoma Solución
Dependencia del orden Falla al ejecutar sola o en otro orden Estado limpio en cada beforeEach
Tiempos de espera fijos Falla en máquinas lentas Esperar a condiciones, no a relojes
Fechas y zonas horarias Falla a medianoche o en otra región Reloj falso con sinon; UTC en CI
Servicio no listo o puerto fijo Falla la primera prueba, o al paralelizar Comprobaciones de salud; puerto 0

Política recomendada: una prueba intermitente se arregla o se elimina en 48 horas. Marcarla como omitida es aceptable como medida temporal, con una tarea asociada; dejarla fallando, no.

Con la rama principal siempre desplegable. De nada sirve la tubería si se puede fusionar código rojo. En master se configura protección de rama: prohibido el push directo, comprobaciones obligatorias (calidad, unidad, integracion, auditoria), al menos una revisión aprobatoria y rama actualizada antes de fusionar, lo que evita la fusión semántica rota. Con eso, el estado de master es siempre «probado y desplegable», y ese invariante es lo que permite desplegar sin miedo durante el estreno del Festival de Jazz.

  1. Construir una vez y promover el artefacto

Construye una vez, promueve el artefacto. Nunca reconstruyas por entorno.

Si construyes una imagen para preproducción y otra para producción, no son la misma imagen: entre una y otra construcción puede cambiar una dependencia transitiva, una imagen base o un paquete del sistema. Habrías probado una cosa y desplegado otra, y todo el trabajo anterior no valdría nada.

# .github/workflows/despliegue.yml
name: Despliegue

on:
  push:
    branches: [master]
  workflow_dispatch:          # Permite lanzarlo a mano desde la interfaz.

jobs:
  construir:
    runs-on: ubuntu-latest
    outputs:
      etiqueta: ${{ steps.meta.outputs.etiqueta }}
    steps:
      - uses: actions/checkout@v4
      - id: meta
        name: Etiqueta = version + hash del commit
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "etiqueta=${VERSION}-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/escena-viva/api:${{ steps.meta.outputs.etiqueta }}
          cache-from: type=gha        # Reaprovecha las capas de 11-04.
          cache-to: type=gha,mode=max

  # preproduccion: identico al trabajo de abajo, con environment: preproduccion,
  # sus propios secretos _PRE y humo contra https://pre.escenaviva.test

  produccion:
    needs: [construir, preproduccion]
    runs-on: ubuntu-latest
    environment: produccion       # Con revisores obligatorios configurados.
    steps:
      - uses: actions/checkout@v4
      - name: Migrar el esquema
        env: { URL_POSTGRES: '${{ secrets.URL_POSTGRES_PROD }}' }
        run: npm ci --omit=dev && npm run migrar
      - name: Promover LA MISMA imagen, sin reconstruir
        env: { TOKEN_PLATAFORMA: '${{ secrets.TOKEN_PLATAFORMA_PROD }}' }
        run: ./scripts/desplegar.sh produccion ${{ needs.construir.outputs.etiqueta }}
      - name: Pruebas de humo
        id: humo
        run: node scripts/humo.js https://escenaviva.test
      - name: Revertir si el humo falla
        if: failure() && steps.humo.outcome == 'failure'
        env: { TOKEN_PLATAFORMA: '${{ secrets.TOKEN_PLATAFORMA_PROD }}' }
        run: ./scripts/revertir.sh produccion

Claves del flujo: outputs propaga la etiqueta calculada en la construcción a los trabajos de despliegue, de modo que preproducción y producción reciben la misma cadena y despliegan la misma imagen. El etiquetado con versión y hash (1.4.0-8f3a1c2) combina lo que un humano entiende con la verdad exacta del commit, y evita latest, que no es una versión sino un alias mutable. environment: produccion activa los entornos de GitHub, donde se configuran revisores obligatorios: el trabajo se detiene y espera aprobación humana, y ahí está la frontera entre entrega continua y despliegue continuo — moverla es cambiar una línea. workflow_dispatch permite lanzar el flujo a mano para redesplegar sin un commit nuevo. Y cache-from/cache-to guardan las capas de Docker aprovechando el orden de capas de 11-04: si package-lock.json no cambió, la capa de npm ci se reutiliza.

  1. Migraciones, pruebas de humo y reversión automática

Las migraciones se ejecutan antes del despliegue del código, en un paso propio y una sola vez. Es la misma decisión de 11-04 (fuera del CMD) y de 11-05 (fase de liberación). Y funciona sin cortes solo porque siguen la estrategia expandir → migrar → contraer: durante el reemplazo progresivo de instancias conviven el código antiguo y el nuevo sobre el esquema ya migrado, así que la migración debe ser compatible con ambos.

Paso Qué ocurre Si falla
1 Migración compatible hacia atrás Se aborta; el código antiguo sigue con el esquema antiguo
2 Despliegue progresivo de la imagen nueva La plataforma detiene el reemplazo
3 Pruebas de humo Reversión automática al artefacto anterior
4 (Despliegue posterior) migración de contracción Solo cuando nada usa ya lo que se elimina

Un aviso sobre los tiempos: una migración que bloquee una tabla grande durante minutos convierte un despliegue sin cortes en una caída, así que mide su duración sobre una copia del volumen real antes de fusionarla. En cuanto a las pruebas de humo, comprueban que lo desplegado responde y hace lo esencial; no sustituyen a las de integración, sino que verifican que el despliegue en sí ha ido bien.

// scripts/humo.js
'use strict';

const base = process.argv[2];

const comprobaciones = [
  {
    nombre: 'sonda de disponibilidad',
    async ejecutar() {
      const respuesta = await fetch(`${base}/salud/listo`);
      if (respuesta.status !== 200) throw new Error(`estado ${respuesta.status}`);
      // Las tres dependencias deben responder: el valor de la sonda de 11-02.
      const { detalle } = await respuesta.json();
      const caidas = Object.entries(detalle).filter(([, ok]) => !ok).map(([n]) => n);
      if (caidas.length > 0) throw new Error(`dependencias caidas: ${caidas.join(', ')}`);
    },
  },
  {
    nombre: 'catalogo publico',
    async ejecutar() {
      const { datos } = await (await fetch(`${base}/api/eventos`)).json();
      if (!Array.isArray(datos) || datos.length === 0) throw new Error('catalogo vacio');
    },
  },
  {
    // Sin token debe responder 401. Si responde 200, algo grave ha cambiado.
    nombre: 'autenticacion exigida',
    async ejecutar() {
      const respuesta = await fetch(`${base}/api/compras`, { method: 'POST' });
      if (respuesta.status !== 401) throw new Error(`esperaba 401, llego ${respuesta.status}`);
    },
  },
];

// Espera creciente: el despliegue progresivo tarda en completarse.
async function conReintentos(comprobacion, intentos = 5) {
  for (let intento = 1; intento <= intentos; intento += 1) {
    try {
      return await comprobacion.ejecutar();
    } catch (error) {
      if (intento === intentos) throw error;
      await new Promise((resolver) => setTimeout(resolver, intento * 2000));
    }
  }
}

(async () => {
  let fallos = 0;
  for (const comprobacion of comprobaciones) {
    try {
      await conReintentos(comprobacion);
      process.stdout.write(`OK    ${comprobacion.nombre}\n`);
    } catch (error) {
      process.stderr.write(`FALLO ${comprobacion.nombre}: ${error.message}\n`);
      fallos += 1;
    }
  }
  process.exit(fallos > 0 ? 1 : 0);
})();

Tres decisiones deliberadas: los reintentos con espera creciente son imprescindibles porque el despliegue progresivo tarda en completarse y las primeras peticiones pueden llegar a una instancia aún arrancando; se ejecutan todas las comprobaciones antes de salir, para ver el cuadro completo; y comprobar que /api/compras devuelve 401 es un centinela de seguridad que detectaría en segundos un despliegue que rompiera el autenticar.js del módulo 8. Que el script salga con código distinto de cero es lo que dispara el paso de reversión con if: failure(), y ahí el trabajo de 11-05 cierra el círculo: revertir es volver al artefacto anterior, operación de segundos porque las imágenes están etiquetadas y las migraciones son compatibles hacia atrás.

  1. Versionado y notas de la versión

El módulo 5 nos dio npm version, que actualiza package.json, crea un commit y una etiqueta de Git: patch para correcciones, minor para funcionalidad compatible, major para cambios incompatibles. Para automatizar las notas hacen falta mensajes de commit legibles por máquina, y los commits convencionales son el formato estándar: feat(compras): permitir seleccionar butaca en el Auditorio Ribera, fix(aforo): corregir el conteo al cancelar una reserva provisional, chore(deps): actualizar pino a 9.5.0.

Prefijo Significado Efecto en la versión
fix: Corrección de error patch
feat: Funcionalidad nueva minor
BREAKING CHANGE: en el cuerpo Cambio incompatible major
chore:, docs:, test:, refactor: Sin impacto en el usuario ninguno

Con eso, herramientas como semantic-release cierran el ciclo: analizan los commits desde la última etiqueta, deciden la versión, generan el CHANGELOG.md, etiquetan y publican. El equipo deja de discutir si un cambio es minor o patch, porque lo decide el formato del commit. Empezar por lo simple —commits convencionales y npm version a mano— es perfectamente válido; la automatización total se añade cuando el ritmo de releases lo justifique.

  1. Qué no debe estar en la tubería

Antipatrón Por qué es un problema
Secretos en claro en el YAML Se leen en el repositorio y en cada registro de ejecución
npm install en lugar de npm ci Instala versiones distintas a las probadas y modifica el lock
Pasos manuales no documentados «Antes de desplegar hay que ejecutar X» se olvida, y quien lo sabía se va
Reconstruir la imagen por entorno Despliegas algo distinto de lo que probaste
Etiquetar con latest No sabes qué está desplegado ni a qué revertir
Pruebas contra producción Datos reales contaminados; correos a personas reales

El caso de los pasos manuales merece énfasis. Una tubería con un agujero —«y luego alguien limpia la caché de Redis a mano»— no es una tubería automatizada: es una lista de comandos con partes ocultas. Si un paso es necesario, va dentro; si no cabe dentro, va escrito en un procedimiento que cualquiera pueda seguir.

  1. DORA: saber si estás mejorando

El programa de investigación DORA identificó cuatro métricas que correlacionan con el rendimiento de los equipos que entregan software. Sirven para saber si tu proceso mejora, no para comparar equipos ni evaluar personas.

Métrica Qué mide Rendimiento alto Rendimiento bajo
Frecuencia de despliegue Cada cuánto llega un cambio a producción Varias veces al día Menos de una vez al mes
Tiempo de entrega De commit a producción Menos de un día Más de un mes
Tasa de fallos en cambios Qué porcentaje causa una incidencia 0-15 % 46-60 %
Tiempo de recuperación Cuánto tardas en restaurar el servicio Menos de una hora Días o semanas

El hallazgo más contraintuitivo: velocidad y estabilidad no están en conflicto. Los equipos que despliegan más a menudo también fallan menos y se recuperan antes, porque desplegar a menudo obliga a que cada despliegue sea pequeño, y un cambio pequeño es fácil de revisar, probar y revertir. El miedo a desplegar produce despliegues grandes, y los grandes son los que rompen cosas. Lo construido en este módulo mueve las cuatro: la tubería permite desplegar cuando se quiera, de commit a preproducción pasan minutos, linter y pruebas y humo atrapan los fallos antes que los usuarios, y la reversión automática restaura el servicio en segundos. Un consejo final: mídelas, pero no las conviertas en objetivo de nadie, porque en cuanto la frecuencia de despliegue es una cifra que alguien debe alcanzar aparecen despliegues vacíos. Son un termómetro, no una nota.

Errores Comunes y Consejos

  • Convivir con pruebas intermitentes. Es la principal causa de que el equipo deje de mirar CI. Arréglalas o elimínalas en 48 horas.
  • Servicios sin comprobación de salud. Producen fallos aleatorios que parecen bugs del código y no lo son.
  • Usar el nombre del servicio en lugar de localhost en Actions. En compose sí, en Actions no.
  • npm install en CI. Rompe la reproducibilidad que package-lock.json garantiza.
  • Reconstruir por entorno. El artefacto que probaste no es el que despliegas.
  • Umbral de cobertura irreal. Alguien lo desactivará. Fija el nivel actual y sube despacio.
  • Tubería sin protección de rama. Se puede fusionar código rojo, y entonces no sirve de nada.
  • Consejo: haz que el fallo de CI sea visible donde el equipo ya mira. Una tubería roja que nadie ve es inútil.
  • Consejo: mide el tiempo de tu tubería y trátalo como un indicador de producto. Cada minuto ahorrado se multiplica por todas las ejecuciones del año.

Ejercicios

Ejercicio 1 — Diagnóstico de una prueba intermitente

Una prueba de integración falla en CI aproximadamente una de cada cinco ejecuciones, siempre con ECONNREFUSED contra PostgreSQL, y nunca falla en local. Enumera las tres causas más probables y la corrección de cada una.

Ejercicio 2 — Prueba de humo del flujo de compra

Amplía scripts/humo.js con una comprobación que verifique el camino de compra en preproducción: autenticarse con un usuario de prueba, consultar el aforo de evt-003 y comprobar que el endpoint de compra responde con un 400 controlado ante un cuerpo vacío, en lugar de un 500.

Soluciones

Ejercicio 1. Primera y más probable: falta la comprobación de salud del servicio, o el trabajo empieza antes de que PostgreSQL acepte conexiones; explica que nunca falle en local, donde la base de datos lleva horas arrancada, y se corrige con --health-cmd "pg_isready ..." y reintentos suficientes. Segunda: las conexiones no se cierran entre pruebas y se agota el max_connections del contenedor; se corrige cerrando el pool en el after global de mocha y comprobando que test/ayudas/ no crea una conexión por fichero. Tercera: la máquina de CI es más lenta y algún tiempo de espera fijo del código de conexión se agota; se corrige aumentando el tiempo de adquisición del pool en el entorno de prueba y esperando a condiciones, nunca a relojes.

Ejercicio 2.

{
  nombre: 'flujo de compra accesible',
  async ejecutar() {
    const json = { 'content-type': 'application/json' };
    // 1. Usuario sembrado SOLO en preproduccion; contrasena desde secrets.
    const cuerpo = { correo: '[email protected]', contrasena: process.env.CONTRASENA_HUMO };
    const acceso = await fetch(`${base}/api/sesiones`, {
      method: 'POST', headers: json, body: JSON.stringify(cuerpo),
    });
    if (acceso.status !== 200) throw new Error(`login: estado ${acceso.status}`);
    const auth = { authorization: `Bearer ${(await acceso.json()).token}` };

    // 2. Aforo del Festival de Jazz de Primavera.
    const aforo = await fetch(`${base}/api/eventos/evt-003/aforo`, { headers: auth });
    if (aforo.status !== 200) throw new Error(`aforo: estado ${aforo.status}`);

    // 3. Validacion: cuerpo vacio debe dar 400, NUNCA 500.
    const compra = await fetch(`${base}/api/compras`, {
      method: 'POST', headers: { ...auth, ...json }, body: '{}',
    });
    if (compra.status !== 400) throw new Error(`esperaba 400, llego ${compra.status}`);
  },
}

El detalle importante es el último: comprobar que un cuerpo inválido produce 400 y no 500 verifica de un golpe que la validación zod, el manejador de errores del módulo 6 y el mapa ESTADO_POR_CODIGO siguen conectados. Un 500 ahí significaría que algo se ha desconectado en el montaje de la aplicación, y es exactamente el tipo de rotura que una prueba de humo debe atrapar antes que un usuario.

Conclusión

Escena Viva empezó este módulo siendo un proyecto que funcionaba en un portátil, con los secretos en un .env local, sin registros centralizados, sin supervisor, sin contenedor y sin despliegue automático. Termina siendo otra cosa. Su configuración es un contrato explícito que se valida al arrancar y mata el proceso con un mensaje claro si falta un secreto, con rotación de claves sin desconectar a nadie. Cuenta lo que hace: registros JSON estructurados donde cada línea lleva el identificador de petición que permite reconstruir una compra fallida entera, métricas de latencia, entradas vendidas, tamaño de cola y retraso del bucle de eventos, y sondas de vivacidad y disponibilidad que un supervisor puede interrogar. Está supervisada por un fichero de ecosistema que declara sus dos procesos y los recarga sin cortar una sola venta. Viaja empaquetada en una imagen de 97 MB que corre como usuario sin privilegios y recibe SIGTERM de verdad, junto a un docker compose que levanta el entorno completo en un comando. Está desplegada en una plataforma que le asigna el puerto, le inyecta la configuración, termina TLS en el borde y la escala horizontalmente porque cada pieza de estado se sacó del proceso a su debido tiempo. Y ahora está automatizada: cada push pasa por el linter, las pruebas unitarias, las de integración contra tres bases de datos reales, un umbral de cobertura y una auditoría de dependencias; la imagen se construye una sola vez y se promueve entre entornos; las migraciones se ejecutan en su paso, compatibles hacia atrás; y unas pruebas de humo contra /salud/listo deciden si la versión se queda o se revierte sola.

Escena Viva es un producto en producción, no un proyecto en un portátil. Y la diferencia no está en el código —el de las compras es el mismo del módulo 7—, sino en todo lo que lo rodea: configuración, observabilidad, supervisión, empaquetado, despliegue y automatización. Esa es la distancia que separa saber programar en Node de saber entregar software en Node, y acabas de recorrerla entera.

Queda una última parte del viaje. En el Módulo 12, Proyectos del Mundo Real, dejamos de construir una sola aplicación para construir cuatro completas, cada una con su propio reto: una aplicación de chat en tiempo real con Socket.IO, una API de comercio electrónico, una plataforma de blogs y una herramienta de gestión de tareas. Cada proyecto aplicará lo aprendido en los once módulos anteriores —módulos, streams, Express, bases de datos, autenticación, pruebas, rendimiento y todo lo que acabas de ver sobre despliegue— y el módulo cerrará el curso con el paso que hoy ya sabes dar: de proyecto a producción.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados