Steven
Steven8 min de lectura

Posar la release a CI, dues vegades

GeekBye v1.8.4 va moure les release-builds a CI per a macOS i Windows. El que el changelog no diu és que ja ho vam provar una vegada abans, ho vam esborrar un mes després pel cost dels runners, i només va quallar la segona vegada — perquè per llavors el script de release ja funcionava a mà.

Engineering
CI/CD
Desktop
GeekBye Releases
Posar la release a CI, dues vegades

El changelog de la v1.8.4 té una línia que sona a pura tasca de manteniment: "Les release-builds ara vénen de CI tant per a macOS com per a Windows." Sembla el tipus de cosa que passa una vegada, net, i en què no es torna a pensar mai més. No va ser així. L'equip ja havia posat les releases a CI una vegada — i les n'havia arrencat. Aquesta release és el segon intent, i la raó que va quallar és la lliçó d'enginyeria de debò, així que expliquem-ho en ordre.

El primer intent, i per què va morir

L'octubre de 2025 hi havia un release.yml. Es disparava en fer push d'un tag — empeny un tag v*.*.* i un runner de macOS arrencava, construïa i publicava. Feia la seva pròpia configuració de signat a mà: un pas que descodificava el certificat de signat, creava i desbloquejava un keychain temporal, importava el certificat perquè codesign el pogués trobar, i un pas de neteja corresponent al final. Funcionava. També era només per a macOS — les releases de Windows no eren gens a CI.

El 4 de novembre, es va esborrar. El missatge de commit és inusualment franc sobre el perquè: "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." Aquesta és tota la història del primer intent en una frase. GitHub factura el temps de runner de macOS a deu vegades la tarifa de Linux, i una release disparada per tag vol dir que cada tag — inclosos els d'usar i llençar, els re-tags, els de "vaja, m'he oblidat de pujar la versió" — gasta discretament cent minuts facturats. L'automatització que no controles és automatització que gasta diners mentre dorms. Així que les releases van tornar al Mac d'un desenvolupador durant els tres mesos i mig següents.

On es van resoldre de veritat els problemes difícils

Aquí ve la part que fa funcionar el segon intent, i va passar completament fora de CI. Durant aquells tres mesos i mig de releases locals, scripts/release.js — el simple script de Node que un desenvolupador executa a mà — va absorbir cada problema d'empaquetat que tenia l'app, una solució cada vegada:

  • Restore white-label. Aquesta és una codebase white-label que construeix tant GeekBye com Pavleur des d'una sola font, cosa que vol dir que una release intercanvia camps de package.json i assets d'icones per al producte objectiu. Dues solucions van ensenyar el script a tornar-ho tot al seu lloc després, així construir Pavleur no et deixava l'arbre de git brut amb la identitat de Pavleur.
  • Windows dual-arch. Una solució per construir tots dos installers de Windows x64 i arm64 en lloc d'un.
  • L'apany per a Windows sense signar. Un electron-builder més nou ignorava el flag de config que havia de desactivar el signat de Windows, així que el script va aprendre a forçar una build sense signar posant CSC_IDENTITY_AUTO_DISCOVERY=false a l'entorn — un plec que només trobes topant-hi.
  • Autodetecció de notarització. El script va aprendre a mirar el seu entorn i decidir: hi ha credencials de signat? Signa. Hi ha també credencials de notarització d'Apple? Notaritza. Cap de les dues? Construeix sense signar. Cap flag per recordar; la presència dels secrets és la configuració.

Res d'això és glamurós. Tot plegat és el tipus de cosa que, si et sorprèn dins un runner de CI, et costa una hora de push-esperar-fallar-llegir-logs per intent — a facturació 10x. Resolt en un Mac que tens al davant, cadascuna costa un minut.

El segon intent: seixanta-sis línies

Quan CI va tornar a la v1.8.4, el workflow tenia 66 línies, i la seva qualitat definitòria és el poc que fa. El commit ho descriu clarament: "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ó de disseny que hi ha és una lliçó apresa a cop dur del primer intent:

  • workflow_dispatch, no disparat per tag. Comences una release fent clic a "Run workflow." Un humà controla cada minut pagat de macOS. El problema de cost que va matar la primera versió es resol simplement no automatitzant el disparador — l'únic lloc on l'automatització era activament perjudicial.
  • Una entrada product. El dispatch pren un desplegable — geekbye o pavleur — així el mateix workflow publica qualsevol de les dues marques. La costura white-label arriba fins al botó de release.
  • Dos jobs paral·lels. build-mac a macos-latest, build-win a windows-latest, executant-se alhora. Windows a CI és genuïnament nou aquí; el workflow d'octubre no el va construir mai.
  • Cap lògica de signat al YAML. Aquest és tot el punt. No hi ha jocs malabars amb keychain, ni pas d'importació de certificat, ni neteja. El job del Mac executa node scripts/release.js <product> --publish i el job de Windows executa el mateix amb --no-sign. Tot el que el primer workflow feia a mà al YAML ara viu al script que ja funciona. El workflow és un orquestrador, no una implementació.

Les dues plataformes volen coses oposades, i els jobs ho reflecteixen honestament. macOS construeix sota un hardened runtime, signat i notaritzat a través del camí integrat d'electron-builder (@electron/notarize), amb les credencials proporcionades des de secrets del repositori i autodetectades pel script. Windows construeix installers NSIS sense signar per a x64 i arm64. Un workflow, dos jobs, dues nocions completament diferents de "fet."

La prova és en el que no va passar

Així saps que la seqüència era correcta: després dels dos retocs del mateix dia que van posar nom a les execucions i van arreglar el nom d'un secret de token, release.yml no es va tornar a tocar durant quatre mesos. Cap hotfix per fallada de signat. Cap correguda per un rebuig de notarització. Cap pànic de "el binari natiu falta al runner." Per a un pipeline de signat-de-codi-i-notarització — el gènere de CI més tristament famós pels seus daltabaixos — quatre mesos de silenci és gairebé inaudit.

Estava tranquil perquè el soroll ja havia passat en algun lloc més barat. Les baralles que normalment es despleguen en una pestanya de logs de CI, a facturació 10x, un force-push cada vegada, s'havien desplegat als Macs dels desenvolupadors durant l'era local. CI no havia de ser on es depurava l'empaquetat, perquè l'empaquetat ja estava depurat. Aquesta és la tesi en una línia: treu la build del teu Mac només després que el teu Mac hagi deixat de sorprendre't.

L'únic acoblament que encara et pot fer mal

No està del tot sense un punt feble, i val la pena anomenar-lo perquè és subtil. Els binaris de Swift dels quals depèn l'app de macOS — OCR, captura de pantalla, els transcriptors — estan comitejats a git. Però la release no envia les còpies comitejades; la build els recompila al runner. I una porta check-swift-version.js fa fallar en dur tota la release si el compilador no és la versió fixada. En aquesta release aquesta versió fixada era Swift 6.2.x, i res al workflow l'instal·la — el job simplement confia que el Swift per defecte de macos-latest coincideixi. El dia que GitHub pugi la imatge del seu runner més enllà de la versió fixada, la release s'atura, no perquè hi hagi res dolent amb l'app, sinó perquè el pipeline està discretament acoblat a una imatge de màquina que no controla. És l'únic detall d'implementació que aquest workflow prim no va empènyer cap avall a alguna cosa que posseeix.

L'altra meitat de la release: payloads més petits

La release portava una funcionalitat no relacionada que val la pena esmentar, perquè és un exemple net d'una solució de latència per sostracció. L'acció Assist Me estava enviant al backend més context del que necessitava a cada petició. Un commit va retallar dues coses: l'historial de transcripció que s'enviava va baixar de les últimes 30 entrades a les últimes 15, i els fitxers de context del perfil de l'usuari — injectats al system prompt a cada crida — es van limitar en dur a 4,000 caràcters amb un marcador de truncament. Menys a serialitzar, menys a pujar, menys perquè el model llegeixi abans de començar a respondre, que és la mètrica que el commit optimitza: time-to-first-token. En interès de l'honestedat: el commit reclama l'acceleració però no la mesura — no hi ha cap xifra abans/després a l'historial, així que tracta-ho com una retallada ben raonada més que com una victòria mesurada.

Tres coses que aquesta release ens va ensenyar

  1. CI és un embolcall prim sobre un script que ja funciona. El workflow de 66 línies no té lògica de signat pròpia; crida un script que tres mesos de releases locals ja havien depurat. Posa el mecanisme en una cosa que pots executar a mà, i deixa que CI només decideixi quan executar-lo.
  2. Automatitza la feina, no necessàriament el disparador. El primer intent va morir perquè les releases disparades per tag gastaven automàticament minuts de runner facturats a 10x. Un workflow_dispatch manual manté l'automatització i elimina la part que costava diners — de vegades l'humà al bucle és la funcionalitat.
  3. Depura on iterar és barat. Cada baralla d'empaquetat resolta al Mac d'un desenvolupador és una baralla que mai no passa en una pestanya de logs de CI a deu vegades el preu. Surt del Mac l'últim, no el primer.

Per al capítol anterior de la història de la v1, publicar trenta llengües sense xarxa de seguretat (v1.8.3); i per a tot l'arc, l'anatomia de publicar programari fins a la perfecció.