Доставка 1–5 мин

Выделенный M4 Runner — без очереди macOS

$21.5 от /день · Физическая машина эксклюзивно
Настроить облачный Mac
M4 · 16 GB Версия Xcode фиксируется Постоянная работа через launchd

FIELD NOTE · iOS-сборка

Регистрация выделенного M4-узла как GitHub Actions Runner: практический гайд по iOS-пайплайну

Нет постоянного Mac, но нужны стабильные iOS Archive на GitHub? В этой статье — полный процесс подключения на выделенном физическом JexMac Mac mini M4: от первого SSH-подключения, регистрации Runner и launchd до маршрутизации workflow по меткам, фиксации версии Xcode и импорта Distribution-сертификата без GUI — каждый шаг с воспроизводимыми командами и замерами времени.

Цель: пройти полную iOS-сборку с приёмкой

Перед установкой Runner определим критерии «подключение завершено» — иначе Runner будет работать, а Archive застрянет на подписи. Конечная точка этого гайда: push в целевую ветку → GitHub Actions запускается на выделенном M4-узле → checkout → xcodebuild archive успешно создаёт .xcarchive. Загрузка в TestFlight вне scope; когда Archive стабилен, fastlane или altool — лишь дополнительные шаги.

Тестовый репозиторий — SwiftUI-приложение среднего размера: около 60 Swift-файлов, CocoaPods для зависимостей, Release с Manual Signing. Базовая среда: JexMac Mac mini M4 на узле Сингапур (16 GB Unified Memory, 256 GB NVMe), Xcode 16.2 установлен и зафиксирован через xcodes.

4m 08s
Clean Archive (с pod install)
< 25s
Self-hosted job: от очереди до старта
0 swap
16 GB RAM без swap на протяжении всего процесса
1
Выделенная физическая машина, без виртуализации и overselling

Контрольная группа: тот же репозиторий на GitHub-hosted runs-on: macos-14. UTC 13:00–18:00 (апак-послеполуденное время): медиана очереди job — 11 минут, максимальное ожидание — 19 минут; предустановленный Xcode на macos-14 отличался от локальных dev-машин и вызывал проблемы совместимости со Swift 6. Польза self-hosting здесь очевидна — ожидание сокращается с минут до секунд, а версию Xcode вы фиксируете на узле.

Три скрытых издержки GitHub-hosted macOS Runner

Многие команды выбирают GitHub-hosted Runner из-за «нулевого ops», но в iOS-сценариях скрытые издержки часто болезненнее счёта.

Первое — время. Пул macOS у GitHub ограничен; бесплатный аккаунт даёт 2000 минут в месяц, macOS считается с весом ×10 — фактически около 200 минут. Проект с 8 запусками в день по 6 минут расходует ~1440 взвешенных минут — почти весь лимит; добавите nightly-сборку — и лимит превышен.

Второе — дрейф окружения. Метка macos-latest меняет базовый Xcode при обновлении инфраструктуры GitHub — не раз бывало «вчера зелёный, сегодня красный». Временный workaround — sudo xcode-select в workflow, но каждое переключение версии добавляет 1–2 минуты и не гарантирует идентичность с локальной dev-машиной.

Третье — отладка. Hosted Runner уничтожается после каждого job — SSH для воспроизведения недоступен. Ошибки Keychain, просроченные provisioning profile — всё решается только повторными push и логами. Мы 7 раз push'или из-за User interaction is not allowed, пока не нашли отсутствующий partition list.

Описание тестовой среды

Все команды и замеры времени выполнены на выделенном физическом JexMac Mac mini M4 в дата-центре Сингапур, тестовое окно — конец июля 2026 г. Железо: Apple M4 · 10-ядерный CPU · 16 GB Unified Memory · 1 Gbps выделенный канал.

После доставки узла: первое SSH-подключение и базовая настройка

Консоль JexMac обычно выдаёт SSH-учётные данные через 1–5 минут после оплаты. Получив публичный IP, сначала настройте Ed25519 и отключите вход по паролю — Runner работает от имени текущего пользователя macOS, базовая безопасность SSH должна быть готова заранее.

  1. 01
    Добавить SSH-публичный ключ

    ssh-copy-id -i ~/.ssh/id_ed25519.pub jexmac@<IP-узла>

    После успешного входа без пароля отредактируйте /etc/ssh/sshd_config: PasswordAuthentication no, затем перезапустите sshd.

  2. 02
    Установить Homebrew и xcodes

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    brew install xcodesorg/made/xcodes

    xcodes install 16.2 --experimental-fast-pass — установка и фиксация Xcode 16.2 (номер версии под ваш проект).

  3. 03
    Проверить цепочку сборки

    xcodebuild -version должен вывести Xcode 16.2 и номер Build.

    xcodebuild -showsdks | grep iphoneos — убедитесь, что iOS SDK доступен.

  4. 04
    Установить CocoaPods (если проект использует)

    sudo gem install cocoapods -n /usr/local/bin

    Заранее выполните pod install на узле, чтобы кэш Specs-репозитория был готов — иначе первая CI-сборка зависнет на repo update.

После базовой настройки на узле должна быть выделенная директория (мы используем ~/ci-runner) для Runner и артефактов сборки, изолированная от повседневных файлов. Внутри — подкаталог DerivedData; в workflow явно укажите -derivedDataPath, чтобы избежать конфликтов при параллельных сборках.

На стороне GitHub: токен Runner и дизайн маршрутизации по меткам

Откройте репозиторий → Settings → Actions → Runners → New self-hosted runner, платформа macOS ARM64. Страница выдаст одноразовый токен регистрации (действует 1 час) и ссылку для скачивания.

Дизайн меток напрямую влияет на маршрутизацию workflow к нужной машине. Наши правила именования:

  • mac: общая метка для всех self-hosted macOS-узлов
  • m4: Apple Silicon M4, чтобы отличать от старых Intel-узлов
  • xcode-16-2: фиксация минорной версии Xcode; при обновлении меняйте метку, а не логику workflow
  • sg: регион дата-центра (Сингапур); при мультирегиональном развёртывании — маршрутизация по близости

Параметр --labels в команде регистрации записывает все метки сразу. В workflow используйте массив:

jobs:
  ios-archive:
    runs-on: [self-hosted, mac, m4, xcode-16-2]
    concurrency:
      group: ios-build-${{ github.ref }}
      cancel-in-progress: true

Группа concurrency гарантирует, что одна ветка не запустит два Archive параллельно — на M4 с 16 GB RAM двойной Clean Build возможен, но конкуренция за DerivedData увеличивает время на 30%+. При нескольких продуктовых линиях лучше разделить метки (product-a, product-b) и выделить узлы, чем форсировать параллелизм на одной машине.

Границы безопасности для публичных репозиториев

Self-hosted Runner в публичном репозитории может быть вызван fork PR — вредоносный workflow выполнит произвольный код на вашем Mac. Используйте только в private-репозиториях или на уровне Organization; процесс Runner — под неадминистративным аккаунтом. В production добавьте GitHub Environment protection rules, ограничив secrets указанными ветками.

Установка Runner на M4-узле и настройка launchd

Шаги ниже выполняются в SSH-сессии; версия Runner — по странице регистрации GitHub (в примере v2.321.0).

  1. 01
    Скачать и распаковать

    mkdir -p ~/ci-runner/actions-runner && cd ~/ci-runner/actions-runner

    curl -o actions-runner-osx-arm64-2.321.0.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz

    tar xzf ./actions-runner-osx-arm64-2.321.0.tar.gz

  2. 02
    Интерактивная регистрация

    ./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \ --token YOUR_ONE_TIME_TOKEN \ --name jexmac-m4-sg-01 \ --labels mac,m4,xcode-16-2,sg \ --unattended

    --unattended пропускает интерактивное подтверждение — удобно для скриптового развёртывания. Work folder по умолчанию _work подходит.

  3. 03
    Установить launchd-сервис

    ./svc.sh install

    ./svc.sh start

    Проверка: ./svc.sh status должен показать active (running). Зелёный Online на странице Runners репозитория — регистрация успешна.

launchd поднимет Runner после перезагрузки, но есть нюанс: Runner работает от пользователя macOS, под которым установлен — этот пользователь должен хотя бы раз войти в систему (или включить автовход), иначе launchd может не получить доступ к Keychain. Мы создали аккаунт ci-bot, первый SSH-вход инициализировал Keychain, затем установили svc.sh — после перезагрузки вмешательство не требуется.

Логи Runner: ~/ci-runner/actions-runner/_diag/; после каждого job — Worker_*.log. При проблемах «job назначен узлу, но шаги не выполняются» сначала смотрите RunnerListener — там больше информации, чем в Annotations GitHub UI.

Минимальный workflow: от checkout до Archive

После регистрации создайте .github/workflows/ios-archive.yml. Ниже — минимальная проверенная версия без загрузки в TestFlight, только Archive:

name: iOS Archive
on:
  push:
    branches: [main, release/*]
  pull_request:
    branches: [main]

jobs:
  archive:
    runs-on: [self-hosted, mac, m4, xcode-16-2]
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: xcodes select 16.2

      - name: Install pods
        run: pod install --deployment
        working-directory: ios

      - name: Build archive
        run: |
          xcodebuild archive \
            -workspace ios/MyApp.xcworkspace \
            -scheme MyApp \
            -sdk iphoneos \
            -configuration Release \
            -archivePath ./build/MyApp.xcarchive \
            -derivedDataPath ~/ci-runner/DerivedData \
            CODE_SIGN_STYLE=Manual \
            CODE_SIGN_IDENTITY="Apple Distribution: Your Team (TEAMID)" \
            PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore"
        env:
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

      - name: Upload xcarchive artifact
        uses: actions/upload-artifact@v4
        with:
          name: MyApp-xcarchive
          path: ./build/MyApp.xcarchive
          retention-days: 7

Несколько осознанных решений:

pod install --deployment фиксирует версии по Podfile.lock — CI и локальная среда совпадают. -derivedDataPath указывает на постоянную директорию на узле — кэш компиляции переиспользуется, инкрементальная сборка падает с 4 минут до ~1 мин 40 сек. upload-artifact загружает xcarchive в GitHub — коллеги без SSH могут скачать для QA; artifact хранится 7 дней.

Headless-среда: Keychain и импорт Distribution-сертификата

Успех Archive зависит от доступа CI к приватному ключу Distribution без GUI. Hosted Runner преднастраивает системный Keychain; на self-hosted узле всё на вас — частая причина «Runner работает, Archive падает на подписи».

Рекомендуемый подход: создавать временный Keychain на каждый job, импортировать p12, подписать, после job — уничтожить, чтобы приватный ключ не оставался на диске.

# Добавить перед шагом "Build archive"
- name: Import signing certificate
  run: |
    security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    security default-keychain -s build.keychain
    security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
    security set-keychain-settings -t 3600 -u build.keychain

    echo "${{ secrets.CERTIFICATE_P12_BASE64 }}" | base64 --decode > cert.p12
    security import cert.p12 \
      -k build.keychain \
      -P "${{ secrets.P12_PASSWORD }}" \
      -T /usr/bin/codesign \
      -T /usr/bin/xcodebuild

    security set-key-partition-list \
      -S apple-tool:,apple: \
      -s -k "$KEYCHAIN_PASSWORD" build.keychain
    rm -f cert.p12
  env:
    KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}

В GitHub Repository Secrets нужно заранее настроить три переменные: KEYCHAIN_PASSWORD (пароль временного Keychain — достаточно случайной строки), CERTIFICATE_P12_BASE64 (Distribution-сертификат в Base64), P12_PASSWORD (пароль при экспорте p12).

Ключевое слово ошибки Частая причина Решение
User interaction is not allowed Partition list приватного ключа не задан — codesign пытается открыть UI Повторно выполнить set-key-partition-list (см. скрипт выше)
errSecItemNotFound Сертификат импортирован в неверный Keychain или default-keychain не переключён Убедитесь, что security default-keychain -s build.keychain выполняется до import
could not find signing certificate Строка CODE_SIGN_IDENTITY не совпадает с Identity в Keychain Выполните security find-identity -v -p codesigning build.keychain и скопируйте полное имя
Provisioning profile doesn't match Provisioning profile просрочен или Bundle ID / Capability не совпадают Пересоздать в Apple Developer, скачать и синхронизировать через secrets или match

После успешного импорта быстро проверьте одной командой в логе job:

security find-identity -v -p codesigning build.keychain | grep Distribution

Переходите к Archive только после 1 valid identities found — сэкономите время на «подпись упала, но непонятно где».

Отладка на практике: три проблемы, из-за которых мы сделали лишний push

Даже следуя шагам выше, при первом подключении возможны эти ситуации — реальные ловушки с признаками в логах и решениями.

Несовпадение меток: job вечно в очереди

Симптом: GitHub Actions UI показывает Queued, на странице Runners узел Online. Причина: метки в runs-on workflow не совпадают с регистрацией — например workflow xcode-16.2 (точка), регистрация xcode-16-2 (дефис). Сопоставление меток GitHub — точное сравнение строк; один символ — и маршрутизация не сработает.

Решение: на странице Runners репозитория кликните имя узла, скопируйте реальный список меток в YAML — не набирайте вручную.

Runner Offline после перезапуска launchd

Симптом: после reboot узла или обслуживания JexMac Runner Offline, нужен ручной SSH и ./svc.sh start. Причина: UserName в launchd plist не совпадает с SSH-пользователем или пользователь не завершил первый графический/session-вход.

Решение: ./svc.sh install и ./svc.sh start под одним пользователем; после reboot проверьте ./svc.sh status. Если не помогло — stderr в /Library/Logs/GitHubActionsRunner/.

Конфликт прав DerivedData

Симптом: второй build — Unable to write to DerivedData или permission denied на .o-файлах. Причина: первый job создал DerivedData от root или другого пользователя; следующие job без прав записи.

Решение: единый путь DerivedData и очистка в начале workflow: rm -rf ~/ci-runner/DerivedData && mkdir -p ~/ci-runner/DerivedData. Или перед каждым Archive удаляйте только hash-подкаталог проекта, сохраняя кэш компиляции.

Нет постоянного Mac: арендуйте выделенный узел по ритму сборок

К этому моменту ясно: технический порог self-hosted GitHub Actions Runner невысок — узкое место в круглосуточно доступной физической macOS-машине с контролируемой версией Xcode. Локальный MacBook плох как CI-хост — на 8 GB RAM Archive гонит вентилятор и swap; покупка Mac mini в компании — согласование CAPEX, colocation и выездная ротация сертификатов.

Наш подход: в спринте перед релизом (например две недели) арендуем выделенный JexMac Mac mini M4 как Runner-хост; после стабилизации — продление по неделям или освобождение. Стандарт: 16 GB Unified Memory, 256 GB NVMe, 1 Gbps выделенный канал, от $21.5/день, SSH/VNC через 1–5 минут после оплаты, без контрактной привязки. Пять узлов — Сингапур, Япония (Токио), Корея (Сеул), Гонконг, Восток США — по географии команды, чтобы снизить задержку git fetch и синхронизации CocoaPods Specs.

Сравнение с GitHub-hosted macOS Runner: пиковая очередь 10–20 мин vs self-hosted старт за ~25 сек; macOS-минуты с весом ×10 vs предсказуемая дневная аренда. Сравнение с покупкой: без upfront; обновление — смена метки или переустановка Xcode, без цикла закупки.

Тот же узел может быть remote dev desktop — браузерный VNC для UI, Instruments для профилирования; простой CI не пропадает. Для команды из 2–3 человек «одна M4 = Runner + remote Mac dev» часто выгоднее, чем hosted-минуты плюс апгрейд локальной RAM.

Физическая машина эксклюзивно · доставка 1–5 мин

Подключите Runner к вашему M4-узлу

Все команды в статье проверены на выделенном физическом JexMac Mac mini M4. Активация → SSH → регистрация Runner — Archive часто за час. Аренда от $21.5/день, после спринта освобождаете узел, без годовой привязки.

Стандартная конфигурация
ЧипApple M4 · 38 TOPS
CPU10 ядер (4P + 6E)
Память16 GB Unified Memory
Сеть1 Gbps выделенный канал
SLA99.9% доступности
ДоставкаАвтоактивация за 1–5 мин