В основном идея использования CI/CD для iOS, да и для других платформ, — это автоматизация рутинной работы. Когда мы работаем над одним приложением, можем вручную собирать небольшой проект. Но команда растёт, хочется тратить время эффективнее, чем вручную собирать проект или объяснять новичкам, что же там с Code-signing нужно делать.  Пожалуй, самое рутинное и самое важное занятие, которое берёт на себя CI, — это прогон тестов. Нет зелёных тестов? В master не попадёшь. А с ростом команды вероятность того, что кто-то вольёт в master нерабочий код, будет только увеличиваться. Нужна автоматизация. В этой статье я хочу подробно рассказать о пути настройки Gitlab CI + Fastlane + Firebase + Testflight. Примеры приводятся на основе одного проекта, в котором участвовали 10 разработчиков. В конце будут описаны проблемы, с которыми мы сталкивались, и их решения. Для кого будет полезен этот опыт? Для всех, кому нужен CI/CD и кто сидит на Gitlab. Для Github будет другая связка, например с Travis, — остальные компоненты неизменны. В нашей команде все используют Gitlab CI, Fastlane вместо голого xcodebuild для быстроты и удобства разработки, Firebase и Testflight. Если у нас бесплатный Gitlab и мы укладываемся в лимит Firebase, то получаем бесплатное решение по настройке CI/CD. Gitlab CI — это система автоматической сборки. Она занимается в том числе и отслеживанием изменений в репозитории, что важно для наших целей. Отслеживает событие, смотрит инструкции к нему, которые вы указали в файле .gitlab-ci.yml. Описанная в статье работа ведётся на ноутбуках разработчиков, есть выделенные машины на MacOS — их мы регистрируем в Gitlab CI. Fastlane — верхнеуровневое управление сборками с помощью команд в терминале. Для него есть много плагинов на сайте, которые помогают выполнять задачи разного уровня: от генерации AppIcon до сборки проекта и публикации в AppStore. Плагины, как и сам Fastlane, написаны на ruby, поэтому и управлять этим инструментом придётся на ruby. Gitlab CI посылает инструкции в виде shell-команд на раннер, в набор команд входит Fastlane. Таким образом, они взаимодействуют, выполняя каждая свои задачи. Firebase — система дистрибьютинга сборок, хорошее решение для быстрой доставки сборок до ваших тестировщиков. Мы используем Firebase App Distribution — он бесплатный и без ограничений на место. Code coverage — процент покрытия вашего кода тестами. Упрощённо можно привести к формуле: количество скомпилированных строк кода, по которым пробежали тесты / общее количество скомпилированных строк кода) * 100% Первое, что нам нужно сделать на пути самурая автоматизации, — это подружить наш mac с CI. Для этого на самом маке нужно выполнить пару команд: Установить программу gitlab-runner      на mac (можно с помощью Homebrew). Ссылка на установку Homebrew.  Вводим в терминал: brew install gitlab-runner Теперь надо зарегистрировать наш mac как раннер для CI: gitlab-runner register После этого надо будет ввести URL до гитлаба, где хостимся. Ещё понадобится токен для CI. Его можно получить, только будучи Owner’ом или Maintainer’ом проекта в настройках проекта. В Settings должен быть раздел CI/CD. Раздел Runners. Вводим токен туда. Описание раннера ни на что не влияет, оно даётся только для справки. Обратите внимание на теги раннера. Без тегов ничего не будет работать, нужен минимум один. По тегам можно фильтровать разные раннеры: например, один будет с доступом к сети и сможет выкладывать сборки, а другой — нет. Тогда можно обозначить разные теги для разных функций раннеров. Например, для одного впишем теги ios_tests,ios_firebase, а в другом — только ios_tests. И сможем разделять исполнителей по типу.  Тип исполнения для нас всегда будет shell. Зарегистрировали наш раннер. Теперь давайте посмотрим и убедимся, что он находится в том же списке Runners в настройках проекта. Восклицательный знак указывает, что раннер ещё не запущен и не готов к работе. Идём к следующему шагу. Запускаем раннер. Выполняем команды в терминале: Теперь наш раннер должен загореться зелёным в списке. У раннеров есть проблема с логами. Для того чтобы мы могли просматривать полный файл логов, нужно прописать максимальный размер. Для этого проходим по пути “~/.gitlab-runner/”. Там будет файл config.toml, в котором хранится вся информация о зарегистрированных раннерах, в нём нам нужно прописать параметр output_limit, — это максимальный размер в байтах хранимых логов в джобах. Для всех параметров есть документация, для output_limit написано: “Maximum build log size in kilobytes. Default is 4096 (4MB)”. То есть по умолчанию — 4 мегабайта.  Если на одной машине мы регистрируем несколько раннеров, например для разных IOS-проектов, то в каждом случае изначально указываются дефолтные параметры для всего. Значит, если мы хотим увеличить объём логов, надо дописывать в каждом случае. Всё, что будет превышать этот порог, не будет залогировано и не будет отображаться в информации о джобах. Примерно так это должно выглядеть: Нужно быть аккуратным с логами в джобах, потому что один файл логов в среднем весит 37 Мб, он занимает много места. Все логи хранятся в гитлабе и увеличивают размер репозитория. Готово, Gitlab CI теперь может работать с раннером! Установить Fastlane можно тоже через Homebrew (документация). Необходимо наличие ruby в системе, обычно он установлен в macos по дефолту. Набираем: Открываем терминал, переходим в папку с проектом с помощью cd./path_to_project и выполняем команду Сгенерится папка Fastlane с нужными файлами. Можно использовать Fastlane с помощью Swift: описывать лайны на Swift, а не на неизвестном DSL, — это упростит понимание скрипта для коллег (документация).  Самый важный файл здесь — это Fastfile. Он состоит из lane. Каждый из них — отдельный сценарий Fastlane. Можно представить сам Fastlane как некий класс (в ООП), а lane — как его функции. Синтаксис довольно простой.  Разберём на примере lane для юнит-тестов. Для этого можно использовать разные плагины. Мы в компании выбрали scan как самый удобный и функциональный. Ниже пример кода. Если хотите, можете ознакомиться со всеми параметрами и их предназначением в подробном описании по плагину scan.  Получился lane, который запускает тесты для схемы CoreTests, которую мы берём из параметров вызова lane. Ему можно скармливать как схемы обычных таргетов, так и отдельные схемы под тесты. Передавать надо названия схем, а не сами таргеты. В большинстве проектов один таргет, одна схема — и нет путаницы. Но при создании CI больших проектов, когда 30–40 конфигураций, а таргетов ещё больше, важно ориентироваться на правильные понятия. Таргет — это executable(buildable)-проект, у него есть описательная схема с Build Phase, Build Settings, General. Схемы — это инструкции, описывающие, как комбинировать таргеты. Например, одна схема для тестов может запускать несколько тестовых таргетов под собой. В основном плагины Fastlane работают со схемами для запуска. Дальше для проверки работоспособности lane введём в терминал команду: В ответ мы увидим логи компиляции и сами логи Fastlane. По итогу получим папку ./tests/CoreTests_report/ с отчётом о тестировании в формате .xcresult. По дефолту плагин scan генерирует нам ещё два файла — report.html & report.junit. Первый обычно используется для показа людям, а второй — для Gitlab CI в качестве отчёта тестирования, — об этом позже в разделе про артифакты в пайплайнах. На основе сгенерированных отчётов можно проверять, были ли зафейленные тесты и в каких местах. У нас есть отдельный скрипт на Swift, который на основе report.junit проверяет наличие проваленных тестов и говорит, в каких фреймворках.  Также мы считаем покрытие тестами и на основе этих данных ограничиваем мерж в master. Это работает с помощью Fastlane. Он уже выдал нам .xcresult, и на его основе мы будем получать значение покрытия. Плагин для Fastlane xcov может выдавать разные форматы отчётов. Нам нужен json, так как нас интересует сам процент покрытия, а проще всего его получить из json. В самом начале функции есть махинации с файлом .xcresult потому, что xcov требует, чтобы он находился в определённом месте иерархии папки derived. Поэтому мы переносим его в это место перед подсчётом code coverage. И желательно, чтобы папка output_directory была пуста, т. к. xcov не перезатирает отчёты, а просто падает, если видит, что в этой папке уже есть файлы с таким названием.  На выходе мы получим report.json, в котором содержится много информации, параметр coverage — это наш процент. Этот файл содержит информацию по общему coverage, каждому таргету, каждому файлу и каждому исполняемому скоупу. Считывание ковеража в Gitlab CI динамическое. В настройках вы задаёте Regex-паттерн, который будет проверять в каждой строчке логов, подходит ли число под паттерн. Мы используем такой формат: С его помощью парсится вывод скрипта Swift. Подходят записи типа “(51.67%)covered” Наш скрипт написан на Swift. Он нужен только для того, чтобы вывести coverage в нужном формате. Всё, что от него требуется, — чтобы какая-нибудь рандомная строчка в коде не попала под это regexp. Мы используем .junit, потому что это стандартный формат отчётов о тестировании в Gitlab CI. Внутри пайплайнов можно увидеть, сколько и какие тесты прошли, а какие нет, информацию по ним, — для этого необходим отчёт junit. .xcresult, который в себе содержит намного больше информации, включая code coverage. Сама настройка Regex находится в настройках проекта Settings -> CI/CD -> General pipelines, в графе Test coverage parsing. Подробнее про Regex можно почитать в этой статье. Там же есть ссылка на сайты, в которых можно протестировать работу формулы (например, тут). Теперь устанавливаем Firebase CLI, — это управление сборками в Firebase из командной строки. В обычном режиме разработчик идёт на сам сайт Firebase, закидывает туда сборки и управляет проектом, но это можно сделать с помощью командной строки. Для этого нам и нужен Firebase. Мы используем бесплатный Firebase App Distribution (в Firebase есть другие платные фичи, которые не относятся к CI/CD).  Как настроить Google Firebase: документация Вот краткий план: 1. Устанавливаем плагин к Fastlane — Firebase. 2. Устанавливаем Firebase CLI. 3. Получаем токен учётной записи, у которой есть доступ в проект, куда мы будем отправлять наши сборки. 4. Вот lane, который будет отправлен в Firebase. Функция умеет собирать сборку для публикации в AppStore, для тестировщиков, для общего пользования. Они отличаются кастомными параметрами в ExportOptions. При указании export method: “development” ExportOptions генерируется автоматически. Для production ExportOptions надо описывать вручную. Можно приложить ссылки для production- и development-опций экспорта. Дальше можно объединить это в один lane или сделать отдельный для каждого действия, чтобы CI мог дёргать эти функции в каком угодно порядке. У нас это разделено. Всё, что мы написали на Fastlane, нужно проверять с помощью консольных команд. Например, для публикации в Firebase можно выполнить две команды подряд: Эти же строки можно объединить в одну для удобства, поставив между ними “;” Для публикации используются git tags. Например, если мы для коммита запушим тег dev_1.72.44.TASK-123, то в Firebase отправится сборка с номером 1.72.44.TASK-123. Сами вызовы с указанием версии идут из gitlab-ci.yml. Если что-то пойдёт не так, джоба зафейлится и разработчику придёт уведомление об этом. Посмотрев на логи джобы, он увидит причину: например, проект удалили или что-то ещё серьёзное случилось. Firebase позволяет публиковать приложения с одинаковой версией. Testflight — это система обработки сборок от Apple. В ней можно делать сборки для тестировщиков, а также релизить протестированные сборки. По схеме CI/CD она будет на уровне с Firebase, т. к. они взаимозаменяемы, только Firebase более удобен для дистрибуции тестовых сборок. А Testflight мы используем для релиза итоговых сборок для пользователей. Firebase — для теста, потому что удобнее и быстрее. Testflight — для production, потому что релизить можно только через неё. Мы загружаем наш артефакт в ItunesConnect, и сама система предоставляет нам возможность отправить его сразу в Testflight. Testflight даёт возможность скачивать приложения, над которыми работаем, прямо на айфон. Основные этапы публикации в AppStore те же. Нам нужно собрать архив и отправить его, только теперь отправляем в AppStore. Тестировщик скачает Testflight и получит доступ к тому, что мы задеплоили.  Следующий этап — изменение версии сборки, чтобы мы могли разные сборки публиковать для тестировщиков. Например, разные команды делают сборки под себя и будут отличать их билд-версиями. Так вот, AppStore не позволит загрузить сборки с одинаковыми версиями, поэтому нужно автоматизировать изменение версии проекта. Есть небольшой lane для изменения версии и номера билда с помощью Fastlane. Тут описаны два lane: один валидирует версию, чтобы она подходила для публикации в AppStore, второй может подставить в версию что угодно (ну или почти что угодно). Первый мы используем только для Testflight, а второй — для Firebase, т. к. Firebase позволяет писать буквы в версию и не придирчив к повторам. Ещё нам понадобится сам lane публикации в AppStore:  Lane несложный, поэтому тут нечего дополнять, просто передаём нужные параметры в плагин Fastlane. Порядок вызова lane для публикации в AppStore прост: Здесь $right_version — это желательно валидный номер версии. Если он будет невалидным, то lane провалится и на этом путь публикации закончится. ${ITUNES_USERNAME} — это email юзера, от имени которого мы будем публиковать архив в Testflight. Теперь нужно описать правила, по которым у нас будут происходить взаимодействия ивентов в Gitlab и написанных скриптов. И имя ему  —  gitlab-ci.yml. Этот файл задаёт стандарт сборки и проверки приложения. Это тот самый файл, который запускает все lane, считает нужные параметры, управляет артефактами. Один из его минусов(а может, и плюсов) — скрипты запускаются на bash. Поэтому переиспользование тут не совсем обычное. Об этом расскажу позже, но сначала ознакомимся с простыми примерами файла: Если мы запушим в репозиторий файл .gitlab-ci.yml с таким содержимым, то у нас будут на каждый ивент гитлаба создаваться джобы, которые будут запускать скрипт echo “Hello world”. Сам по себе пайплайн прост: Исполняемый код может находиться в трёх стадиях, они указаны по порядку: before_script script after_script Здесь мы и будем писать вызовы скриптов Fastlane, Swift и что душе угодно. Эксперименты с .gitlab-ci.yml Приведу немного примеров из личного опыта, чтобы объяснить, как мы настраиваем пайплайны под свои запросы. Попробуем посмотреть, что будет с таким файлом CI, в котором 5 стадий и 8 джоб, и разберём, как это будет работать. Тут есть несколько правил: Пайплайны (pipeline) состоят из стадий (stage), а они в свою очередь — из джоб (job) Каждая джоба может состоять только в одной стадии Каждая стадия может содержать нескольких джоб, которые запускаются последовательно и параллельно. Значит, если у вас есть несколько раннеров, то test_job1 и test_job2 стартанули бы почти одновременно, не ожидая завершения друг друга  Стадии идут только последовательно, каждая перед стартом ожидает завершения всех джоб из предыдущей стадии Если при выполнении джобы вызвалась команда exit с ненулевой ошибкой, то джоба фейлится: exit 0 означает, что джоба успешно завершилась; любая другая цифра будет обозначать какую-то ошибку Если в стадии одна из джоб зафейлилась без флага allow_failure: true, то следующая стадия не стартует. Для test_job3 этот флаг включёен, а для test_job5 нет Если в стадии несколько джоб и одна из них зафейлилась, то другие джобы стадии продолжают выполнение В разных джобах можно писать coverage. Например, в джобе 1 мы написали coverage 0%, в джобе 2 получили 10%, и в джобе 4 написали 100%. Тогда общий coverage пайплайна будет считаться как среднее среди всех джоб (0 + 10 + 100) / 3. Это можно использовать, например, для мультимодульности. Создать стадию тестов и на этой стадии — по одной джобе на каждый фреймворк. Но можно сделать, как мы в компании, на уровне Xcode. У нас есть выделенная схема для юнит-тестов всех фреймворков, в которую добавлены все тестовые таргеты. Результатом тестирования этой схемы являются   прогнанные тесты в этих таргетах и средний coverage по ним Coverage учитывается, даже если джоба зафейлилась В джобе 8 выставлен флаг when: on_failure, — это значит, что джоба выполнится только в том случае, если пайплайн зафейлился. В случае если пайплайн прошёл без проблем, эта джоба не запустится. Такой флаг полезно ставить, например, в джобы, которые оповещают о зафейленном пайплайне Порядок стадий в пайплайне зависит от их порядка в файле CI в фазе stages: первыми будут выполняться те, что находятся выше по порядку. Джобы — так же: чем выше вы их описали в файле, тем раньше они выполнятся в пайплайне В файле CI есть подобие наследования — так называемые extends:. Мы создаем шаблонную джобу, которую будем расширять нужными нам свойствами.  Важное замечание: если мы в шаблоне указали какой-то параметр, например script или tags, а не в джобе, которая расширяет этот шаблон, то если мы в джобе напишем собственный script, то исполнятся будет только тот, что написан в джобе. Переопределение полностью отменяет свойство шаблона. Например, мы хотим в конце каждой джобы выполнять какое-то действие, тогда можем это прописать в before_script шаблона, а в самих джобах исполняемые команды писать в фазу script. А теперь реальный пример того, как мы гоняем тесты и считаем coverage. Рассмотрим .gitlab-ci.yml такого вида — сверху вниз по каждому пункту. В файле .gitlab-ci.yml есть раздел с переменными, где мы можем задать кастомные свойства для всего файла. Также стоит учитывать, что помимо параметров, которые укажете в самом файле, вам будет доступен целый набор Predefined CI/CD variables и набор кастомных параметров, указанных в настройках проекта (Settings ⃗⃗→ CI/CD → Variables). Все три категории свойств будут доступны во всех ваших описываемых джобах внутри .gitlab-ci.yml. Здесь мы указываем набор стадий для пайплайнов, пока что они просто обозначают, как джобы будут группироваться внутри пайплайнов. Расскажу подробнее про структуру джоб: extends — нужно, чтобы переиспользовать базовые вещи; stage — указываем, в какой стадии участвует джоба. Внутри одной стадии они выполняются параллельно; before script, script, after script — набор команд Shell в том порядке, в котором исполняются; only/except — описывают, под какие правила попадает / точно не попадает джоба; tags — теги, которыми мы пометили раннер, когда регистрировали его; allow failure — свойство, которое разрешает / не разрешает зафейленную джобу, здесь можно указывать допустимые exit codes; artifacts — будем использовать для хранения артефактов джобы. Gitlab-runner предоставляет возможность запускать локально ваши джобы для их тестирования перед пушем в репозиторий, что упрощает жизнь. Для этого нужно выполнить команду Команда выполнялась для такого gitlab-ci.yml. У такого дебаггинга есть минус:  вам не будут доступны реальные глобальные переменные по типу CI_JOB_ID, CI_PIPELINE_ID, т. к. эти параметры передаются Gitlab на раннер. Но такой дебаггинг вполне сгодится для проверки работоспособности локальных скриптов. Вот ещё пример вывода глобальной переменной Мы научились настраивать Gitlab-runner на машине Разобрались, как работает Fastlane Можем написать lane, которые помогут: протестировать, собрать информацию о code coverage, собрать архив и отправить его в Firebase или Testflight Ещё раз повторим действия в командной строке: Устанавливаем gitlab-runner brew install gitlab-runner Делаем mac раннером для CI gitlab-runner register Запускаем раннер gitlab-runner install gitlab-runner start Устанавливаем Fastlane brew install fastlane Папка Fastlane с нужными      файлами fastlane init Проверим      работоспособность lane fastlane unit_test scheme:CoreTests Публикуем в Firebase fastlane archive_project scheme:“Dev” filename:“dev_1.2.3” fastlane distribute_to_firebase filename:“dev_1.2.3” scheme:“Dev” Вызов lane для публикации в AppStore fastlane update_version version:“$right_version” fastlane archive_beta scheme:${scheme} filename:${filename} fastlane distribute_to_testflight itunes_username:”${ITUNES_USERNAME}” filename:${filename} Локально запустим джобы, чтобы тестировать их перед пушем в репозиторий gitlab-runner exec shell exec shell test_job1 Использование Gitlab CI + Fastlane + Firebase + Testflight обходится недорого. Можно иметь 2–3 машины и настроить работу. Работа команды выглядит так: Разработчик сделал задачу, написал на неё тесты и отправил МР на код-ревью.          Внутри МР CI/CD считает, не слишком ли низкий code coverage, и проверяет, собирается ли проект.                        Отправив MP на ревью, разработчик ставит тег на последний коммит, например  dev_1.33.22.TASK-123 cicd         .                   Он готовит сборку и отправляет ее в Firebase.                        Из Firebase тестировщик скачивает сборку. Проверяет, получает ревью и   нужные апрувы.                   Мы готовы мержить: держим код в master и на ней запускается пайплайн обновления значения code coverage, чтобы уже другие MP не попадали в master с понижением. Как видите, большую часть работы делает за нас Gitlab CI, а нам главное — расставить триггеры в правильных местах и настроить наш процесс сборки. Документируйте настроенный CI/CD, научите всех пользоваться им, предлагайте идеи по автоматизации процессов и спрашивайте ваших коллег о болях. Возможно, пара скриптов, встроенных в CI/CD, сэкономят деньги для бизнеса или нервы ваших коллег. Задавайте свои вопросы в комментариях! IOS developer