Movimos un monorepo de TypeScript y Go en producción a los runners de Boat. El proveedor anterior fallaba al preparar máquinas y dejaba jobs en cola hasta 24 horas, así que el cambio tenía sentido. Lo que no esperaba era cuánto tenía que cambiar la forma del CI.
Boat cobra y limita por arranque de máquina, no por minuto. Cada job de GitHub que recibe un runner cuesta un arranque. Un job que se salta con if: no cuesta nada. Un job cancelado después de recibir su máquina ya lo pagó.
Cobrando por minuto, muchos jobs chicos en paralelo salen casi gratis. Cobrando por arranque, son la factura.
Lo que costaba el CI de antes
Era el esquema de siempre: un job que detecta cambios y después jobs separados para la calidad de TypeScript, suites con base de datos real, Playwright, Go, builds de imágenes (una máquina por imagen), Python, coverage, un benchmark de LLM, una revisión de docs y un job agregado de “todo en verde”. Los jobs de pruebas en main tenían cancel-in-progress: true, que es lo correcto cuando pagas por minuto.
El plan permitía 840 arranques al día, 210 por hora y 30 por minuto. Los números:
- Unos 3.9 arranques por corrida de CI.
- El día más pesado tuvo 964 arranques, arriba del límite de 840. Su minuto pico tuvo 42, arriba del límite de 30. Los dos límites se rompieron el mismo día.
- Un día en main, 31 de 52 corridas por push terminaron canceladas, y 55 jobs de calidad ya habían arrancado cuando el siguiente push los canceló. Cada uno pagó su arranque.
Cómo se ve ahora
Un job pesado por corrida de pull request. Hace el trabajo de los jobs de antes como tareas en segundo plano, en paralelo en una sola máquina más grande, y aun así publica un check separado por tarea.
El resultado es 1 arranque por corrida de PR, unos 2 en main (calidad y deploy). El día más pesado estimado queda en unos 270 arranques, el 32% del límite. El job pesado tarda unos 380 segundos.
Antes y después, con números
Antes de Boat, este CI vivió en otros dos lados: dos servidores dedicados propios con runners siempre prendidos, y después un proveedor que prendía una VM nueva por cada job y cobraba por minuto. Saqué todas las corridas de cada etapa de la API de GitHub y medí lo mismo en todas.
| Servidores propios | Proveedor por minuto | Boat, sin cambios | Boat, con la skill | |
|---|---|---|---|---|
| Máquinas | 2 servidores dedicados, 12 vCPU / 31 GB y 8 vCPU / 15 GB, siempre prendidos | 1 VM por job, de 1 vCPU / 2 GB a 4 vCPU / 16 GB | 1 VM por job, 2 vCPU / 4 GB u 8 vCPU / 16 GB | 1 VM por corrida, 16 vCPU / 32 GB |
| Arranques por corrida de PR, mediana | ninguno (9 jobs en runners ya prendidos) | 9 | 3.5 | 1 |
| Día más pesado, en arranques | — | 2,112 | 964 | 111 en sus primeras 22 horas |
| Espera por máquina, p90 | 18 min | 26 min | 14 s | 19 s |
| De PR a verde, mediana | 19 min | 10 min | 5.4 min | 7.2 min |
| De PR a verde, p90 | 62 min | 34 min | 10 min | 12 min |
Un arranque es un job que recibió máquina, contado por corrida de CI de un pull request, reruns incluidos. La espera es cuánto se quedó un job en la cola antes de recibirla. De PR a verde es el tiempo desde que se abre el pull request hasta que el CI termina en verde, en los pull requests cuya primera corrida pasó. Los que necesitaron un arreglo miden cuánto tardó el arreglo, no el CI.
Lo que dice la tabla:
- Mis servidores no tenían arranques que pagar, pero tenían cola. Las ráfagas de ramas caían en las mismas dos máquinas, y en el percentil 90 un job esperaba 18 minutos a que se liberara un runner.
- El proveedor por minuto bajó a la mitad la mediana de PR a verde, pero no la cola larga: 26 minutos de espera en el percentil 90. Y 9 máquinas por corrida está bien cuando pagas por minuto, pero esa misma forma en Boat habría sido más de 2,000 arranques en un día cargado, muy arriba del límite de 840.
- Boat con el esquema viejo ya había quitado la cola, y como el trabajo se repartía en varias máquinas, era el más rápido en llegar a verde. También fue el día de 964 arranques.
- Con la skill es 1 arranque por corrida. La mediana a verde subió unos 2 minutos, de 5.4 a 7.2, porque ahora el trabajo comparte una sola máquina; el percentil 90 casi no se movió. Dos minutos a cambio de menos de un tercio de los arranques es el trato que quería.
No es un experimento limpio. Son semanas distintas con distinta cantidad de trabajo, el código siguió cambiando en el camino y la última columna cubre menos de un día, 42 pull requests. Tómalo como una dirección, no como un número final. Las ventanas fueron del 12 al 23 de octubre, del 25 al 28 de octubre, del 29 de octubre al 1 de noviembre y del 1 al 2 de noviembre de 2026.
La skill
Empaqueté la migración como una skill, boated-ci, para que un agente la pueda hacer en otro repo. Sigue el mismo orden que seguí yo:
- Primero auditar. Un script lee la API de GitHub y reporta arranques por día, por hora pico y por minuto pico, por job, etiqueta y evento, con un veredicto contra tu plan. Si tu pico queda debajo de la mitad de cada límite, te dice que no reestructures y que solo quites el desperdicio, como los reruns completos y los jobs cancelados en main.
- Consolidar. Los jobs chicos se vuelven tareas en segundo plano dentro de un job pesado, con
startywait, así la instalación corre una vez y no una por job. Los deploys, otros sistemas o arquitecturas, y los jobs con secretos que el código del PR no debe ver se quedan como jobs aparte. - Un check por tarea. El log de un solo job llegó a unas 195k líneas, y los agentes que arreglaban PRs tenían que leerlo. Ahora cada tarea publica un check llamado
<job> / <task>con solo el fallo recortado, con extractores para bun test, go test, Playwright, tsc, eslint, oxlint, golangci-lint, pgTAP y una cola genérica. Los logs de las tareas se enmascaran antes de subirlos. - Saltar en el rerun.
gh run rerun --failedse salta las tareas que ya pasaron sobre el mismo merge commit. Solo confía en checks publicados por el token del propio workflow. - Medir antes de cambiar de tamaño. Un muestreador registra CPU, steal y memoria de toda la máquina cada 2 segundos y lo reparte por tarea. Algunas VMs tenían entre 15 y 29% de steal de CPU, y eso las hacía entre 50 y 70% más lentas. Sin medir eso, comparas ruido.
Los scripts son de Bun y TypeScript, sin dependencias de npm, y tienen pruebas. También trae una plantilla de workflow completa para empezar.
Todo es GitHub Actions normal; Boat solo es el caso que me hizo escribirla. Los precios y límites de planes en las referencias los junté alrededor del 1 de noviembre de 2026, así que revisa los números actuales de tu proveedor.
Instálala
npx skills add OmarAlex24/skills --skill boated-ci
Funciona con cualquier agente que lea skills: Claude Code, Codex, Cursor, opencode. Después pregúntale por tu CI, por ejemplo: “Ayer topamos el límite de arranques por día. Audita .github/workflows y planea cómo consolidar el CI.”
La página de la skill tiene las instrucciones completas, y el código está en GitHub.
Las ideas del router de reúso de máquinas en las referencias (reusar máquinas tras un reset ligero, un registro compartido de arranques con ritmo, máquinas locales primero) vienen de pnkfluffy/boat-router.