Руководство — CI/CD

Как настроить self-hosted раннер GitHub Actions на Mac Mini M4

Пошаговое руководство по настройке self-hosted раннера GitHub Actions на выделенном Mac Mini M4. Быстрые сборки iOS, нативный Apple Silicon и до 10 раз дешевле GitHub-hosted macOS раннеров.

30 мин чтения Обновлено в январе 2025

1. Зачем использовать self-hosted Mac-раннер?

GitHub-hosted macOS раннеры удобны, но дороги. При стоимости $0.08 в минуту команда, использующая 75 часов сборок в месяц, платит около $360. Выделенный Mac Mini M4 от MyRemoteMac стоит $75/месяц с неограниченным временем сборки, что даёт такое же (или лучшее) оборудование за долю стоимости.

Характеристика GitHub-Hosted раннер Self-hosted MyRemoteMac
Стоимость $0.08/мин (~$350/мес за 75 ч) $75/мес (неограниченные минуты)
Архитектура Intel x86 (некоторые M1) Apple M4 (последняя модель)
Скорость сборки ~12 мин (средний проект) ~4 мин (тот же проект)
Постоянный кеш Нет (эфемерный) Да (постоянный диск)
Пользовательское ПО Ограничено Полный root-доступ
Параллельные задачи 5 (бесплатно) / 20 (платно) Неограниченно (ваше оборудование)

Ключевое преимущество: Поскольку раннер постоянный, DerivedData, кеши SPM и CocoaPods сохраняются между сборками. Одно это может сократить время сборки на 50–70% по сравнению с эфемерными GitHub-hosted раннерами, которые каждый раз начинают с нуля.

2. Предварительные требования

Прежде чем начать, убедитесь, что у вас есть следующее:

  • Сервер Mac Mini M4 от MyRemoteMac (от $75/мес)
  • Аккаунт GitHub с правами администратора вашего репозитория или организации
  • SSH-доступ к вашему Mac Mini (предоставляется с подпиской MyRemoteMac)
  • Аккаунт Apple Developer (для подписи кода и профилей обеспечения)
  • Базовое знакомство с YAML и командами терминала

3. Шаг 1: Подключение к Mac Mini по SSH и установка Xcode

Сначала подключитесь к Mac Mini M4 через SSH. Вы получили учётные данные при настройке сервера MyRemoteMac.

Подключение через SSH

# Connect to your Mac Mini M4
ssh admin@your-server-ip

# Verify you're on Apple Silicon
uname -m
# Expected output: arm64

# Check macOS version
sw_vers
# ProductName:    macOS
# ProductVersion: 15.2
# BuildVersion:   24C101

Установка Xcode Command Line Tools

# Install Command Line Tools
xcode-select --install

# Accept the license agreement
sudo xcodebuild -license accept

# Verify installation
xcode-select -p
# /Library/Developer/CommandLineTools

Установка Xcode (полная версия)

Для сборок iOS вам нужно полное приложение Xcode. Самый быстрый способ установить его на безголовом сервере — использовать xcodes:

# Install Homebrew (if not already installed)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Add Homebrew to PATH
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

# Install xcodes CLI tool
brew install xcodes

# List available Xcode versions
xcodes list

# Install the latest stable Xcode
xcodes install 16.2

# Set it as the active Xcode
sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer

# Verify
xcodebuild -version
# Xcode 16.2
# Build version 16C5032a

Установка iOS-симуляторов

# Install the iOS 18 simulator runtime
xcodebuild -downloadPlatform iOS

# Verify simulator availability
xcrun simctl list runtimes
# == Runtimes ==
# iOS 18.2 (18.2 - 22C150) - com.apple.CoreSimulator.SimRuntime.iOS-18-2

4. Шаг 2: Установка раннера GitHub Actions

Теперь скачаем и настроим агент раннера GitHub Actions. Перейдите в ваш репозиторий на GitHub, откройте Settings > Actions > Runners > New self-hosted runner и выберите macOS + ARM64.

Скачивание и настройка

# Create a directory for the runner
mkdir -p ~/actions-runner && cd ~/actions-runner

# Download the latest runner package (ARM64)
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

# Extract the package
tar xzf actions-runner-osx-arm64-2.321.0.tar.gz

# Configure the runner
# Replace YOUR_TOKEN with the token from GitHub Settings
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \
  --token YOUR_TOKEN \
  --name "mac-mini-m4-runner" \
  --labels "self-hosted,macOS,ARM64,M4" \
  --work "_work"

# Test the runner interactively first
./run.sh

Установка как постоянный сервис launchd

Запуск агента в интерактивном режиме подходит для тестирования, но для продакшена нужен автозапуск при загрузке и перезапуск при сбое. GitHub предоставляет встроенный скрипт установки сервиса для macOS:

# Install as a launchd service
cd ~/actions-runner
sudo ./svc.sh install

# Start the service
sudo ./svc.sh start

# Check the service status
sudo ./svc.sh status
# Expected: "active (running)"

# View the launchd plist (for reference)
cat /Library/LaunchDaemons/actions.runner.*.plist

Теперь сервис будет автоматически запускаться при загрузке и перезапускаться при аварийном завершении процесса. Вы можете убедиться, что раннер отображается как "Idle" на странице Settings > Actions > Runners вашего GitHub репозитория.

Настройка раннера для нескольких репозиториев (уровень организации)

# For an organization-level runner, use the organization URL:
./config.sh --url https://github.com/YOUR_ORG \
  --token YOUR_ORG_TOKEN \
  --name "mac-mini-m4-org-runner" \
  --labels "self-hosted,macOS,ARM64,M4" \
  --runnergroup "Default" \
  --work "_work"

# This allows ALL repositories in your organization to use this runner

5. Шаг 3: Создание рабочего процесса сборки iOS

Создайте файл рабочего процесса в вашем репозитории по пути .github/workflows/ios-build.yml. Этот рабочий процесс будет выполняться на вашем self-hosted раннере Mac Mini M4.

name: iOS Build & Test

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: [self-hosted, macOS, ARM64, M4]

    env:
      SCHEME: "MyApp"
      DESTINATION: "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2"

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Select Xcode version
        run: |
          sudo xcode-select -s /Applications/Xcode-16.2.app/Contents/Developer
          xcodebuild -version

      - name: Resolve Swift Package Dependencies
        run: |
          xcodebuild -resolvePackageDependencies \
            -scheme "$SCHEME" \
            -clonedSourcePackagesDirPath .spm-cache

      - name: Build the app
        run: |
          xcodebuild build \
            -scheme "$SCHEME" \
            -destination "$DESTINATION" \
            -clonedSourcePackagesDirPath .spm-cache \
            -derivedDataPath DerivedData \
            | xcbeautify

      - name: Run unit tests
        run: |
          xcodebuild test \
            -scheme "$SCHEME" \
            -destination "$DESTINATION" \
            -clonedSourcePackagesDirPath .spm-cache \
            -derivedDataPath DerivedData \
            -resultBundlePath TestResults.xcresult \
            | xcbeautify

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: TestResults.xcresult

Добавление подписи кода для релизных сборок

Для деплоя в TestFlight или App Store добавьте шаги подписи кода. Храните сертификаты и профили обеспечения как GitHub Secrets:

  deploy:
    needs: build
    runs-on: [self-hosted, macOS, ARM64, M4]
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install certificate and provisioning profile
        env:
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
          KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
        run: |
          # Create a temporary keychain
          KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
          security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
          security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

          # Import certificate
          CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
          echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
          security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
            -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
          security list-keychain -d user -s $KEYCHAIN_PATH

          # Install provisioning profile
          PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
          echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
          mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
          cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles

      - name: Build for distribution
        run: |
          xcodebuild archive \
            -scheme "MyApp" \
            -archivePath DerivedData/MyApp.xcarchive \
            -destination "generic/platform=iOS" \
            CODE_SIGN_STYLE=Manual

      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath DerivedData/MyApp.xcarchive \
            -exportOptionsPlist ExportOptions.plist \
            -exportPath DerivedData/Export

      - name: Upload to TestFlight
        env:
          APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
        run: |
          xcrun altool --upload-app \
            -f DerivedData/Export/MyApp.ipa \
            -t ios \
            --apiKey $APP_STORE_CONNECT_API_KEY

6. Шаг 4: Оптимизация производительности

Одно из главных преимуществ self-hosted раннера — постоянное кеширование. Вот ключевые оптимизации для максимальной отдачи от вашего Mac Mini M4.

Включение кеширования DerivedData

Поскольку раннер постоянный, DerivedData сохраняется между сборками. Используйте постоянный путь DerivedData:

# In your workflow, always use:
-derivedDataPath DerivedData

# On the runner, periodically clean old DerivedData to save space:
# Add a cron job to clean builds older than 7 days
echo "0 3 * * 0 find ~/actions-runner/_work/*/DerivedData -maxdepth 0 -mtime +7 -exec rm -rf {} +" \
  | crontab -

Кеширование SPM-пакетов

# Use clonedSourcePackagesDirPath to keep SPM packages on disk
xcodebuild build \
  -scheme "MyApp" \
  -clonedSourcePackagesDirPath ~/spm-cache \
  -derivedDataPath DerivedData

# This avoids re-downloading packages on every build

Параллельное выполнение тестов

# Run tests in parallel across multiple simulators
xcodebuild test \
  -scheme "MyApp" \
  -destination "platform=iOS Simulator,name=iPhone 16 Pro,OS=18.2" \
  -destination "platform=iOS Simulator,name=iPhone 15,OS=17.5" \
  -parallel-testing-enabled YES \
  -maximum-parallel-testing-workers 4 \
  -derivedDataPath DerivedData \
  | xcbeautify

Установка xcbeautify для улучшенных логов

# xcbeautify formats Xcode output for CI environments
brew install xcbeautify

# Use it by piping xcodebuild output:
xcodebuild build -scheme "MyApp" | xcbeautify

7. Устранение типичных проблем

Раннер отображается как "Offline" в GitHub

Обычно это означает, что сервис launchd не запущен. Проверьте статус сервиса и логи:

# Check service status
sudo ./svc.sh status

# View logs
cat ~/actions-runner/_diag/Runner_*.log | tail -50

# Restart the service
sudo ./svc.sh stop
sudo ./svc.sh start

Подпись кода не работает — "No signing certificate"

Сервис launchd работает под другим контекстом пользователя. Убедитесь, что Keychain доступен:

# Ensure the login keychain is unlocked for the runner user
security unlock-keychain -p "YOUR_PASSWORD" ~/Library/Keychains/login.keychain-db

# Or use a dedicated keychain in your workflow (recommended)
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain

Симулятор не запускается

Симуляторы иногда зависают. Сбрасывайте их между сборками:

# Shutdown all running simulators
xcrun simctl shutdown all

# Erase all simulator data (nuclear option)
xcrun simctl erase all

# Boot a specific simulator
xcrun simctl boot "iPhone 16 Pro"

Заканчивается дисковое пространство

Сборки Xcode генерируют много данных. Настройте автоматическую очистку:

# Clean old DerivedData
rm -rf ~/Library/Developer/Xcode/DerivedData/*

# Remove old simulator runtimes
xcrun simctl runtime delete all

# Clean Homebrew cache
brew cleanup --prune=7

# Remove old Xcode archives
rm -rf ~/Library/Developer/Xcode/Archives/*

8. Анализ стоимости

Вот детальное сравнение стоимости для разных размеров команд и объёмов сборок:

Размер команды Сборок/месяц Стоимость GitHub-Hosted Стоимость MyRemoteMac Ежемесячная экономия
Один разработчик 100 сборок (в среднем 10 мин) $80/мес $75/мес $5/мес
Малая команда (5) 500 сборок (в среднем 10 мин) $400/мес $75/мес $325/мес
Средняя команда (15) 1500 сборок (в среднем 10 мин) $1,200/мес $179/мес (M4 Pro) $1,021/мес
Корпоративная (50+) 5000+ сборок $4,000+/мес $358/мес (2x M4 Pro) $3,642+/мес

Итог: Для команд, выполняющих более ~100 сборок в месяц, self-hosted Mac Mini M4 окупается мгновенно. А поскольку сборки быстрее на постоянном оборудовании с тёплыми кешами, ваша команда экономит и время разработчиков.

Похожие руководства

Готовы ускорить ваши iOS-сборки?

Разверните выделенный Mac Mini M4 как раннер GitHub Actions. От $75/месяц с 7-дневным бесплатным пробным периодом.