usharik.dev

← Все статьи

Приложение для iOS без Mac: GitHub Actions вместо сборочной машины

Опубликовано · · 5 мин чтения

Приложению для iOS нужен Xcode, а Xcode нужна macOS. Я работаю на виртуальной машине с Linux в облаке, и Mac на Apple silicon у меня нет. Тем не менее два моих приложения уже в TestFlight, причём собраны, протестированы, подписаны и загружены целиком в GitHub Actions. Рассказываю, как это устроено и где больно. (Как приложения делят код с Android — в предыдущей статье.)

Варианты

Я выбрал раннеры и одно правило: всё, что завязано на Apple, выполняется в CI, а скрипт, который выполняет CI, точно так же запустится и на настоящем Mac, если он когда-нибудь появится.

Что умеет Linux-машина

Больше, чем я думал. Kotlin/Native компилирует iOS-таргеты общего кода на Linux. Слинковать iOS-фреймворк он там не может, но большинство ошибок видно уже при компиляции. Поэтому агенты на Linux-машине проверяют каждое изменение командой

./gradlew :shared:compileKotlinIosSimulatorArm64

и общими тестами на JVM. Код на Swift, проект Xcode, симулятор и всё, что связано с подписью, требуют macOS и уходят в CI.

Workflow для iOS

Один workflow запускается на каждое изменение общего кода или iOS-оболочки и на каждый пул-реквест. Он вызывает один shell-скрипт, который:

  1. Выбирает симулятор запросом, а не по имени. Имена устройств меняются с каждым выпуском Xcode, поэтому скрипт спрашивает у simctl самый новый рантайм iOS и первый iPhone в нём.
  2. Запускает общие тесты на Kotlin на этом симуляторе и заодно линкует фреймворк для настоящего устройства: ошибка линковки, которая бывает только для устройств, должна всплыть здесь, а не в первой сборке для TestFlight.
  3. Генерирует проект Xcode с помощью XcodeGen из одного YAML-файла. .xcodeproj в git не хранится, поэтому нет конфликтов слияния в файле, который никто не может прочитать.
  4. Запускает юнит- и UI-тесты приложения через xcodebuild test и повторяет проверки для маленького экрана на самом маленьком iPhone, который есть в симуляторе.
  5. Снимает видео каждого прогона, выгружает скриншоты из результатов тестов, а при падении приложения печатает в лог крэш-репорты из симулятора.

Съёмка занимает несколько строк:

xcrun simctl boot "$UDID" || true
xcrun simctl io "$UDID" recordVideo --codec=h264 --force "media/$NAME.mp4" &
RECORDER=$!
xcodebuild test -project App.xcodeproj -scheme App \
  -destination "id=$UDID" -resultBundlePath "results/$NAME.xcresult" CODE_SIGNING_ALLOWED=NO
kill -INT "$RECORDER"
xcrun xcresulttool export attachments --path "results/$NAME.xcresult" --output-path "media/$NAME"

Видео и скриншоты сохраняются как артефакт, который живёт день или неделю. Раз устройства на столе нет, через них я и вижу приложение: упавший UI-тест приходит с записью того, что на самом деле происходило на экране.

Цифры

За время переезда тренажёра грамматики workflow для iOS запускался 55 раз: 29 зелёных, 14 красных и 12 отменённых, потому что их сменил более новый push. Зелёный прогон длится около 20 минут (от 8 до 33). Минуты macOS — самые дорогие у GitHub, а в приватном репозитории каждая из них засчитывается как десять минут тарифа. Поэтому workflow стартует, только когда меняются файлы, способные повлиять на iOS, а новый push отменяет текущий прогон.

Чему научили раннеры

Большинство красных прогонов были настоящими ошибками. Некоторые оказались уроками про окружение:

Подпись без кнопок Xcode

На Mac Xcode создаёт сертификаты и профили одним щелчком. Без Mac всё идёт через App Store Connect API по API-ключу:

Одну из первых загрузок отклонили ещё до TestFlight: сборка, которая поддерживает iPad, должна поддерживать там все ориентации, а моя была только портретной. Пока приложения только для iPhone. Чтобы это закрепить, понадобился ещё один прогон: пресет iOS в XcodeGen задаёт семейство устройств на уровне таргета и молча перекрывает ту же настройку на уровне проекта.

App Store Connect из скрипта

Большую часть страницы в App Store тоже можно настроить через API: тексты и скриншоты на семи языках, страны, возрастной рейтинг, встроенную покупку. Две вещи нельзя:

Что CI не заменит

Настоящее устройство. У симулятора нет настоящего сенсорного экрана, настоящего StoreKit и настоящего динамика, а по видео не понять, как звучит фортепиано. Для этого и нужен TestFlight: на iPhone приложение впервые запускается из сборки, которую сделал CI.

И человека в App Store Connect: ответы о приватности, соглашения, последнюю кнопку «отправить на проверку». Всё остальное — сборка, тесты, подпись и загрузка — работает с Linux-машины и из YAML-файла.

Приложения из статьи