
Mettre la release dans la CI, deux fois
GeekBye v1.8.4 a déplacé les builds de release vers la CI pour macOS et Windows. Ce que le changelog ne dit pas, c'est qu'on avait déjà tenté le coup une fois, qu'on l'a supprimé un mois plus tard à cause du coût des runners, et qu'on ne l'a fait tenir qu'à la deuxième fois — parce qu'à ce moment-là le script de release marchait déjà à la main.
Le changelog de la v1.8.4 a une ligne qui sonne comme du pur ménage : « Les builds de release viennent désormais de la CI, pour macOS comme pour Windows. » Ça se lit comme le genre de chose qui arrive une fois, proprement, et à laquelle on ne repense jamais. Ce n'était pas le cas. L'équipe avait déjà mis les releases dans la CI une fois — et l'avait arraché. Cette release est la deuxième tentative, et la raison pour laquelle elle a tenu est la vraie leçon d'ingénierie, alors racontons-la dans l'ordre.
La première tentative, et pourquoi elle est morte
En octobre 2025, il y avait un release.yml. Il était déclenché au push d'un tag — vous plantez un tag v*.*.* et un runner macOS démarrait, compilait et publiait. Il faisait sa propre préparation de signature à la main : une étape qui décodait le certificat de signature, créait et déverrouillait un keychain temporaire, importait le cert pour que codesign puisse le trouver, et une étape de nettoyage correspondante à la fin. Ça marchait. C'était aussi macOS seulement — les releases Windows n'étaient pas du tout dans la CI.
Le 4 novembre, il a été supprimé. Le message de commit est inhabituellement franc sur le pourquoi : « 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. » Voilà toute l'histoire de la première tentative en une phrase. GitHub facture le temps de runner macOS à dix fois le tarif de Linux, et une release déclenchée par tag signifie que chaque tag — y compris les jetables, les re-tags, les « oups, oublié de monter la version » — dépense tranquillement cent minutes facturées. Une automatisation que vous ne contrôlez pas est une automatisation qui dépense de l'argent pendant que vous dormez. Alors les releases sont retournées sur le Mac d'un développeur pour les trois mois et demi suivants.
Où les problèmes difficiles ont vraiment été résolus
Voici la partie qui fait fonctionner la deuxième tentative, et elle s'est entièrement passée en dehors de la CI. Pendant ces trois mois et demi de releases locales, scripts/release.js — le simple script Node qu'un développeur lance à la main — a absorbé chaque problème d'empaquetage de l'app, un correctif à la fois :
- Restore white-label. C'est une codebase white-label qui compile à la fois GeekBye et Pavleur depuis une seule source, ce qui veut dire qu'une release échange des champs de
package.jsonet des assets d'icônes pour le produit cible. Deux correctifs ont appris au script à tout remettre en place ensuite, pour que compiler Pavleur ne laisse pas votre arbre git sali par l'identité de Pavleur. - Windows dual-arch. Un correctif pour compiler les deux installeurs Windows, x64 et arm64, au lieu d'un seul.
- Le contournement pour Windows non signé. Les versions plus récentes d'electron-builder ignoraient le flag de config censé désactiver la signature Windows, alors le script a appris à forcer une build non signée en posant
CSC_IDENTITY_AUTO_DISCOVERY=falsedans l'environnement — un pli qu'on ne trouve qu'en le heurtant. - L'autodétection de la notarisation. Le script a appris à regarder son environnement et à décider : des identifiants de signature présents ? On signe. Des identifiants de notarisation Apple aussi présents ? On notarise. Ni l'un ni l'autre ? Build non signée. Aucun flag à retenir ; la présence des secrets est la configuration.
Rien de tout ça n'est glorieux. Tout ça, c'est le genre de chose qui, si elle vous surprend à l'intérieur d'un runner CI, vous coûte une heure de push-attendre-échouer-lire-les-logs par tentative — à facturation 10x. Résolu sur un Mac devant lequel vous êtes assis, chacun coûte une minute.
La deuxième tentative : soixante-six lignes
Quand la CI est revenue en v1.8.4, le workflow faisait 66 lignes, et sa qualité déterminante est le peu qu'il fait. Le commit le décrit sans détour : « 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. » Chaque décision de conception qu'il contient est une cicatrice de la première tentative :
workflow_dispatch, pas déclenché par tag. Vous démarrez une release en cliquant sur « Run workflow ». Un humain contrôle chaque minute macOS payante. Le problème de coût qui a tué la première version est résolu en n'automatisant tout simplement pas le déclencheur — le seul endroit où l'automatisation était activement nuisible.- Une entrée
product. Le dispatch prend un menu déroulant —geekbyeoupavleur— pour que le même workflow livre l'une ou l'autre marque. La couture white-label va jusqu'au bouton de release lui-même. - Deux jobs parallèles.
build-macsurmacos-latest,build-winsurwindows-latest, tournant en même temps. Windows dans la CI est vraiment nouveau ici ; le workflow d'octobre ne l'a jamais compilé. - Aucune logique de signature dans le YAML. C'est tout l'intérêt. Pas de jonglage avec le keychain, pas d'étape d'import de cert, pas de nettoyage. Le job Mac lance
node scripts/release.js <product> --publishet le job Windows lance la même chose avec--no-sign. Tout ce que le premier workflow faisait à la main en YAML vit désormais dans le script qui marche déjà. Le workflow est un orchestrateur, pas une implémentation.
Les deux plateformes veulent des choses opposées, et les jobs le reflètent honnêtement. macOS compile sous un hardened runtime, signé et notarisé via le chemin intégré d'electron-builder (@electron/notarize), avec les identifiants fournis depuis les secrets du dépôt et autodétectés par le script. Windows compile des installeurs NSIS non signés pour x64 et arm64. Un workflow, deux jobs, deux notions entièrement différentes de « terminé ».
La preuve, c'est ce qui n'est pas arrivé
Voici comment vous savez que l'ordre était le bon : après les deux retouches du même jour qui ont nommé les exécutions et corrigé le nom d'un secret de token, release.yml n'a plus été touché pendant quatre mois. Aucun hotfix d'échec de signature. Aucune panique de rejet de notarisation. Aucune panique du type « le binaire natif manque sur le runner ». Pour un pipeline de signature-et-notarisation de code — le genre de CI le plus tristement célèbre pour son instabilité — quatre mois de silence, c'est presque du jamais-vu.
C'était calme parce que le bruit s'était déjà produit ailleurs, à moindre coût. Les batailles qui se jouent d'habitude dans un onglet de logs CI, à facturation 10x, un force-push à la fois, s'étaient jouées sur les Macs des développeurs pendant l'ère locale. La CI n'a pas eu à être l'endroit où l'empaquetage se débogue, parce que l'empaquetage était déjà débogué. Voilà la thèse en une ligne : ne déplacez la build hors de votre Mac qu'une fois que votre Mac a cessé de vous surprendre.
Le seul couplage qui peut encore mordre
Ce n'est pas entièrement sans point faible, et ça vaut la peine de le nommer parce que c'est subtil. Les binaires Swift dont dépend l'app macOS — OCR, capture d'écran, les transcripteurs — sont committés dans git. Mais la release ne livre pas les copies committées ; la build les recompile sur le runner. Et une barrière check-swift-version.js fait échouer net toute la release si le compilateur n'est pas la version épinglée. À cette release, cette épingle était Swift 6.2.x, et rien dans le workflow ne l'installe — le job fait simplement confiance au fait que le Swift par défaut de macos-latest correspond. Le jour où GitHub fait passer l'image de son runner au-delà de l'épingle, la release s'arrête, non pas parce que quelque chose cloche dans l'app, mais parce que le pipeline est discrètement couplé à une image de machine qu'il ne contrôle pas. C'est le seul détail d'implémentation que ce workflow mince n'a pas poussé vers quelque chose qui lui appartient.
L'autre moitié de la release : des payloads plus légers
La release portait une fonctionnalité sans rapport qui mérite une mention, parce que c'est un exemple net de correctif de latence par soustraction. L'action Assist Me envoyait au backend plus de contexte qu'il n'en fallait à chaque requête. Un commit a rogné deux choses : l'historique de transcription joint est passé des 30 dernières entrées aux 15 dernières, et les fichiers de contexte du profil de l'utilisateur — injectés dans le system prompt à chaque appel — ont été plafonnés net à 4,000 caractères avec un marqueur de troncature. Moins à sérialiser, moins à téléverser, moins à lire pour le modèle avant qu'il commence à répondre, ce qui est la métrique que le commit optimise : le time-to-first-token. Par souci d'honnêteté : le commit revendique le gain de vitesse mais ne le mesure pas — il n'y a aucun chiffre avant/après dans l'historique, alors traitez-le comme une coupe bien raisonnée plutôt qu'une victoire chronométrée.
Trois choses que cette release nous a apprises
- La CI est une fine enveloppe par-dessus un script qui marche déjà. Le workflow de 66 lignes n'a aucune logique de signature propre ; il appelle un script que trois mois de releases locales avaient déjà débogué. Mettez le mécanisme dans une chose que vous pouvez lancer à la main, et laissez la CI décider juste quand la lancer.
- Automatisez le travail, pas forcément le déclencheur. La première tentative est morte parce que les releases déclenchées par tag dépensaient toutes seules des minutes de runner facturées 10x. Un
workflow_dispatchmanuel garde l'automatisation et retire la partie qui coûtait de l'argent — parfois, l'humain dans la boucle est la fonctionnalité. - Déboguez là où itérer est bon marché. Chaque bataille d'empaquetage réglée sur le Mac d'un développeur est une bataille qui n'arrive jamais dans un onglet de logs CI à dix fois le prix. Quittez le Mac en dernier, pas en premier.
Pour le chapitre précédent de l'histoire v1, livrer trente langues sans filet de sécurité (v1.8.3) ; et pour tout l'arc, l'anatomie de livrer un logiciel jusqu'à la perfection.