
Релізи в CI, двічі
GeekBye v1.8.4 переніс збірки релізів у CI для macOS і Windows. Чого changelog не каже — що ми вже пробували це раніше, видалили місяць потому через вартість ранерів і змусили прижитися лише з другого разу, бо на той момент скрипт релізу вже працював вручну.
Changelog v1.8.4 містить рядок, що звучить як чиста господарська рутина: «Збірки релізів тепер ідуть із CI і для macOS, і для Windows». Читається це як щось, що стається один раз, чисто, і про що більше ніколи не думаєш. Це не так. Команда вже одного разу помістила релізи в CI — і вирвала назад. Цей реліз — друга спроба, а причина, з якої вона прижилася, і є справжній інженерний урок, тож розкажемо по порядку.
Перша спроба і чому вона померла
У жовтні 2025 існував release.yml. Він запускався пушем тега — закинь тег v*.*.*, і ранер macOS підніметься, збере й опублікує. Він сам робив своє налаштування підписання, вручну: крок, який декодував сертифікат підпису, створював і розблоковував тимчасовий 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.» Це вся історія першої спроби в одному реченні. GitHub тарифікує час ранера macOS вдесятеро дорожче за Linux, а реліз за тегом означає, що кожен до єдиного тег — включно з одноразовими, повторними тегами, тими самими «ой, забув підняти версію» — тихо витрачає сотню тарифікованих хвилин. Автоматизація, якої ти не контролюєш, — це автоматизація, що витрачає гроші, поки ти спиш. Тож релізи повернулися на Mac розробника на наступні три з половиною місяці.
Де насправді розв'язалися важкі проблеми
Ось частина, завдяки якій друга спроба працює, і сталася вона цілком поза CI. За ті три з половиною місяці локальних релізів scripts/release.js — звичайний Node-скрипт, який розробник запускає вручну — увібрав кожну проблему пакування, що була в застосунку, по одній правці за раз:
- Відновлення white-label. Це white-label кодова база, що збирає і GeekBye, і Pavleur з одного джерела, а отже реліз підмінює поля
package.jsonі ассети іконок під цільовий продукт. Дві правки навчили скрипт повертати все назад, щоб збірка Pavleur не лишала твоє дерево git брудним з ідентичністю Pavleur. - Двоархітектурний Windows. Правка, щоб збирати обидва інсталятори Windows, x64 і arm64, замість одного.
- Обхід для непідписаного Windows. Новіший electron-builder ігнорував прапорець конфігурації, що мав вимикати підпис Windows, тож скрипт навчився примусово робити непідписану збірку, виставляючи натомість
CSC_IDENTITY_AUTO_DISCOVERY=falseв оточенні — зморшка, яку знаходиш, лише наштовхнувшись на неї. - Автовизначення нотаризації. Скрипт навчився дивитися на своє оточення й вирішувати: є облікові дані для підпису? Підписуй. Є ще й облікові дані нотаризації Apple? Нотаризуй. Ні тих, ні інших? Збирай непідписане. Жодних прапорців запам'ятовувати; наявність секретів і є конфігурація.
Нічого з цього не гламурне. Усе це того ґатунку, що якщо застане тебе зненацька всередині ранера CI, коштує тобі години циклу пуш-чекай-провал-читай-логи за спробу — за тарифікації 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, а не запуск за тегом. Реліз ти починаєш, натиснувши «Run workflow». Людина пропускає через ворота кожну платну хвилину macOS. Проблему вартості, що вбила першу версію, розв'язано простою відмовою автоматизувати тригер — єдине місце, де автоматизація була активно шкідливою.- Вхід
product. Диспетч приймає випадний список —geekbyeабоpavleur— тож той самий workflow випускає будь-який бренд. Шов white-label тягнеться аж до кнопки релізу. - Дві паралельні задачі.
build-macнаmacos-latest,build-winнаwindows-latest, що працюють одночасно. Windows у CI тут по-справжньому новий; жовтневий workflow його ніколи не збирав. - Жодної логіки підписання в YAML. У цьому весь сенс. Немає жонглювання keychain, немає кроку імпорту сертифіката, немає очищення. Задача Mac запускає
node scripts/release.js <product> --publish, а задача Windows — те саме з--no-sign. Усе, що перший workflow робив вручну в YAML, тепер живе в скрипті, який уже працює. Workflow — оркестратор, а не реалізація.
Дві платформи хочуть протилежного, і задачі відображають це чесно. macOS збирається під hardened runtime, підписаний і нотаризований вбудованим шляхом electron-builder (@electron/notarize), з обліковими даними, поданими із секретів репозиторію й автовизначеними скриптом. Windows збирає непідписані інсталятори NSIS для x64 і arm64. Один workflow, дві задачі, два цілком різні поняття «готово».
Доказ — у тому, чого не сталося
Ось як зрозуміти, що послідовність була правильною: після двох правок того самого дня, що дали імена запускам і виправили ім'я секрету з токеном, release.yml не чіпали знову чотири місяці. Жодного термінового фіксу на провал підписання. Жодної метушні з відмовою нотаризації. Жодної паніки «нативного бінарника немає на ранері». Як на пайплайн підписання-і-нотаризації — жанр CI, найбільш сумнозвісний своєю смиканиною — чотири місяці тиші майже нечувані.
Було тихо, бо шум уже стався десь дешевше. Сутички, що зазвичай розігруються у вкладці логів CI, за тарифікації 10x, по одному форс-пушу за раз, розігралися на Mac розробників у локальну епоху. CI не мусило бути місцем, де налагоджують пакування, бо пакування вже було налагоджене. Ось теза в один рядок: переноси збірку з Mac лише після того, як твій Mac перестане тебе дивувати.
Єдине зчеплення, що ще може вкусити
Не зовсім без слабкого місця, і його варто назвати, бо воно невловне. Бінарники Swift, від яких залежить застосунок macOS — OCR, захоплення екрана, транскрайбери — закомічені в git. Але реліз не відвантажує закомічені копії; збірка перекомпільовує їх на ранері. А ворота check-swift-version.js жорстко валять весь реліз, якщо компілятор не закріпленої версії. На цьому релізі закріпленням був Swift 6.2.x, і ніщо в workflow його не встановлює — задача просто довіряє, що дефолтний Swift macos-latest збігається. У день, коли GitHub підніме образ ранера вище закріплення, реліз стане — не тому, що з застосунком щось не так, а тому, що пайплайн тихо зчеплений з образом машини, якого не контролює. Це єдина деталь реалізації, яку цей тонкий workflow не спустив униз, у щось своє.
Друга половина релізу: менші пейлоади
Реліз ніс одну неповʼязану фічу, варту згадки, бо це чистий приклад лагодження затримки відніманням. Дія Assist Me надсилала на бекенд більше контексту, ніж було потрібно, на кожному запиті. Один коміт врізав дві речі: додавана історія транскрипту впала з останніх 30 записів до останніх 15, а файли контексту профілю користувача — які вставляються в системний промпт при кожному виклику — дістали жорстке обмеження у 4,000 символів із маркером усічення. Менше серіалізувати, менше вивантажувати, менше читати моделі, перш ніж вона почне відповідати, а це і є метрика, яку коміт оптимізує: time-to-first-token. Заради чесності: коміт заявляє прискорення, але не вимірює його — в історії немає числа до/після, тож стався до цього як до добре обґрунтованого врізання, а не як до перемоги, підтвердженої бенчмарком.
Три речі, яких нас навчив цей реліз
- CI — тонка обгортка над скриптом, який уже працює. 66-рядковий workflow не має власної логіки підписання; він викликає скрипт, який три місяці локальних релізів уже налагодили. Помісти механізм у те, що можеш запустити вручну, і хай CI лише вирішує, коли його запустити.
- Автоматизуй роботу, не обовʼязково тригер. Перша спроба померла, бо релізи за тегом автоматично витрачали тарифіковані 10x хвилини ранера. Ручний
workflow_dispatchзберігає автоматизацію й прибирає частину, що коштувала грошей — іноді людина в петлі і є та сама фіча. - Налагоджуй там, де ітерація дешева. Кожна сутичка за пакування, розв'язана на Mac розробника, — це сутичка, що ніколи не стається у вкладці логів CI за десятикратну ціну. Іди з Mac останнім, а не першим.
Про попередній розділ історії v1 — випуск тридцяти мов без страхувальної сітки (v1.8.3); а про всю дугу — анатомію доставки програмного забезпечення до досконалості.