Непрерывная интеграция (Continuous Integration, CI) — один из главных столпов продвинутых разработчиков.
Если вы живете и дышите большими проектами, то возможность пройти горнило настройки эффективных конвейеров сборки, тестирования и развертывания — обычное дело, но что делать, если вы работаете над инди-проектом?
Если вы раньше не настраивали CI-конвейеры, это ставит вас в невыгодное положение как инженера, поскольку такая автоматизация является обязательной для любого нового проекта. Сегодня я покажу вам, как настроить CI для ваших побочных проектов. Бесплатно!
Я использую CI для Bev, моего открытого тестового проекта, но вы можете последовать этому примеру и использовать такую автоматизацию для собственных кодовых баз.
Часть I: Fastlane
Fastlane — это набор Ruby-скриптов с открытым исходным кодом, которые автоматизируют сборку и развертывание. По сути, это удобные обертки над командами xcodebuild или Gradle, которые помогают автоматизировать стандартные рабочие процессы.
Чтобы настроить Fastlane, просто прочитайте эту чертову документацию.
Давайте настроим два очень простых конвейера для тестирования (для запуска на каждом PR) и развертывания (для запуска каждый раз, когда мы мерджим в main).
fastlane test
Наша тестовая линия, прописанная в Fastfile, не может быть более простой:
desc "Run tests on each PR" lane :test do scan(device: "iPhone 15 Pro", scheme: "BevTests") end
scan запускает тесты для определенной схемы в нашем приложении — здесь мы создали план тестирования, который включает в себя полный набор юнит- и UI-тестов. Мы можем запустить тест Fastlane локально и быстро увидеть нашу первую победу
fastlane deploy
Линия развертывания немного сложнее, поскольку нам нужно управлять сертификатами, архивировать наше приложение и отправить его в App Store Connect. Вот немного упрощенная форма:
desc "Deploy app to App Store Connect" lane :deploy do match gym api_key = app_store_connect_api_key() deliver(api_key: api_key) end
Разберемся снизу вверх:
deliver
загружает наш подписанный .ipa-файл в App Store Connectapp_store_connect_api_key
загружает наш API-токен так, что мы можем использовать его с другими командами Fastlanegym
создает Release-вариант приложения и упаковывает наш .ipa-файлmatch
устанавливает сертификаты и профили инициализации
Вряд ли вы удивитесь, если узнаете, что с первого раза это не сработает — вам придется пройти через значительный объем работ по настройке.
match
— это первый серьезный всплеск сложности в нашей непрерывной интеграции.
fastlane match
Команда match
помогает автоматизировать и унифицировать создание, хранение и управление сертификатами и профилями подписи кода для вашей команды. Что еще более важно, она дает вашей CI-машине возможность подписывать релизы без необходимости хранить учетные данные в Git.
Это одна из основных причин, по которой Senior-разработчики предпочитают сами управлять CI: match
одновременно и обязателен для понимания, и немного пугает.
Если вы страдаете от синдрома самозванца, можете посмотреть на количество неудачных сборок в моем репозитории.
Для настройки запустите fastlane match init
и следуйте пошаговому руководству в CLI для безопасного хранения ваших сертификатов и профилей инициализации.
Лично я предпочитаю режим хранения Google Cloud, поскольку (1) управлять секретом довольно просто, (2) у него есть щедрый бесплатный уровень, (3) у большинства мобильных разработчиков уже есть аккаунт в облаке Google через Firebase, и (4) настроить проект довольно просто.
Чтобы fastlane match работал, файл gc_keys.json должен находиться в папке вашего проекта, но очень важно, чтобы ключи не попали в систему контроля исходных текстов, поэтому не забудьте сразу же добавить его в .gitignore
. В ближайшее время мы добавим их в качестве секретов в нашу систему CI.
Теперь мы можем запустить fastlane match appstore
, ввести учетные данные App Store Connect и настроить профили распространения на хранилище ключей. fastlane match
генерирует сертификаты и профили и сохраняет их в хранилище ключей Google Cloud, которое мы только что настроили.
Полный цикл развертывания
Теперь, когда мы настроили подписание кода, можно заняться остальными элементами конвейера развертывания:
lane :deploy do match(readonly: true) api_key = app_store_connect_api_key( key_id: "V4D62Q8UQB", issuer_id: "69a6de92-2bb4-47e3-e053-5b8c7c11a4d1", key_content: $APP_STORE_CONNECT_API_KEY_KEY, is_key_content_base64: true ) increment_build_number({ build_number: latest_testflight_build_number(api_key: api_key) + 1 }) gym(export_options: "./fastlane/ExportOptions.plist") deliver( api_key: api_key, force: true, skip_screenshots: true, precheck_include_in_app_purchases: false ) end
Тут есть несколько улучшений по сравнению с упрощенной версией выше:
match
работает в режиме readonly, что рекомендуется для систем CI, поскольку он не будет пытаться создавать новые сертификаты и профилиapi_key
получает наш API-ключ App Store Connect, чтобы мы могли автоматизировать шаг развертывания — мы объясним все в следующем разделеincrement_build_number
просто автоматически увеличивает номер сборки, чтобы нам не нужно было следить за ним, получая последние данные из TestFlightgym
теперь получает параметры экспорта из локального файла, которые указывают xcodebuild, как упаковать приложениеdeliver
переопределяет несколько параметров по умолчанию, чтобы ускорить загрузку.
Теперь, когда наш локальный конвейер на месте и работает локально, мы можем начать собирать нашу инфраструктуру: App Store и раннер GitHub Actions.
Часть II: App Store Connect
Чтобы убедиться, что мы действительно можем развернуть наше приложение, нам нужно зайти в App Store Connect.
Создание API ключа App Store Connect
Перейдите в раздел «Пользователи и доступ», чтобы создать ключ App Store Connect API — он понадобится нам позже, когда мы начнем развертывание!
Скачайте файл в формате .p8 и положите его в надежное место.
С файлами сложно, а со строками легко. Поэтому я предпочитаю кодировать ключ в base-64 для использования в Fastlane — используйте эту команду в терминале из папки, содержащей ключ:
cat AuthKey_A4D72Q2UQC.p8 | base64
Мы можем взять этот закодированный API-ключ, а также key_id
и issuer_id
, указанные на странице App Store Connect «Пользователи и доступ», и добавить их в наш сценарий Fastlane:
api_key = app_store_connect_api_key( key_id: "V4D62Q8UQB", issuer_id: "69a6de92-2bb4-47e3-e053-5b8c7c11a4d1", key_content: $APP_STORE_CONNECT_API_KEY_KEY, is_key_content_base64: true )
Что такое APP_STORE_CONNECT_API_KEY_KEY
в key_content
, спросите вы?
Это наш следующий секрет.
Поскольку этот API-ключ может отправлять приложения в наш аккаунт, нам нужно, чтобы он был под замком и вдали от публикации исходных текстов. Запишите ключ в кодировке base-64 в безопасное место; мы будем работать с секретами в разделе III, когда будем настраивать GitHub Actions.
Для локального тестирования вы можете запустить fastlane deploy
с жестко закодированным API-ключом в base-64, чтобы убедиться, что вы можете корректно запустить конвейер и увидеть загруженное приложение в App Store Connect — только не комитьте fastfile
с API-ключом.
Пароль для конкретного приложения
Есть некоторые сервисы Fastlane, для которых API-ключа недостаточно.
Чтобы охватить все базы, вы также можете сгенерировать App-Specific Password для сервисов Fastlane, который аутентифицирует вашу автоматизацию как ваш собственный аккаунт App Store Connect.
Перейдите на сайт appleid.apple.com и выберите «Sign-In and Security», затем выберите «App-Specific Passwords» и добавьте один.
Опять же, запишите этот пароль в безопасное место, пока мы не добавим его в секреты GitHub Actions в ближайшее время.
Теперь мы готовы к прайм-тайму.
Часть III: GitHub Actions
Мы хорошо настроили наши скрипты Fastlane. Мы настроили наше приложение в App Store Connect и создали хранилище ключей Google Cloud. Наконец, мы можем настроить последний элемент нашей инфраструктуры автоматизации — GitHub Actions.
Безопасное хранение секретов
Давайте сначала избавимся от нашего жестко прописанного секретного ключа и поместим закодированный в base-64 код в $APP_STORE_CONNECT_API_KEY_KEY
в GitHub Actions, в разделе Settings → Secrets and variables → Actions → Repository Secrets.
При работе с GitHub Actions ваш файл рабочего процесса (подробнее об этом ниже), который запускает скрипты Fastlane, будет иметь доступ к секретному ключу в качестве переменной среды.
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }}
Давайте сделаем то же самое с ключами в Google Cloud Storage — добавим gc_keys.json
в качестве секрета, чтобы обеспечить доступ к нему в CI, но при этом не он не попадал в систему контроля кода.
Если вы потеряете его, не беда — запустите fastlane match
снова, чтобы воссоздать ключи, сертификаты и профили.
В общем, обычно в итоге у нас будет 4 секрета:
- Ключ API App Store Connect в кодировке base-64
- Ваш пароль для приложений Apple
- Ключи доступа в Google Cloud Storage Bucket
- Учетные данные вашей сессии Fastlane (смотрите ниже)
Теперь, когда все наши секреты благополучно сохранены, мы можем приступать.
Собственные раннеры
К сожалению, облачные раннеры для MacOS на GitHub Actions стоят в 10 раз больше за минуту, чем раннеры для Linux. Хотя публичным репозиториям предоставляется 200 минут времени работы Mac runner’а в месяц, для частных или особо активных репозиториев это может оказаться мало.
Но я обещал вам, что ваш CI будет стоить 0 долларов.
Вы можете настроить локальную машину — даже ваш стандартный ноутбук для разработки - для хостинга раннера. Это довольно простой процесс, хорошо документированный на GitHub. К вашему сведению, запускать self-hosted runner на публичных репозиториях рискованно.
После того как все готово, вы можете установить свойство run-on
в файле рабочего процесса на self-hosted
, чтобы указать на свою машину (или любые другие теги, которые вы выберете). Наконец, запустите слушателя, выполнив сценарий оболочки action-runner/run.sh
.
Тестирование рабочего процесса
Наши сценарии автоматизации находятся в папке .github/workflows
нашего репозитория. Формат — это старый добрый .yaml
, определяющий, когда, где и что мы что запускаем.
name: Test on: pull_request: branches: [ main ] jobs: test: runs-on: self-hosted timeout-minutes: 10 steps: - uses: actions/checkout@v3 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: 15.2 - name: Run Unit Tests run: fastlane test
Параметр on:
определяет, когда запускать рабочий процесс: в данном случае мы хотим запускать наш тестовый набор каждый раз, когда в основную ветку вносится PR.
Параметр jobs:
показывает только одно задание, которое мы хотим запустить, — тестовый набор.
Как упоминалось выше, параметр runs-on:
позволяет выбрать, на какой машине (или машинах) запускать конвейер. self-hosted
указывает, что он будет использовать наши локальные устройства, но мы также можем поставить здесь метки или даже запросить облачный хостинг, указав macos-13 (или любой другой номер версии).
Отказ от ответственности: раннеры для mac, размещенные в облаке, довольно ненадежные. Ознакомьтесь с моим сравнением с AppCircle, чтобы получить более подробное объяснение сложности облачных раннеров GitHub Actions.
Теперь, когда мы знаем, когда (триггер on:
) и где (run-on:
), мы можем определить, что именно. Шаги: определяют, что выполнять на нашем тестовом конвейере:
actions/checkout
— стандартное встроенное действие для проверки нашего репозитория исходного кодаsetup-xcode
— это необходимое зло, которое обеспечивает использование правильной версии Xcode и связанных с ним цепочек инструментов сборки. При локальном запуске необходимо иметь установленную версию.fastlane test
, наконец, запускает наш скрипт Fastlane
Теперь, когда у нас все настроено в рабочем процессе, мы можем создать PR и увидеть его выполнение на вкладке Actions в нашем исходном репозитории.
Настроив этот рабочий процесс тестирования, мы будем знать, создали ли мы регрессию, нарушающую тестирование, каждый раз, когда создаем пул-реквест.
Но мы можем пойти еще дальше. В Настройках репозитория, в разделе Ветви, вы можете установить правило защиты. Вы можете применить правило, которое гарантирует, что test action
пройдет перед мерджем в основную ветку.
Теперь, когда ваш поток PR находится на должном уровне, мы можем встретиться с последним боссом: рабочим процессом развертывания.
Рабочий процесс развертывания
Вполне вероятно, что выполнение единственной команды Fastlane в тестовом процессе сработало просто отлично, особенно если вы использовали собственный хостинг.
Рабочий процесс развертывания — это совершенно другой зверь.
Процессов, в которых могут возникнуть проблемы на этапах инициализации, аутентификации и сборки, гораздо больше. Ситуация становится еще хуже, если вы мазохистски выбираете облачный хостинг, и вам нужно будет заниматься версионированием всего сразу.
Это последний серьезный всплеск сложности. Настойчивое преодоление этой боли — вот что отделяет зерна от плевел.
Но сеньоры никогда не создавали CI, чтобы доставлять себе боль. Они хотели избавиться от нее.
Наш финальный, проверенный в боях рабочий процесс развертывания выглядит так:
name: Deploy env: FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }} GOOGLE_CLOUD_KEYS: ${{ secrets.GOOGLE_CLOUD_KEYS }} APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }} on: push: branches: [ main ] jobs: deploy: runs-on: self-hosted timeout-minutes: 10 steps: - uses: actions/checkout@v3 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: 15.2 - name: Create gc_Keys run: echo $GOOGLE_CLOUD_KEYS >> gc_keys.json - name: App Store Deploy run: fastlane deploy
Этот рабочий процесс выглядит довольно похоже, в нем много одинаковых шагов:
- Мы настраиваем
env:
и берем все наши секреты, которые доступны как переменные окружения в рабочем процессе. Это делает их видимыми для нашего скрипта Fastlane. - Триггер теперь
on: push to main
, то есть он запускается, когда ваш PR из вышеописанного рабочего процесса будет смерджен. - Аналогичным образом мы делаем
checkout
репозитория иsetup-xcode
. - Далее мы запускаем очень простой скрипт для записи секрета
GOOGLE_CLOUD_KEYS
в локальный .json-файл, доступный скрипту Fastlane. - Наконец, мы запускаем
fastlane deploy
, чтобы включить скрипт Fastlane, который мы настроили ранее.
Повторюсь еще раз: самостоятельные хостинги (т. е. ваши собственные машины) обычно работают довольно хорошо. Облачные прогоны на GitHub Actions часто выдают непостижимые ошибки, особенно при использовании последних версий ОС и SDK (SwiftData останавливает это на корню).
Я вкратце описал эту проблему, но вы можете ознакомиться с моими многочисленными попытками заставить раннер в облачном хостинге работать. Честно говоря, собственный хостинг намного проще!
Но, как я уже говорил, это руководство посвящено настройке рабочих процессов CI за $0, и все сделал в рамках этого бюджета.
После долгих трудов и проблем наш рабочий процесс развертывания отлично работает на self-hosted раннере!
Если вы уже несколько раз были в этой ситуации, вы можете спросить: «Джейкоб, почему ты просто пишешь fastlane deploy
вместо bundle exec fastlane deploy
?».
На что я отвечу: я предлагаю вам сделать так, чтобы версионность Ruby вела себя хорошо вместе с GitHub Actions.
Заключение
Настройка конвейеров непрерывной интеграции для тестирования, сборки и развертывания ваших проектов может быть сложной задачей, если вы не знакомы с ней, но на самом деле это довольно просто.
С помощью Fastlane и GitHub Actions вы можете создать «песочницу», в которой будете отрабатывать эти методы автоматизации, потратив при этом ровно ноль долларов. Надеюсь, вы узнали что-то новое, и если вы раньше не настраивали CI, настоятельно рекомендую вам попробовать это в своем проекте!
Это не гламурно, но это честная работа.