Saltar al contenido principal
Escritos
11 min de lectura

Contingencia de CI/CD: por qué el runner self-hosted no salvó a nadie en la caída de GitHub Actions

Cuando GitHub Actions se cayó nueve horas, quien tenía runner self-hosted se detuvo también, y la cola de eventos perdidos no volvió con el servicio. La contingencia de CI suele cubrir la máquina que corre el build y olvidar la capa que sabe qué build hay que correr.

  • GitHub Actions
  • CI/CD
  • Self-hosted Runner
  • DevOps
  • Contingencia
  • Infraestructura
  • Confiabilidad

Jueves 6 de agosto de 2026, 15:22 UTC. GitHub marca Actions como "degraded performance". Veinte minutos después, también cae la disponibilidad. De ahí a la mitigación pasaron casi nueve horas, y el incidente recién se cerró a las 2:04 de la madrugada del día siguiente.

Aquí nuestro pipeline se detuvo junto con el resto del mundo durante algunas horas. Y no quiero convertir esto en un post de queja, porque caerse es parte del juego y Actions sigue siendo una pieza de infraestructura absurdamente buena para lo que cuesta. Lo que me llamó la atención fue otra cosa: pasé el día viendo gente decir que estaba tranquila porque tenía un runner self-hosted.

No lo estaba.

El runner propio te da la máquina, no la decisión

El texto oficial del incidente es bien específico en este punto: "Both GitHub-hosted and self-hosted runners are affected". Y, más adelante, que quien usa runner propio podría ver errores o rate limiting en el momento en que el runner se registra.

Eso dice mucho sobre dónde vive realmente el servicio. Cuando levantamos un runner self-hosted, lo que entra a casa es el cómputo: el procesador que corre pnpm test, el disco que guarda la caché, la red que baja la imagen. Solo que correr el build es la parte final de una cadena mucho más larga. Antes de eso, alguien tiene que recibir el evento de push, decidir que eso dispara un workflow, leer el YAML, armar la matriz de jobs, elegir a qué runner va cada job, autenticar a ese runner, entregar el token del checkout y después recibir el resultado de vuelta para pintar el check verde en el PR.

Nada de eso ocurre en tu máquina. Todo eso es el servicio.

Es decir: el runner self-hosted no te saca de la dependencia, le cambia la naturaleza. Dejaste de depender de la capacidad de GitHub y pasaste a depender del despacho de GitHub. Y fue exactamente el despacho lo que se rompió, lo que explica por qué los dos tipos de runner cayeron en el mismo minuto.

Hubo un detalle todavía más incómodo para quien tenía runner propio. En las actualizaciones del incidente, GitHub avisó que algunos pods del Actions Runner Controller quedaron atascados en estado idle y que la recuperación necesitaba mano humana: borrar los pods con kubectl o redesplegar la aplicación del ARC. Quien invirtió en runner propio para tener más control recibió, en el peor momento del día, una tarea manual más. El propio GitHub lo reconoció y dijo que las próximas versiones del Runner y del ARC traerán recuperación automática.

Lo que de verdad dolió vino después

Ahora el punto que casi nadie comentó, y que es el más caro de toda la historia.

En la actualización de cierre, GitHub avisó que algunos eventos que disparan workflows, incluidos push y pull request, no fueron procesados durante el incidente y no pueden reproducirse automáticamente. La recomendación fue esta: empuja un commit nuevo, actualiza el PR, o ve y manda a correr el workflow a mano.

Vale la pena leerlo de nuevo con calma, porque la implicación es grande. No es que los builds fallaron. Fallar es genial: una falla es rojo en la pantalla, es una notificación, es alguien dándose cuenta. Lo que pasó fue más silencioso: una parte de los eventos simplemente no se convirtió en nada. Ningún run, ningún check pendiente, ningún registro. Desde afuera, ese commit parece que pasó.

Entonces el daño no terminó a las 2:04 cuando se cerró el incidente. Siguió a la mañana siguiente, en forma de una pregunta que nadie del equipo sabía responder: ¿qué quedó exactamente atrás?

Quien tenía "esperar a que pase" como plan de contingencia descubrió que esperar recupera el servicio, pero no recupera el trabajo.

La parte que nadie duplica

Aquí es adonde quiero llegar.

Cuando nos sentamos a pensar en contingencia de CI, el instinto manda replicar lo que es visible y fácil de dibujar en la pizarra: la máquina que corre el build. Compro un runner. Levanto un Jenkins. Dejo un GitLab CI de reserva. Todo eso es dinero y trabajo invertidos en la capa de ejecución.

Solo que la capa de ejecución no fue la que cayó. Y si miras los tres hechos del incidente uno al lado del otro, todos apuntan al mismo lugar: los dos tipos de runner cayeron juntos porque lo que se rompió fue el despacho; los pods del ARC se trabaron porque perdieron el vínculo con quien les da órdenes; y los eventos perdidos no volvieron porque su cola no existe en ningún otro lugar.

Todo el mundo duplica la parte que corre el build. Nadie duplica la parte que sabe qué build hay que correr, y esa fue la que cayó.

Esa capa es mucho menos glamorosa que un runner: es la cola de eventos, es la asociación entre commit y ejecución, es el historial de qué corrió y qué no. No aparece en ningún diagrama de arquitectura, nadie la pone en el presupuesto, y es lo único de lo que realmente no tienes copia.

Y eso que es barata de reconstruir después, si lo pensaste antes. No hace falta un segundo CI, ni Kubernetes, ni nada caro. Hace falta una pregunta que se pueda responder: ¿qué commits entraron y no tienen una ejecución asociada?

# Commits que entraron a main en la ventana del incidente
# y que no tienen ningún workflow run asociado.
git log --since="2026-08-06 15:00" --format=%H origin/main | while read sha; do
  runs=$(gh run list --commit "$sha" --json databaseId --jq 'length')
  [ "$runs" -eq 0 ] && echo "sin run: $sha"
done

Son seis líneas. No impiden que el CI se caiga y no aceleran ni un minuto la recuperación de GitHub. Pero convierten "creo que está todo bien" en una lista finita de commits para reprocesar, y esa es la diferencia entre cerrar el incidente y creer que lo cerraste. ¿Tiene sentido?

El pipeline que cabría en cualquier lugar

Dicho esto, está la mitad de la contingencia que trata de adónde llevar el build. Y aquí vale la pena enunciar la versión menos obvia del problema.

Lo que ata a la mayoría de los equipos a Actions no es Actions. Es haber escrito el build entero dentro de él: treinta pasos de YAML, quince uses: del marketplace, secretos amarrados a la sintaxis del proveedor y lógica de negocio desparramada en if: de workflow. Quien lo hace así no tiene un plan B malo, tiene una reescritura por delante. Y una reescritura no se hace en medio de un incidente.

El diseño que sobrevive es el contrario: la lógica vive en un script, y el CI es una cáscara fina que llama a ese script.

# Makefile — el build de verdad, que corre igual en tu máquina y en el CI.
ci: lint typecheck test build

lint:
	pnpm biome check .

typecheck:
	pnpm tsc --noEmit

test:
	pnpm vitest run --coverage

build:
	docker build -t $(IMAGE):$(SHA) .

Y luego la cáscara, en tres lugares distintos:

# .github/workflows/ci.yml
- uses: actions/checkout@v4
- run: make ci
# .gitlab-ci.yml
ci:
  script: make ci
// Jenkinsfile
stage('ci') { steps { sh 'make ci' } }

Fíjate en que el núcleo no cambia. Cambia la línea que sabe hacer checkout y la línea que sabe llamar a make. Por eso no me gusta la conversación de "cambiar Actions por Jenkins": cambiar de CI no debería ser un proyecto, debería ser una tarde. Y, siendo bien honesto, cambiar Actions por un Jenkins mal cuidado es pésimo negocio: cambias la indisponibilidad de GitHub, que tiene un equipo de guardia, por la tuya propia: parche de seguridad atrasado, plugin que se rompe en el upgrade, disco llenándose, sin escala elástica y un punto único de falla que nadie monitorea. Sumadas en el año, las horas que Actions te deja tirado probablemente cuestan menos que eso.

Jenkins y GitLab CI aquí no son una recomendación de cambio. Son la prueba de que el diseño portable funciona.

Portabilidad no es tener dos CI encendidos. Es tener un pipeline que cabría en cualquiera.

Es el mismo razonamiento que seguí construyendo Lisa, la CLI que mantengo: la lógica de abrir el PR, revisar el CI y hacer merge es una sola, y existe una capa fina de traducción por plataforma para GitHub, GitLab y Bitbucket. Cuando necesito preguntar si el CI pasó, la pregunta es siempre la misma; lo que cambia es si se convierte en un gh pr checks o en una llamada a la API de pipelines de GitLab. De hecho, es un buen ejemplo justamente porque no es perfecto: en Bitbucket esa verificación devuelve unknown, porque no existe un camino barato a partir de la URL del PR. La abstracción aguanta el hueco sin contaminar el resto.

La escalera, de lo barato a lo caro

La contingencia es cuestión de dosis, y creo que la mejor forma de decidir es mirarla como escalones, sabiendo el precio de cada uno:

  1. Pipeline portable. El build se vuelve script, el CI se vuelve cáscara. Cuesta unas horas y lo vas a usar todos los días, incluso sin ningún incidente, porque make ci corre igual en tu máquina.
  2. Saber qué quedó atrás. Las seis líneas de arriba, o el equivalente en tu stack. Cuesta una tarde y es el único escalón que ataca el daño que queda después de que el servicio vuelve.
  3. Deploy manual documentado y probado. Un docker build, un push al registry, un comando de reinicio. La palabra que sostiene el escalón es probado: un runbook que nadie ejecutó en los últimos seis meses es ficción. Ponlo en el calendario y córrelo una vez por trimestre, aun sin incidente.
  4. Mirror del repositorio. Git es distribuido, así que el código en sí ya está en la notebook de todos. Lo que pierdes en una caída mayor no es el código, es el hub: PR, issues, pipeline, registry. Un espejo en otro proveedor es un git push más y resuelve la continuidad del código por casi nada.
  5. CI secundario encendido de verdad. Aquí el precio sube de nivel: dos conjuntos de secretos, dos configuraciones de runner, dos lugares que mantener cuando alguien toca el build. Solo compensa si el escalón 1 ya está listo; si no, vas a mantener dos pipelines divergentes.

Los cuatro primeros escalones, sumados, dan menos de una semana de trabajo y no crean nada nuevo que operar. Es hasta donde yo llegaría en la mayoría de los proyectos que pasan por mis manos, y es donde recomiendo parar a la abrumadora mayoría de los equipos.

Del quinto escalón en adelante, CI secundario, Jenkins en alta disponibilidad, runners en dos nubes, la conversación deja de ser técnica y se vuelve financiera. El criterio es una sola pregunta, y no me toca a mí responderla: ¿cuánto te cuesta una hora sin poder hacer deploy?

Para un SaaS que entrega correcciones de bugs críticos varias veces al día, el número es alto y el escalón cinco se paga solo. Para un producto que hace un release semanal planificado, nueve horas detenido cuestan una reorganización de agenda y nada más. Si no sabes responder esa pregunta con un número, el próximo paso no es montar la contingencia: es descubrir el número. Una contingencia dimensionada por miedo sale mucho más cara que una dimensionada por cuentas.

Y para dejar claro que esto no es cosa de un evento aislado: el historial de incidentes del propio GitHub registra seis incidentes que tocaron Actions entre el 17 de julio y el 7 de agosto. Tres semanas. Esa es la tasa normal de cualquier servicio de esa escala, y es sobre ella que vale la pena planificar, no sobre el pico de nueve horas, que es el número que asusta y justamente por eso es el peor consejero.

Para cerrar

La contingencia de CI/CD suele discutirse como un problema de redundancia de máquinas, y esa es justamente la mitad fácil. La mitad que muerde es la que no se ve: el estado.

Si tuviera que dejarlo en cuatro frases:

  1. El runner propio es capacidad, no independencia. Te da el procesador, pero quien encola, despacha, autentica y reporta sigue del otro lado. Por eso los dos tipos de runner cayeron en el mismo minuto.
  2. El incidente no termina cuando el servicio vuelve. Un evento que no se convirtió en ejecución no se recupera solo. Ten una forma de listar lo que quedó atrás antes de necesitarla.
  3. El build vive en un script, el CI es una cáscara fina. Si cambiar de CI es un proyecto, no tienes plan B, tienes una reescritura. Y una reescritura no se hace durante el incidente.
  4. El tamaño de la contingencia es el costo de la parada. Haz los cuatro primeros escalones, que son baratos y los usas todos los días. Del quinto en adelante, solo después de tener el número en la mano.

En el fondo es la misma incomodidad que ya había descrito hablando del release de una app móvil: nunca controlamos cuándo actualiza el otro lado, solo controlamos lo que recibe. Allá el otro lado es el usuario que no instaló la versión nueva; aquí es la plataforma que despacha tus jobs. Cambia el objeto, no cambia la naturaleza del problema.

Lo que esta caída me dejó no fueron ganas de irme de GitHub. Fue darme cuenta de que había pensado toda la contingencia en torno a dónde iba a correr el build, y ni un minuto en torno a cómo descubriría lo que no corrió. La segunda parte cuesta una tarde de trabajo. Naturalmente, es la que voy a usar primero la próxima vez.

Y si llegaste hasta aquí sin poder responder cuánto cuesta una hora sin deploy en tu caso, esa es exactamente la conversación que me gusta tener. Escríbeme y lo pensamos juntos.

¿Tienes un sistema que necesita sobrevivir al crecimiento?

Ese es el tipo de decisión que ayudo a tomar. Si encaja con tu momento, hablemos.