Всем привет! Меня зовут Ярослав Фоменко, я iOS-разработчик в компании Даблтап. Мы с моим коллегой по отделу с конца мая работаем над внедрением, улучшением и масштабированием CI/CD на наших проектах. В этой статье мы хотим поделиться гайдом по подготовке проекта в Xcode и настройке раннеров, скриптов и конфигов, а также расскажем, как нам помогает CI/CD. О том, как и почему мы пришли к решению использовать Mac mini для CI/CD, можно почитать здесь. После развертывания автоматизации на первом проекте задачки стали доходить до тестирования быстрее.  Теперь у нас 1 задача = 1 сборка. Мы решили не мержить задачи в dev, пока тестирование не пропустит задачку дальше. Это позволяет более гибко действовать, если пора релизиться, а не все задачки протестированы. Это экономит время на рутине: Не нужен разработчик, ответственный за деплой сборок на тестирование. В сборки добавляется название. На доске добавляется информация о версии сборки и меняется статус. Вероятность занести в dev невалидный код стремится к нулю. 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 ограничивает количество задач на конкретном раннере. Для этой стадии нам нужен Xcode и xcpretty (gem install xcpretty) для логирования. CI содержит 2 стадии: build, которая проверяет сборку, и test, которая прогоняет файл с тестами. Также мы хотим, чтобы эти стадии выполнялись только тогда, когда мы открываем merge request. И если запускается несколько pipeline одновременно, то чтобы каждая  job выполнялась в своей папке. Также у нас для зависимостей используется Cococapods. Приведем скрипт, отвечающий нашим условиям: LC_ALL и Lang нужны для того, чтобы не возникало конфликтов кодировок. .general: &general_config является объектом, на который в дальнейшем указывается ссылка, и в то место подставляется написанный код.  В xcodebuild clean, build и test мы просто подставляем название нужного workspace/проекта и девайса. Как упоминалось ранее, от tags зависит то, возьмет ли раннер работу. У себя в компании мы работаем в youTrack. Основными статусами для нас как для разработчиков являются статусы «Открыт»  → «В работе»  → «Ревью»  → «Можно тестировать». Поэтому после build передвинем карточку с названием, равным названию PR в ревью, с помощью './scripts/youTrack.py " Данный скрипт уже пригоден для использования, но он не делает самого важного — деплоя. Для деплоя мы используем 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 в папке проекта). Рассмотрим скрипт: перед выполнением 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, которая как раз таки и будет ждать завершения обработки билда. Для этого с помощью скрипта мы читаем нужные нам данные из файла, а затем в pilot указываем distribute_only: true для того, чтобы ничего не выгружать, а сделать действие по распространению сборки. В localized_build_info мы передаем одно и то же описание, чтобы все корректно отображалась на iPhone с разными языками. Затем скрипты удалят информацию о задаче из файла и переведут карточку в «Можно тестировать». Разделение добавления описания на разные lane позволяет более гибко действовать, если вдруг что-то сломается во время работы. Стадию deploy мы запускаем вручную, когда задача прошла ревью. Стадия distribute выполняется только в том случае, если предыдущая стадия завершилась успешно. Теперь допишем в наш yml файл стадии deploy и distribute task_name:"$CI_MERGE_REQUEST_TITLE" как раз и является той переменной, что ждет наш скрипт в массиве options. Теперь осталось только лишь запушить наши изменения на Gitlab 🎉 Рабочее общение у нас происходит в Discord, поэтому вишенкой на торте является очень простая в плане настройки интеграция с Discord. Для этого необходимо на нужном сервере зайти в Настройки — Интеграции — Вебхуки — Новый вебхук. Копируем его URL и идем в репозиторий. Settings — Integrations — Discord Notification. Ставим нужные галочки и скопированный URL. Тестируем, сохраняем и начинаем ждать оповещения). После выполнения действий из гайда должны получиться: три общих раннера и один специфический; YML файл с четырьмя стадиями: build, test, deploy и distribute; fastlane файл, выполняющий deploy и distribute в Testflight; интеграция с Discord. А также вы получите кучу сэкономленного времени на деплое)  Если у вас есть опыт, которым хотите поделиться, или вопросы, то ждем вас в комментариях. Если вам показалось мало данной статьи и хочется узнать о том, какие еще есть варианты настройки, то приглашаем почитать статью о сравнении подходов по настройке CI/CD.