Steven
Steven7 мин. чтения

Релизы в CI, дважды

GeekBye v1.8.4 перенёс сборки релизов в CI для macOS и Windows. Чего changelog не говорит — что мы уже пробовали это однажды, удалили месяц спустя из-за стоимости раннеров и заставили прижиться только со второго раза, потому что к тому моменту скрипт релиза уже работал вручную.

Инженерия
CI/CD
Десктоп
Релизы GeekBye
Релизы в CI, дважды

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. Ради честности: коммит заявляет ускорение, но не измеряет его — в истории нет числа до/после, так что относись к этому как к хорошо обоснованному урезанию, а не к победе, подтверждённой бенчмарком.

Три вещи, которым нас научил этот релиз

  1. CI — тонкая обёртка над скриптом, который уже работает. 66-строчный workflow не имеет собственной логики подписания; он вызывает скрипт, который три месяца локальных релизов уже отладили. Помести механизм в то, что можешь запустить вручную, и пусть CI лишь решает, когда его запустить.
  2. Автоматизируй работу, не обязательно триггер. Первая попытка умерла, потому что релизы по тегу автоматически тратили тарифицируемые 10x минуты раннера. Ручной workflow_dispatch сохраняет автоматизацию и убирает часть, что стоила денег — иногда человек в петле и есть та самая фича.
  3. Отлаживай там, где итерация дешёвая. Каждая схватка за упаковку, решённая на Mac разработчика, — это схватка, которая никогда не случается во вкладке логов CI по десятикратной цене. Уходи с Mac последним, а не первым.

Про предыдущую главу истории v1 — выпуск тридцати языков без страховочной сетки (v1.8.3); а про весь путь — анатомию доведения софта до совершенства.

Похожие статьи

Два режима отказа накладки, пропускающей клики
Steven
Steven8 мин. чтения

Два режима отказа накладки, пропускающей клики

Окно GeekBye плавает поверх всего и пропускает твои клики насквозь — кроме тех мест, где у него есть кнопки. Это двусторонний контракт, а v1.8.5 и v1.8.14 показывают, как выглядит поломка каждой из сторон: один релиз, где накладка проглотила системный диалог, и другой, где она украла твои нажатия клавиш. Победившим лекарством для второго стало удаление кода.

Инженерия
Electron
Десктоп
Выпуск тридцати языков без страховочной сетки
Steven
Steven7 мин. чтения

Выпуск тридцати языков без страховочной сетки

GeekBye v1.8.3 подключил react-i18next и перевёл всё приложение на 30 языков. Часть с библиотекой была рутиной. Интересно то, чего не хватало: ни проверки паритета ключей, ни экстрактора строк — так что зашитый в код английский просочился, в продакшн уехала датская опечатка, а changelog не мог договориться, на скольких языках говорит приложение — 28, 29 или 30.

Инженерия
i18n
Десктоп
Четыре релиза за двадцать шесть часов: языковая сантехника и «bridge release»
Steven
Steven5 мин. чтения

Четыре релиза за двадцать шесть часов: языковая сантехника и «bridge release»

GeekBye от v1.7.6 до v1.8.1: настройка языка, которая ни разу не покидала ноутбук, оверлей Cmd+/, показывающий твои настоящие горячие клавиши, версия без единого изменения в коде — и проблема курицы и яйца при миграции фида автообновлений.

Инженерия
Релизы
Десктоп