
Poner la release en CI, dos veces
GeekBye v1.8.4 movió las builds de release a CI para macOS y Windows. Lo que el changelog no dice es que ya lo intentamos una vez antes, lo borramos un mes después por los costes de los runners, y solo se afianzó a la segunda vez — porque para entonces el script de release ya funcionaba a mano.
El changelog de la v1.8.4 tiene una línea que suena a puro mantenimiento: «Las builds de release ahora vienen de CI, tanto para macOS como para Windows». Se lee como el tipo de cosa que pasa una vez, limpiamente, y en la que no se vuelve a pensar. No fue así. El equipo ya había puesto las releases en CI una vez — y lo había arrancado de cuajo. Esta release es el segundo intento, y la razón por la que cuajó es la verdadera lección de ingeniería, así que contémoslo en orden.
El primer intento, y por qué murió
Allá por octubre de 2025 había un release.yml. Se disparaba con el push de un tag — plantas un tag v*.*.* y un runner de macOS arrancaba, compilaba y publicaba. Hacía su propia preparación de firma a mano: un paso que decodificaba el certificado de firma, creaba y desbloqueaba un keychain temporal, importaba el cert para que codesign pudiera encontrarlo, y un paso de limpieza correspondiente al final. Funcionaba. También era solo para macOS — las releases de Windows no estaban en CI en absoluto.
El 4 de noviembre se borró. El mensaje del commit es inusualmente franco sobre el porqué: «remove GitHub Actions release workflow to conserve minutes / macOS runners cost 10x multiplier (100 billed minutes per 10-minute release). Releases will be done locally.» Esa es toda la historia del primer intento en una sola frase. GitHub factura el tiempo de runner de macOS a diez veces la tarifa de Linux, y una release disparada por tag significa que cada tag — incluidos los desechables, los re-tags, los de «uy, se me olvidó subir la versión» — gasta calladamente cien minutos facturados. La automatización que no controlas es automatización que gasta dinero mientras duermes. Así que las releases volvieron al Mac de un desarrollador durante los siguientes tres meses y medio.
Dónde se resolvieron de verdad los problemas difíciles
Aquí está la parte que hace funcionar el segundo intento, y ocurrió enteramente fuera de CI. Durante esos tres meses y medio de releases locales, scripts/release.js — el sencillo script de Node que un desarrollador corre a mano — absorbió cada problema de empaquetado que tenía la app, un arreglo cada vez:
- Restore white-label. Este es un codebase white-label que compila tanto GeekBye como Pavleur desde una sola fuente, lo que significa que una release intercambia campos de
package.jsony assets de iconos por los del producto objetivo. Dos arreglos le enseñaron al script a dejarlo todo como estaba después, para que compilar Pavleur no dejara tu árbol de git sucio con la identidad de Pavleur. - Windows dual-arch. Un arreglo para compilar ambos instaladores de Windows, x64 y arm64, en vez de uno.
- El workaround de Windows sin firmar. Las versiones más nuevas de electron-builder ignoraban el flag de configuración que debía desactivar la firma en Windows, así que el script aprendió a forzar una build sin firmar poniendo
CSC_IDENTITY_AUTO_DISCOVERY=falseen el entorno — una arruga que solo encuentras cuando chocas con ella. - Autodetección de notarización. El script aprendió a mirar su entorno y decidir: ¿hay credenciales de firma? Firma. ¿También hay credenciales de notarización de Apple? Notariza. ¿Ninguna? Build sin firmar. Ningún flag que recordar; la presencia de los secrets es la configuración.
Nada de eso es glamuroso. Todo ello es del tipo de cosa que, si te sorprende dentro de un runner de CI, te cuesta una hora de push-esperar-fallar-leer-logs por intento — a facturación de 10x. Resuelto en un Mac que tienes delante, cada uno cuesta un minuto.
El segundo intento: sesenta y seis líneas
Cuando CI volvió en la v1.8.4, el workflow tenía 66 líneas, y su cualidad definitoria es lo poco que hace. El commit lo describe sin adornos: «Manual workflow_dispatch trigger that builds both platforms in parallel, reusing existing release.js script. macOS builds are signed and notarized, Windows builds are unsigned.» Cada decisión de diseño en él es una cicatriz del primer intento:
workflow_dispatch, no disparado por tag. Empiezas una release haciendo clic en «Run workflow». Un humano controla cada minuto de macOS de pago. El problema de coste que mató a la primera versión se resuelve simplemente con no automatizar el disparador — el único punto donde la automatización era activamente dañina.- Un input
product. El dispatch toma un desplegable —geekbyeopavleur— para que el mismo workflow publique cualquiera de las dos marcas. La costura white-label llega hasta el propio botón de release. - Dos jobs paralelos.
build-macenmacos-latest,build-winenwindows-latest, corriendo a la vez. Windows en CI es algo genuinamente nuevo aquí; el workflow de octubre nunca lo compiló. - Sin lógica de firma en el YAML. Este es todo el quid. No hay malabares con el keychain, ni paso de importación de cert, ni limpieza. El job del Mac corre
node scripts/release.js <product> --publishy el de Windows corre lo mismo con--no-sign. Todo lo que el primer workflow hacía a mano en YAML ahora vive en el script que ya funciona. El workflow es un orquestador, no una implementación.
Las dos plataformas quieren cosas opuestas, y los jobs lo reflejan con honestidad. macOS compila bajo un hardened runtime, firmado y notarizado a través de la ruta integrada de electron-builder (@electron/notarize), con las credenciales suministradas desde los secrets del repositorio y autodetectadas por el script. Windows compila instaladores NSIS sin firmar para x64 y arm64. Un workflow, dos jobs, dos nociones completamente distintas de «terminado».
La prueba está en lo que no pasó
Así es como sabes que el orden fue el correcto: tras los dos retoques del mismo día que nombraron las ejecuciones y arreglaron el nombre de un secret de token, release.yml no se volvió a tocar durante cuatro meses. Ningún hotfix por fallo de firma. Ningún apuro por un rechazo de notarización. Ningún pánico de «el binario nativo falta en el runner». Para un pipeline de firma-y-notarización de código — el género de CI más famoso por su inestabilidad — cuatro meses de silencio es algo casi inaudito.
Estuvo tranquilo porque el ruido ya había ocurrido en un sitio más barato. Las peleas que normalmente se libran en una pestaña de logs de CI, a facturación de 10x, un force-push cada vez, se habían librado en los Macs de los desarrolladores durante la era local. CI no tuvo que ser donde se depurara el empaquetado, porque el empaquetado ya estaba depurado. Esa es la tesis en una línea: saca la build de tu Mac solo cuando tu Mac haya dejado de darte sorpresas.
El único acoplamiento que todavía puede morderte
No está del todo libre de un punto débil, y vale la pena nombrarlo porque es sutil. Los binarios de Swift de los que depende la app de macOS — OCR, captura de pantalla, los transcriptores — están commiteados en git. Pero la release no publica las copias commiteadas; la build las recompila en el runner. Y una barrera check-swift-version.js hace fallar la release entera en seco si el compilador no es la versión fijada. En esta release esa fijación era Swift 6.2.x, y nada en el workflow la instala — el job simplemente confía en que el Swift por defecto de macos-latest coincide. El día que GitHub suba la imagen de su runner más allá de la versión fijada, la release se para, no porque nada ande mal con la app, sino porque el pipeline está calladamente acoplado a una imagen de máquina que no controla. Es el único detalle de implementación que este workflow fino no empujó hacia abajo, hacia algo que sea suyo.
La otra mitad de la release: payloads más pequeños
La release traía una feature no relacionada que merece una mención, porque es un ejemplo limpio de un arreglo de latencia por sustracción. La acción Assist Me estaba enviándole al backend más contexto del que necesitaba en cada request. Un commit recortó dos cosas: el historial de transcripción que se adjuntaba bajó de las últimas 30 entradas a las últimas 15, y los archivos de contexto del perfil del usuario — inyectados en el system prompt en cada llamada — quedaron limitados en seco a 4,000 caracteres con un marcador de truncamiento. Menos que serializar, menos que subir, menos que leer para el modelo antes de empezar a responder, que es la métrica que el commit optimiza: el time-to-first-token. En aras de la honestidad: el commit afirma la mejora de velocidad pero no la mide — no hay ningún número de antes/después en el historial, así que tómalo como un recorte bien razonado más que como una victoria con benchmark.
Tres cosas que nos enseñó esta release
- CI es una envoltura fina sobre un script que ya funciona. El workflow de 66 líneas no tiene lógica de firma propia; llama a un script que tres meses de releases locales ya habían depurado. Pon el mecanismo en algo que puedas correr a mano, y deja que CI solo decida cuándo correrlo.
- Automatiza el trabajo, no necesariamente el disparador. El primer intento murió porque las releases disparadas por tag gastaban solas minutos de runner facturados a 10x. Un
workflow_dispatchmanual conserva la automatización y quita la parte que costaba dinero — a veces el humano en el bucle es la feature. - Depura donde iterar es barato. Cada pelea de empaquetado resuelta en el Mac de un desarrollador es una pelea que nunca ocurre en una pestaña de logs de CI a diez veces el precio. Sal del Mac el último, no el primero.
Para el capítulo anterior de la historia v1, publicar treinta idiomas sin red de seguridad (v1.8.3); y para todo el arco, la anatomía de publicar software hasta la perfección.