Publicamos mientras dormíamos
FinButler se construye con ejecuciones programadas: tareas por la mañana, un navegador real por la noche y producción mientras dormimos. Esta es la maquinaria.
Por Erick Agrazal, Fundador
El domingo pasado me desperté, hice café y leí una discusión que cuatro revisores habían tenido mientras yo dormía.
Discutían sobre un pull request que ninguno había escrito. El cambio corregía cómo una importación masiva decide en qué equipo debe guardar las cuentas. Un agente de IA lo había preparado durante la noche, trabajando solo en una copia aislada del repositorio. Un revisor lo aprobó. Otro, encargado de vigilar una sola cosa, se negó. El cambio podía permitir que una importación tocara cuentas de una familia distinta a la que subió el archivo. Tenía razón. El pull request volvió, se corrigió y llegó a nuestra rama de integración antes de que terminara el café.
Nadie del equipo vio nada de esto mientras ocurría. Construimos FinButler así a propósito. Esta nota explica la maquinaria.
Cómo funciona
Nuestro proceso tiene tres ejecuciones programadas cada día. Por la mañana, la primera toma las tareas de la cola y las implementa. Por la tarde, QA valida el trabajo en un entorno real con un navegador real. Por la noche, la última promueve lo que pasó y lo publica.
Entre una ejecución y otra no hay nada corriendo. No existe un servicio orquestador, un daemon ni un trabajador de cola enviando señales de vida. El estado vive en herramientas que ya existen. GitHub Issues guarda las tareas y su ciclo. Git guarda el código. La plataforma de despliegue sabe qué versión está publicada.
Cada ejecución empieza de cero. Lee el estado del mundo, hace una parte limitada del trabajo y termina. Si se cae a la mitad, la siguiente vuelve a leer el mundo y continúa. Si se ejecuta dos veces, la segunda no encuentra nada pendiente. Decidir que no habría un daemon resolvió más problemas que cualquier otra pieza del sistema.
La entrada es el trabajo creativo
Las tareas viven en GitHub Issues y una persona las escribe. Ese suele ser el trabajo humano con más impacto del día.
No porque redactar sea difícil. Lo difícil es decidir el tamaño. Las tareas demasiado grandes causan más problemas que el código malo. “Internacionalizar toda la aplicación” no es una tarea. Es un proyecto disfrazado de tarea. Un agente puede pasar un día entero en eso y dejar una revisión que nunca termina. El mismo trabajo dividido en nueve partes claras avanza en dos días. Lo aprendimos de la forma molesta, más de una vez.
Aquí se concentra el juicio: qué construir, qué no construir, dónde separar las piezas y qué debe ocurrir primero. Después viene la maquinaria.
La ejecución de construcción
Cada tarea recibe su propio agente, Claude Code en nuestro caso, dentro de un worktree aislado donde no puede pisar el trabajo de otro. El agente empieza por una prueba que falla, escribe la implementación mínima, deja todo en verde y crea el commit. Después abre un pull request hacia la rama de integración.
La parte que defendería durante una discusión en un bar es la revisión. No usamos un solo revisor. Usamos un grupo donde cada miembro se ocupa de una cosa: corrección, seguridad, honestidad de las pruebas o cualquier consulta capaz de cruzar el límite entre equipos de nuestro producto financiero.
Un revisor general tiende a aprobar. Un revisor al que le pedimos encontrar una forma concreta de rechazar se comporta distinto. El revisor que vigila el alcance de los equipos existe por un susto anterior y ya ha pagado su lugar muchas veces.
La integración continua tiene que pasar y todos los revisores tienen que quedar satisfechos. Una falla no detiene el lote. La tarea vuelve a la cola con un comentario claro y las demás siguen avanzando.
La ejecución de QA
Un merge no significa que terminamos. Por la tarde, otra ejecución publica la rama de integración en QA y valida cada tarea como lo haría una persona. Playwright abre un navegador real, inicia sesión y recorre la función en inglés y en español porque nuestros usuarios viven en ambos idiomas.
Este paso existe porque las pruebas simuladas pueden mentir. Hemos tenido cambios verdes en las pruebas unitarias y en CI que simplemente no funcionaban al consultar el backend real. Una vez, el frontend esperaba un campo que la API nunca enviaba y todas las simulaciones estaban de acuerdo con el frontend. Desde entonces, la única evidencia válida es ver la función publicada haciendo su trabajo.
Si una tarea falla en vivo, retiramos sus cambios de la rama de integración y devolvemos el issue a la cola. Sale la tarea que falló, no todo el lote.
La ejecución de producción
La ejecución nocturna es aburrida a propósito. Un script determinista comprueba que cada cambio en la rama de integración pertenece a una tarea validada. Después promueve, publica y ejecuta pruebas de humo. Si aparece algo que no puede explicar, se detiene y deja una alerta.
En esta etapa quitamos la IA a propósito. Construir se beneficia de varios agentes. La revisión mejora cuando distintos revisores buscan problemas diferentes. La promoción a producción no necesita creatividad. Necesita una secuencia cerrada con un solo responsable.
Decidir dónde no usar IA terminó siendo tan importante como decidir dónde usarla.
Lo que sale mal
Bastantes cosas. Un error de concurrencia permitió que dos ejecuciones reclamaran la misma tarea y abrieran pull requests duplicados. La máquina de CI se quedó atascada a la mitad de un lote y dos tareas aprobadas tuvieron que esperar. Algunas tareas vuelven dos o tres veces antes de que una persona admita que estaban mal definidas y las reescriba.
Todo eso se puede soportar porque fallar cuesta poco. El estado vive fuera de la ejecución, así que una sesión fallida no corrompe nada. La tarea regresa a la cola y la siguiente ejecución recibe también el comentario del fallo. Los problemas que duran tampoco desaparecen en un archivo de logs. Cada ejecución publica lo pendiente en un issue fijo y evita repetir el mismo aviso.
Nos despertamos con una lista corta, no con silencio.
Lo que queda para nosotros
Definir el alcance. Aplicar criterio. Decidir si algo sale o no. Leer las alertas. Decidir que una función no debe existir sigue siendo una decisión humana y quizá sea la más importante.
La respuesta honesta a “¿entonces ya no haces falta?” es que el trabajo no se redujo. Se movió. Esa es la última nota de esta serie.
Esta nota también fue una tarea. Se definió al entrar, se escribió en un worktree, pasó por los revisores, se comprobó en un navegador en QA y la publicó la ejecución nocturna. Igual que todo lo demás.