Site icon AppTractor

Конвейеры мобильного развертывания за $0

Непрерывная интеграция (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

Разберемся снизу вверх:

Вряд ли вы удивитесь, если узнаете, что с первого раза это не сработает — вам придется пройти через значительный объем работ по настройке.

match — это первый серьезный всплеск сложности в нашей непрерывной интеграции.

fastlane match

Команда match помогает автоматизировать и унифицировать создание, хранение и управление сертификатами и профилями подписи кода для вашей команды. Что еще более важно, она дает вашей CI-машине возможность подписывать релизы без необходимости хранить учетные данные в Git.

Это одна из основных причин, по которой Senior-разработчики предпочитают сами управлять CI: match одновременно и обязателен для понимания, и немного пугает.

Если вы страдаете от синдрома самозванца, можете посмотреть на количество неудачных сборок в моем репозитории.

Для настройки запустите fastlane match init и следуйте пошаговому руководству в CLI для безопасного хранения ваших сертификатов и профилей инициализации.

Лично я предпочитаю режим хранения Google Cloud, поскольку (1) управлять секретом довольно просто, (2) у него есть щедрый бесплатный уровень, (3) у большинства мобильных разработчиков уже есть аккаунт в облаке Google через Firebase, и (4) настроить проект довольно просто.

Советы по Google Cloud

Если вы выбрали облачное хранилище Google, шаги в CLI немного непонятны, поэтому я решил дать несколько дополнительных указаний:

Файл gc_keys.json, созданный fastlane match init, содержит client_email, например jacobsapps@jacobsapps.iam.gserviceaccount.com.

В корзине Google Cloud Storage, содержащей ваши ключи, дайте администратору доступ к этому адресу электронной почты.

Чтобы 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

Тут есть несколько улучшений по сравнению с упрощенной версией выше:

Теперь, когда наш локальный конвейер на месте и работает локально, мы можем начать собирать нашу инфраструктуру: 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-ключом.

Хотя GitHub Actions позволяет давать секретам любое имя, вам нужно использовать точное имя $APP_STORE_CONNECT_API_KEY_KEY для ссылки на переменную окружения, где хранится .p8 в кодировке base-64. Загляните в репо Fastlane, чтобы узнать больше об этом.

Пароль для конкретного приложения

Есть некоторые сервисы 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 секрета:

Этот последний элемент позволяет запускать CI без использования двухфакторной аутентификации, поскольку 2FA в App Store Connect смехотворно не работает.

Если вы запускаете свой CI-конвейер, служба 2FA отправит вам код, но вы не сможете ввести его в CI-сессию. Вам нужно ввести его вручную и локально, чтобы аутентифицироваться снова, но другой код не будет отправлен в течение 8 часов!

fastlane spaceauth позволяет создать сеанс аутентификации без риска получить непригодный для использования код 2FA. Поскольку это выходит за рамки данного руководства, ознакомьтесь с аутентификацией в документации 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:), мы можем определить, что именно. Шаги: определяют, что выполнять на нашем тестовом конвейере:

Теперь, когда у нас все настроено в рабочем процессе, мы можем создать 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

Этот рабочий процесс выглядит довольно похоже, в нем много одинаковых шагов:

Повторюсь еще раз: самостоятельные хостинги (т. е. ваши собственные машины) обычно работают довольно хорошо. Облачные прогоны на GitHub Actions часто выдают непостижимые ошибки, особенно при использовании последних версий ОС и SDK (SwiftData останавливает это на корню).

Я вкратце описал эту проблему, но вы можете ознакомиться с моими многочисленными попытками заставить раннер в облачном хостинге работать. Честно говоря, собственный хостинг намного проще!

Но, как я уже говорил, это руководство посвящено настройке рабочих процессов CI за $0, и все сделал в рамках этого бюджета.

После долгих трудов и проблем наш рабочий процесс развертывания отлично работает на self-hosted раннере!

Если вы уже несколько раз были в этой ситуации, вы можете спросить: «Джейкоб, почему ты просто пишешь fastlane deploy вместо bundle exec fastlane deploy?».

На что я отвечу: я предлагаю вам сделать так, чтобы версионность Ruby вела себя хорошо вместе с GitHub Actions.

Заключение

Настройка конвейеров непрерывной интеграции для тестирования, сборки и развертывания ваших проектов может быть сложной задачей, если вы не знакомы с ней, но на самом деле это довольно просто.

С помощью Fastlane и GitHub Actions вы можете создать «песочницу», в которой будете отрабатывать эти методы автоматизации, потратив при этом ровно ноль долларов. Надеюсь, вы узнали что-то новое, и если вы раньше не настраивали CI, настоятельно рекомендую вам попробовать это в своем проекте!

Это не гламурно, но это честная работа.

Источник

Exit mobile version