
Релизы в 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); а про весь путь — анатомию доведения софта до совершенства.