Un manual de fallas de CI para un proyecto Rails de una sola persona
Vengo tratando al CI de blog-manager como un regaño automático personal: chequeo verde o lo arreglo. Eso funciona bien hasta que algo se rompe en main y tengo que recordar cuáles son mis propias reglas. Así que a principios de mayo me senté a escribir las reglas en docs/ci.md, un manual de fallas de CI para un proyecto donde soy todo el equipo. Esto es una foto de ese día; el manual creció desde entonces.
Tres cosas tenían que estar ahí:
- Qué corre realmente el CI.
- Qué hago cuando se pone en rojo.
- Qué significa “la fusión está bloqueada” sin branch protection.
La tercera fue la sorpresa.
El plan, antes de empezar
Un issue de Plane listaba cinco decisiones por capturar: branch protection, política de flakes, notificaciones, reproducción local, regla de rollback. Asumí que branch protection sería la fácil. Activar el interruptor, exigir CI verde en main, listo. GitHub no estuvo de acuerdo.
Upgrade to GitHub Pro or make this repository public to enable this feature.
GitHub no aplica los chequeos de estado requeridos en repositorios privados de cuentas personales a menos que se pague por Pro. El repositorio es privado (tiene una contraseña del registro de Gitea y algunos tokens de API en fixtures sembrados), y no iba a hacerlo público por una función de 4 dólares al mes. Así que branch protection entró al manual como diferido, con esta justificación:
Estado de branch protection: diferido. Se requiere GitHub Pro para aplicar los chequeos de estado requeridos en repositorios privados de una cuenta personal. El costo no se justifica para un proyecto de un solo desarrollador. Revisar si el repositorio pasa a una organización de GitHub o se vuelve público.
Eso significa que la puerta de fusión se autoaplica. La regla del CLAUDE.md (nunca fusionar un PR en rojo) y una nota de cinco líneas en docs/ci.md son la puerta. Es un comentario que sostiene todo el peso, pero es preciso. Soy la única persona que puede romperlo, y la única que tiene que vivir con las consecuencias.
Flakes: skip más un issue, sin bucles de reintento
Vieja costumbre: volver a correr un job inestable hasta que se ponga verde, y seguir. La regla nueva es un reintento manual. Si sigue fallando, se pone en cuarentena.
def test_something_flaky
skip "flaky - see #123"
# ...
end
El truco está en hacer visible el skip. Un skip con una etiqueta de issue aparece en la salida de las pruebas y en el tablero de issues. Un bucle de reintentos esconde el problema en un historial de ejecuciones que nadie revisa. Prefiero un amarillo obvio antes que un verde invisible.
Casi publiqué una versión donde skip estaba al nivel de la clase, antes del def. Eso lanza NoMethodError en vez de saltar la prueba, porque skip solo funciona dentro del cuerpo de una prueba. La revisión de código lo detectó. Vale la pena mencionarlo porque es una trampa fácil de pisar.
La regla de rollback que reescribí el mismo día
Mi primera versión del manual tenía una regla plana: si main está en rojo y no hay un arreglo limpio listo en una hora, revertir el commit responsable. git revert <sha>, push, merge, y arreglarlo bien en una rama. Simple.
Después la releí y estaba mal para este proyecto. blog-manager todavía no está en producción. No hay usuarios que proteger de un main en rojo. La mayoría de mis roturas son infraestructura a medio terminar: un runner que no está registrado, una dependencia faltante, un workflow que todavía estoy conectando. Revertir eso esconde el problema en vez de resolverlo. Para una app solitaria que aún no está en producción, lo honesto por defecto es arreglar hacia adelante.
Así que reescribí la sección unas horas después. La regla de revertir en una hora todavía existe, pero solo entra en acción cuando las tres condiciones siguientes son verdaderas: la app está en producción con usuarios reales afectados, un arreglo limpio va a tomar más de una hora, y la rotura es visible para el usuario. Hasta entonces, arreglar hacia adelante.
La razón por la que la regla está escrita es la tentación que aparece en el momento en que se rompe main: “ya mismo empujo el arreglo.” A veces ese “ya mismo” son dos horas después, y para entonces los bisects son más difíciles y cualquier commit nuevo aterriza sobre una base rota. Escribir la condición es cómo evito negociar con esa tentación a las 2am.
Reproducción local que refleja el CI
El chequeo previo al push que realmente corro:
bin/brakeman --no-pager
bin/bundler-audit
bin/importmap audit
bin/rubocop
bin/rails db:test:prepare test
bin/rails db:test:prepare test:system
Seis comandos, unos 90 segundos en esta laptop, que se corresponden con los cinco jobs que el CI corre en paralelo (Brakeman y bundler-audit comparten un job). El punto no es reemplazar el CI. Es atrapar lo tonto, una violación suelta de RuboCop o un import sin usar, antes de que el runner tenga que hacerlo.
Qué haría distinto
El manual empezó corto, unas 80 líneas de Markdown. Eso es intencional. Cuanto más largo sea, menos lo voy a leer de verdad durante un momento de main en rojo. Lo próximo que quiero agregar es una chuleta de “primeros 5 minutos” hasta arriba de todo: el uno o dos comandos de gh que permiten decidir entre flake y real sin hacer scroll.
El otro pendiente fue el auto-deploy a staging en cada merge a main, que construí al día siguiente. Se ganó su propia sección en el manual, que es la mayor razón por la que el documento casi duplicó su longitud desde entonces.
Lecturas relacionadas
Mudar el CI a un runner propio después de que GitHub rompiera la facturación
CI muerto, despliegues vivos: fusionar todos los jobs en el runner del homelab, borrar la maquinaria de compensación para runners alojados, y el backlog de CVEs esperando detrás del portón.
Desplegando Rails 8 a staging automáticamente con Kamal y un runner autoalojado de GitHub Actions
Hacer que cada merge despliegue staging automáticamente: un runner autoalojado, cuatro obstáculos seguidos, y la trampa de los secretos de Kamal que más costó resolver.
Backfill de la realidad en un rastreador de sindicación
Producción decía cero posts en Medium; las rake tasks lo arreglaron en minutos. Luego el reconciliador se cayó en todos lados, se extrajo un dashboard del DOM, y el CI falló de tres formas distintas.
También te podría ser útil
AdGuard para iOS
Bloqueo de anuncios y rastreadores en todo el sistema en iOS, sin necesidad de un servidor DNS aparte.
Como afiliado de AdGuard, obtengo ingresos por las compras que califican.
Más informacióneSIM Airalo
eSIM de datos local para viajes - sin necesidad de cambiar una SIM física.
Este es mi enlace de referido de Airalo. Obtienes un descuento en tu primer eSIM y yo obtengo crédito de Airalo para el mío.
Más informaciónProton Mail
Correo electrónico cifrado de extremo a extremo, con arquitectura de acceso cero.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más información