Как мы автоматизируем iOS: настройка Gitlab CI + Fastlane + Firebase + ItunesConnect

В основном идея использования 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%

[Gitlab CI] Gitlab-runner

Первое, что нам нужно сделать на пути самурая автоматизации, — это подружить наш mac с CI. Для этого на самом маке нужно выполнить пару команд:

  1. Установить программу gitlab-runner на mac (можно с помощью Homebrew). Ссылка на установку Homebrew.

    Вводим в терминал:

    brew install gitlab-runner

  2. Теперь надо зарегистрировать наш mac как раннер для CI:

    gitlab-runner register

После этого надо будет ввести URL до гитлаба, где хостимся.

Ещё понадобится токен для CI. Его можно получить, только будучи Owner«ом или Maintainer«ом проекта в настройках проекта. В Settings должен быть раздел CI/CD. Раздел Runners.

Раздел в настройках CI/CDРаздел в настройках CI/CDimage-loader.svg

Вводим токен туда.

Описание раннера ни на что не влияет, оно даётся только для справки.

Обратите внимание на теги раннера. Без тегов ничего не будет работать, нужен минимум один. По тегам можно фильтровать разные раннеры: например, один будет с доступом к сети и сможет выкладывать сборки, а другой — нет. Тогда можно обозначить разные теги для разных функций раннеров. Например, для одного впишем теги ios_tests, ios_firebase, а в другом — только ios_tests. И сможем разделять исполнителей по типу.

Тип исполнения для нас всегда будет shell.

gitlab-runner registergitlab-runner register

Зарегистрировали наш раннер. Теперь давайте посмотрим и убедимся, что он находится в том же списке Runners в настройках проекта.

image-loader.svg

Восклицательный знак указывает, что раннер ещё не запущен и не готов к работе. Идём к следующему шагу.

Запускаем раннер. Выполняем команды в терминале:

gitlab-runner install
gitlab-runner start

Теперь наш раннер должен загореться зелёным в списке.

image-loader.svg

У раннеров есть проблема с логами. Для того чтобы мы могли просматривать полный файл логов, нужно прописать максимальный размер. Для этого проходим по пути »~/.gitlab-runner/». Там будет файл config.toml, в котором хранится вся информация о зарегистрированных раннерах, в нём нам нужно прописать параметр output_limit, — это максимальный размер в байтах хранимых логов в джобах. Для всех параметров есть документация, для output_limit написано: «Maximum build log size in kilobytes. Default is 4096 (4MB)». То есть по умолчанию — 4 мегабайта.

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

Всё, что будет превышать этот порог, не будет залогировано и не будет отображаться в информации о джобах. Примерно так это должно выглядеть:

[[runners]]
  output_limit = 500000
  name = "My first runner"
  url = "https://gitlab-01"
  token = "7qybRaWukobUuXYoiSMc"
  executor = "shell"
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]

Нужно быть аккуратным с логами в джобах, потому что один файл логов в среднем весит 37 Мб, он занимает много места. Все логи хранятся в гитлабе и увеличивают размер репозитория. Готово, Gitlab CI теперь может работать с раннером!

Настройка Fastlane

Установить Fastlane можно тоже через Homebrew (документация). Необходимо наличие ruby в системе, обычно он установлен в macos по дефолту. Набираем:

brew install fastlane

Открываем терминал, переходим в папку с проектом с помощью cd./path_to_project и выполняем команду

fastlane init

Сгенерится папка Fastlane с нужными файлами. Можно использовать Fastlane с помощью Swift: описывать лайны на Swift, а не на неизвестном DSL, — это упростит понимание скрипта для коллег (документация).

Самый важный файл здесь — это Fastfile. Он состоит из lane. Каждый из них — отдельный сценарий Fastlane. Можно представить сам Fastlane как некий класс (в ООП), а lane — как его функции. Синтаксис довольно простой.

Разберём на примере lane для юнит-тестов. Для этого можно использовать разные плагины. Мы в компании выбрали scan как самый удобный и функциональный. Ниже пример кода. Если хотите, можете ознакомиться со всеми параметрами и их предназначением в подробном описании по плагину scan. 

desc "Прогон юнит тестов для определенных схем. Пример вызова: fastlane unit_test scheme:CoreTests"
lane :unit_test do |options|
    scheme = options[:scheme]
    scan(
        scheme: scheme,
        device: "iPhone 11 Pro",
        fail_build: false,
        clean: true,
        derived_data_path: "./tests/derived",
        max_concurrent_simulators: "3",
        code_coverage: true,
        output_directory: "./tests/#{scheme}_report/",
        result_bundle: true
    )
end

Получился lane, который запускает тесты для схемы CoreTests, которую мы берём из параметров вызова lane. Ему можно скармливать как схемы обычных таргетов, так и отдельные схемы под тесты. Передавать надо названия схем, а не сами таргеты.

В большинстве проектов один таргет, одна схема — и нет путаницы. Но при создании CI больших проектов, когда 30–40 конфигураций, а таргетов ещё больше, важно ориентироваться на правильные понятия. Таргет — это executable (buildable)-проект, у него есть описательная схема с Build Phase, Build Settings, General. Схемы — это инструкции, описывающие, как комбинировать таргеты. Например, одна схема для тестов может запускать несколько тестовых таргетов под собой. В основном плагины Fastlane работают со схемами для запуска.

image-loader.svg

Дальше для проверки работоспособности lane введём в терминал команду:

fastlane unit_test scheme:CoreTests

В ответ мы увидим логи компиляции и сами логи Fastlane. По итогу получим папку ./tests/CoreTests_report/ с отчётом о тестировании в формате .xcresult. По дефолту плагин scan генерирует нам ещё два файла — report.html & report.junit. Первый обычно используется для показа людям, а второй — для Gitlab CI в качестве отчёта тестирования, — об этом позже в разделе про артифакты в пайплайнах.

На основе сгенерированных отчётов можно проверять, были ли зафейленные тесты и в каких местах. У нас есть отдельный скрипт на Swift, который на основе report.junit проверяет наличие проваленных тестов и говорит, в каких фреймворках.

Code coverage

Также мы считаем покрытие тестами и на основе этих данных ограничиваем мерж в master. Это работает с помощью Fastlane. Он уже выдал нам .xcresult, и на его основе мы будем получать значение покрытия.

Плагин для Fastlane xcov может выдавать разные форматы отчётов. Нам нужен json, так как нас интересует сам процент покрытия, а проще всего его получить из json.

desc "Расчет тестового покрытия кода для указанных схем. Должно вызываться только после выполнения лейна unit_test. Пример вызова: fastlane code_coverage scheme:BaseTests"
lane :code_coverage do |options|
    scheme = options[:scheme]
    FileUtils.mv("../tests/#{scheme}_report/#{scheme}.xcresult", "../tests/derived/Logs/Test/#{scheme}.xcresult")
    begin
        FileUtils.rm_rf("../tests/#{scheme}_report/coverage_result")
    rescue
        puts "Folder alredy deleted"
    end
    xcov(
        workspace: "PSB.xcworkspace",
        scheme: scheme,
        derived_data_path: "./tests/derived",
        output_directory: "./tests/#{scheme}_report/coverage_result",
        html_report: false,
        markdown_report: false,
        json_report: true
    )
end

В самом начале функции есть махинации с файлом .xcresult потому, что xcov требует, чтобы он находился в определённом месте иерархии папки derived. Поэтому мы переносим его в это место перед подсчётом code coverage. И желательно, чтобы папка output_directory была пуста, т. к. xcov не перезатирает отчёты, а просто падает, если видит, что в этой папке уже есть файлы с таким названием.

На выходе мы получим report.json, в котором содержится много информации, параметр coverage — это наш процент. Этот файл содержит информацию по общему coverage, каждому таргету, каждому файлу и каждому исполняемому скоупу.

{
   "coverage":0.5167708082636994,
   "targets":[
      {
         "name":"SomeFramework.framework",
         "coverage":0.8335146898803046,
         "files":{
            
         }
      },
      {
         "name":"SomeFramework2.framework",
         "coverage":0.4323423536236234,
         "files":{
            
         }
      }
   ]
}

Считывание ковеража в Gitlab CI динамическое. В настройках вы задаёте Regex-паттерн, который будет проверять в каждой строчке логов, подходит ли число под паттерн. Мы используем такой формат:

\((\d+.\d+|\d+)\%\) covered

С его помощью парсится вывод скрипта 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 deploy

Теперь устанавливаем Firebase CLI, — это управление сборками в Firebase из командной строки. В обычном режиме разработчик идёт на сам сайт Firebase, закидывает туда сборки и управляет проектом, но это можно сделать с помощью командной строки. Для этого нам и нужен Firebase.

Мы используем бесплатный Firebase App Distribution (в Firebase есть другие платные фичи, которые не относятся к CI/CD).

Как настроить Google Firebase: документация

Вот краткий план:

1. Устанавливаем плагин к Fastlane — Firebase.

2. Устанавливаем Firebase CLI.

3. Получаем токен учётной записи, у которой есть доступ в проект, куда мы будем отправлять наши сборки.

4. Вот lane, который будет отправлен в Firebase.

def archive_project(scheme, forTestflight, filename)
    if forTestflight
        build_ios_app(
            workspace: "PSB.xcworkspace",
            configuration: scheme,
            scheme: scheme,
            silent: true,
            clean: true,
            output_directory: "./ipas",
            output_name: "#{filename}.ipa",
            export_options: "./ExportOptions.plist"
        )
    else
        build_ios_app(
            workspace: "PSB.xcworkspace",
            configuration: scheme,
            scheme: scheme,
            silent: true,
            clean: true,
            output_directory: "./ipas",
            output_name: "#{filename}.ipa",
            export_method: "development",
            include_bitcode: false
        )
    end
end

Функция умеет собирать сборку для публикации в AppStore, для тестировщиков, для общего пользования. Они отличаются кастомными параметрами в ExportOptions. При указании export method: «development» ExportOptions генерируется автоматически. Для production ExportOptions надо описывать вручную. Можно приложить ссылки для production- и development-опций экспорта.

def send_to_firebase(ipa_name, scheme)
    firebase_apps = {
        "Dev" => "1:1234567890:ios:0a1b2c3d4e5f67890",
        "PreProd" => "1:0987654321:ios:0a1b2c3d4e5f67890"
    }
    firebase_app_distribution(
        app: firebase_apps[scheme], # для разных схем разные айди
        # groups: "ВСЕМ",
        # testers: "tester@gmail.com",
        ipa_path: "./ipas/#{ipa_name}.ipa",
        firebase_cli_path: "/usr/local/bin/firebase",
        firebase_cli_token: "ваш токен",
        debug: true
    )
end

Дальше можно объединить это в один lane или сделать отдельный для каждого действия, чтобы CI мог дёргать эти функции в каком угодно порядке. У нас это разделено.

desc "Собрать проект с указанной схемой. Пример вызова: fastlane archive_project scheme:\"Dev\" filename:\"dev_1.2.3\""
lane :archive_project do |options|
    scheme = options[:scheme]
    filename = options[:filename]
    archive_project(scheme, false, filename)
end
desc "Собрать релизную сборку проекта. Пример вызова: fastlane archive_beta scheme:\"Prod\" filename:\"release_1.2.3\""
lane :archive_beta do |options|
    scheme = options[:scheme]
    filename = options[:filename]
    archive_project(scheme, true, filename)
end
desc "Отправить уже собранный архив ipa в фаербейз с указанным названием файла. Пример вызова: fastlane distribute_to_firebase filename:\"dev_1.2.3\" scheme:\"Dev\""
lane :distribute_to_firebase do |options|
    filename = options[:filename]
    scheme = options[:scheme]
    send_to_firebase(filename, scheme)
end

Всё, что мы написали на Fastlane, нужно проверять с помощью консольных команд. Например, для публикации в Firebase можно выполнить две команды подряд:

fastlane archive_project scheme:"Dev” filename:"dev_1.2.3”
fastlane distribute_to_firebase filename:"dev_1.2.3” scheme:"Dev”

Эти же строки можно объединить в одну для удобства, поставив между ними »;»

fastlane archive_project scheme:"Dev” filename:"dev_1.2.3”
fastlane distribute_to_firebase filename:"dev_1.2.3” scheme:"Dev”

Для публикации используются git tags. Например, если мы для коммита запушим тег dev_1.72.44.TASK-123, то в Firebase отправится сборка с номером 1.72.44.TASK-123. Сами вызовы с указанием версии идут из gitlab-ci.yml. Если что-то пойдёт не так, джоба зафейлится и разработчику придёт уведомление об этом. Посмотрев на логи джобы, он увидит причину: например, проект удалили или что-то ещё серьёзное случилось. Firebase позволяет публиковать приложения с одинаковой версией.

Testflight deploy для itunesconnect и отправки в AppStore

Testflight — это система обработки сборок от Apple. В ней можно делать сборки для тестировщиков, а также релизить протестированные сборки. По схеме CI/CD она будет на уровне с Firebase, т. к. они взаимозаменяемы, только Firebase более удобен для дистрибуции тестовых сборок. А Testflight мы используем для релиза итоговых сборок для пользователей.

Firebase — для теста, потому что удобнее и быстрее. Testflight — для production, потому что релизить можно только через неё.

Мы загружаем наш артефакт в ItunesConnect, и сама система предоставляет нам возможность отправить его сразу в Testflight. Testflight даёт возможность скачивать приложения, над которыми работаем, прямо на айфон.

Основные этапы публикации в AppStore те же. Нам нужно собрать архив и отправить его, только теперь отправляем в AppStore. Тестировщик скачает Testflight и получит доступ к тому, что мы задеплоили.

Следующий этап — изменение версии сборки, чтобы мы могли разные сборки публиковать для тестировщиков. Например, разные команды делают сборки под себя и будут отличать их билд-версиями. Так вот, AppStore не позволит загрузить сборки с одинаковыми версиями, поэтому нужно автоматизировать изменение версии проекта.

Есть небольшой lane для изменения версии и номера билда с помощью Fastlane.

desc "Меняет версию проекта на новую с валидацией самой версии. Допускается версия вида 1.56.45 или 1.34.21(12), где 12 - номер билда. Пример вызова: fastlane update_version version:\"1.46.45\""
lane :update_version do |options|
    git_tag_version = options[:version]
    puts git_tag_version
    major = ""
    minor = ""
    patch = ""
    build = ""
    errorMsg = "Wrong version number: \"#{git_tag_version}\"!\nPlease use versioning like {major_number}.{minor_number}.{patch_number}({build_number})\n or {major_number}.{minor_number}.{patch_number}"
    patternWithBuild = /\A(\d+).(\d+).(\d+)\((\d+)\)\z/
    pattern = /\A(\d+).(\d+).(\d+)\z/
    if match = patternWithBuild.match(git_tag_version)
        major, minor, patch, build = match.captures
    elsif match = pattern.match(git_tag_version)
        major, minor, patch = match.captures
    else
        UI.user_error!(errorMsg)
    end
    if major.to_s.empty? || minor.to_s.empty? || patch.to_s.empty?
        UI.user_error!(errorMsg)
    end
    if build.to_s.empty?
        build = "1"
    end
    version = "#{major}.#{minor}.#{patch}"
    increment_version_number_in_plist(version_number: "#{version}" , target: "PSB")
    increment_version_number_in_plist(version_number: "#{version}", target: "notificationService")
    increment_build_number_in_plist(build_number: "#{build}", target: "PSB")
    increment_build_number_in_plist(build_number: "#{build}", target: "notificationService")
end
desc "Меняет версию проекта на новую без валидации самой версии. Пример вызова: fastlane force_update_version version:\"1.46.45_TASK\""
lane :force_update_version do |options|
    git_tag_version = options[:version]
    puts git_tag_version
    increment_version_number_in_plist(version_number: "#{git_tag_version}" , target: "PSB")
    increment_version_number_in_plist(version_number: "#{git_tag_version}", target: "notificationService")
end

Тут описаны два lane: один валидирует версию, чтобы она подходила для публикации в AppStore, второй может подставить в версию что угодно (ну или почти что угодно). Первый мы используем только для Testflight, а второй — для Firebase, т. к. Firebase позволяет писать буквы в версию и не придирчив к повторам.

Ещё нам понадобится сам lane публикации в AppStore:  

desc "Отправить уже собранный архив ipa в Testflight с указанным названием файла. Пример вызова: fastlane distribute_to_testflight itunes_username:\"email@gg.ru\" filename:\"release_1.2.3\""
lane :distribute_to_testflight do |options|
    username = options[:itunes_username]
    filename = options[:filename]
    puts "username #{username} and filename #{filename}"
    if username.to_s.empty? 
        UI.user_error!("Need to pass ITUNES_USERNAME to testflight lane")
    end
    upload_to_testflight(
        username: "#{username}",
        ipa: "./ipas/#{filename}.ipa",
        skip_waiting_for_build_processing: true
    )
end

Lane несложный, поэтому тут нечего дополнять, просто передаём нужные параметры в плагин Fastlane. Порядок вызова 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}

Здесь $right_version — это желательно валидный номер версии. Если он будет невалидным, то lane провалится и на этом путь публикации закончится. ${ITUNES_USERNAME} — это email юзера, от имени которого мы будем публиковать архив в Testflight.

[Gitlab CI].gitlab-ci.yml

Теперь нужно описать правила, по которым у нас будут происходить взаимодействия ивентов в Gitlab и написанных скриптов. И имя ему —  gitlab-ci.yml. Этот файл задаёт стандарт сборки и проверки приложения.

Это тот самый файл, который запускает все lane, считает нужные параметры, управляет артефактами. Один из его минусов (а может, и плюсов) — скрипты запускаются на bash. Поэтому переиспользование тут не совсем обычное. Об этом расскажу позже, но сначала ознакомимся с простыми примерами файла:

stages:
  - test_stage
test_job:
  stage: test_stage
  tags:
    - ios_tests
  script:
    - echo "Hello world"

Если мы запушим в репозиторий файл .gitlab-ci.yml с таким содержимым, то у нас будут на каждый ивент гитлаба создаваться джобы, которые будут запускать скрипт echo «Hello world».

image-loader.svg

Сам по себе пайплайн прост:

image-loader.svg

Исполняемый код может находиться в трёх стадиях, они указаны по порядку:

  • before_script

  • script

  • after_script

Здесь мы и будем писать вызовы скриптов Fastlane, Swift и что душе угодно.

Эксперименты с .gitlab-ci.yml

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

Попробуем посмотреть, что будет с таким файлом CI, в котором 5 стадий и 8 джоб, и разберём, как это будет работать.

image-loader.svg

Тут есть несколько правил:

  1. Пайплайны (pipeline) состоят из стадий (stage), а они в свою очередь — из джоб (job)

  2. Каждая джоба может состоять только в одной стадии

  3. Каждая стадия может содержать нескольких джоб, которые запускаются последовательно и параллельно. Значит, если у вас есть несколько раннеров, то test_job1 и test_job2 стартанули бы почти одновременно, не ожидая завершения друг друга

  4.  Стадии идут только последовательно, каждая перед стартом ожидает завершения всех джоб из предыдущей стадии

  5. Если при выполнении джобы вызвалась команда exit с ненулевой ошибкой, то джоба фейлится: exit 0 означает, что джоба успешно завершилась; любая другая цифра будет обозначать какую-то ошибку

  6. Если в стадии одна из джоб зафейлилась без флага allow_failure: true, то следующая стадия не стартует. Для test_job3 этот флаг включёен, а для test_job5 нет

  7. Если в стадии несколько джоб и одна из них зафейлилась, то другие джобы стадии продолжают выполнение

  8. В разных джобах можно писать coverage. Например, в джобе 1 мы написали coverage 0%, в джобе 2 получили 10%, и в джобе 4 написали 100%. Тогда общий coverage пайплайна будет считаться как среднее среди всех джоб (0 + 10 + 100) / 3. Это можно использовать, например, для мультимодульности. Создать стадию тестов и на этой стадии — по одной джобе на каждый фреймворк. Но можно сделать, как мы в компании, на уровне Xcode. У нас есть выделенная схема для юнит-тестов всех фреймворков, в которую добавлены все тестовые таргеты. Результатом тестирования этой схемы являются   прогнанные тесты в этих таргетах и средний coverage по ним

  9. Coverage учитывается, даже если джоба зафейлилась

  10. В джобе 8 выставлен флаг when: on_failure, — это значит, что джоба выполнится только в том случае, если пайплайн зафейлился. В случае если пайплайн прошёл без проблем, эта джоба не запустится. Такой флаг полезно ставить, например, в джобы, которые оповещают о зафейленном пайплайне

  11. Порядок стадий в пайплайне зависит от их порядка в файле CI в фазе stages: первыми будут выполняться те, что находятся выше по порядку. Джобы — так же: чем выше вы их описали в файле, тем раньше они выполнятся в пайплайне

В файле CI есть подобие наследования — так называемые extends: . Мы создаем шаблонную джобу, которую будем расширять нужными нам свойствами.

Важное замечание: если мы в шаблоне указали какой-то параметр, например script или tags, а не в джобе, которая расширяет этот шаблон, то если мы в джобе напишем собственный script, то исполнятся будет только тот, что написан в джобе. Переопределение полностью отменяет свойство шаблона. Например, мы хотим в конце каждой джобы выполнять какое-то действие, тогда можем это прописать в before_script шаблона, а в самих джобах исполняемые команды писать в фазу script.

А теперь реальный пример того, как мы гоняем тесты и считаем coverage. Рассмотрим .gitlab-ci.yml такого вида — сверху вниз по каждому пункту.

Variables

В файле .gitlab-ci.yml есть раздел с переменными, где мы можем задать кастомные свойства для всего файла. Также стоит учитывать, что помимо параметров, которые укажете в самом файле, вам будет доступен целый набор Predefined CI/CD variables и набор кастомных параметров, указанных в настройках проекта (Settings ⃗⃗→ CI/CD → Variables). Все три категории свойств будут доступны во всех ваших описываемых джобах внутри .gitlab-ci.yml.

Stages

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

Jobs

Расскажу подробнее про структуру джоб:

  • extends — нужно, чтобы переиспользовать базовые вещи;

  • stage — указываем, в какой стадии участвует джоба. Внутри одной стадии они выполняются параллельно;

  • before script, script, after script — набор команд Shell в том порядке, в котором исполняются;

  • only/except — описывают, под какие правила попадает / точно не попадает джоба;

  • tags — теги, которыми мы пометили раннер, когда регистрировали его;

  • allow failure — свойство, которое разрешает / не разрешает зафейленную джобу, здесь можно указывать допустимые exit codes;

  • artifacts — будем использовать для хранения артефактов джобы.

Как дебажить

Gitlab-runner предоставляет возможность запускать локально ваши джобы для их тестирования перед пушем в репозиторий, что упрощает жизнь. Для этого нужно выполнить команду

gitlab-runner exec shell exec shell test_job1

Команда выполнялась для такого gitlab-ci.yml.

image-loader.svg

У такого дебаггинга есть минус:  вам не будут доступны реальные глобальные переменные по типу CI_JOB_ID, CI_PIPELINE_ID, т. к. эти параметры передаются Gitlab на раннер. Но такой дебаггинг вполне сгодится для проверки работоспособности локальных скриптов. Вот ещё пример вывода глобальной переменной

image-loader.svg

Итого

  1. Мы научились настраивать Gitlab-runner на машине

  2. Разобрались, как работает Fastlane

  3. Можем написать lane, которые помогут: протестировать, собрать информацию о code coverage, собрать архив и отправить его в Firebase или Testflight

Ещё раз повторим действия в командной строке:

  1. Устанавливаем gitlab-runner

    brew install gitlab-runner

  2. Делаем mac раннером для CI

    gitlab-runner register

  3. Запускаем раннер

    gitlab-runner install

    gitlab-runner start

  4. Устанавливаем Fastlane

    brew install fastlane

  5. Папка Fastlane с нужными файлами

    fastlane init

  6. Проверим работоспособность lane

    fastlane unit_test scheme:CoreTests

  7. Публикуем в Firebase

    fastlane archive_project scheme:"Dev” filename:"dev_1.2.3”

    fastlane distribute_to_firebase filename:"dev_1.2.3” scheme:"Dev”

  8. Вызов 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}

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

    gitlab-runner exec shell exec shell test_job1

Использование Gitlab CI + Fastlane + Firebase + Testflight обходится недорого. Можно иметь 2–3 машины и настроить работу.

Работа команды выглядит так:

  1. Разработчик сделал задачу, написал на неё тесты и отправил МР на код-ревью.  

  2. Внутри МР CI/CD считает, не слишком ли низкий code coverage, и проверяет, собирается ли проект.                      

  3. Отправив MP на ревью, разработчик ставит тег на последний коммит, например dev_1.33.22.TASK-123 cicd         .            

  4. Он готовит сборку и отправляет ее в Firebase.                      

  5. Из Firebase тестировщик скачивает сборку. Проверяет, получает ревью и нужные апрувы.            

Мы готовы мержить: держим код в master и на ней запускается пайплайн обновления значения code coverage, чтобы уже другие MP не попадали в master с понижением.

Как видите, большую часть работы делает за нас Gitlab CI, а нам главное — расставить триггеры в правильных местах и настроить наш процесс сборки.

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

Задавайте свои вопросы в комментариях!

© Habrahabr.ru