Автоматическая поставка iOS приложений через Fastlane и GitlabCI

Oleg Katkov
Mad Devs — блог об IT
8 min readDec 3, 2019

Никогда такого не было, и вот опять…

Здесь будет рассказ о пользе автоматизации и вреде той ручной работы, которую должен делать компьютер. Если вам лень читать или нужно было сдать готовую CI ещё вчера, то все необходимые файлы лежат в нашем репозитории.

Началось всё с того, что юзеры (!!!) начали жаловаться на то, что приложение падает. Обновление было 2 недели назад — ̶к̶а̶к̶о̶г̶о̶ ̶х̶е̶р̶а̶ почему же тогда мы ничего не знаем о сбоях и спим спокойно? Сrashlytics настроен и всегда давал нам возможность оперативно реагировать на такие случаи, но в этот раз все уведомлялки (Slack, почта) предательски молчали. Что пошло не так? Всё очень просто — мы забыли загрузить dSYM файл, без которого невозможно получить хоть какие-то подробности о сбое. Это не совсем так, конечно. Наверняка можно узнать о факте сбоя, о количестве крашей и т.п. Но без отладочных символов проанализировать причину крайне затруднительно.

Надо сказать, что до этого мы долгое время отправляли приложения в релиз вручную. Обычно этим занимался разработчик, который вручную запускает (если не забывает) UNIT/UI тесты, затем собирает приложение, подписывает, делает ещё какую-то черную магию и заливает билд в Testflight. И уже после этого, этот же человек обновляет dSYM файлы для сервиса Сrashlytics. С таким порядком больше невозможно было мириться, и мы приступили к полной автоматизации всего процесса. Дальше будет просто описание того, что и зачем было сделано.

Что понадобится

  • Рабочий Mac. В нашем случае используется OS Darwin Kernel Version 18.5.0 — MacOS catalina. Mac может быть даже с разбитым экраном или ещё как-либо повреждённый, лишь бы рабочий. Мы его будем использовать в качестве CI сервера. Чтобы процесс тестирования/билда не превратился в пытку нужно, чтоб процессор был достаточно мощным. На i5 прогон билда с тестами занимал около 40 минут. На i7 — уже 10 минут, жить можно.
  • Apple APP ID, созданный как на Developer Portal, так и в iTunes Connect.
  • Xcode 11.1 (на момент написания статьи при использовании 11.2 часть библиотек еще не собиралась)
  • Ruby 2.6.5
  • Очень большой запас терпения

GitLab CI

GitLab — сайт и система управления репозиториями кода для git. Он предоставляет очень много дополнительных возможностей: вики, issue tracker и многое другое. Среди них — собственная CI, упрощающая жизнь разработчика в разы. Самое главное — её не нужно поднимать самому. Всё сделано умными дяденьками из gitlab, нужно только прочитать документацию, настроить .gitlab-ci.yml и установить (не обязательно) gitlab-runner.

Здесь же в настройках gitlab CI можно хранить переменные окружения. К примеру, мы там храним версию XCode, с помощью которой мы хотим собирать приложение.

Переменные окружения для проекта

gitlab-runner

Для сборки ios приложений необходимо использовать свой specific runner, позволяющий выполнять на нашем сервере любые скрипты. Собственно gitlab-runner — это штука, которая будет выполнять код, определенный в .gitlab-ci.yml файле. Они бывают разные (shared|specific, docker/shell… и т.п., все подробности в документации) и можно установить его у себя на сервере, чтобы, во-первых, иметь полный контроль над окружением (особенно если это shell runner), а, во-вторых, не платить gitlab-у :).

С ним в MacOS не всё прошло гладко, но могло быть и хуже. Не рекомендуется устанавливать его через brew, хотя особой разницы в работе между brew версией и рекомендованной замечено не было. После установки нужно выполнить gitlab-runner install, чтоб добавить его в автозагрузку . Здесь у меня и начались приключения, потому что я выполнил эту команду под своим пользователем, а кто-то до меня — из под root. Разница в том, что если выполнить от своего пользователя, то конфигурационный файл появится в папке ~/Library/LaunchAgents, а если из под root — /Library/LaunchDaemons . Если в системе одновременно стартуют оба, то будут всякие интересные эффекты. К примеру, rbenv настроен для пользователя, а не для root-а. В общем не стоит миксовать эти 2 раннера.

Если используется runner, стартующий из LaunchAgents, то необходимо настроить автоматический логин в систему для этого пользователя, чтоб при неожиданном рестарте компьютера сервер нормально поднялся. Такое случилось один раз, когда по недосмотру поставили монитор на Mac и он перегрелся 😐.

Так же для удобства стоит отредактировать файл gitlab-runner.plist, а именно секцию working-directory. По умолчанию это просто домашняя директория пользователя, а в процессе билда всякие артефакты могут её замусорить, что не очень хорошо.

После того, как убедились, что стартует 1 gitlab-runner — нужно зарегистрировать его и настроить проект на его использование. Для этого идём в gitlab -> your project -> settings -> CI/CD -> Runners. Следуем инструкциям из раздела “Set up a specific Runner manually”. Результат должен быть приблизительно такой:

Работающий gitlab-runner

При этом нужно отключить использование Shared Runners. Теперь всё готово со стороны gitlab CI и нужно настроить сборку приложений на сервере.

fastlane

Все задачи, связанные с поставкой, тестированием и т.п. были решены с помощью fastlane. Fastlane — это инструмент для автоматизации процессов сборки и публикации мобильных приложений, которая включает в себя также генерирование скриншотов, запуск Unit/UI тестов, подключение к Crashlytics, генерирование Change Log и многие другие полезные вещи, упрощающие жизнь.

Fastlane представляет из себя набор процедур (lanes), которые можно вызывать с помощью команды fastlane lane_name arguments. Подробней о синтаксисе можно почитать здесь.

Процедура before_all используется только для настройки переменных. В нашем случае она выглядит так:

Всё это можно спокойно вынести и в настройки CI, но для локальных тестов было удобнее сделать отдельным шагом + меньше переменных в настройках gitlab CI.

Далее в файле находятся 2 процедуры установки переменных для staging и для prod версий приложений.

Затем вспомогательные функции для установки pods и обновлении пути до plist файла. Они вызываются практически везде, поэтому, скорее всего, мы их вынесем в before_all.

Далее работа с сертификатами. Здесь стоит заметить, что если используется свой закрытый репозиторий сертификатов, то просто обязательно нужно ssh ключ CI сервера добавить в deployment keys в настройках этого репозитория.

Собственно процедура запуска тестов. suppress_xcode_output = true, потому что вывод может быть очень большим. В нашем случае — 3 GB логов. А так он просто будет в лог файлик аккуратно складывать. Для тестов это файл /Users/<user>/Library/Logs/scan/<APP_NAME>.log, для билда: /Users/<user>/Library/Logs/gym/<APP_NAME>.log. Пути до этих файлов берутся из консоли билда.

В случае ошибок всегда можно будет посмотреть в этих файлах что произошло (на первых порах очень пригодилось).

Следующая фаза нужна для того, чтоб дождаться публикации билда на testflight, и уже потом пытаться обновить dSYM файл. Вообще говоря после загрузки билд может находиться в стадии “waiting for review” до 48 часов. В это время проверяются эти пункты. Задача в gitlab CI может просто аварийно завершиться с таймаутом в это случае. И чтобы не загрузить один и тот же билд повторно используется следующая функция:

Следующий шаг build and upload. Сначала это были разные шаги и *.ipa файл прокидывался через gitlab artifacts. Это оказалось значительно медленнее, поэтому на данный момент объединили эти шаги. Единственное неочевидное место здесь — вот эта проверка if Actions.lane_context[SharedValues::LATEST_TESTFLIGHT_BUILD_NUMBER].to_s == ENV["CI_PIPELINE_ID"].to_s . Эта проверка нужна, чтобы не загружать билд, который уже есть в testflight. В качестве версии мы используем CI_PIPELINE_ID , потому что, в случае чего, можно посмотреть как прошли тесты, как проходила загрузка, билд и т.п.

Скрипт, который собирает приложение. Здесь тоже ничего необычного, собирается и подписывается приложение.

И, наконец, обновление dSYM файла. Здесь есть интересный момент. После загрузки приложения в testflight файлы dSYM становятся доступны не сразу, а через какое-то время. Связано это с тем, что Apple пересобирает приложения (собственно поэтому нельзя взять и загрузить dSYM файл с локальной машины, если используется bitcode). Генерируется новый dSYM файл, и уже его можно загрузить в crashlytics. Поэтому в этой функции есть проверка на то, что загрузка dSYM файла прошла успешно. Если это не так, то pipeline падает и в слэк такое прилетает уведомление:

Единственная серьезная проблема возникла при первом запуске — cocoa pods ставились так долго, что отвалились с таймаутом несколько раз. Решается так: git clone https://github.com/CocoaPods/Specs.git ~/.cocoapods/repos/master на маке. После того, как всё склонируется, можно запускать fastlane install_pods . Опять же, запускается один раз вручную, потому что очень много времени занимает в первый раз.

После того, как протестировали этот скрипт локально можно было конфигурировать gitlab CI.

.gitlab-ci.yml

В gitlab CI разделили всё на 3 этапа:

  1. Тестирование. Запускается при merge request’ах, в dev, в master. Сделано так, чтоб не допустить поломанных тестов в ветках dev и master.
  2. Сборка и загрузка приложения в testflight.
  3. Обновление dSYM файла.

При этом шаги 2 и 3 разделены для staging и production приложений — production собирается из master ветки, staging — из dev.

Грабли

gudim.anton
  1. Сначала работал с MacOS по ssh. Неведомые ошибки валились при попытке собрать и подписать приложение. Оказалось, что без личного присутствия возле компьютера и авторизации через логин скрин этого сделать нельзя. Когда подошел к макбуку и начал работать с ним напрямую — все ошибки исчезли.
  2. Установил gitlab-runner для root пользователя. В результате у меня сразу 2 раннера зарегистрировались и было вообще непонятно какой работает и где. Вывод — не использовать root для работы с gitlab-runner-ом, да и вообще забыть про него в Mac OS.
  3. Забыл добавить ssh key в репозиторий с сертификатами. Прождал 2 часа, прежде чем понял, что у меня CI спрашивает пароль. Решается так: идем в gitlab->your_project->settings->repository->deploy keys и добавляем ssh ключ своего CI сервера. Обычно это файл ~/.ssh/id_rsa.pub. Если такого файла нет, то его нужно сгенерить командой ssh-keygen.
  4. Настроил rbenv для пользователя через .zhsrc . Нужно было явно прописать в before_all скрипте.
  5. Использовали дефолтный протокол iTMSTransporter-а для загрузки приложения в Testflight. Он очень медленный, процесс занимал от 40 минут и больше. После подбора выяснили, что заливка по протоколу Aspera выполняется иногда в 10 раз, но в среднем в 5–8 раз быстрее. Для того, чтобы использовать нужный протокол, небходимо объявить переменную в фастайле ENV["DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS”]="-t Aspera"

Результат

Мы получили CI, которую теперь используем во всех проектах мобильной разработки. Конечно, поначалу было трудно привыкнуть, что на все MR запускаются тесты, что приложение собирается дольше, чем раньше. Время сборки увеличилось, потому что CI собирает приложение с нуля, в то время как на машине разработчика пересобираются только изменения с последнего запуска сборки.

Но, во-первых, разработчики начали более тщательно тестировать приложения локально и серьёзнее относиться к написанию unit/ui тестов. Во-вторых, хоть какая-то уверенность появилась, что dSYM файл будет загружен. А если и не будет, то мы хотя бы узнаем об этом. В-третьих, разработчики продолжают решать задачи, а не ждут, пока приложение загрузится в testflight и т.п. (все шаги после тестирования).

Файлы доступны в нашем репозитории. Комментарии и проблемы в виде issues приветствуются.

--

--