
Слагане на издаването в CI, два пъти
GeekBye v1.8.4 премести release build-овете в CI за macOS и Windows. Това, което changelog-ът не казва, е че вече опитахме това веднъж, изтрихме го месец по-късно заради цената на runner-ите и го накарахме да се задържи чак на втория път — защото дотогава release скриптът вече работеше на ръка.
Changelog-ът на v1.8.4 има ред, който звучи като чиста домакинска работа: „Release build-овете вече идват от CI и за macOS, и за Windows." Чете се като нещо, което се случва веднъж, чисто, и за което повече не се мисли. Не беше така. Екипът вече беше сложил изданията в CI веднъж — и ги изтръгна оттам. Това издание е вторият опит, а причината да се задържи е истинският инженерен урок, така че нека го разкажем по ред.
Първият опит и защо умря
Още през октомври 2025 съществуваше release.yml. Задействаше се при push на таг — бутваш таг v*.*.* и macOS runner се вдигаше, изграждаше и публикуваше. Правеше си собствената настройка за подписване на ръка: стъпка, която декодираше сертификата за подписване, създаваше и отключваше временен keychain, импортираше сертификата, за да може codesign да го намери, и съответстваща стъпка за почистване накрая. Работеше. Беше и само за macOS — изданията за Windows изобщо не бяха в CI.
На 4 ноември беше изтрит. Съобщението на комита е необичайно откровено за причината: "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." (премахни release workflow-а на GitHub Actions за пестене на минути / macOS runner-ите струват 10x множител — 100 таксувани минути на 10-минутно издание. Изданията ще се правят локално.) Това е цялата история на първия опит в едно изречение. GitHub таксува времето на macOS runner-ите десет пъти по-скъпо от Linux, а издание, задействано от таг, означава, че всеки таг — включително еднократните, повторните тагове, тези от типа „опа, забравих да вдигна версията" — тихо харчи сто таксувани минути. Автоматизация, която не контролираш, е автоматизация, която харчи пари, докато спиш. Така че изданията се върнаха на Mac-а на един разработчик за следващите три месеца и половина.
Къде всъщност се решиха трудните проблеми
Ето частта, която кара вторият опит да проработи, и тя се случи изцяло извън CI. През тези три месеца и половина локални издания scripts/release.js — обикновеният Node скрипт, който разработчик пуска на ръка — погълна всеки проблем с пакетирането, който приложението имаше, по един fix наведнъж:
- White-label възстановяване. Това е white-label codebase, който изгражда и GeekBye, и Pavleur от един източник, което значи, че изданието разменя полета в
package.jsonи иконни asset-и за целевия продукт. Два fix-а научиха скрипта да връща всичко обратно след това, така че изграждането на Pavleur не оставяше git дървото ти мръсно с идентичността на Pavleur. - Dual-arch Windows. Fix, който да изгражда и двата Windows инсталатора, x64 и arm64, вместо един.
- Workaround-ът за неподписан Windows. По-нов electron-builder игнорираше config флага, предназначен да изключи подписването на Windows, така че скриптът се научи да форсира неподписан build, като вместо това задава
CSC_IDENTITY_AUTO_DISCOVERY=falseв средата — гънка, която откриваш само като я удариш. - Автоматично разпознаване на нотаризацията. Скриптът се научи да гледа средата си и да решава: налични ли са credentials за подписване? Подписвай. Налични ли са и Apple credentials за нотаризация? Нотаризирай. Никакви? Неподписан build. Няма флагове за помнене; наличието на секретите е конфигурацията.
Нищо от това не е бляскаво. Всичко е от онзи вид, който, ако те изненада вътре в CI runner, ти струва час push-чакай-провал-чети-логовете на опит — при 10x таксуване. Решени на Mac, пред който седиш, всеки струва по минута.
Вторият опит: шейсет и шест реда
Когато CI се върна във v1.8.4, workflow-ът беше 66 реда, и определящото му качество е колко малко прави. Комитът го описва просто: "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." (ръчен workflow_dispatch тригер, който изгражда двете платформи паралелно, преизползвайки съществуващия release.js скрипт. macOS build-овете са подписани и нотаризирани, Windows build-овете са неподписани.) Всяко дизайнерско решение в него е белег от първия опит:
workflow_dispatch, не задействан от таг. Започваш издание, като натиснеш „Run workflow". Човек пази всяка платена macOS минута. Проблемът с цената, който уби първата версия, се решава просто като не автоматизираш тригера — единственото място, където автоматизацията беше активно вредна.- Вход
product. Dispatch-ът приема dropdown —geekbyeилиpavleur— така че същият workflow доставя всеки от брандовете. White-label шевът минава чак до бутона за издание. - Два паралелни job-а.
build-macнаmacos-latest,build-winнаwindows-latest, работещи по едно и също време. Windows в CI е наистина нов тук; октомврийският workflow никога не го изграждаше. - Никаква логика за подписване в YAML. Това е целият смисъл. Няма жонглиране с keychain, няма стъпка за импорт на сертификат, няма почистване. Mac job-ът пуска
node scripts/release.js <product> --publish, а Windows job-ът пуска същото с--no-sign. Всичко, което първият workflow правеше на ръка в YAML, сега живее в скрипта, който вече работи. Workflow-ът е оркестратор, не имплементация.
Двете платформи искат противоположни неща, и job-овете го отразяват честно. macOS изгражда под hardened runtime, подписан и нотаризиран през вградения път на electron-builder (@electron/notarize), с credentials, подадени от секретите на repository-то и автоматично разпознати от скрипта. Windows изгражда неподписани NSIS инсталатори за x64 и arm64. Един workflow, два job-а, две напълно различни представи за „готово".
Доказателството е в това, което не се случи
Ето как разбираш, че подредбата беше правилна: след двете корекции от същия ден, които именуваха пусканията и поправиха името на един token секрет, release.yml не беше пипан цели четири месеца. Никакъв hotfix за провал на подписването. Никаква паника от отхвърлена нотаризация. Никакво „нативният бинар липсва на runner-а". За pipeline за подписване-и-нотаризация — жанрът CI, най-прословут с мятането си — четири месеца тишина е почти нечувано.
Беше тихо, защото шумът вече беше станал някъде по-евтино. Битките, които обикновено се разиграват в таб с CI логове, при 10x таксуване, по един force-push наведнъж, се бяха разиграли на Mac-овете на разработчиците през локалната ера. CI не трябваше да е мястото, където пакетирането се дебъгва, защото пакетирането вече беше дебъгнато. Това е тезата в един ред: премести build-а от своя Mac едва след като своят Mac спре да те изненадва.
Единствената свързаност, която още може да ухапе
Не е съвсем без слабо място, и си струва да се назове, защото е фино. Swift бинарите, от които зависи macOS приложението — OCR, заснемане на екрана, транскрайбърите — са комитнати в git. Но изданието не доставя комитнатите копия; build-ът ги прекомпилира на runner-а. А порта check-swift-version.js сурово проваля цялото издание, ако компилаторът не е фиксираната версия. При това издание тази фиксация беше Swift 6.2.x, и нищо в workflow-а не го инсталира — job-ът просто вярва, че Swift-ът по подразбиране на macos-latest съвпада. В деня, в който GitHub вдигне образа на runner-а си над фиксацията, изданието спира, не защото нещо е наред с приложението, а защото pipeline-ът е тихо свързан с образ на машина, който не контролира. Това е единственият имплементационен детайл, който този тънък workflow не избута надолу в нещо, което притежава.
Другата половина на изданието: по-малки payload-и
Изданието носеше една несвързана функция, достойна за споменаване, защото е чист пример за fix на латентност чрез изваждане. Действието Assist Me пращаше на backend-а повече контекст, отколкото му трябваше при всяка заявка. Един комит отряза две неща: изпращаната история на транскрипцията падна от последните 30 записа до последните 15, а контекстните файлове на профила на потребителя — инжектирани в system prompt-а при всяко извикване — бяха сурово ограничени до 4,000 знака с маркер за отрязване. По-малко за сериализиране, по-малко за качване, по-малко за четене от модела преди да започне да отговаря, което е метриката, която комитът оптимизира: time-to-first-token. В името на честността: комитът твърди ускорението, но не го измерва — няма число преди/след в историята, така че го третирай като добре обоснована резитба, а не като измерена с бенчмарк печалба.
Три неща, на които ни научи това издание
- CI е тънка обвивка над скрипт, който вече работи. Workflow-ът от 66 реда няма собствена логика за подписване; той извиква скрипт, който три месеца локални издания вече бяха дебъгнали. Сложи механизма в нещо, което можеш да пуснеш на ръка, и остави CI просто да решава кога да го пусне.
- Автоматизирай работата, не непременно тригера. Първият опит умря, защото изданията, задействани от таг, автоматично харчеха runner минути, таксувани с 10x. Ръчен
workflow_dispatchзапазва автоматизацията и премахва частта, която струваше пари — понякога човекът в цикъла е функцията. - Дебъгвай там, където итерацията е евтина. Всяка битка с пакетирането, решена на Mac-а на разработчик, е битка, която никога не се случва в таб с CI логове при десет пъти цената. Премести се от Mac-а последен, не първи.
За предишната глава от историята на v1 — доставяне на трийсет езика без предпазна мрежа (v1.8.3); а за цялата дъга — анатомията на доставянето на софтуер до съвършенство.