Steven
Steven7 min czytania

Wpięcie wydań do CI, dwa razy

GeekBye v1.8.4 przeniósł buildy wydań do CI dla macOS i Windows. Czego changelog nie mówi, to że próbowaliśmy tego już raz wcześniej, skasowaliśmy to miesiąc później z powodu kosztów runnerów i dopiero za drugim razem się przyjęło — bo do tego czasu skrypt wydania już działał ręcznie.

Inżynieria
CI/CD
Desktop
Wydania GeekBye
Wpięcie wydań do CI, dwa razy

Changelog v1.8.4 ma linijkę, która brzmi jak czysta sprawa porządkowa: „Buildy wydań pochodzą teraz z CI zarówno dla macOS, jak i Windows". Czyta się to jak coś, co dzieje się raz, czysto, i o czym nikt już nigdy nie myśli. Tak nie było. Zespół już raz wpiął wydania do CI — i wyrwał to z powrotem. To wydanie jest drugą próbą, a powód, dla którego się przyjęło, to właściwa lekcja inżynierska, więc opowiedzmy to po kolei.

Pierwsza próba i dlaczego padła

W październiku 2025 istniał release.yml. Był wyzwalany wypchnięciem taga — wrzuć tag v*.*.*, a runner macOS się podniesie, zbuduje i opublikuje. Sam robił swoją konfigurację podpisywania, ręcznie: krok, który dekodował certyfikat podpisujący, tworzył i odblokowywał tymczasowy keychain, importował certyfikat, żeby codesign mógł go znaleźć, i pasujący krok sprzątający na końcu. Działało. Było też tylko dla macOS — wydań Windows w CI nie było wcale.

4 listopada został skasowany. Komunikat commita jest nietypowo szczery co do powodu: „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." To cała historia pierwszej próby w jednym zdaniu. GitHub liczy czas runnera macOS dziesięciokrotnie drożej niż Linuksa, a wydanie wyzwalane tagiem oznacza, że każdy pojedynczy tag — w tym te jednorazowe, ponowne tagi, te „ups, zapomniałem podbić wersję" — po cichu wydaje sto rozliczanych minut. Automatyzacja, której nie kontrolujesz, to automatyzacja, która wydaje pieniądze, gdy śpisz. Więc wydania wróciły na Maca programisty na kolejne trzy i pół miesiąca.

Gdzie naprawdę rozwiązano trudne problemy

Oto część, dzięki której druga próba działa, a wydarzyła się w całości poza CI. Przez te trzy i pół miesiąca lokalnych wydań scripts/release.js — zwykły skrypt Node, który programista odpala ręcznie — wchłonął każdy problem pakowania, jaki miała aplikacja, jedną poprawką naraz:

  • Przywracanie white-label. To baza kodu typu white-label, która buduje i GeekBye, i Pavleur z jednego źródła, co znaczy, że wydanie podmienia pola package.json i assety ikon na docelowy produkt. Dwie poprawki nauczyły skrypt, by potem wszystko przywracał, więc zbudowanie Pavleura nie zostawiało twojego drzewa gita brudnego tożsamością Pavleura.
  • Dwuarchitekturowy Windows. Poprawka, by budować oba instalatory Windows, x64 i arm64, zamiast jednego.
  • Obejście dla niepodpisanego Windows. Nowszy electron-builder ignorował flagę konfiguracji, która miała wyłączać podpisywanie Windows, więc skrypt nauczył się wymuszać niepodpisany build, ustawiając zamiast tego CSC_IDENTITY_AUTO_DISCOVERY=false w środowisku — zmarszczka, którą znajdujesz dopiero, gdy na nią wpadniesz.
  • Autodetekcja notaryzacji. Skrypt nauczył się patrzeć na swoje środowisko i decydować: są poświadczenia do podpisywania? Podpisz. Są też poświadczenia notaryzacji Apple? Notaryzuj. Żadnych? Buduj niepodpisany. Żadnych flag do zapamiętania; obecność sekretów jest konfiguracją.

Nic z tego nie jest efektowne. Wszystko to jest tego rodzaju, że jeśli zaskoczy cię wewnątrz runnera CI, kosztuje cię godzinę cyklu push-czekaj-porażka-czytaj-logi na próbę — przy rozliczaniu 10x. Rozwiązane na Macu, przed którym siedzisz, każde kosztuje minutę.

Druga próba: sześćdziesiąt sześć linii

Kiedy CI wróciło w v1.8.4, workflow miał 66 linii, a jego definiującą cechą jest to, jak mało robi. Commit opisuje to wprost: „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." Każda decyzja projektowa w nim to blizna po pierwszej próbie:

  • workflow_dispatch, nie wyzwalanie tagiem. Wydanie zaczynasz, klikając „Run workflow". Człowiek bramkuje każdą płatną minutę macOS. Problem kosztów, który zabił pierwszą wersję, rozwiązano po prostu przez nieautomatyzowanie wyzwalacza — jedynego miejsca, w którym automatyzacja była aktywnie szkodliwa.
  • Wejście product. Dispatch przyjmuje listę rozwijaną — geekbye lub pavleur — więc ten sam workflow wysyła którąkolwiek markę. Szew white-label sięga aż do przycisku wydania.
  • Dwa równoległe zadania. build-mac na macos-latest, build-win na windows-latest, działające jednocześnie. Windows w CI jest tu naprawdę nowy; październikowy workflow nigdy go nie budował.
  • Żadnej logiki podpisywania w YAML-u. O to właśnie chodzi. Nie ma żonglowania keychainem, nie ma kroku importu certyfikatu, nie ma sprzątania. Zadanie Maca uruchamia node scripts/release.js <product> --publish, a zadanie Windows to samo z --no-sign. Wszystko, co pierwszy workflow robił ręcznie w YAML-u, żyje teraz w skrypcie, który już działa. Workflow jest orkiestratorem, nie implementacją.

Obie platformy chcą przeciwnych rzeczy, a zadania odzwierciedlają to uczciwie. macOS buduje pod hardened runtime, podpisany i notaryzowany wbudowaną ścieżką electron-buildera (@electron/notarize), z poświadczeniami dostarczanymi z sekretów repozytorium i wykrywanymi automatycznie przez skrypt. Windows buduje niepodpisane instalatory NSIS dla x64 i arm64. Jeden workflow, dwa zadania, dwa zupełnie różne pojęcia „gotowe".

Dowód jest w tym, co się nie wydarzyło

Oto jak poznać, że kolejność była słuszna: po dwóch tego samego dnia poprawkach, które nazwały uruchomienia i naprawiły nazwę sekretu z tokenem, release.yml nie został tknięty przez cztery miesiące. Żadnej pilnej poprawki na porażkę podpisywania. Żadnej szarpaniny z odrzuceniem notaryzacji. Żadnej paniki „natywnego pliku binarnego brakuje na runnerze". Jak na pipeline podpisywania-i-notaryzacji — gatunek CI najbardziej osławiony z nieustannego szarpania się — cztery miesiące ciszy to niemal rzecz niesłychana.

Było cicho, bo hałas już się wydarzył gdzieś taniej. Walki, które zwykle rozgrywają się w zakładce logów CI, przy rozliczaniu 10x, po jednym force-pushu naraz, rozegrały się na Macach programistów w erze lokalnej. CI nie musiało być miejscem, w którym debuguje się pakowanie, bo pakowanie było już zdebugowane. Oto teza w jednej linii: przenieś build z Maca dopiero, gdy twój Mac przestanie cię zaskakiwać.

Jedno sprzężenie, które wciąż może ugryźć

Nie jest całkiem bez czułego punktu i warto go nazwać, bo jest subtelny. Binaria Swift, od których zależy aplikacja macOS — OCR, przechwytywanie ekranu, transkrybery — są wrzucone do gita. Ale wydanie nie wysyła wrzuconych kopii; build rekompiluje je na runnerze. A bramka check-swift-version.js twardo wywala całe wydanie, jeśli kompilator nie jest przypiętą wersją. Przy tym wydaniu tym przypięciem był Swift 6.2.x i nic w workflow go nie instaluje — zadanie po prostu ufa, że domyślny Swift macos-latest się zgadza. W dniu, w którym GitHub podbije obraz runnera ponad przypięcie, wydanie się zatrzyma — nie dlatego, że coś jest nie tak z aplikacją, ale dlatego, że pipeline jest po cichu sprzężony z obrazem maszyny, którego nie kontroluje. To jedyny szczegół implementacyjny, którego ten cienki workflow nie zepchnął w dół do czegoś, co sam posiada.

Druga połowa wydania: mniejsze payloady

Wydanie niosło jeden niepowiązany feature wart wzmianki, bo to czysty przykład naprawy opóźnienia przez odejmowanie. Akcja Assist Me wysyłała do backendu więcej kontekstu, niż potrzebowała, przy każdym żądaniu. Jeden commit przyciął dwie rzeczy: dołączana historia transkrypcji spadła z ostatnich 30 wpisów do ostatnich 15, a pliki kontekstu profilu użytkownika — wstrzykiwane do promptu systemowego przy każdym wywołaniu — dostały twardy limit 4,000 znaków ze znacznikiem obcięcia. Mniej do serializacji, mniej do wysłania, mniej do przeczytania dla modelu, zanim zacznie odpowiadać, a to jest metryka, którą commit optymalizuje: time-to-first-token. W trosce o uczciwość: commit deklaruje przyspieszenie, ale go nie mierzy — w historii nie ma liczby przed/po, więc traktuj to jako dobrze uzasadnione przycięcie, a nie zwycięstwo potwierdzone benchmarkiem.

Trzy rzeczy, których nauczyło nas to wydanie

  1. CI to cienka nakładka na skrypt, który już działa. 66-liniowy workflow nie ma własnej logiki podpisywania; wywołuje skrypt, który trzy miesiące lokalnych wydań już zdebugowały. Umieść mechanizm w czymś, co umiesz odpalić ręcznie, a CI niech tylko decyduje, kiedy to odpalić.
  2. Automatyzuj pracę, niekoniecznie wyzwalacz. Pierwsza próba padła, bo wydania wyzwalane tagiem automatycznie przepalały rozliczane 10x minuty runnera. Ręczny workflow_dispatch zachowuje automatyzację i usuwa część, która kosztowała pieniądze — czasem człowiek w pętli jest właśnie tą funkcją.
  3. Debuguj tam, gdzie iteracja jest tania. Każda walka o pakowanie rozwiązana na Macu programisty to walka, która nigdy nie dzieje się w zakładce logów CI po dziesięciokrotnej cenie. Zejdź z Maca na końcu, nie na początku.

Po poprzedni rozdział historii v1 — wysłanie trzydziestu języków bez siatki asekuracyjnej (v1.8.3); a po cały łuk — anatomia dostarczania oprogramowania do perfekcji.