Настраиваем iOS CI/CD в GitHub Actions с нуля
Здесь весь пайплайн для iOS-приложения в GitHub Actions, в том порядке, в котором его собирают. Сначала компиляция и тесты. Потом подпись. Потом архив и загрузка. Каждый шаг небольшой, а YAML можно копировать как есть. Это работает и на hosted раннерах GitHub, и на выделенном Mac. Меняется только строка runs-on.
1. Каркас
name: ios
on:
pull_request:
push:
branches: [main]
concurrency:
group: ios-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: macos-latest # or [self-hosted, macOS, your-label]
timeout-minutes: 30
steps:
- uses: actions/checkout@v42. Выберите Xcode
В hosted образах есть несколько версий Xcode. Выберите одну явно, чтобы обновление образа не поменяло вам сборку незаметно:
- run: sudo xcode-select -s /Applications/Xcode_26.6.app
- run: xcodebuild -versionНа выделенном Mac версия фиксируется на машине один раз, и этот шаг не нужен.
3. Зависимости
- uses: actions/cache@v4
with:
path: |
~/Library/Caches/org.swift.swiftpm
Pods
key: ${{ runner.os }}-deps-${{ hashFiles('**/Package.resolved', '**/Podfile.lock') }}
- run: pod install --repo-update # CocoaPods projects only4. Сборка и тесты на симуляторе
- run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-resultBundlePath TestResults \
| xcpretty
- uses: actions/upload-artifact@v4
if: failure()
with: { name: test-results, path: TestResults }Для pull request на этом остановитесь. Всё, что ниже, относится к релизу и запускается только на main.
5. Подпись кода без общего входа в Mac
Используйте ключ App Store Connect API вместо Apple ID. Сохраните ID ключа, issuer ID и содержимое .p8 как секреты репозитория. Пусть xcodebuild подписывает с профилями, которыми управляет облако:
- run: echo "${{ secrets.ASC_KEY_P8 }}" > AuthKey.p8
- run: |
xcodebuild archive \
-scheme MyApp -configuration Release \
-archivePath build/MyApp.xcarchive \
-allowProvisioningUpdates \
-authenticationKeyPath "$PWD/AuthKey.p8" \
-authenticationKeyID "${{ secrets.ASC_KEY_ID }}" \
-authenticationKeyIssuerID "${{ secrets.ASC_ISSUER_ID }}"6. Экспорт и загрузка в TestFlight
- run: |
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportPath build/export \
-exportOptionsPlist ExportOptions.plist \
-allowProvisioningUpdates \
-authenticationKeyPath "$PWD/AuthKey.p8" \
-authenticationKeyID "${{ secrets.ASC_KEY_ID }}" \
-authenticationKeyIssuerID "${{ secrets.ASC_ISSUER_ID }}"
- run: |
xcrun altool --upload-app -f build/export/MyApp.ipa -t ios \
--apiKey "${{ secrets.ASC_KEY_ID }}" --apiIssuer "${{ secrets.ASC_ISSUER_ID }}"Многие команды оборачивают шаги 5 и 6 в fastlane. Команды под капотом те же.
7. Куда уходят минуты и какой раннер выбрать
На hosted раннере шаги 2 и 3 повторяются в каждой задаче и стоят примерно от $0.062 до $0.102 за минуту. На выделенном Mac они выполняются один раз, а кэши остаются прогретыми. Поэтому команды, которые тратят больше примерно 2,242 минут в месяц, переносят весь пайплайн на машину за фиксированную цену. Они меняют только строку runs-on. Руководство по пайплайну на выделенном железе описывает эту сторону.
Частые вопросы
Нужен ли Mac, чтобы запускать iOS CI в GitHub Actions?
+
Да. Для сборки и подписи iOS-приложений нужен Xcode, а он работает только на macOS. Поэтому задаче сборки нужен раннер macOS: hosted у GitHub или Mac под вашим управлением.
Как подписать iOS-приложение в CI без моего Apple ID?
+
Создайте ключ App Store Connect API. Сохраните его ID, issuer ID и содержимое .p8 как секреты и передайте их xcodebuild вместе с -allowProvisioningUpdates. Ни Apple ID, ни пароль от связки ключей на раннере не нужны.
Нужно ли запускать тесты на каждый pull request?
+
Собирайте и тестируйте каждый pull request на одном симуляторе. Архивирование, подпись и загрузку в TestFlight оставьте только для main. Это медленные и дорогие шаги.
Что меняется, когда я переношу этот пайплайн на выделенный Mac?
+
Строка runs-on. Выбор Xcode и кэширование зависимостей становятся не нужны, потому что машина сохраняет всё между задачами. И перед сборкой нет очереди.