Saltar al contenido

Mantenimiento de dependencias en piloto automático con Renovate

Los scanners de la parte 12 te dicen qué dependencias son vulnerables, pero los upgrades siguen siendo manuales y nunca alcanzan. Renovate self-hosted como pipeline programado, los dos tokens que lo mantienen vivo y el chequeo previo que convirtió una sesión recurrente de debugging en una línea de log.

6 min de lectura
  • gitlab
  • ci-cd
  • devops

La parte 12 terminó con scanners que te dicen, en cada merge request, qué dependencias tienen vulnerabilidades conocidas. Esa es la mitad del problema: la detección. Los upgrades siguen siendo trabajo manual, y lo manual no escala. Con diez repos, los bumps de dependencias son esa tarea que todos reconocen como importante y que nadie agarra. Los lockfiles envejecen en silencio hasta que un hallazgo del scanner te obliga a saltar dos versiones mayores con la presión encima.

Esta última parte le entrega la tarea a un bot: Renovate self-hosted corriendo como pipeline programado en su propio proyecto, abriendo merge requests contra todo el grupo mientras el equipo duerme.

Un proyecto cuyo único trabajo es abrir MRs

Renovate se distribuye como imagen de contenedor, así que la pregunta de dónde alojarlo tiene una respuesta cómoda: corre como un job de CI, igual que todo lo demás en esta serie. La imagen procesa los repositorios que le indiques y termina; programar las corridas te toca a ti, que es exactamente para lo que existen los pipeline schedules. Dale un proyecto dedicado, group/renovate-runner, con un pipeline de un solo job:

renovate:
  image: renovate/renovate:41 # fija un major (lección :latest de la parte 12)
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  variables:
    RENOVATE_PLATFORM: gitlab
    RENOVATE_ENDPOINT: $CI_API_V4_URL
    RENOVATE_AUTODISCOVER: 'true'
    RENOVATE_AUTODISCOVER_FILTER: group/*
  script:
    - renovate

Un pipeline schedule (el mismo mecanismo que la parte 11 usó para los E2E nocturnos) lo dispara una vez por noche. Con autodiscover, Renovate recorre todos los repositorios que su token alcanza, acotados por el filtro; si eso te parece demasiado amplio, puedes listar los repos de forma explícita en el archivo de configuración. Lo que pasa dentro de cada repo cuando Renovate lo visita (qué updates se agrupan en un solo MR, cuándo se abren, cuáles se mergean solos) vive en el renovate.json de ese repo. El nuestro es deliberadamente aburrido: patches y minors agrupados, majors por separado, y un tope de MRs abiertos para que nadie amanezca con treinta pendientes. Ese archivo pertenece más a la documentación de Renovate que a esta serie, así que ahí lo dejo.

El equipo de Renovate mantiene un proyecto renovate-runner con templates de CI listos para exactamente este montaje; el nuestro empezó como una copia de ese.

Dos tokens, ambos enmascarados

Renovate necesita credenciales para dos hosts distintos, y conviene tener claro qué hace cada una.

La primera es RENOVATE_TOKEN: un access token de GitLab con scope api y al menos rol Developer en cada repo que administra, el mínimo para abrir merge requests. Créalo bajo una cuenta bot. La parte 10 ya planteó ese argumento para los commits de release y aplica completo aquí (la automatización atada al token de una persona muere con su offboarding), con una razón adicional: los pipelines programados corren con los permisos del dueño del schedule, así que el schedule del proyecto runner también debería pertenecer al bot.

La segunda es RENOVATE_GITHUB_COM_TOKEN, la que muchos se saltan porque nada se rompe de forma visible sin ella. La mayoría de paquetes open source publica sus releases en github.com, y Renovate saca de ahí los changelogs que incrusta en la descripción de cada MR. Sin token, esas consultas mueren por rate limit y tus MRs llegan como bumps de versión pelados; con token, el reviewer lee las release notes dentro del MR en lugar de abrir una pestaña del navegador por cada dependencia. Solo necesita acceso público de lectura.

Ambas viven como variables CI/CD enmascaradas en el proyecto del runner, para que no puedan filtrarse a los logs, y ambas tienen fecha de expiración. Guarda ese detalle, porque enseguida importa.

Fallar antes de que falle Renovate

Una corrida de Renovate sobre un grupo entero toma su tiempo. Cuando un token está malo, el error aparece como stack trace muchos minutos después, enterrado en una pared de logs, y no dice «tu token expiró», dice lo que el cliente HTTP haya tenido ganas de decir. La primera vez que nos pasó, averiguar cuál de los dos tokens había muerto y dónde se arreglaba nos costó buena parte de una mañana.

Por eso el job ahora valida ambos tokens antes de arrancar Renovate. Un script pequeño hace curl a un endpoint autenticado barato por cada host y falla con un mensaje escrito para la persona que va a leer el log:

#!/usr/bin/env bash
set -euo pipefail
 
code=$(curl -s -o /dev/null -w '%{http_code}' \
  --header "PRIVATE-TOKEN: ${RENOVATE_TOKEN}" \
  "${CI_API_V4_URL}/user")
if [ "$code" != "200" ]; then
  echo "RENOVATE_TOKEN is invalid or expired (GitLab replied ${code})."
  echo "Rotate it: group/renovate-runner > Settings > CI/CD > Variables."
  exit 1
fi
 
code=$(curl -s -o /dev/null -w '%{http_code}' \
  --header "Authorization: Bearer ${RENOVATE_GITHUB_COM_TOKEN}" \
  "https://api.github.com/rate_limit")
if [ "$code" != "200" ]; then
  echo "RENOVATE_GITHUB_COM_TOKEN is invalid (github.com replied ${code})."
  echo "Changelogs will stop rendering in MRs until it is rotated."
  exit 1
fi

Una corrida fallida ahora se lee en una línea: qué token, qué pasó, dónde se rota. El script vive en el repo del runner y corre en el before_script del job; Renovate solo arranca si los dos chequeos pasan.

Las semanas en que el bot se quedó callado

El chequeo previo existe por lo que pasó sin él. Nuestro token de GitLab llegó a su fecha de expiración, Renovate empezó a fallar todas las noches, y durante varias semanas no pasó absolutamente nada visible. Dejaron de aparecer MRs de dependencias, pero la ausencia de MRs se ve idéntica a una temporada tranquila en el ecosistema. El pipeline programado fallaba puntualmente cada noche, y sus notificaciones le llegaban al dueño del schedule: la cuenta bot, cuyo buzón nadie leía. Nos dimos cuenta cuando alguien preguntó por qué un paquete que habíamos parchado semanas atrás seguía viejo en un repo.

Lo arreglaron dos cambios, y los dos hicieron falta. El chequeo previo volvió el diagnóstico una línea de log en lugar de una sesión de debugging. Y las fallas del schedule ahora llegan a un lugar donde la gente sí mira: reenviar el correo de notificaciones de la cuenta bot al alias del equipo funciona, igual que un job final pequeño con when: on_failure que publica en el chat. Los dos cambios entraron la misma semana; el token ha expirado dos veces desde entonces, y ambas veces el arreglo tomó los cinco minutos que siempre debió tomar.

Upgrades de un clic para todo lo demás

A Renovate, deliberadamente, no le toca todo. Un major de framework que requiere leer la guía de migración, un bump del design system que necesita revisión visual: esos upgrades quieren a un humano decidiendo el momento. Aun así, no deberían costar un checkout manual, el bump, el push y el formulario del MR.

Para eso, la librería compartida de CI de la parte 8 trae un job manual que hace la parte mecánica:

.bump-dependency:
  rules:
    - when: manual
      allow_failure: true
  script:
    - git checkout -b "bump-${DEPENDENCY}"
    - pnpm update --latest "${DEPENDENCY}"
    - git commit -am "chore: bump ${DEPENDENCY}"
    - git push "https://bot:${BOT_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
      "HEAD:bump-${DEPENDENCY}"
    - >
      curl --fail --request POST
      --header "PRIVATE-TOKEN: ${BOT_TOKEN}"
      --data-urlencode "source_branch=bump-${DEPENDENCY}"
      --data-urlencode "target_branch=${CI_DEFAULT_BRANCH}"
      --data-urlencode "title=chore: bump ${DEPENDENCY}"
      "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests"

El allow_failure: true importa: un job manual que entra por rules bloquea el pipeline por defecto, y este no debería frenar nada. La versión real además configura la identidad git del bot (el user.name/user.email de la parte 10) y contempla que la rama ya exista; lo que ves aquí es el núcleo legible. Para usarlo, cualquier repo consumidor extiende el hidden job, alguien abre el pipeline, presiona play y escribe DEPENDENCY=react en el formulario de variables que GitLab muestra para jobs manuales. El push dispara un pipeline en la rama nueva, así que el MR llega con CI ya corriendo, firmado por el mismo bot que firma los releases y los MRs de Renovate.

Con qué te quedas

Esta serie empezó hace trece partes con un .gitlab-ci.yml vacío y un solo job corriendo lint. Lo que corre hoy construye únicamente lo que cambió, publica releases exactamente una vez por merge, reporta E2E cada mañana, escanea el diff en lugar de alarmar por todo, y abre sus propios MRs de upgrade. Cada pieza entró porque un problema concreto la pidió, en un orden donde cada peldaño solo asume los de abajo. Si llegaste primero a este post, la parte 1 es donde empieza la escalera.

Renovate abrió dos MRs mientras escribía esto. Mergeé uno. Buen bot.

Referencias