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
- Integración, entrega y despliegue continuos: tres cosas distintas
- Anatomía de la tubería de Escena Viva
- GitHub Actions: el fichero de CI, bloque a bloque
- Secretos en CI
- Qué hace que una tubería sea útil
- Construir una vez y promover el artefacto
- Migraciones, pruebas de humo y reversión automática
- Versionado y notas de la versión
- Qué no debe estar en la tubería
- DORA: saber si estás mejorando
- 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.
- 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.
- 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=devon, 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:
localhost, no el nombre del servicio. A diferencia dedocker 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.- 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. - 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.
- 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
rundirectamente.docker login -p ${{ secrets.TOKEN }}deja el valor en la línea de comandos, visible en la tabla de procesos. Pásalo porenvy 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.
- 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.
- 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 produccionClaves 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.
- 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.
- 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.
- 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.
- 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
localhosten Actions. En compose sí, en Actions no. npm installen CI. Rompe la reproducibilidad quepackage-lock.jsongarantiza.- 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
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
