StackPractices
beginner Por Mathias Paulenko

Runbook de Actualización de Dependencias

Un runbook de cinco fases para actualizar las dependencias del proyecto de forma segura, cubriendo preparación, actualización, validación, reversión y monitoreo post-deploy.

Temas: devops

Visión General

Las dependencias desactualizadas exponen los proyectos a vulnerabilidades de seguridad, bugs de compatibilidad y funciones faltantes. Este runbook te da un proceso repetible de cinco fases para actualizarlas con riesgo mínimo. El mismo checklist sirve para un parche CVE puntual o una ventana de mantenimiento trimestral.

Cuándo Usar

  • Se publica un parche de seguridad crítico para una dependencia directa o transitiva y lo necesitas en producción hoy, no el próximo sprint.
  • Se necesita una actualización de versión mayor para soporte a largo plazo, funciones nuevas o compatibilidad con el resto de tu stack.
  • Ejecutas refrescos de dependencias programados por sprint o trimestre y quieres el mismo procedimiento cada vez.
  • Un ingeniero junior corre su primera actualización y necesita el checklist, no el conocimiento tribal.

El proceso del runbook de un vistazo

Diagrama de flujo de las cinco fases de actualización de dependencias: preparación, actualización, validación, reversión y merge con monitoreo.

El Runbook

# Runbook de Actualización de Dependencias

## 1. Preparación

- [ ] Identificar la dependencia y la versión objetivo
- [ ] Revisar el changelog / notas de la versión en busca de cambios incompatibles
- [ ] Comprobar issues abiertos en el repositorio de la dependencia relacionados con la actualización
- [ ] Crear una rama dedicada: `deps/upgrade-<nombre>-<version>`
- [ ] Asegurar que el CI esté verde en la rama principal actual

## 2. Actualización

- [ ] Actualizar la versión en `package.json`, `requirements.txt`, `pom.xml`, etc.
- [ ] Ejecutar el comando de instalación de dependencias
- [ ] Revisar advertencias de dependencias peer o conflictos
- [ ] Ejecutar pruebas automatizadas (unitarias, integración, lint)
- [ ] Ejecutar pruebas de humo contra un entorno local o staging

## 3. Validación

- [ ] Revisar informes de cobertura de pruebas en busca de regresiones
- [ ] Comprobar logs de la aplicación en busca de nuevas advertencias o errores
- [ ] Verificar rutas críticas del usuario manualmente si cambió el comportamiento
- [ ] Ejecutar escaneo de seguridad (`npm audit`, `safety check`, OWASP dependency check)

## 4. Plan de Reversión

- [ ] Etiquetar el último commit conocido como bueno antes del merge
- [ ] Documentar cualquier cambio manual de datos o configuración requerido
- [ ] Confirmar que la reversión puede ejecutarse en 15 minutos

## 5. Merge y Monitoreo

- [ ] Abrir un pull request con resumen del changelog
- [ ] Desplegar en staging y dejarlo madurar por 24 horas
- [ ] Desplegar en producción durante una ventana de bajo tráfico
- [ ] Monitorear tasas de error y latencia durante 48 horas después del despliegue

Cómo Funcionan las Cinco Fases

El runbook divide las actualizaciones en fases donde cada una atrapa un modo de fallo distinto. La preparación mata sorpresas: la mayoría de incidentes de actualización vienen de un changelog que nadie leyó. La actualización se queda aislada en una rama para que el diff sea revisable. La validación mezcla checks automáticos con una pasada manual sobre lo que realmente cambió. El plan de reversión existe antes de necesitarlo. “Revertir rápido” es una decisión que se toma en calma, no durante un incidente. El merge con monitoreo cierra el ciclo: el staging más 48 horas de vigilancia de errores atrapa las regresiones que los tests no ven.

Cada fase también tiene un costo si te la saltas. Salta la preparación y descubres los cambios incompatibles en producción. Salta la rama aislada y la actualización se enreda con lo que sea que estaba en vuelo esa semana. Salta la validación y “los tests pasan” cubre en silencio la única ruta que nadie ejercitó. Salta el plan de reversión y la reversión se vuelve un proyecto de arqueología. Salta el monitoreo y te enteras de la regresión por un reporte de usuario en vez de un dashboard.

Elegir la Versión Objetivo

“La última” no siempre es el objetivo correcto. Elige la versión según la intención:

  • Fix de seguridad. Toma la versión más pequeña que parchee el CVE, aunque sea una rama de backport como 2.4.x en vez de 3.x. Los saltos grandes bajo presión de incidente multiplican el riesgo.
  • Mantenimiento rutinario. La última estable está bien, pero prefiere una versión con algunas semanas de uso real sobre una publicada ayer. Deja que otro encuentre los bugs del .0.
  • Migración mayor. Apunta a la línea LTS más reciente, no al borde sangrante. En frameworks, revisa primero la ventana de soporte oficial: una actualización que te deja en una versión que muere en tres meses te compra una segunda emergencia.

Dos comprobaciones antes de fijar el número. Primero, los requisitos de runtime: muchas majors suben en silencio el mínimo de Node, Python o JVM, y la actualización se convierte en dos. Segundo, el rezago del ecosistema: las definiciones de tipos, plugins y wrappers (@types/*, plugins de webpack, integraciones de ORM) suelen ir semanas detrás de una major. Si los wrappers que usas no se han puesto al día, esperar es más barato que parchear alrededor.

Comandos de Actualización por Ecosistema

Las cinco fases son las mismas en todas partes; solo cambian los comandos. Un mapa rápido de las cadenas de herramientas habituales:

EcosistemaQué está desactualizadoActualizar un paqueteLock fileAuditoría de vulnerabilidades
npmnpm outdatednpm install pkg@x.y.zpackage-lock.jsonnpm audit
yarn / pnpmyarn outdated / pnpm outdatedyarn up pkg / pnpm update pkgyarn.lock / pnpm-lock.yamlyarn npm audit / pnpm audit
pip + pip-toolspip list --outdatedpip-compile --upgrade-package pkgrequirements.txtpip-audit
poetrypoetry show --outdatedpoetry update pkgpoetry.lockpip-audit sobre reqs exportados
Mavenmvn versions:display-dependency-updatesmvn versions:use-latest-versions(versiones en pom.xml)dependency-check (OWASP)
Go modulesgo list -u -m allgo get -u pkg@vX.Y.Zgo.sumgovulncheck ./...
Cargocargo outdatedcargo update -p pkgCargo.lockcargo audit
Bundlerbundle outdatedbundle update gemGemfile.lockbundler-audit

Sea cual sea el ecosistema, el flujo de la fase 2 es idéntico: sube el manifest, regenera el lock file, corre los tests y lee el diff del lock antes de commitear. En ese diff es donde aparecen las actualizaciones transitivas sorpresa.

Variantes

ContextoEnfoqueNotas
Parche de seguridadRama de vía rápidaOmitir tiempo de maduración solo para CVEs con exploits activos
Versión mayorDespliegue con feature flagsAislar el comportamiento nuevo bajo banderas durante la transición
MonorepoActualizaciones en loteActualizar las librerías compartidas primero, luego los consumidores
CVE solo transitivoForzar versión con overrides / resolutionsDocumentar por qué existe el pin y cuándo quitarlo

Mejores Prácticas

  1. Actualiza una dependencia mayor a la vez. Dos majors en un PR hacen imposible atribuir los fallos.
  2. Fija versiones exactas e incluye los lock files en el commit. package-lock.json y poetry.lock hacen los builds reproducibles y los diffs revisables.
  3. Usa npm ci en CI, no npm install. Instala exactamente desde el lock file y falla cuando el archivo se desincroniza de package.json:
# GitHub Actions
- name: Install dependencies
  run: npm ci
  1. Automatiza el 90% aburrido. Dependabot o Renovate pueden auto-mergear bumps de parche y menores; deja los majors para humanos:
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    groups:
      patch-and-minor:
        update-types: ["patch", "minor"]
  1. Lleva un calendario de deprecación. Registra las fechas de fin de soporte de frameworks y runtimes para que las actualizaciones sean planificadas, no forzadas por un CVE.

Errores Comunes

  1. Actualizar varias majors a la vez. Cuando fallan los tests, no puedes saber qué bump rompió las cosas.
  2. Ignorar advertencias de dependencias peer. Se convierten en errores de runtime en producción.
  3. Omitir el plan de reversión. Improvisar una reversión bajo presión extiende el downtime.
  4. No revisar los cambios transitivos. git diff package-lock.json suele mostrar actualizaciones sorpresa que merecen escrutinio.
  5. Actualizar devDependencies sin un build completo. Webpack, babel o eslint pueden romper el bundle de producción aunque los tests pasen:
npm run build && npm run test
  1. Desestimar advertencias de deprecación. Los warnings de una versión menor se vuelven errores en la siguiente major. Captúralos y rastréalos:
npm test 2>&1 | grep -i "deprecat" > deprecation-warnings.txt

Ejemplo Práctico: Actualizar React 18 a 19

Un paseo concreto por el runbook hace tangibles las fases. Supón que la actualización es React 18 a 19 en una app con una semana de margen.

Preparación. El changelog de React 19 señala APIs eliminadas (defaultProps en componentes de función, string refs) y comportamiento cambiado en el timing de limpieza de useEffect y en ref como prop. El issue tracker del proyecto marca un problema conocido con una librería de formularios popular en 19.0.0, corregido en 19.0.2, así que el objetivo es 19.0.2, no 19.0.0. La versión de Node de la app ya cumple el requisito. Rama: deps/upgrade-react-19.

Actualización. Sube react y react-dom juntas (se publican en lockstep), más @types/react y @types/react-dom. Corre el codemod oficial para ref como prop. npm install reporta una advertencia peer de una versión vieja de una librería de gráficos que topea React en 18, así que esa librería se actualiza en la misma rama, porque un conflicto peer es un fallo de la fase 2, no un proyecto aparte.

Validación. Las suites unitarias y de integración pasan. La pasada manual ejercita las tres rutas que el changelog marcó como arriesgadas: un formulario con inputs no controlados, un modal con createPortal y un componente que dependía de defaultProps. El codemod reescribió el último automáticamente.

Reversión. El tag pre-actualización existe; la reversión es git checkout <tag> -- package.json package-lock.json && npm ci. Probada una vez en una rama desechable: cuatro minutos.

Merge y monitoreo. El PR enlaza el resumen del changelog y lista el diff del codemod por separado para que los revisores hojeen los cambios generados. Staging madura un día, producción despliega el martes por la mañana, y la tasa de errores y el INP se mantienen planos durante la ventana de 48 horas.

Manejar Cambios Incompatibles

Los cambios incompatibles son donde las actualizaciones van a morir. La defensa es aburrida pero efectiva: lee la guía de migración antes de tocar el manifest, no después de que el build se rompa. La mayoría de los proyectos maduros publican una (MIGRATION.md, docs de actualización o notas con sección “Breaking”). Si el proyecto solo publica un changelog crudo, busca APIs renombradas, opciones eliminadas, defaults que cambiaron y deprecaciones que estabas usando.

Para las majors, el trabajo mecánico suele tener tooling:

  • Codemods: muchos ecosistemas publican transformaciones AST: npx @org/codemod, ng update de Angular, los codemods oficiales de React u OpenRewrite para Java. Córralos en una rama limpia y revisa el diff como cualquier otro cambio de código.
  • Capas adaptadoras: cuando llamas a la API vieja en cuarenta lugares, envuélvela. Escribe un módulo delgado con la superficie vieja que delega a la nueva, migra los call sites detrás de ella y luego bota el adaptador. Convierte un flag-day arriesgado en muchos diffs pequeños.
  • Ejecución dual: para dependencias de infraestructura (clientes HTTP, serializadores, ORM), corre vieja y nueva en paralelo detrás de una bandera cuando la librería lo permita, y conmuta por entorno.

Una advertencia sobre la cobertura: si tu suite es delgada, una actualización es el peor momento para descubrirlo. Antes del salto major, añade tests de contrato sobre las rutas que tocan la dependencia: fronteras de serialización, comportamiento de reintentos, formas de error. Esos tests son la diferencia entre “los tests pasan” y “funciona”.

Coordinar Actualizaciones entre Repos

Dentro de un repo el runbook alcanza. Entre repos, con una librería compartida más sus consumidores, el orden importa más que la mecánica:

  1. Actualiza primero el productor. Publica la nueva versión de la librería compartida en un registry (o un tag de prerelease) antes de tocar consumidores.
  2. Declara una ventana de compatibilidad. Si la librería puede soportar consumidores viejos y nuevos durante un par de versiones, dilo explícitamente. Desacopla los calendarios de los equipos y quita la presión de un flag-day coordinado.
  3. Migra un consumidor canario. Elige el servicio de menor riesgo, córrele el runbook completo de cinco fases y déjalo madurar en producción. Sus modos de fallo te enseñan lo que los otros veinte consumidores van a encontrar.
  4. Agrupa a los seguidores. Cuando el canario esté limpio, actualiza los consumidores restantes en una ventana de mantenimiento por equipo, no una por servicio.

Los tests de contrato entre productor y consumidores atrapan las roturas sutiles (una forma de error cambiada, un enum reordenado) que los tests unitarios de cualquiera de los dos repos no pueden ver.

Avanzado: Automatizar las Actualizaciones

Configuración de Renovate con auto-merge seguro

{
  "extends": ["config:base"],
  "schedule": ["before 6am on Monday"],
  "packageRules": [
    {
      "updateTypes": ["patch", "minor"],
      "automerge": true,
      "groupName": "patch and minor updates"
    },
    {
      "updateTypes": ["major"],
      "automerge": false,
      "labels": ["major-upgrade", "needs-review"],
      "dependencyDashboardApproval": true
    }
  ],
  "vulnerabilityAlerts": {
    "enabled": true,
    "labels": ["security"],
    "schedule": ["at any time"]
  }
}

Script de reversión para actualizaciones npm

#!/bin/bash
set -euo pipefail

BRANCH=$(git rev-parse --abbrev-ref HEAD)
TAG="pre-upgrade-$(date +%Y%m%d-%H%M%S)"

git tag "$TAG"
echo "Created safety tag: $TAG"

# Si la actualización falla, restaura los manifests desde el tag:
# git checkout "$TAG" -- package.json package-lock.json && npm ci

echo "To rollback: git checkout $TAG -- package.json package-lock.json && npm ci"

Actualizaciones Python pineadas con pip-tools

#!/bin/bash
set -euo pipefail

# requirements.in guarda restricciones sueltas; requirements.txt es el output bloqueado
echo "package-name==2.0.0" >> requirements.in

pip-compile --upgrade-package package-name --output-file requirements.txt requirements.in

pip install -r requirements.txt --dry-run   # detecta dependencias transitivas en conflicto
pytest tests/ -x

git add requirements.in requirements.txt
git commit -m "deps: upgrade package-name to 2.0.0"

Reporte de auditoría y SBOM para compliance

#!/bin/bash
set -euo pipefail

npx @cyclonedx/cyclonedx-npm --output-file sbom.json
npm audit --json > audit-report.json

node -e "
const audit = require('./audit-report.json');
const vulns = audit.vulnerabilities || {};
const high = Object.entries(vulns).filter(([,v]) => v.severity === 'high' || v.severity === 'critical');
if (high.length > 0) {
  high.forEach(([name, info]) => console.log(name + ': ' + info.severity));
  process.exit(1);
}
"

Cuándo NO Actualizar

Un runbook también es permiso para esperar. Razones legítimas para aplazar:

  • Un freeze de release está a días. Actualizar justo antes de un congelamiento de código mete el cambio más arriesgado en la ventana menos probada.
  • La dependencia está programada para eliminarse. Si migras fuera de la librería el próximo trimestre, parchearla hoy es trabajo que vas a tirar; documenta la exposición y monitorea en su lugar.
  • La actualización fuerza una mayor. Un paquete cuya nueva versión exige un runtime o framework más nuevo convierte una tarea pequeña en un proyecto. Agenda el proyecto; no lo contrabandees dentro del mantenimiento.
  • Nada está roto y nada está expuesto. Una dependencia fijada en una versión vieja pero parcheada, sin CVEs y sin funciones necesarias, puede esperar al próximo ciclo. Aplazar a propósito es una decisión; olvidar no.

Sea cual sea la razón, escríbela junto a la dependencia. Un “ya lo haremos” sin documentar es como las dependencias terminan tres majors atrás.

Métricas que Vale la Pena Seguir

No puedes mejorar un proceso que no mides. Cuatro números te dicen si las actualizaciones funcionan:

  • Cadencia de actualización: tiempo entre refrescos de dependencias. Si los parches se acumulan meses, las actualizaciones “rutinarias” se vuelven saltos arriesgados.
  • Tiempo medio hasta el parche (MTTP): cuánto tarda un CVE publicado en llegar a producción. Para CVEs críticos con exploits activos, mídelo en días, no en semanas.
  • Frescura: porcentaje de dependencias directas a una major de la última versión. Observa la tendencia, no el número absoluto.
  • Tasa de actualizaciones fallidas: con qué frecuencia un bump termina revertido. Una tasa creciente suele significar que las fases 1 y 3 se están saltando, no que actualizar sea peligroso en sí.

Regístralas en lo que ya uses para métricas de ingeniería; una hoja de cálculo funciona bien. El punto es detectar la deriva antes de que un CVE fuerce la conversación.

Lectura Adicional

Preguntas frecuentes

¿Con qué frecuencia debo actualizar las dependencias?

Versiones de parche semanalmente o de forma automática con Dependabot/Renovate. Menores, mensualmente. Mayores, trimestralmente o antes cuando bloqueen fixes de seguridad o soporte LTS. La peor cadencia es "cuando algo se rompe"; convierte cada actualización en una emergencia.

¿Qué pasa si una dependencia transitiva tiene un CVE?

No tienes que esperar a que la dependencia directa publique un fix. Usa npm audit fix, overrides en package.json, pip-audit para Python, o dependency management de Maven para forzar una versión transitiva parcheada.

¿Debo incluir los lock files en el control de versiones?

Siempre. Los lock files te dan builds reproducibles entre máquinas y CI, y convierten las actualizaciones de dependencias en diffs revisables. npm ci, pip install -r requirements.txt y comandos similares asumen que el lock file está en el repositorio.

¿Cómo manejo una dependencia sin mantenimiento?

Evalúa alternativas mantenidas primero: una migración suele ser más barata que cargar una dependencia muerta. Si nada encaja, haz un fork (si la licencia lo permite) o inclúyela directamente en tu repositorio (vendoring), y agrégala al calendario de deprecación con fecha límite de reemplazo. Escanea la última versión publicada para conocer al menos tu exposición.

¿Cuál es la diferencia entre ~ y ^ en semver?

^1.2.3 permite cualquier versión que no cambie el dígito no-cero más a la izquierda (o sea 1.x.x), mientras ~1.2.3 permite solo cambios de parche (1.2.x). Caret vale para la mayoría de dependencias; usa tilde o pines exactos para paquetes de cuya estabilidad dependes.

¿Cuándo necesita un monorepo tooling especial?

Nx, Turborepo o Lerna justifican su complejidad cuando los paquetes comparten dependencias y las actualizaciones deben aterrizar juntas. Añaden hoisting de workspace, caching y comandos de actualización en lote. Para un puñado de paquetes, un solo package.json y el tooling normal es más simple e igual de seguro.

¿Cómo pruebo una actualización cuando la cobertura de tests es escasa?

Apóyate en el entorno, no en la suite. Despliega primero en staging y corre una pasada de humo con script sobre las rutas críticas del usuario: login, las tres llamadas API principales, el batch job que importa. Si puedes, haz un despliegue canario a una porción pequeña del tráfico de producción y observa las tasas de error unas horas antes del rollout completo. Y captura la línea base antes de la actualización: no puedes detectar una regresión de latencia del 2% sin saber el número de la semana pasada.

¿Debo actualizar a una versión mayor nueva de inmediato?

Normalmente no. A menos que parchee un CVE al que estás expuesto, dale a una major recién salida unas semanas: la .0 colecciona los reportes de bugs del mundo real, y la .1 o .2 que sigue es donde la mayoría de los equipos debería aterrizar. Usa el tiempo de espera para vigilar el issue tracker por reportes específicos de la actualización y para que @types, plugins y wrappers se pongan al día.