Всем привет! Меня зовут Ярослав Фоменко, я iOS-разработчик в компании Даблтап. Мы с моим коллегой по отделу с конца мая работаем над внедрением, улучшением и масштабированием CI/CD на наших проектах. В этой статье мы хотим поделиться гайдом по подготовке проекта в Xcode и настройке раннеров, скриптов и конфигов, а также расскажем, как нам помогает CI/CD.
О том, как и почему мы пришли к решению использовать Mac mini для CI/CD, можно почитать здесь.
Как нам помогает CI/CD
После развертывания автоматизации на первом проекте задачки стали доходить до тестирования быстрее.
Теперь у нас 1 задача = 1 сборка. Мы решили не мержить задачи в dev, пока тестирование не пропустит задачку дальше. Это позволяет более гибко действовать, если пора релизиться, а не все задачки протестированы.
Это экономит время на рутине:
Не нужен разработчик, ответственный за деплой сборок на тестирование.
В сборки добавляется название.
На доске добавляется информация о версии сборки и меняется статус.
Вероятность занести в dev невалидный код стремится к нулю.
Внедрение CI/CD на проект
Используемые технологии
CI/CD работает на Mac mini (2018) с 3,2 GHz 6-ядерный процессор Intel Core i7, 16 ГБ 2667 MHz DDR4, macOS Monterey 12.4
Gitlab Runner
Fastlane 2.208.0
Xcode 13.4 и Xcode Command Line Tools
rbenv и ruby 2.6.8. Рекомендуем использовать именно этот менеджер зависимостей, а не rvm, т.к. с ним задачи начинают неожиданно падать.
Python 3.10
Youtrack API
Discord
Подготовка проекта
Большинство наших проектов имеют зависимости через Cocoapods и используют Rx и Firebase.
Все сборки, которые собираются автоматически, мы заливаем в корпоративный аккаунт App Store Connect для внутреннего тестирования. Поэтому для начала создаем конфигурацию CI/CD, прописываем bundleID, ставим нужный аккаунт и проверяем, что все capabilities работают, а схемы имеют галочку Shared.
Если используется Firebase, то создаем в нем объект приложения с нужным ID и генерируем ключи в Connect для пушей (и прочие ключи, нужные вашему бэкенду), которые следует закинуть в Firebase. И не забываем скачать с Firebase Google plist и добавить в проект, а также изменить свой скрипт, который выбирает нужный файл при компиляции приложения.
Настройка раннеров
Как установить Gitlab runner, можно посмотреть здесь.
Для дальнейших действий понадобится доступ к репозиторию не ниже уровня Maintainer.
Во время регистрации необходимо будет ввести:
URL;
токен;
название раннера;
теги раннера;
executor.
После установки зарегистрируем раннеры в терминале с помощью команды gitlab runner register, используя token, который можно найти в репозитории: Setting-CI/CD-Runners. Также в этой вкладке необходимо отключить Shared Runners для того, чтобы ненужные раннеры не брали наши job.
Нам нужно несколько общих раннеров, которые мы сможем использовать на нескольких проектах, и один специфичный для deploy с лимитом один.
NOTE: Во время регистрации раннера важно не запустить команду под sudo, т.к. в дальнейшем это приведет к некорректной работе раннера.
Для названий специфичных раннеров предлагаем использовать следующую схему: ProjectName/jobName/number. Для общих: jobName/number
Для тегов: job:jobName (например, job:build). Указываемые теги в дальнейшем будут использоваться в yml файле для того, чтобы отдавать раннеру работу только при совпадении тегов (если на раннере включена проверка тегов).
В качестве executor выбираем shell, т.к. мы выполняем действия напрямую на macOS. Теперь наш раннер зарегистрирован и отображается в Specific runners. Нам нужно сделать их общими, нажав на карандашик и изменив статус блокировки под проект.
Если мы так и оставим наши раннеры, то при попытке запустить несколько задач одновременно они будут перезаписывать друг друга и падать. Для этого в ~/.gitlab-runner/config.toml допишем в самое начало строчку concurrent = 4 (или любое другое число)
А в каждый раннер напишем limit = 2 и разрешим кастомную директорию для билда.
Отличие concurrent от limit в том, что concurrent обозначает количество задач, которое может выполняться суммарно на всех раннерах, а limit ограничивает количество задач на конкретном раннере.
Настройка стадии CI
Для этой стадии нам нужен Xcode и xcpretty (gem install xcpretty) для логирования.
CI содержит 2 стадии: build, которая проверяет сборку, и test, которая прогоняет файл с тестами.
Также мы хотим, чтобы эти стадии выполнялись только тогда, когда мы открываем merge request. И если запускается несколько pipeline одновременно, то чтобы каждая job выполнялась в своей папке.
Также у нас для зависимостей используется Cococapods.
Приведем скрипт, отвечающий нашим условиям:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
when: always
- when: never
stages:
- build
- test
variables:
LC_ALL: "en_US.UTF-8"
LANG: "en_US.UTF-8"
.general: &general_config
before_script:
- pod install
build:
stage: build
<<: *general_config
tags:
- job:build
script:
- xcodebuild clean -workspace project-ios.xcworkspace -scheme "Scheme debug" | xcpretty
- xcodebuild build -workspace project-ios.xcworkspace -scheme "Scheme debug" -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.5' | xcpretty -s
- './scripts/youTrack.py "$CI_MERGE_REQUEST_TITLE" "Review"'
test:
stage: test
<<: *general_config
tags:
- job:test
script:
- xcodebuild clean -workspace project-ios.xcworkspace -scheme " Scheme debug" | xcpretty
- xcodebuild test -workspace project-ios.xcworkspace -scheme ProjectTests -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.5' | xcpretty -s
LC_ALL и Lang нужны для того, чтобы не возникало конфликтов кодировок.
.general: &general_config является объектом, на который в дальнейшем указывается ссылка, и в то место подставляется написанный код.
В xcodebuild clean, build и test мы просто подставляем название нужного workspace/проекта и девайса.
Как упоминалось ранее, от tags зависит то, возьмет ли раннер работу.
У себя в компании мы работаем в youTrack. Основными статусами для нас как для разработчиков являются статусы «Открыт» → «В работе» → «Ревью» → «Можно тестировать». Поэтому после build передвинем карточку с названием, равным названию PR в ревью, с помощью './scripts/youTrack.py "
Данный скрипт уже пригоден для использования, но он не делает самого важного — деплоя.
Настройка стадии CD
Для деплоя мы используем Fastlane.
Перед тем как писать какой-либо скрипт, установим Fastlane с помощью команды gem install fastlane.
Перейдем в терминале в папку нашего проекта и проинициализируем Fastlane с помощью fastlane init. Данная команда запросит данные от аккаунта App Store Connect (необходим аккаунт, который может создавать приложения и загружать сборки). Также команда создаст все нужные файлы и объект приложения в Connect, если он еще не создан.
После создания появится папка Fastlane, в которой будет fastfile (файл с исполняемыми скриптами) и Appfile, который содержит информацию о bundleId, аккаунте и команде.
От Fastlane мы хотим, чтобы он создавал нужные сертификаты, архивировал наш проект и загружал .ipa файл в Connect. Также рекомендуем использовать для авторизации запросов App Store Connect API Key (нужны права владельца аккаунта). Но хранить его прямо в папке проекта не рекомендуем: для этого лучше воспользоваться переменными в Gitlab.
Также нам понадобится плагин versioning для получения номера версии из project (fastlane add_plugin versioning в папке проекта).
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
before_all do
app_store_connect_api_key(
key_id: "KEY_ID",
issuer_id: "issuer_ID",
key_filepath: "fastlane/AuthKey_KEYID.p8"
)
end
lane :upload do |options|
version = get_version_number_from_plist(
target: "TargetName",
plist_build_setting_support: true,
build_configuration_name:"CICD"
)
build = latest_testflight_build_number(version: version, initial_build_number: 0) + 1
increment_build_number_in_xcodeproj(
build_number: build.to_s,
target: " TargetName ",
build_configuration_name:"CICD"
)
cert
sigh
gym(
archive_path:"./Project.xcarchive",
scheme: "Project debug",
configuration: "CICD",
skip_package_dependencies_resolution: true
)
pilot(
skip_waiting_for_build_processing:true,
changelog: options[:task_name],
app_version: version,
build_number: build.to_s
)
sh("../scripts/csvFileExecutor.py '#{options[:task_name]};#{version}-#{build}' append")
end
end
Рассмотрим скрипт: перед выполнением lane мы устанавливаем ключ, а в самой lane под названием upload ждем передаваемых переменных.
Получаем версию из проекта, проверяем последний билд из коннекта, инкрементируем и устанавливаем это значение в проект в нужную конфигурацию.
Подписываем и генерируем нужные сертификаты с помощью cert и sigh. Архивируем проект в эту же папку, чтобы не засорять Mac сборками. Ставим skip_package_dependencies_resolution
, если не используется SPM, а затем выгружаем с заданными данными билд, пропуская ожидание обработки сборки, чтобы сэкономить время на этой стадии.
sh("../scripts/csvFileExecutor.py '#{options[:task_name]};#{version}-#{build}' append")
используется для того, чтобы сохранить данные о сборке и задаче в файл на отдельном репозитории, чтобы в дальнейшем занести эту информацию в сборку. task_name — название переменной, которая должна быть передана в скрипт.
lane :distribute do |options|
var = sh("../scripts/csvFileExecutor.py '#{options[:task_name]}' read")
splitted = var.split("\n").last()
version = splitted.split("-").first()
build = splitted.split("-").last()
pilot(
app_platform: "ios",
distribute_only: true,
app_version: version,
build_number: build,
localized_build_info: {
"default": {whats_new: options[:task_name]},
"ru": {whats_new: options[:task_name]},
"en-GB": {whats_new: options[:task_name]},
"en-US": {whats_new: options[:task_name]}
})
sh("../scripts/csvFileExecutor.py '#{options[:task_name]}' remove")
sh("../scripts/youTrack.py '#{options[:task_name]}' 'Можно тестировать' '#{version}(#{build})'")
end
Также наш файл содержит lane: distribute
, которая как раз таки и будет ждать завершения обработки билда. Для этого с помощью скрипта мы читаем нужные нам данные из файла, а затем в pilot указываем distribute_only: true
для того, чтобы ничего не выгружать, а сделать действие по распространению сборки. В localized_build_info
мы передаем одно и то же описание, чтобы все корректно отображалась на iPhone с разными языками. Затем скрипты удалят информацию о задаче из файла и переведут карточку в «Можно тестировать».
Разделение добавления описания на разные lane позволяет более гибко действовать, если вдруг что-то сломается во время работы.
Стадию deploy мы запускаем вручную, когда задача прошла ревью. Стадия distribute выполняется только в том случае, если предыдущая стадия завершилась успешно.
Теперь допишем в наш yml файл стадии deploy и distribute
stages:
- build
- test
- deploy
- distribute
…
testflight_build:
stage: deploy
<<: *general_config
tags:
- job:deploy
script:
- fastlane upload task_name:"" class="formula inline">CI_MERGE_REQUEST_TITLE"
rules:
- if:
when: manual
distribute:
stage: distribute
tags:
- job:distribute
script:
- fastlane distribute task_name:"" class="formula inline">CI_MERGE_REQUEST_TITLE"
needs: ["testflight_build"]
when: on_success
task_name:"$CI_MERGE_REQUEST_TITLE"
как раз и является той переменной, что ждет наш скрипт в массиве options.
Теперь осталось только лишь запушить наши изменения на Gitlab ????
Интеграция с Discord
Рабочее общение у нас происходит в Discord, поэтому вишенкой на торте является очень простая в плане настройки интеграция с Discord. Для этого необходимо на нужном сервере зайти в Настройки — Интеграции — Вебхуки — Новый вебхук. Копируем его URL и идем в репозиторий. Settings — Integrations — Discord Notification. Ставим нужные галочки и скопированный URL. Тестируем, сохраняем и начинаем ждать оповещения).
Вывод
После выполнения действий из гайда должны получиться:
три общих раннера и один специфический;
YML файл с четырьмя стадиями: build, test, deploy и distribute;
fastlane файл, выполняющий deploy и distribute в Testflight;
интеграция с Discord.
А также вы получите кучу сэкономленного времени на деплое)
Если у вас есть опыт, которым хотите поделиться, или вопросы, то ждем вас в комментариях.
Если вам показалось мало данной статьи и хочется узнать о том, какие еще есть варианты настройки, то приглашаем почитать статью о сравнении подходов по настройке CI/CD.