Всем привет! Меня зовут Ярослав Фоменко, я 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.
ссылка на оригинал статьи https://habr.com/ru/company/doubletapp/blog/707988/
Добавить комментарий