Steven
Steven8 мин четене

Слагане на издаването в CI, два пъти

GeekBye v1.8.4 премести release build-овете в CI за macOS и Windows. Това, което changelog-ът не казва, е че вече опитахме това веднъж, изтрихме го месец по-късно заради цената на runner-ите и го накарахме да се задържи чак на втория път — защото дотогава release скриптът вече работеше на ръка.

Инженерство
CI/CD
Desktop
Издания на GeekBye
Слагане на издаването в CI, два пъти

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. В името на честността: комитът твърди ускорението, но не го измерва — няма число преди/след в историята, така че го третирай като добре обоснована резитба, а не като измерена с бенчмарк печалба.

Три неща, на които ни научи това издание

  1. CI е тънка обвивка над скрипт, който вече работи. Workflow-ът от 66 реда няма собствена логика за подписване; той извиква скрипт, който три месеца локални издания вече бяха дебъгнали. Сложи механизма в нещо, което можеш да пуснеш на ръка, и остави CI просто да решава кога да го пусне.
  2. Автоматизирай работата, не непременно тригера. Първият опит умря, защото изданията, задействани от таг, автоматично харчеха runner минути, таксувани с 10x. Ръчен workflow_dispatch запазва автоматизацията и премахва частта, която струваше пари — понякога човекът в цикъла е функцията.
  3. Дебъгвай там, където итерацията е евтина. Всяка битка с пакетирането, решена на Mac-а на разработчик, е битка, която никога не се случва в таб с CI логове при десет пъти цената. Премести се от Mac-а последен, не първи.

За предишната глава от историята на v1 — доставяне на трийсет езика без предпазна мрежа (v1.8.3); а за цялата дъга — анатомията на доставянето на софтуер до съвършенство.

Свързани статии

Три глагола, които поддържат Web Audio жив
Steven
Steven9 мин четене

Три глагола, които поддържат Web Audio жив

Две точкови издания на GeekBye, на два месеца разстояние и в два различни файла, научиха нашия аудио код на един и същ урок от противоположни краища: спри да третираш AudioContext на браузъра като за еднократна употреба. Едно издание се научи да прави resume() на контекст, който macOS тихо беше суспендирал по средата на записа; другото се научи да прави suspend() вместо close(), за да спрат последователните сесии да се блъскат в тавана на Chromium от приблизително шест контекста. Resume, suspend, close — това е целият сюжет.

Инженерство
Audio
Desktop
Да различиш обаждане от отворено приложение
Steven
Steven8 мин четене

Да различиш обаждане от отворено приложение

GeekBye може да забележи, че си се присъединил към видео среща, и да ти предложи да я запише. Разпознаването се оказва лесната половина — Swift binary, който чете заглавия на прозорци на всеки десет секунди. Трудната половина е прецизността: да не се задейства, когато Zoom е просто отворен, да не пита за среща, която вече записваш, и да не заглушава микрофона в обаждането, в което всъщност си. Три издания, всяко от които е предпазител, който трябваше да се научи да не побеждава сам себе си.

Инженерство
macOS
Desktop
Изваждане на backend-а от пътя на качването
Steven
Steven8 мин четене

Изваждане на backend-а от пътя на качването

GeekBye записва екрана ти и запазва видеото в твоя Google Drive. Първата версия прекарваше всеки запис през собствените сървъри на GeekBye по пътя натам; едно издание по-късно, файлът тръгваше директно от машината ти към Drive, а backend-ът беше понижен до това да държи един-единствен указател. Интересната част е колко малко код всъщност съдържа „директната, resumable" версия — защото resumability дойде от изтриването на един proxy, не от написването на такъв.

Инженерство
Архитектура
Desktop