
Punând lansarea în CI, de două ori
GeekBye v1.8.4 a mutat build-urile de lansare în CI pentru macOS și Windows. Ce nu spune changelog-ul e că am mai încercat asta o dată, am șters-o o lună mai târziu din cauza costurilor runnerelor și am făcut-o să reziste abia a doua oară — pentru că până atunci scriptul de lansare deja funcționa manual.
Changelog-ul v1.8.4 are o linie care sună a pură administrare de rutină: „Build-urile de lansare vin acum din CI atât pentru macOS, cât și pentru Windows." Pare genul de lucru care se întâmplă o dată, curat, și la care nu te mai gândești niciodată. Nu a fost așa. Echipa mai pusese o dată lansările în CI — și le smulsese de acolo. Această lansare e a doua încercare, iar motivul pentru care a rezistat e adevărata lecție de inginerie, așa că hai s-o spunem în ordine.
Prima încercare și de ce a murit
Încă din octombrie 2025 exista un release.yml. Era declanșat la push de tag — împingi un tag v*.*.* și un runner macOS pornea, construia și publica. Își făcea propria configurare de semnare manual: un pas care decoda certificatul de semnare, crea și debloca un keychain temporar, importa certificatul ca să-l poată găsi codesign și un pas de curățare pe măsură la final. Funcționa. Era și doar pentru macOS — lansările Windows nu erau deloc în CI.
Pe 4 noiembrie, a fost șters. Mesajul de commit e neobișnuit de sincer despre motiv: "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." (elimină workflow-ul de lansare din GitHub Actions pentru a economisi minute / runnerele macOS costă un multiplicator de 10x — 100 de minute facturate per lansare de 10 minute. Lansările se vor face local.) Asta e toată povestea primei încercări într-o singură propoziție. GitHub facturează timpul de runner macOS la de zece ori tariful Linux-ului, iar o lansare declanșată de tag înseamnă că fiecare tag — inclusiv cele de unică folosință, re-tag-urile, cele de „ups, am uitat să urc versiunea" — consumă în tăcere o sută de minute facturate. Automatizarea pe care n-o controlezi e automatizare care cheltuiește bani cât dormi. Așa că lansările s-au întors pe Mac-ul unui developer pentru următoarele trei luni și jumătate.
Unde s-au rezolvat de fapt problemele grele
Iată partea care face a doua încercare să funcționeze, și s-a întâmplat în întregime în afara CI. În timpul acelor trei luni și jumătate de lansări locale, scripts/release.js — scriptul Node simplu pe care un developer îl rulează manual — a absorbit fiecare problemă de împachetare pe care o avea aplicația, câte un fix pe rând:
- Restaurare white-label. Acesta e un codebase white-label care construiește atât GeekBye, cât și Pavleur dintr-o singură sursă, ceea ce înseamnă că o lansare schimbă câmpuri din
package.jsonși asset-uri de iconuri pentru produsul-țintă. Două fix-uri au învățat scriptul să pună totul la loc după aceea, așa că build-ul de Pavleur nu-ți lăsa arborele git murdar cu identitatea lui Pavleur. - Windows dual-arch. Un fix ca să construiască ambele installere Windows, x64 și arm64, în loc de unul.
- Workaround-ul pentru Windows nesemnat. Un electron-builder mai nou ignora flag-ul de config menit să dezactiveze semnarea pe Windows, așa că scriptul a învățat să forțeze un build nesemnat setând în schimb
CSC_IDENTITY_AUTO_DISCOVERY=falseîn mediu — o cută pe care o descoperi doar lovind-o. - Autodetecția notarizării. Scriptul a învățat să se uite la mediul lui și să decidă: credențiale de semnare prezente? Semnează. Sunt prezente și credențialele de notarizare Apple? Notarizează. Niciuna? Build nesemnat. Niciun flag de reținut; prezența secretelor este configurația.
Niciuna dintre astea nu e glamuroasă. Toate sunt genul de lucru care, dacă te surprinde înăuntrul unui runner CI, te costă o oră de push-așteaptă-eșuează-citește-logurile per încercare — la facturare de 10x. Rezolvate pe un Mac în fața căruia stai, fiecare costă un minut.
A doua încercare: șaizeci și șase de linii
Când CI a revenit în v1.8.4, workflow-ul avea 66 de linii, iar calitatea lui definitorie e cât de puțin face. Commit-ul îl descrie simplu: "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." (declanșator manual workflow_dispatch care construiește ambele platforme în paralel, reutilizând scriptul release.js existent. Build-urile macOS sunt semnate și notarizate, build-urile Windows sunt nesemnate.) Fiecare decizie de design din el e o cicatrice de la prima încercare:
workflow_dispatch, nu declanșat de tag. Începi o lansare apăsând „Run workflow". Un om aprobă fiecare minut macOS plătit. Problema de cost care a ucis prima versiune se rezolvă pur și simplu neautomatizând declanșatorul — singurul loc unde automatizarea era activ dăunătoare.- Un input
product. Dispatch-ul primește un dropdown —geekbyesaupavleur— așa că același workflow livrează oricare brand. Cusătura white-label ajunge tocmai până la butonul de lansare. - Două joburi paralele.
build-macpemacos-latest,build-winpewindows-latest, rulând în același timp. Windows în CI e cu adevărat nou aici; workflow-ul din octombrie nu-l construia niciodată. - Nicio logică de semnare în YAML. Ăsta e tot rostul. Nu există jonglerie cu keychain, niciun pas de import de certificat, nicio curățare. Jobul Mac rulează
node scripts/release.js <product> --publish, iar jobul Windows rulează la fel, cu--no-sign. Tot ce făcea primul workflow manual în YAML trăiește acum în scriptul care deja funcționează. Workflow-ul e un orchestrator, nu o implementare.
Cele două platforme vor lucruri opuse, iar joburile o reflectă cinstit. macOS construiește sub un hardened runtime, semnat și notarizat prin calea încorporată a lui electron-builder (@electron/notarize), cu credențialele furnizate din secretele repository-ului și autodetectate de script. Windows construiește installere NSIS nesemnate pentru x64 și arm64. Un singur workflow, două joburi, două noțiuni complet diferite de „gata".
Dovada e în ce nu s-a întâmplat
Iată cum știi că ordinea a fost corectă: după cele două ajustări din aceeași zi care au numit rulările și au reparat numele unui secret de token, release.yml n-a mai fost atins timp de patru luni. Niciun hotfix de eșec de semnare. Nicio panică de respingere a notarizării. Niciun „binarul nativ lipsește de pe runner". Pentru un pipeline de semnare-și-notarizare — genul de CI cel mai notoriu pentru zbateri — patru luni de liniște e aproape nemaiauzit.
A fost liniște pentru că zgomotul se întâmplase deja undeva mai ieftin. Luptele care de obicei se desfășoară într-un tab de loguri CI, la facturare de 10x, câte un force-push pe rând, se desfășuraseră pe Mac-urile developerilor în timpul erei locale. CI n-a trebuit să fie locul unde se depana împachetarea, pentru că împachetarea era deja depanată. Asta e teza într-o singură linie: mută build-ul de pe Mac-ul tău abia după ce Mac-ul tău a încetat să te mai surprindă.
Singura cuplare care încă poate mușca
Nu e complet fără un punct slab, și merită numit pentru că e subtil. Binarele Swift de care depinde aplicația macOS — OCR, captura de ecran, transcriptoarele — sunt comise în git. Dar lansarea nu livrează copiile comise; build-ul le recompilează pe runner. Iar o poartă check-swift-version.js oprește dur întreaga lansare dacă compilatorul nu e versiunea fixată. La această lansare, acea fixare era Swift 6.2.x, și nimic din workflow nu-l instalează — jobul pur și simplu se încrede că Swift-ul implicit al lui macos-latest se potrivește. În ziua în care GitHub își urcă imaginea de runner peste versiunea fixată, lansarea se oprește, nu pentru că ar fi ceva în neregulă cu aplicația, ci pentru că pipeline-ul e cuplat în tăcere de o imagine de mașină pe care n-o controlează. E singurul detaliu de implementare pe care acest workflow subțire nu l-a împins în jos, într-un lucru pe care-l deține el.
Cealaltă jumătate a lansării: payload-uri mai mici
Lansarea a purtat o funcționalitate neînrudită care merită menționată, pentru că e un exemplu curat de fix de latență prin scădere. Acțiunea Assist Me trimitea backend-ului mai mult context decât îi trebuia la fiecare cerere. Un commit a tăiat două lucruri: istoricul transcrierii trimis odată cu ea a scăzut de la ultimele 30 de intrări la ultimele 15, iar fișierele de context ale profilului utilizatorului — injectate în system prompt la fiecare apel — au fost plafonate dur la 4,000 de caractere cu un marcaj de trunchiere. Mai puțin de serializat, mai puțin de urcat, mai puțin de citit pentru model înainte să înceapă să răspundă, care e metrica pe care commit-ul o optimizează: time-to-first-token. De dragul onestității: commit-ul revendică accelerarea, dar n-o măsoară — nu există niciun număr înainte/după în istoric, așa că tratează-l ca pe o tăiere bine argumentată, nu ca pe un câștig măsurat cu benchmark.
Trei lucruri pe care ni le-a predat această lansare
- CI e un wrapper subțire peste un script care deja funcționează. Workflow-ul de 66 de linii nu are logică de semnare proprie; apelează un script pe care trei luni de lansări locale îl depanaseră deja. Pune mecanismul într-un lucru pe care-l poți rula manual și lasă CI-ul doar să decidă când să-l ruleze.
- Automatizează munca, nu neapărat declanșatorul. Prima încercare a murit pentru că lansările declanșate de tag consumau automat minute de runner facturate la 10x. Un
workflow_dispatchmanual păstrează automatizarea și înlătură partea care costa bani — uneori omul din buclă e funcționalitatea. - Depanează acolo unde iterația e ieftină. Fiecare luptă de împachetare rezolvată pe Mac-ul unui developer e o luptă care nu se întâmplă niciodată într-un tab de loguri CI la de zece ori prețul. Mută-te de pe Mac ultimul, nu primul.
Pentru capitolul anterior al poveștii v1, livrarea a treizeci de limbi fără plasă de siguranță (v1.8.3); iar pentru întregul arc, anatomia livrării de software până la perfecțiune.