El día en que el pipeline tenía la razón y yo no
Read in English →Hoy por la mañana abrí Mattermost y me encontré una alerta roja del pipeline que despliega este sitio. Renovate había hecho merge de una actualización de Astro a main, corrió el build y se murió. Un martes normal. Le di clic al link de la alerta para ver el log y no abrió nada. El link apuntaba a http://gitea-http.gitea.svc.cluster.local:3000. Esa es la dirección del servicio de Gitea adentro de mi clúster de Kubernetes. Desde un navegador no sirve para nada.
Así que antes del café ya tenía dos problemas. Un deploy caído y una alerta que no podía abrir.
La falla que no era falla
Entré a Gitea a mano y abrí el run #1434. El paso del build murió en pnpm install con esto:
Error: ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION
× installing dependencies
╰─▶ 4 lockfile entries failed verification:
astro@7.3.6 was published at 2026-10-06T12:41:24.000Z, within the
minimumReleaseAge cutoff (2026-10-05T16:25:38.040Z)
Mi primera sospecha fue un lockfile desactualizado. Renovate sube la versión en package.json, se le olvida el lockfile y pnpm se queja. Esa ya la había visto. Pero el log decía “Lockfile is up to date”. El lockfile estaba bien.
Mi segunda sospecha fue que pnpm 12 había roto algo. Habíamos pasado a pnpm 12.9.1 apenas dos días antes, por otro PR de Renovate. Major nuevo, bugs nuevos, ¿no?
Tampoco. pnpm 12 hizo exactamente lo que fue diseñado para hacer. Trae una política de supply chain que se llama minimumReleaseAge, con 24 horas por defecto. Si cualquier paquete de tu lockfile fue publicado hace menos de 24 horas, pnpm se niega a instalarlo. La idea es sencilla. Si alguien se roba la cuenta de un mantenedor y publica una versión envenenada, casi siempre la bajan en cuestión de horas. Si tú nunca instalas nada con menos de un día de vida, te saltas casi toda esa ventana.
Astro 7.3.6 se publicó a las 12:41 UTC. Renovate abrió el PR, la regla de automerge para minors hizo lo suyo, y cuatro horas después ya estaba en main. pnpm miró la fecha y dijo que no.
Volver a correr el job no ayuda
Hice lo que hace todo el mundo. Le di “Re-run failed jobs”. Falló otra vez. El mismo error, con la hora del corte un poquito distinta. Ahí caí en cuenta. El corte no es fijo. Siempre es “ahora menos 24 horas”. Cada re-run mueve la ventana hacia adelante exactamente el tiempo que pasó desde el intento anterior. El paquete nunca iba a alcanzar la ventana a punta de re-runs. Me tocaba esperar hasta las 12:42 UTC del día siguiente, o cambiar algo.
También noté que cuando uno vuelve a correr un run viejo en Gitea, usa el archivo del workflow tal como estaba en ese commit. Ya había subido el arreglo del link de la alerta y el re-run me siguió mandando el link interno. Esa me costó diez minutos de confusión.
Dos arreglos, en dos lugares
El problema de fondo era un desajuste. Renovate estaba dispuesto a hacer merge de un paquete con cuatro horas de vida. pnpm no estaba dispuesto a instalar nada con menos de un día. Uno de los dos tenía que ceder.
Moví a Renovate. En la configuración global de Renovate que corre en mi clúster agregué:
"minimumReleaseAge": "3 days",
"internalChecksFilter": "strict"
Tres días pasan la regla de pnpm con margen, y el filtro estricto hace que Renovate escoja la versión más nueva que ya cumpla la edad mínima, en vez de saltarse la actualización completa. Ahora todos mis repos tienen esto, no solo el homepage.
Eso arregla el futuro. No arreglaba el hoy. No quería esperar hasta mañana para que el sitio desplegara, así que agregué una excepción puntual en pnpm-workspace.yaml:
minimumReleaseAgeExclude:
- astro
- "@astrojs/*"
No me encanta. Excluir justo los paquetes que dispararon la regla es de esas cosas que se sienten ingeniosas y envejecen mal. Pero con Renovate esperando tres días aguas arriba, la regla de pnpm pasa a ser la segunda línea de defensa, no la primera. Lo subí, el run #43 quedó en verde y Astro 7.3.6 está en producción.
Los links de las alertas
Volvamos al link que no abría. Todos los workflows de mis repos arman la URL del run con ${{ github.server_url }}. En un runner de GitHub eso te da github.com. En mis pods de act_runner, que se registran contra el servicio interno de Gitea para que los clones nunca salgan del clúster, te da el nombre DNS del servicio.
Pensé en arreglarlo de raíz y registrar los runners contra https://git.xjohnyx.me. Después revisé a qué resuelve ese hostname. Cloudflare. Cada clone y cada push de imagen desde los runners saldría de mi casa, iría hasta Cloudflare, volvería por el túnel y llegaría al mismo pod de Gitea que está a un salto de distancia. Ojo, Cloudflare además limita el cuerpo de las peticiones a 100 MB, y eso rompería el push de capas de imagen a Nexus. Pésimo negocio por un link más bonito.
Así que puse la URL pública a mano en el texto de la alerta y dejé los runners quietos. Cinco repos lo necesitaban: homepage, los dos repos de Terraform de los sitios en S3, el sitio de tuopenmind y johnylab-infra. Otros diez repos ya tenían la URL pública escrita a mano. El Johny del pasado ya se había topado con esto y no lo arregló en todas partes.
En johnylab-infra encontré un segundo bug ya que estaba ahí. Dos alertas armaban el link con run_number en vez de run_id. Gitea muestra “#43” en la interfaz, pero la URL usa el id global, algo como 1436. Esos links llevaban meses devolviendo 404 y nadie se dio cuenta, porque solo se disparan cuando falla un apply de Talos o un snapshot de etcd, y esos no habían fallado.
Gitea 28, antes del almuerzo
Mientras estaba en el panel de administración vi el aviso. Gitea 28.0.0 está disponible, estás corriendo 1.27.3. Ese número me asustó un segundo. Es simplemente 1.28 sin el prefijo “1.”. Una versión minor normal con nombre nuevo.
Igual leí las notas de la versión. Git 2.25 mínimo, mi imagen trae 2.54. Registro de usuarios deshabilitado por defecto, aquí ya estaba deshabilitado. El ajuste DOMAIN ahora se ignora a favor de ROOT_URL, los dos apuntan al mismo host. Los runs de Actions terminados se borran a los 400 días por defecto, así que fijé la retención explícitamente. Nada en mi configuración estorbaba.
El chart de Helm todavía no había alcanzado la versión. El último tag del chart sigue trayendo 1.27, y solo la rama main tiene 28.0.0 como versión de la aplicación. Mi HelmRelease ya sobreescribe el tag de la imagen, así que cambié una sola cadena.
Primero saqué un backup manual con CNPG y esperé a que terminara. Las migraciones no corren hacia atrás, y restaurar es el único rollback. Después hice push. El pod se recreó en más o menos un minuto, cero errores de migración, y los dos runners se reconectaron solos.
Y entonces encontré lo que llevaba un año queriendo. Admin Settings, Actions, Job queue. Una sola página que lista todos los jobs corriendo y en espera en todos mis repos, con filtros. En 1.27 esa página no existía. Yo abría los repos uno por uno para ver qué estaban haciendo los runners. Cuando la abrí por primera vez ya mostraba un job en vivo: el escaneo de seguridad de johnylab-flux, disparado por el mismo commit de la actualización.
Lo que me llevo de hoy
Una protección de supply chain haciendo su trabajo se ve exactamente igual que un pipeline roto. La misma X roja, el mismo deploy muerto, las mismas ganas de darle re-run. La diferencia es que volver a correr una falla real a veces funciona, y volver a correr esta no va a funcionar nunca. Si pasas a pnpm 12 y tienes Renovate haciendo automerge de minors, ponle a Renovate un minimumReleaseAge más largo que la regla de pnpm el mismo día. Si no, la primera versión recién publicada te va a enseñar esta lección a una hora peor que un martes por la mañana.