
De release in CI zetten, twee keer
GeekBye v1.8.4 verplaatste release-builds naar CI voor macOS en Windows. Wat de changelog niet vertelt, is dat we dit één keer eerder probeerden, het een maand later verwijderden vanwege runnerkosten, en het pas de tweede keer lieten beklijven — omdat het release-script tegen die tijd al met de hand werkte.
De changelog van v1.8.4 heeft een regel die klinkt als pure huishouding: "Release-builds komen nu uit CI voor zowel macOS als Windows." Het leest als het soort ding dat één keer gebeurt, netjes, en waar nooit meer over wordt nagedacht. Dat was het niet. Het team had releases al één keer in CI gezet — en er weer uitgetrokken. Deze release is de tweede poging, en de reden dat hij beklijfde is de eigenlijke engineeringles, dus laten we het op volgorde vertellen.
De eerste poging, en waarom die sneuvelde
In oktober 2025 was er een release.yml. Die werd getriggerd op tag-push — duw een v*.*.*-tag en een macOS-runner startte op, bouwde en publiceerde. Hij deed zijn eigen signeer-setup met de hand: een stap die het signeercertificaat decodeerde, een tijdelijke keychain aanmaakte en ontgrendelde, het certificaat importeerde zodat codesign het kon vinden, en een bijpassende opschoonstap aan het eind. Het werkte. Het was ook alleen macOS — Windows-releases zaten helemaal niet in CI.
Op 4 november werd hij verwijderd. Het commitbericht is ongewoon openhartig over het waarom: "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." Dat is het hele verhaal van de eerste poging in één zin. GitHub factureert macOS-runnertijd tegen tien keer het tarief van Linux, en een tag-getriggerde release betekent dat elke tag — inclusief de wegwerptags, de her-tags, de "oeps, versie-bump vergeten"-tags — stilletjes honderd gefactureerde minuten uitgeeft. Automatisering die je niet in de hand hebt, is automatisering die geld uitgeeft terwijl jij slaapt. Dus gingen releases de volgende drieënhalve maand terug naar de Mac van een ontwikkelaar.
Waar de lastige problemen echt werden opgelost
Hier is het deel dat de tweede poging laat werken, en het gebeurde volledig buiten CI. Tijdens die drieënhalve maand lokale releases absorbeerde scripts/release.js — het simpele Node-script dat een ontwikkelaar met de hand draait — elk packaging-probleem dat de app had, fix voor fix:
- White-label restore. Dit is een white-label codebase die zowel GeekBye als Pavleur uit één bron bouwt, wat betekent dat een release
package.json-velden en icon-assets omwisselt voor het doelproduct. Twee fixes leerden het script om achteraf alles terug te zetten, zodat het bouwen van Pavleur je git-tree niet vervuild achterliet met de identiteit van Pavleur. - Dual-arch Windows. Een fix om beide x64- en arm64-Windows-installers te bouwen in plaats van één.
- De omweg voor ongesigneerde Windows. Nieuwere electron-builder negeerde de config-vlag die Windows-signeren moest uitschakelen, dus leerde het script een ongesigneerde build af te dwingen door in plaats daarvan
CSC_IDENTITY_AUTO_DISCOVERY=falsein de omgeving te zetten — een plooi die je pas vindt door erop te stuiten. - Notarisatie-autodetectie. Het script leerde naar zijn omgeving te kijken en te beslissen: signeer-credentials aanwezig? Signeren. Apple-notarisatie-credentials ook aanwezig? Notariseren. Geen van beide? Ongesigneerd bouwen. Geen vlaggen om te onthouden; de aanwezigheid van de secrets is de configuratie.
Niets daarvan is glamoureus. Het is allemaal het soort ding dat, als het je verrast binnen een CI-runner, je een uur push-wachten-falen-logs-lezen per poging kost — tegen 10x-facturering. Opgelost op een Mac waar je voor zit, kost elk daarvan een minuut.
De tweede poging: zesenzestig regels
Toen CI terugkwam in v1.8.4, was de workflow 66 regels, en zijn bepalende eigenschap is hoe weinig hij doet. De commit beschrijft het simpel: "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." Elke ontwerpbeslissing erin is een les die de eerste poging met pijn heeft opgeleverd:
workflow_dispatch, niet tag-getriggerd. Je start een release door op "Run workflow" te klikken. Een mens knijpt elke betaalde macOS-minuut af. Het kostenprobleem dat de eerste versie de das omdeed, wordt opgelost door de trigger simpelweg niet te automatiseren — de ene plek waar automatisering actief schadelijk was.- Een
product-input. De dispatch neemt een dropdown —geekbyeofpavleur— zodat dezelfde workflow beide merken uitbrengt. De white-label-naad loopt helemaal door tot aan de release-knop. - Twee parallelle jobs.
build-macopmacos-latest,build-winopwindows-latest, tegelijk draaiend. Windows in CI is hier echt nieuw; de oktober-workflow bouwde het nooit. - Geen signeer-logica in de YAML. Dit is het hele punt. Er is geen keychain-gejongleer, geen cert-importstap, geen opschoning. De Mac-job draait
node scripts/release.js <product> --publishen de Windows-job draait hetzelfde met--no-sign. Alles wat de eerste workflow met de hand in YAML deed, leeft nu in het script dat al werkt. De workflow is een orkestrator, geen implementatie.
De twee platforms willen tegengestelde dingen, en de jobs weerspiegelen dat eerlijk. macOS bouwt onder een hardened runtime, gesigneerd en genotariseerd via het ingebouwde pad van electron-builder (@electron/notarize), met de credentials aangeleverd vanuit repository-secrets en autogedetecteerd door het script. Windows bouwt ongesigneerde NSIS-installers voor x64 en arm64. Eén workflow, twee jobs, twee volledig verschillende opvattingen van "klaar."
Het bewijs zit in wat niet gebeurde
Zo weet je dat de volgorde klopte: na de twee tweaks van dezelfde dag die de runs een naam gaven en de naam van een token-secret repareerden, werd release.yml vier maanden lang niet meer aangeraakt. Geen hotfix voor een signeerfout. Geen paniekerige scramble om een notarisatie-afwijzing. Geen "de native binary ontbreekt op de runner"-paniek. Voor een code-signing-en-notarisatie-pipeline — het genre CI dat het beruchtst is om zijn geworstel — is vier maanden stilte bijna ongehoord.
Het was stil omdat het lawaai al ergens goedkopers had plaatsgevonden. De gevechten die zich meestal afspelen in een CI-logs-tab, tegen 10x-facturering, één force-push tegelijk, hadden zich tijdens het lokale tijdperk afgespeeld op de Macs van ontwikkelaars. CI hoefde niet de plek te zijn waar de packaging werd uitgedebugd, want de packaging was al uitgedebugd. Dat is de stelling in één regel: verhuis de build pas van je Mac af nadat je Mac je niet meer verrast.
De ene koppeling die je nog kan opbreken
Het is niet helemaal zonder zwakke plek, en die is het benoemen waard omdat hij subtiel is. De Swift-binaries waar de macOS-app van afhangt — OCR, schermopname, de transcribers — staan in git. Maar de release levert niet de ingecheckte kopieën; de build hercompileert ze op de runner. En een check-swift-version.js-poort laat de hele release hard falen als de compiler niet de vastgezette versie is. Bij deze release was die vastgezette versie Swift 6.2.x, en niets in de workflow installeert het — de job vertrouwt er simpelweg op dat de standaard-Swift van macos-latest overeenkomt. De dag dat GitHub zijn runner-image voorbij de vastgezette versie tilt, stopt de release, niet omdat er iets mis is met de app, maar omdat de pipeline stilletjes gekoppeld is aan een machine-image dat hij niet in de hand heeft. Het is het ene implementatiedetail dat deze dunne workflow niet heeft weggeduwd in iets wat hij zelf bezit.
De andere helft van de release: kleinere payloads
De release bracht één ongerelateerde feature die een vermelding waard is, want het is een schoon voorbeeld van latentiewinst door weglaten. De Assist Me-actie stuurde de backend bij elke request meer context dan nodig. Eén commit snoeide twee dingen: de meegestuurde transcriptgeschiedenis daalde van de laatste 30 entries naar de laatste 15, en de profielcontextbestanden van de gebruiker — bij elke call in de systeemprompt geïnjecteerd — werden hard afgetopt op 4,000 tekens met een afkapmarkering. Minder om te serialiseren, minder om te uploaden, minder voor het model om te lezen voordat het begint met antwoorden, wat de metriek is waarvoor de commit optimaliseert: time-to-first-token. In het belang van eerlijkheid: de commit claimt de versnelling maar meet die niet — er staat geen voor/na-getal in de geschiedenis, dus behandel het als een goed beredeneerde snoeibeurt in plaats van een gebenchmarkte winst.
Drie dingen die deze release ons leerde
- CI is een dun laagje boven een script dat al werkt. De workflow van 66 regels heeft geen eigen signeer-logica; hij roept een script aan dat drie maanden lokale releases al hadden uitgedebugd. Stop het mechanisme in iets wat je met de hand kunt draaien, en laat CI alleen beslissen wanneer het draait.
- Automatiseer het werk, niet per se de trigger. De eerste poging sneuvelde omdat tag-getriggerde releases automatisch 10x-gefactureerde runnerminuten opsoupeerden. Een handmatige
workflow_dispatchbehoudt de automatisering en verwijdert het deel dat geld kostte — soms is de mens in de lus juist de feature. - Debug waar itereren goedkoop is. Elk packaging-gevecht dat op de Mac van een ontwikkelaar wordt beslecht, is een gevecht dat nooit plaatsvindt in een CI-logs-tab tegen tien keer de prijs. Verhuis als laatste van de Mac af, niet als eerste.
Voor het vorige hoofdstuk in het v1-verhaal, dertig talen uitbrengen zonder vangnet (v1.8.3); en voor de hele boog, de anatomie van software tot in de perfectie uitbrengen.