Привет, меня зовут Дмитрий, и я iOS разработчик в компании Triada. В этой статье я расскажу, как настроить CI/CD для вашего iOS приложения, и приведу пошаговую инструкцию, как сделать это правильно с первого раза – чтобы не пришлось переделывать.
Мы настроим CI/CD для iOS проекта с репозиторием на GitLab с использованием Fastlane. Сборки будем отправлять в TestFlight и в Firebase, если он у вас настроен. Полный код решения находится здесь.
Что нам потребуется:
-
3 Gitlab репозитория:
-
репозиторий с проектом, для которого мы настраиваем CI/CD (PROJECT repo).
Предполагается, что на проекте настроен линтер, однако, его отсутствие не критично. Также для тестирования проекта будут использоваться Unit тесты. -
нужно создать
-
репозиторий для хранения сертификатов (CERTS repo)
-
репозиторий с файлами CI/CD (Если вы работаете исключительно с одним проектом, то скрипты можно расположить и в репозитории с проектом. В рамках этой статьи будем считать, что вы работаете с несколькими проектами)
-
-
-
MacOS машина, на которой будет работать CI/CD (CI/CD SERVER).
-
Apple ID с доступом к проектам, от имени которого будет публиковаться приложение
-
(Опционально) Firebase Service Account — для доступа к проектам. Авторизация CI/CD будет происходить от имени данного пользователя. Firebase здесь будет использоваться исключительно для предоставления сборок тестировщикам.
-
(Опционально) Gitlab (Premium or Ultimate) для использования Gitlab API запросов на отправку сообщений
-
(Опционально) Discord сервер — стоит учесть, что на канале необходимы привилегии для создания вебхуков только в рамках настройки.
-
(Опционально) Jira — так как в данном решении управление задачами осуществляется c помощью Jira, то потребуется аккаунт с доступом на чтение задач.
Нам понадобится два вспомогательных репозитория — один для безопасного хранения сертификатов, а второй для хранения скриптов CI/CD. В первой части статьи расскажу про то, как будет выглядеть процесс настройки CI/CD в целом, а во второй части подробно опишу каждый шаг:
-
Создание и настройка репозитория для хранения сертификатов
-
Создание репозитория для скриптов CI/CD
-
Настройка iOS проекта для работы с GitLab
-
Настройка машины (хоста) для раннеров CI/CD
Введение
Рассмотрим следующий пайплайн:
При открытии мердж реквеста (MR) автоматически запускается сборка текущей ветки и прогон тестов. После их успешного завершения пайплайн ожидает ручного запуска следующего шага, чтобы разработчик мог при необходимости внести корректировки в код. На первом этапе пайплайна можно также прогнать линтер/форматтер. Если в MR вносятся правки, пайплайн запускается заново.
При запуске следующего этапа create_archive
повышает версию приложения, генерирует .ipa-архив, а также release notes для Firebase. Затем этот архив будет отправлен в Firebase и TestFlight для тестирования.
В общем и целом, наш процесс CI/CD выглядит примерно так:
![Выполнение начальных шагов пайплайна: выполнение сборки и запуск тестоРис 2. Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase Выполнение начальных шагов пайплайна: выполнение сборки и запуск тестоРис 2. Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase](https://habrastorage.org/getpro/habr/upload_files/403/a66/4c9/403a664c9fb2e00a53df5845cd73202f.png)
![Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase](https://habrastorage.org/getpro/habr/upload_files/362/4fa/9e5/3624fa9e5022814cb13fef10ad1e9817.png)
Как я упомянул ранее, в нашей реализации на этапе сборки дополнительно выполняется проверка линтером файлов .swift, участвующих в MR, и если будут обнаружены какие-либо конфликты, соответствующие сообщения отправляются в MR. Хочу отметить, что даже если независимо от того, нашел ли линтер какие-либо проблемы или нет — пайплайн не блокируется. Если мы хотим, чтобы в MR на GitLab отображался статус проверки кода линтером, нам нужна подписка, иначе у нас не будет токена для API Gitlab.
Сообщения от линтера выглядят следующим образом:
![Рис 3. Сообщения от линтера в MR Рис 3. Сообщения от линтера в MR](https://habrastorage.org/getpro/habr/upload_files/a32/413/4f7/a324134f70a7b45bc83bba5e4c9fd07a.png)
Там, где возможно, линтер открывает тред в MR.
В текущей реализации нет гарантий, что для каждого конфликта литера будет заведен diff комментарий с указанием кода. Это связано с тем, что в Gitlab API, на мой взгляд, несколько неудобно организована отправка diff комментариев: необходимо указывать начальную и конечную позиции блоков кода как в старом файле, так и в новом, дополнительно предоставляя sha1 для файла. Но если у вас есть время поиграться с Gitlab API, можно написать еще несколько десятков строк кода и решить эту интересную задачку. Подробнее об Gitlab API можно почитать тут.
Шаги deploy_to_fb
и deploy_to_tf
отвечают за отправку архива приложения в Firebase и TestFlight соответственно. Для Firebase дополнительно установлено оповещение группы тестировщиков и прикрепляются release notes.
Несколько слов о Release notes
В случае нахождения задачи в Jira — будет предоставлен номер задачи и ссылка на нее. Если для задачи присутствует еще и эпик — он также предоставляется в подобном формате. На последней строке всегда присутствует номер версии и сборки.
![Пример текущего исполнения release notes Пример текущего исполнения release notes](https://habrastorage.org/getpro/habr/upload_files/bd4/0e5/9d2/bd40e59d25146337b751f88aa8817a7d.png)
![Возможный пример исполнения release notes Возможный пример исполнения release notes](https://habrastorage.org/getpro/habr/upload_files/c71/f93/bfb/c71f93bfb737ec20798e4d4a0667eaca.png)
Если же задача не будет найдена, в release notes будет представлена только информация о версии и номере сборки
![Возможный пример исполнения release notes Возможный пример исполнения release notes](https://habrastorage.org/getpro/habr/upload_files/21d/b29/4df/21db294dfa08734bbf0150fc28da9a6a.png)
Для архива, отправляющегося в TestFlight в рамках данного примера оповещения отключены.
Настройка CI/CD
Настало время приступить к настройке нашего процесса CI/CD
Настройка [CERTS repo]
Прежде всего, нам нужен репозиторий для хранения сертификатов. Он будет использован для хранения сертификатов и поэтому не должен находиться в общем доступе.
Доступ к репозиторию необходимо оформить только определенному кругу лиц и серверу CI/CD, поэтому создаем репо в Gitlab и делаем его приватным.
Вуаля — вы замечательны. На этом настройка репозитория с сертификатами завершена.
Настройка [CI/CD repo]
Мы создаем репозиторий и делаем его приватным. Файлы, перечисленные здесь, представлены только для справки. По окончанию настройки, репозиторий должен содержать следующие файлы, которые вы можете взять из репозитория.
![Корень проекта репозитория со скриптами CI/CD Корень проекта репозитория со скриптами CI/CD](https://habrastorage.org/getpro/habr/upload_files/b01/3f3/c9b/b013f3c9bbb71e1b98c3e59cff214df2.png)
где по пути fastlane/
располагается:
![Содержимое каталога Fastlane Содержимое каталога Fastlane](https://habrastorage.org/getpro/habr/upload_files/db4/e67/e6e/db4e67e6e0e59c9962c456c0defadfeb.png)
Обратите внимание на .gitlab-ci-template.yml
— этот файл содержит необходимую информацию о нашем пайплайне и будет использоваться любым проектом с CI/CD. Как вы могли заметить, он довольно небольшой, и в нем не так много переменных — они должны быть объявлены позже в настройках CI/CD вашего проекта.
Вы можете скорректировать файл удобным для вас образом, например, добавить везде дополнительное условие на именование ветки, как это было сделано в шаге create_archive
, либо убрать его вовсе:
![Условие запускаcreate_archiveдля определенного эвента и имени ветки Условие запускаcreate_archiveдля определенного эвента и имени ветки](https://habrastorage.org/getpro/habr/upload_files/394/282/7e8/3942827e88bfb379f85b07ffdb915516.png)
create_archive
для определенного эвента и имени веткиЕсли вы не собираетесь внедрять Firebase в проект или использовать TestFlight для тестирования, удалите следующие строки:
Для Firebase:
![Код задачи для деплоя сборки в Firebase Код задачи для деплоя сборки в Firebase](https://habrastorage.org/getpro/habr/upload_files/089/bf0/def/089bf0def6fb0b13989381294b67c0e0.png)
Для TestFlight:
![Код задачи для деплоя сборки в TestFlight Код задачи для деплоя сборки в TestFlight](https://habrastorage.org/getpro/habr/upload_files/5a8/09b/33c/5a809b33c164e4db13a6d1b94980d9a1.png)
Настройка [PROJECT repo]
Допустим, у нас уже есть готовый проект с развернутым линтером, нам потребуется выполнить следующие шаги:
-
Убедиться, что проект настроен корректно
-
Завести несколько переменных
-
Завести новый пайплайн
-
Создать для проекта раннеры
-
Настроить Appfile
1. Убеждаемся, что проект настроен корректно
Теперь проверим, что проект настроен корректно.
Настройка схем и таргетов
В проекте должны присутствовать кроме основного таргета еще таргет для тестирования с привязанной к нему схеме.
В данном гайде будет проект с 2 таргетами:
![Таргеты проекта, для которого разворачиваем CI/CD Таргеты проекта, для которого разворачиваем CI/CD](https://habrastorage.org/getpro/habr/upload_files/627/0f6/b9f/6270f6b9f46f29a2fab7df11bc0a953a.jpeg)
Как ранее было упомянуто, таргет для тестирования в нашем случае отвечает за unit тесты. Однако, если у Вас появляется желание или необходимость развернуть и UI тесты, дополнительно заводится таргет и схема под него.
Для работы CI/CD с нашей версией проекта необходимо создать как минимум первые 2 схемы:
1. Схема, с которой будет собираться проект
2. Схема, с которой будут проходить тесты
3. Схема, в которой ведется разработка
Таким образом, мы имеем 3 рабочих схемы, стоит убедиться, что для всех них стоит галочка на shared, в противном случае — схема видна будет только вам.
![Схемы проекта, для которого разворачивается CI/CD Схемы проекта, для которого разворачивается CI/CD](https://habrastorage.org/getpro/habr/upload_files/ae1/fd4/ec1/ae1fd4ec1544dd0223d244960b1b8556.png)
Интеграция с Firebase
Мы подразумеваем, что проект уже привязан к Firebase, поэтому смело пропускаем эту секцию, если все готово или Firebase использовать не планируется.
Шаги по настройке интеграции с Firebase.
Для начала переходим на страницу Firebase.
Если вы еще не создали проект Firebase, нажмите “Add project”.
После создания проекта, ассоциируем его с нашим iOS проектом. Для этого нажимаем “Add app” внутри проекта и выбираем iOS проект.
На данном этапе отобразится 5 шагов, самым главным для нас является “Apple bundle ID” — его берем из настроек проекта в Xcode.
Скачайте GoogleService-Info.plist и добавьте его в корень репозитория с iOS проектом. После завершения всех указанных шагов проект готов к работе.
Добавим возможность выкладывать сборки в Firebase.
![Переходим в "App Distribution" для добавления тестовых групп Переходим в "App Distribution" для добавления тестовых групп](https://habrastorage.org/getpro/habr/upload_files/d12/b86/2df/d12b862df9503fda731a8c1b790c86fb.png)
Перейдите во вкладку “Release & Monitor” и выберете “App Distribution”. В открывшемся окне, нажмите “Get started”.
Настройка почти завершена.
Перейдите в таб “Testers & Groups” и добавьте группу для теста “Add group” (при желании можете добавить в нее себя).
На этом настройка тестовых групп завершена, скопируйте название группы — оно понадобится при настройке GitLab CI/CD
Теперь, перейдите в настройки проекта:
![Переходим в "Project settings" Переходим в "Project settings"](https://habrastorage.org/getpro/habr/upload_files/222/450/874/222450874db045287f20915d7b14ead8.png)
В табе “General” располагается информация о проекте и привязанных к нему приложениях. Нас интересует секция “Your apps” — в ней можно найти информацию о проекте и при необходимости ее скорректировать.
Отмечу, что Bundle ID менять для приложения крайне не рекомендую. При необходимости провести данную процедуру — стоит привязать новое приложение.
Скопируйте значение “App ID” — оно потребуется далее при настройке.
2. Заводим переменные для проекта
Для корректной работы CI/CD потребуется в рамках проекта на Gitlab завести 3 переменные.
«SETTINGS» → «CI/CD» → «Variables» → «Expand» → «Add variable«
![Настройки проекта, где планируется развернуть CI/CD. Добавляем переменные CI/CD Настройки проекта, где планируется развернуть CI/CD. Добавляем переменные CI/CD](https://habrastorage.org/getpro/habr/upload_files/26a/0b6/e2e/26a0b6e2e77b8e598bc7d28a4a455f51.png)
Переменная |
Описание |
Пример |
PROJECT_NAME |
Имя проекта |
FastlaneProject |
PROJECT_REPO_URL |
Ссылка на репозиторий для использования git clone |
https://gitlab.com/XXXXXX/YYYYYY.git вместо XXXXXX и YYYYYY подставьте ваши значения |
XCWORKSPACE |
Eсли установлены поды, то необходимо предоставить имя .xcworkspace файла с расширением |
FastlaneProject.xcworkspace |
Как итог, должно получиться подобное представление:
![Переменные CI/CD в проекте Переменные CI/CD в проекте](https://habrastorage.org/getpro/habr/upload_files/f84/031/145/f840311451f9a6d356f142eb13c896b3.png)
Устанавливать Masked
не обязательно, это свойство определяет лишь для скрытия адреса в логах
3. Заводим пайплайн
Создаем /.gitlab-ci.yml
в корне проекта и копируем в него следующие строки:
include: - project: 'cicdXXXXXX/cicd' file: '/.gitlab-ci-template.yml'
Данный файл ссылается на .gitlab-ci-template.yml
из проекта cicdXXXXXX/cicd
, который в моем случае используется как [CI/CD repo], не забудьте заменить его на имя вашего проекта для CI/CD.
4. Настроим раннеры для проекта
В Gitlab проекте переходим в “Settings” -> “CI/CD” и открываем секцию “Runners”
![Настройки проекта, где планируется развернуть CI/CD. Добавляем раннеры Настройки проекта, где планируется развернуть CI/CD. Добавляем раннеры](https://habrastorage.org/getpro/habr/upload_files/6b9/1aa/6e4/6b91aa6e4f9a36e711d38b2c65f83ed7.png)
Пока мы не можем найти нужные нам раннеры с тегами — создадим их, нажмем на “New project runner”. Необходимо указать теги для раннера.
В качестве названия раннеров можно, но не обязательно, использовать следующую нотацию для упрощения их идентификации: PROJECT_NAME/JOB_NAME/NUMBER.
![Добавление Gitlab раннера в проект Добавление Gitlab раннера в проект](https://habrastorage.org/getpro/habr/upload_files/a57/680/7ac/a576807acbc268008f93cd185b8f4007.png)
После нажатия Create runner будет предложено выполнить несколько команд в среде, где планируется запускать раннер — для корректной настройки этот шаг пропускать нельзя.
Выполнять команды будем на [CI/CD SERVER].
Во время регистрации раннеров важно не запускать команду под sudo — в дальнейшем это может привести к некорректной работе Раннера. В качестве executor выбираем shell
.
По итогу будут зарегистрированы раннеры, файл настройки можно найти тут:
➜ ~ ls -ltrh ~/.gitlab-runner/config.toml -rw-------@ 1 User staff 900B May 4 19:55 /Users/CICDUser/.gitlab-runner/config.toml
Внутри config.toml мы можем увидеть записи, связанные с каждым созданным раннером, следующего вида:
concurrent = 1 check_interval = 0 [[runners]] name = "Runner_name" limit = 1 id = XXXXXXXX url = "https://gitlab.com/" token = "xxxx-XXXXXXXXXXXXXXXXXXXX" executor = "shell" [runners.custom_build_dir] enabled = true [runners.cache] [runners.cache.s3] [runners.cache.gcs] [runners.cache.azure]
Пример заполнения:
[[runners]] name = "FastlaneProject/create_archive/1" limit = 1 url = "https://gitlab.com" id = 36354822 token = "XXXXXXXXXXXXX" token_obtained_at = 2024-05-07T14:48:23Z token_expires_at = 0001-01-01T00:00:00Z executor = "shell" builds_dir = "/Users/CICDUser/YYYYYY/ZZZZZZZ/cicd/builds" cache_dir = "/Users/CICDUser/YYYYYY/ZZZZZZZ/cicd/cache" [runners.custom_build_dir] enabled = true
Здесь стоит также отметить несколько параметров, а именно:
Параметр |
Описание |
concurrent = 1 |
ограничение на количество одновременно работающих раннеров |
limit = 1 |
ограничение для раннера на количество одновременно работающих задач |
builds_dir = «/Users/CICDUser/YYYYYY/ZZZZZZZ/cicd/builds» |
путь, по которому будут выполняться задачи раннера. Обратите внимание, что ему предшествует директория cicd — это директория, где будет настроен весь процесс CI/CD |
cache_dir = «/Users/CICDUser/YYYYYY/ZZZZZZZ/cicd/cache» |
кэш раннера |
5. Настройка Appfile
Для выполнения этого шага необходимо сперва донастроить [CI/CD SERVER].
Как получить и настроить Appfile будет указано ниже в секции “Настройка [CI/CD SERVER]«.
Настроенный Appfile необходимо поместить в Secure Files [PROJECT repo]
Настройка [CI/CD SERVER]
1. Настройка Gitlab раннеров на локальной машинке (сервере)
Для настройки раннеров на локальной машине можно пользоваться официальной документацией GitLab. Переводим командную оболочку на bash
, так как корректную работу на zsh
GitLab не гарантирует. Проверяем текущий shell
:
echo $SHELL
Eсли результат отличен от /bin/bash, то меняем следующей командой и перезапускаем терминал:
chsh -s /bin/bash
Если brew не установлен, то ставим:
/bin/bash -c "$(curl "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh")"
Устанавливаем rbenv согласно шагам, описанным в описании к репо: инструкция по настройке rbenv
Ставим rbenv, чтобы использовать его вместо системного ruby:
brew install rbenv gitlab-runner brew services start gitlab-runner
Добавим rbenv в профайл:
echo 'if which rbenv > /dev/null; then eval "$(rbenv init -)"; fi' >> ~/.bash_profile source ~/.bash_profile
проверяем версии ruby:
rbenv install -l
И ставим актуальную версию ruby, на момент написания статьи — это версия 3.3.1:
rbenv install 3.3.1 rbenv global 3.3.1
Корректируем .bashrc файл, добавив в него следующие строки. Не забудьте заменить заглушку CICDUser актуальным пользователем:
export PATH="/bin:/usr/bin:/usr/local/bin" export LANG=en_US.UTF-8 export LANGUAGE=en_US.UTF-8 export LC_ALL=en_US.UTF-8 eval "$(rbenv init -)" PATH=$PATH:/Users/CICDUser/bin:/usr/local/homebrew PATH=$PATH:/Users/CICDUser/.rbenv/shims/ export PATH
Если не установлен Xcode — ставим его.
Для удобства работы с JSON файлами ставим утилиту jq (инструкция по настройке jq).
brew install jq
Выполняем установку gitlab runner согласно пункту 3 из Настройка [PROJECT repo]
2. Fastlane
Устанавливаем fastlane:
brew install fastlane
Теперь переходим по пути, где будет выполняться вся магия CI/CD — место для репозитория [CI/CD repo]. Мы его уже указывали в настройках раннеров — это родительская директория для кэша и билда раннеров:
«/Users/XXXXXX/YYYYYY/ZZZZZZZ/cicd/». Тут делаем клон репозитория [CI/CD repo] и разворачиваем fastlane (инструкция по установке fastlane).
cd /Users/CICDUser/YYYYYY/ZZZZZZZ/cicd/ git clone https://gitlab.com/AAAAAA/BBBBBB.git ... fastlane init ...
Так как в данном репозитории нет еще проектов, получим следующее предупреждение:
[✔] 🚀 [✔] Looking for iOS and Android projects in current directory... [13:51:43]: Created new folder './fastlane'. [13:51:43]: No iOS or Android projects were found in directory '/Users/WWWW/XXXX/YYYY/ZZZZ' [13:51:43]: Make sure to `cd` into the directory containing your iOS or Android app [13:51:43]: Alternatively, would you like to manually setup a fastlane config in the current directory instead? (y/n)
Соглашаемся со всем. По итогу получаем следующие файлы:
MBP-Workstation:ZZZZ cicd$ ls -la total 24 drwxr-xr-x@ 5 CICDUser staff 160 May 14 13:51 . drwxr-xr-x@ 17 CICDUser staff 544 May 14 13:51 .. -rw-r--r--@ 1 CICDUser staff 46 May 14 13:51 Gemfile -rw-r--r--@ 1 CICDUser staff 5992 May 14 13:51 Gemfile.lock drwxr-xr-x@ 4 CICDUser staff 128 May 14 13:51 fastlane MBP-Workstation:ZZZZ cicd$ ls -la fastlane/ total 16 drwxr-xr-x@ 4 CICDUser staff 128 May 14 13:51 . drwxr-xr-x@ 5 CICDUser staff 160 May 14 13:51 .. -rw-r--r--@ 1 CICDUser staff 242 May 14 13:51 Appfile -rw-r--r--@ 1 CICDUser staff 598 May 14 13:51 Fastfile
GEMFILE
Ставим ruby gems и вместе с ним dotenv (инструкция по настройке dotenv). Это в дальнейшем упростит нам настройку fastlane. Открываем Gemfile и добавляем следующие строки:
gem "dotenv" gem "fastlane"
можно использовать следующие команды:
gem install bundler gem install dotenv
После этого выполняем:
bundle install Bundle complete! 3 Gemfile dependencies, 92 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.
для того, чтобы fastlane работал с Firebase выполним следующую команду:
fastlane add_plugin firebase_app_distribution
Таким образом, будет установлен плагин, позволяющий работать с Firebase CLI
Для того, чтобы подтянулся нужный проект — достаточно скачать Firebase CLI и залогиниться в соответствующий аккаунт:
curl -sL https://firebase.tools | bash firebase login
Appfile
Данный файл специфичен для каждого проекта.
Детальную инструкцию по полям Appfile можно найти тут: инструкция по заполнению Appfile.
Для локального проекта мы ограничимся заполнением всего трех полей: app_identifier, app_id, team_id, itc_team_id. После заполнения — прикрепляем этот файл как Secure File в [PROJECT repo]: «SETTINGS» → «CI/CD» → «Secure Files» → «Expand» → «Upload File«.
По итогу, Appfile будет располагаться в Gitlab проекте. В дальнейшем данный файл будет скачиваться и использоваться CI/CD в рамках пайплайна
![Настройки CI/CD проекта, добавление Appfile как Secure File в проект Настройки CI/CD проекта, добавление Appfile как Secure File в проект](https://habrastorage.org/getpro/habr/upload_files/ab4/cc6/1a2/ab4cc61a258471aebec81942bf5ef955.png)
Fastfile
Сердцем нашего CI/CD является Fastfile — именно здесь будет осуществляться вся логика работы CI/CD.
Перепишем дефолтный Fastfile по пути fastlane/Fastfile следующим содержанием:
Содержимое Fastfile
Так как файл довольно объемный, его можно взять из репозитория.
Здесь он представлен для ознакомления
# This file contains the fastlane.tools configuration # You can find the documentation at https://docs.fastlane.tools # # For a list of all available actions, check out # # https://docs.fastlane.tools/actions # # For a list of all available plugins, check out # # https://docs.fastlane.tools/plugins/available-plugins # # Uncomment the line if you want fastlane to automatically update itself # update_fastlane require 'fileutils' default_platform(:ios) xcode_select "/Applications/Xcode.app" platform :ios do desc "Build step" desc "### Example:" desc "```\n[bundler exec] fastlane build_before_tests [--env FASTLANE_ENVIRONMENT]\n```" lane :build_before_tests do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_CLONE_PATH', 'BUILD_SCHEME', 'CICD_LOGS_HOME', 'CICD_DERIVED_DATA_PATH', 'DESTINATION', 'CICD_BUILD_RESULTS_PATH']) # Perform linting perform_linting() # Define project location based on Xcode project or workspace project_location = ENV['CICD_CLONE_PATH'] + (ENV['XCWORKSPACE'].empty? ? ENV['XCODEPROJ'] : ENV['XCWORKSPACE']) puts "Project location:\t #{ project_location}" # Prepare build with fastlane scan scan( project: ENV['XCWORKSPACE'].empty? ? project_location : nil, workspace: ENV['XCWORKSPACE'].empty? ? nil : project_location, scheme: ENV['BUILD_SCHEME'], configuration: "Release", buildlog_path: ENV['CICD_LOGS_HOME'], derived_data_path: ENV['CICD_DERIVED_DATA_PATH'], destination: ENV['DESTINATION'], code_coverage: true, output_directory: ENV['CICD_BUILD_RESULTS_PATH'], skip_build: false, build_for_testing: true, clean: false ) end desc "Prepare tests for project" desc "### Examples:" desc "```\n[bundler exec] fastlane run_unit_tests [--env FASTLANE_ENVIRONMENT]\n```" lane :run_unit_tests do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_CLONE_PATH', 'TEST_SCHEME', 'CICD_LOGS_HOME', 'CICD_DERIVED_DATA_PATH', 'DESTINATION', 'CICD_TEST_RESULTS_PATH']) # Define project location based on Xcode project or workspace project_location = ENV['CICD_CLONE_PATH'] + (ENV['XCWORKSPACE'].empty? ? ENV['XCODEPROJ'] : ENV['XCWORKSPACE']) puts "Project location:\t #{ project_location}" # Perform test with fastlane scan scan( project: ENV['XCWORKSPACE'].empty? ? project_location : nil, workspace: ENV['XCWORKSPACE'].empty? ? nil : project_location, scheme: ENV['TEST_SCHEME'], configuration: "Debug", buildlog_path: ENV['CICD_LOGS_HOME'], derived_data_path: ENV['CICD_DERIVED_DATA_PATH'], destination: ENV['DESTINATION'], test_without_building: false, output_directory: ENV['CICD_TEST_RESULTS_PATH'], clean: false, include_simulator_logs: false ) end desc "Prepare IPA archive" desc "The lane to run by developers or CI/CD" desc "### Examples:" desc "```\n[bundler exec] fastlane build_archive type:\"development\" export_method:\"development\" [--env FASTLANE_ENVIRONMENT]\n```" desc "```\n[bundler exec] fastlane build_archive type:\"adhoc\" export_method:\"ad-hoc\" [--env FASTLANE_ENVIRONMENT]\n```" desc "### Options:" desc " * **`type`**: must be: [\"appstore\", \"adhoc\", \"development\", \"enterprise\", \"developer_id\", \"mac_installer_distribution\", \"developer_id_installer\"]" desc " * **`export_method`**: export_method must be: [\"app-store\", \"validation\", \"ad-hoc\", \"package\", \"enterprise\", \"development\", \"developer-id\", \"mac-application\"]" lane :build_archive do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_ARCHIVES_LOCATION', 'CICD_RELEASE_NOTES_FILE_PATH', 'CICD_CLONE_PATH', 'XCODEPROJ', 'BUILD_SCHEME', 'PROJECT_NAME', 'CICD_IPA_ARCHIVE_NAME', 'APP_BUNDLE_ID', 'APP_CERTIFICATES_STORE']) # Prepare paths for archive and release notes archive_location = ENV['CICD_ARCHIVES_LOCATION'] release_notes_path = ENV['CICD_RELEASE_NOTES_FILE_PATH'] if File.exist?(release_notes_path) sh "cat /dev/null > #{release_notes_path}" else FileUtils.mkdir_p(archive_location) FileUtils.touch(release_notes_path) end # Sync code signing sync_code_signing( type: options[:type], app_identifier: ENV['APP_BUNDLE_ID'], readonly: true, git_url: ENV['APP_CERTIFICATES_STORE'] ) # Preparing release notes current_branch_name = git_current_branch(ENV['CICD_CLONE_PATH']) ticket_number = current_branch_name.match(/(?:\/)([A-Z]+-\d+)/)[1] prepare_release_notes( ticket_number: "#{ticket_number}" ) parsed_version, parsed_build = parse_version_and_build("#{release_notes_path}") new_build = parsed_build.to_i new_version = parsed_version.to_i # Incrementing build number for archive increment_build_number( xcodeproj: ENV['CICD_CLONE_PATH'] + ENV['XCODEPROJ'], build_number: "#{new_build}" ) # Define project location based on Xcode project or workspace project_location = ENV['CICD_CLONE_PATH'] + (ENV['XCWORKSPACE'].empty? ? ENV['XCODEPROJ'] : ENV['XCWORKSPACE']) puts "Project location:\t #{ project_location}" # Determine configuration configuration = options[:type] == "appstore" ? "Release" : "Debug" # Preparing ipa archive with fastlane gym gym( project: ENV['XCWORKSPACE'].empty? ? project_location : nil, workspace: ENV['XCWORKSPACE'].empty? ? nil : project_location, scheme: ENV['BUILD_SCHEME'], configuration: configuration, clean: true, output_directory: archive_location, output_name: ENV['CICD_IPA_ARCHIVE_NAME'], export_method: options[:export_method], skip_package_dependencies_resolution: true ) end desc "Send IPA archive to Testflight" desc "### Examples:" desc "```\n[bundler exec] fastlane deploy_tf skip_submission:true [--env FASTLANE_ENVIRONMENT]\n```" desc "### Options:" desc " * **`skip_submission`**: skip the distributing action of pilot and only upload the ipa file true|false(by default)" lane :deploy_tf do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_ARCHIVES_LOCATION', 'PROJECT_NAME', 'CICD_IPA_FULL_PATH', 'APP_BUNDLE_ID', 'APP_CERTIFICATES_STORE']) # Sync code signing sync_code_signing( type: "development", app_identifier: ENV['APP_BUNDLE_ID'], readonly: true, git_url: ENV['APP_CERTIFICATES_STORE'] ) # Get credentials for App Store Connect apiKey = app_store_connect_api_key( is_key_content_base64: true, duration: 1200, in_house: false # if it is enterprise or not ) # Send .ipa archive to the Testflight silently testflight( app_identifier: options[:appIdentifier], skip_waiting_for_build_processing: options[:skip_submission], skip_submission: options[:skip_submission], ipa: ENV['CICD_IPA_FULL_PATH'], api_key: apiKey, changelog: "" ) end desc "Send Archive to Firebase" desc "### Examples:" desc "```\n[bundler exec] fastlane deploy_firebase [--env FASTLANE_ENVIRONMENT]\n```" desc "### Options:" desc " * **`fb_groups`**: testers groups created in firebase concole app distribution tab" desc " * **`fb_release_notes`**: release notes for the specified project archive" lane :deploy_firebase do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['FB_APP_KEY', 'FB_TEST_GROUPS', 'CICD_RELEASE_NOTES_FILE_PATH', 'CICD_IPA_FULL_PATH']) # Push ipa archive with specified release notes to the Firebase App Distribution with notification to the specified test groups release = firebase_app_distribution( app: ENV['FB_APP_KEY'], testers: ENV['FB_TEST_GROUPS'], release_notes_file: ENV['CICD_RELEASE_NOTES_FILE_PATH'], ipa_path: ENV['CICD_IPA_FULL_PATH'] ) end desc "Get certificates for specified project" desc "### Examples:" desc "```\n[bundler exec] fastlane certificates [--env FASTLANE_ENVIRONMENT]\n```" lane :certificates do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['APP_BUNDLE_ID', 'APP_CERTIFICATES_STORE']) sync_code_signing( type: "development", app_identifier: ENV['APP_BUNDLE_ID'], force_for_new_devices: true, git_url: ENV['APP_CERTIFICATES_STORE'], readonly: true ) sync_code_signing( type: "adhoc", app_identifier: ENV['APP_BUNDLE_ID'], force_for_new_devices: true, git_url: ENV['APP_CERTIFICATES_STORE'], readonly: true ) sync_code_signing( type: "appstore", app_identifier: ENV['APP_BUNDLE_ID'], git_url: ENV['APP_CERTIFICATES_STORE'], readonly: true ) end desc "Generate new certificates for specified project" desc "### Examples:" desc "```\n[bundler exec] fastlane generate_new_certificates [--env FASTLANE_ENVIRONMENT]\n```" lane :generate_new_certificates do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['APP_BUNDLE_ID', 'APP_CERTIFICATES_STORE']) sync_code_signing( type: "development", app_identifier: ENV['APP_BUNDLE_ID'], git_url: ENV['APP_CERTIFICATES_STORE'], force_for_new_devices: true, readonly: false ) sync_code_signing( type: "adhoc", app_identifier: ENV['APP_BUNDLE_ID'], git_url: ENV['APP_CERTIFICATES_STORE'], force_for_new_devices: true, readonly: false ) sync_code_signing( type: "appstore", app_identifier: ENV['APP_BUNDLE_ID'], git_url: ENV['APP_CERTIFICATES_STORE'], force_for_new_devices: true, readonly: false ) end desc "Lint step" desc "### Examples:" desc "```\n[bundler exec] fastlane perform_linting [--env FASTLANE_ENVIRONMENT]\n```" lane :perform_linting do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_CLONE_PATH', 'CICD_LINTER_LOCK_FILE', 'CICD_LINTER_RESULTS_FILE']) # Frist three variables - just for readability cicd_clone_path = ENV['CICD_CLONE_PATH'] linter_lock_file = ENV['CICD_LINTER_LOCK_FILE'] linter_result_file = ENV['CICD_LINTER_RESULTS_FILE'] previous_merge_commit = git_last_merge_commit(cicd_clone_path) current_commit = git_current_commit(cicd_clone_path) current_branch_name = git_current_branch(cicd_clone_path) puts "Previous merge commit hash:\t #{previous_merge_commit}" puts "Last commit hash:\t\t #{current_commit}" puts "Swiftlint lock file:\t #{linter_lock_file}" # Reading installed lock or prepareing linter internal files linter_commit = read_linter_commit(linter_lock_file) if linter_commit == current_commit puts "Linting was already performed for the current commit. Skipping lint steps." else files_to_lint = git_diff_swift_files(previous_merge_commit, current_commit, cicd_clone_path) puts "Files to lint: #{files_to_lint}" if files_to_lint.empty? puts "No swift files changed. Skipping lint" else lint_swift_files(files_to_lint, linter_result_file, cicd_clone_path, current_branch_name) end File.open(linter_lock_file, "w") { |file| file.write(current_commit) } end end desc "Send message to Gitlab" desc "### Examples:" desc "```\n[bundler exec] fastlane send_message commit_ref:\"BRANCH_NAME\" file_to_comment:\"FILEPATH\" line_to_comment:\"\" gitlab_message:\"MESSAGE_TO_POST\" [--env FASTLANE_ENVIRONMENT]\n```" desc "### Options:" desc "* **`commit_ref`**: current brnach name. This will be used to detect opened MR" desc "* **`gitlab_message`**: message to post in the new MR thread" desc "* **`file_to_comment`**: file to add comment to. Note that all subpaths of the project should be included, providing just file name is not sufficient." desc "* **`line_to_comment`**: line in the file that should be comented" lane :send_message do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['GIT_PROJECT_ID', 'CI_API_TOKEN']) merge_request_iid = get_merge_request_iid(options[:commit_ref]) if merge_request_iid json_data = prepare_json_data(options, merge_request_iid) post_comment_to_merge_request(options, merge_request_iid, json_data) else UI.error("Failed to retrieve merge request IID") end end desc "Get project information from JIRA" desc "### Examples:" desc "```\n[bundler exec] fastlane get_jira_info ticket_number:\"TICKET_NUMBER\" [--env FASTLANE_ENVIRONMENT]\n```" desc "### Options:" desc " * **`ticket_number`**: ticket number usually corresponds to branch_name in project" lane :get_jira_info do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_RELEASE_NOTES_FILE_PATH', 'JIRA_API_KEY','JIRA_HOST_NAME', 'JIRA_TICKET_URL']) # Gather ticket related data ticket_summary = execute_jira_api_get_request("/issue/#{options[:ticket_number]}/?fields=summary") if ticket_summary.include?('errorMessages') || ticket_summary.include?('Not Found') puts "Couldn't find relevant ticket information in JIRA. Skipping all steps." else ticket_link="#{ENV['JIRA_TICKET_URL']}/#{options[:ticket_number]}" ticket_notes="Ticket:\t#{options[:ticket_number]}\t#{ticket_link}\n" # Gather Epic related information editmeta_response = execute_jira_api_get_request("/issue/#{options[:ticket_number]}/editmeta") epic_custom_field_id = `echo '#{editmeta_response}' | jq -r '.fields | to_entries[] | select(.value.name == "Epic Link") | .value.fieldId' | tr -d '\n'` epic_ticket_response = execute_jira_api_get_request("/issue/#{options[:ticket_number]}?fields=#{epic_custom_field_id}") epic_ticket_number = `echo '#{epic_ticket_response}' | jq -r '.fields.#{epic_custom_field_id}' | tr -d '\n'` if epic_ticket_number && !epic_ticket_number.strip.empty? && epic_ticket_number != "null" epic_ticket_link="#{ENV['JIRA_TICKET_URL']}/#{epic_ticket_number}" ticket_notes+="Epic:\t#{epic_ticket_number}\t#{epic_ticket_link}\n" end # Write ticket information to release notes file File.open(ENV['CICD_RELEASE_NOTES_FILE_PATH'], "a") { |file| file.puts ticket_notes } end end desc "Saves version and build info to the release notes" desc "### Examples:" desc "```\n[bundler exec] fastlane prepare_release_notes ticket_number:\"TICKET_NUMBER\" [--env FASTLANE_ENVIRONMENT]\n```" desc "### Options:" desc " * **`ticket_number`**: ticket number usually corresponds to branch_name in project" lane :prepare_release_notes do |options| # Checks if all ENV variables were defined in the .env.PROJECT_NAME file verify_env_variables(['CICD_CLONE_PATH', 'XCODEPROJ']) # Gathering current version and build of the project project_location = ENV['CICD_CLONE_PATH'] + ENV['XCODEPROJ'] version_number = get_version_number(xcodeproj: project_location) build_number = latest_testflight_build_number() puts "Local Version Number:\t\t#{version_number}" puts "Current Testflight Build Number:\t#{build_number}" # Adding ticket relevant information to the release notes if options[:ticket_number] ticket_number = options[:ticket_number]&.upcase puts "Ticket number: #{ticket_number}" get_jira_info(ticket_number: ticket_number) end # Adding version info to the release notes build_number += 1 new_build_info = "Version and build: #{version_number}.#{build_number}" File.open(ENV['CICD_RELEASE_NOTES_FILE_PATH'], "a") do |file| file.puts "" if file.size > 0 file.puts new_build_info end end # Methods # "Method to get last merge commit" def git_last_merge_commit(clone_path) `cd #{clone_path} && git log --merges --oneline --format="%H" | head -n1 | tr -d '\n'` end # "Method to get current commit" def git_current_commit(clone_path) `cd #{clone_path} && git rev-parse HEAD | tr -d '\n'` end # "Method to get current branch" def git_current_branch(clone_path) `cd #{clone_path} && git branch --show-current | tr -d '\n'` end # "Method to get changed files" def git_diff_swift_files(previous_commit, current_commit, clone_path) `cd #{clone_path} && git diff #{previous_commit} #{current_commit} --name-only | grep .swift`.split("\n") end # "Method to read current lock or create linter lock and result files if no lock deteted" desc "this prevents on running linter on already processed iteration of pipeline" def read_linter_commit(lock_file) if File.exist?(lock_file) File.read(lock_file).strip else FileUtils.mkdir_p(File.dirname(lock_file)) FileUtils.touch(lock_file) FileUtils.touch(ENV['CICD_LINTER_RESULTS_FILE']) nil end end # "Method to lint specified file" def lint_swift_files(files_to_lint, result_file, clone_path, current_branch_name) files_to_lint.each do |file| swiftlint( mode: :lint, output_file: result_file, config_file: "#{clone_path}.swiftlint.yml", files: ["#{clone_path}#{file}"], raise_if_swiftlint_error: false, ignore_exit_status: true ) parse_linter_results(ENV['CICD_LINTER_RESULTS_FILE'], file, current_branch_name) end end # "Method to parse swiftlint result file" def parse_linter_results(result_file, filename, current_branch_name) File.open(result_file, "r") do |file| file.each_line do |line| parsed_data = parse_line(line.chomp) if parsed_data prepared_string = "**`#{filename}`** \nSwiftlint #{parsed_data[:issue_level]} at line `#{parsed_data[:line_number]}` \nlinter rule violated: `#{parsed_data[:rule_name]}` \n#{parsed_data[:issue_long_description]}" send_message( commit_ref: "#{current_branch_name}", gitlab_message: "#{prepared_string}", file_to_comment: "#{filename}", line_to_comment: parsed_data[:line_number] ) else puts "Failed to parse line." end end end end # "Method to parse swiftlint result line" def parse_line(line) pattern = /^(.*\/)*(.+):(\d+):(\d+): (\w+): (.+): (.+) \((\w+)\)$/ match = line.match(pattern) if match full_filename = line["#{ENV['CICD_CLONE_PATH']}".length..-1].split(':')[0] puts full_filename filename = full_filename.sub(/^#{Regexp.escape("#{ENV['CICD_CLONE_PATH']}")}/, '') line_number = match[3] column_number = match[4] issue_level = match[5] issue_short_description = match[6] issue_long_description = match[7] rule_name = match[8] return { filename: filename, line_number: line_number.to_i, column_number: column_number.to_i, issue_level: issue_level, issue_short_description: issue_short_description, issue_long_description: issue_long_description, rule_name: rule_name } else return nil end end # "Method to parse release notes" def parse_version_and_build(file_path) version_line = File.readlines(file_path).find { |line| line.start_with?('Version and build:') } if version_line version_build = version_line.split(':').last.strip version, build = version_build.split('.').first(2).join('.'), version_build.split('.').last return version, build else put "Version line not found in file" end end # "Method to get MR id related to the branch" def get_merge_request_iid(commit_ref) response = execute_gitlab_api_get_request("/merge_requests?scope=all&state=opened&source_branch=#{commit_ref}") merge_request_iid = JSON.parse(response).first['iid'] if response && !response.empty? merge_request_iid end #"Method to prepare data for MR comment in Gitlab" def prepare_json_data(options, merge_request_iid) merge_request_info = execute_gitlab_api_get_request("/projects/#{ENV['GIT_PROJECT_ID']}/merge_requests/#{merge_request_iid}") json_data = JSON.parse(merge_request_info) diff_refs = json_data['diff_refs'] base_sha = diff_refs['base_sha'] start_sha = diff_refs['start_sha'] head_sha = diff_refs['head_sha'] characters_to_escape = ['"', "'", '\\', '$', '(', ')', '\\\\'] escaped_gitlab_message = options[:gitlab_message].gsub(/(#{characters_to_escape.map { |c| Regexp.escape(c) }.join('|')})/, '\\\\\1').gsub("\n", "\\n") escaped_gitlab_filepath = options[:file_to_comment].gsub(/(#{characters_to_escape.map { |c| Regexp.escape(c) }.join('|')})/, '\\\\\1') "{\"body\": \"#{escaped_gitlab_message}\", \"position\": {\"base_sha\":\"#{base_sha}\", \"start_sha\":\"#{start_sha}\", \"head_sha\": \"#{head_sha}\", \"new_path\": \"#{escaped_gitlab_filepath}\", \"position_type\": \"text\", \"new_line\": #{options[:line_to_comment]}}}" end # "Method to send comment to Gitlab MR" def post_comment_to_merge_request(options, merge_request_iid, json_data) response = execute_gitlab_api_post_request("/projects/#{ENV['GIT_PROJECT_ID']}/merge_requests/#{merge_request_iid}/discussions?body=comment", json_data) status_code = response.strip.to_i if (200..299).include?(status_code) UI.success("Diff comment posted successfully") else UI.error("Failed to apply comment to a code block. Status code: #{status_code}") UI.message("Trying to send a simple note to...") note_comment_response = execute_gitlab_api_post_request("/projects/#{ENV['GIT_PROJECT_ID']}/merge_requests/#{merge_request_iid}/notes", json_data) note_comment_status_code = note_comment_response.strip.to_i UI.error("Issue with sending messages to the MR request. Status code: #{note_comment_status_code}") unless (200..299).include?(note_comment_status_code) end end # "Method to execute GET request with Gitlab API" def execute_gitlab_api_get_request(endpoint) url = "#{ENV['FASTLANE_GITLAB_API_URL']}#{endpoint}" `curl -s --request GET --header 'PRIVATE-TOKEN: #{ENV['CI_API_TOKEN']}' '#{url}'` end # "Method to execute POST request with Gitlab API" def execute_gitlab_api_post_request(endpoint, json_data) url = "#{ENV['FASTLANE_GITLAB_API_URL']}#{endpoint}" `curl -s -o /dev/null --request POST --header "PRIVATE-TOKEN: #{ENV['CI_API_TOKEN']}" --header "Content-Type: application/json" --data '#{json_data}' -w "%{http_code}\n" '#{url}'` end # "Method to execute GET request with JIRA API" def execute_jira_api_get_request(endpoint) url = "#{ENV['JIRA_HOST_NAME']}#{endpoint}" `curl -s --header 'Authorization: Bearer #{ENV['JIRA_API_KEY']}' '#{url}'` end # "Method to check if ENV variable was defined" def verify_env_variables(variables) variables.each do |var| unless ENV[var] puts "Environment variable #{var} is not defined." error_message = "Please make sure that #{var} was properly defined in your environment." UI.user_error!(error_message) end end end end
В Fastfile предоставлены lane для работы с пайпланом, а также было решено не выносить из этого же файла сопутствующие методы. У каждого lane присутствует описание работы, способ его вызова и используемые переменные. Дополнительно со всей документацией можно ознакомиться в README.md в директории проекта fastlane.
Переменные окружения ENV[‘XXXXX’] — определяются в env файле по пути fastlane/.env.YYYYYYYYY, где YYYYYYYYY = [‘default’,’PROJECT_NAME’].
PROJECT_NAME — берется из переменных CI/CD [PROJECT repo], default — env по умолчанию.
options[:file_to_comment] — переменные, передаваемые lane извне
Все lane должны быть вызваны с указанием env, соответствующего проекту, например:
bundler exec fastlane iOS build_before_tests --env YourProject # YourProject - имя вашего проекта, ему заведен соответсвующий env .env.YourProject
3. Заведение env для проекта
fastlane/.env.default
.env.default — дефолтный env, который используется Fastlane, если мы выполняем команды без указания env. Ниже прикрепил .env.default, необходимый для корректной работы нашего CI/CD:
MATCH_PASSWORD="XXXXXXXXXXXX" MATCH_KEYCHAIN_PASSWORD="YYYYYYYYYY" CICD_HOME_DIR="/Users/CICDUser/YYYYYY/ZZZZZZ/cicd/builds" DESTINATION="platform=iOS Simulator,name=iPhone 15,OS=17.2" APP_STORE_CONNECT_API_KEY_KEY_ID="XXXXXXXX" APP_STORE_CONNECT_API_KEY_ISSUER_ID="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" APP_STORE_CONNECT_API_KEY_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # Fastlane env FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=15 FASTLANE_XCODEBUILD_SETTINGS_RETRIES=6 FASTLANE_GITLAB_API_URL="https://gitlab.com/api/v4"
Далее я предоставлю информацию о том, где можно получить данные для указанных параметров:
Параметр |
Описание |
MATCH_PASSWORD |
Пароль чтобы можно было извлечь сертификаты |
MATCH_KEYCHAIN_PASSWORD |
Пароль к локальному keychain |
CICD_HOME_DIR |
Путь, по которому работают раннеры. В данном случае, может возникнуть небольшая коллизия. Почему переменная указана как CICD_HOME_DIR, а на самом деле приведен путь для билдов раннеров. Все потому что хоть сердцем CI/CD является Fastfile, однако его руками являются раннеры. Вся работа выполняется в этой директории, мы могли указать другой путь, однако он все равно должен быть связан с рабочей директорией раннеров. Данный путь при желании можно сменить, но в таком случае перепроверьте все взаимосвязи |
DESTINATION |
Симуляторы iOS на которых будут собираться сборки и выполняться тестирование. Указанный симулятор должен обязательно быть установлен. |
APP_STORE_CONNECT_API_KEY_KEY_ID |
API KEY ID. Данное значение можно получить из App Store Connect секция «Users and Access» , таб «Integrations» подтаб «App Store Connect API« |
APP_STORE_CONNECT_API_KEY_ISSUER_ID |
API KEY ISSUER ID. Данное значение можно получить из App Store Connect секция «Users and Access«, таб «Integrations» подтаб «App Store Connect API« |
APP_STORE_CONNECT_API_KEY_KEY |
API KEY. Для получения данного значения выполните следующую команду над ключом, скаченным из App Store:
|
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT |
Таймаут для попыток выполнения билда |
FASTLANE_XCODEBUILD_SETTINGS_RETRIES |
Ограничение на количество попыток выполнения билда |
FASTLANE_GITLAB_API_URL |
Gitlab API |
fastlane/.env.PROJECT_NAME
.env.PROJECT_NAME — это env файл нашего проекта, в нем будут предоставлены все необходимые переменные для работы CI/CD.
# Certificate store APP_CERTIFICATES_STORE="https://gitlab.com/XXXXXX/YYYYYY.git" APP_BUNDLE_ID="XXXXXXXXXXXXXXXXXXXXXXXXX" # Xcode Project specific variables XCODEPROJ="XXXXXXXXXX.xcodeproj" XCWORKSPACE="XXXXXXXXXX.xcworkspace" BUILD_SCHEME="YYYYYYYYYY" TEST_SCHEME="ZZZZZZZZZ" PROJECT_NAME="XXXXXXXXXX" # Firebase variables FB_APP_KEY="1:XXXXXXXXXXXX:ios:YYYYYYYYYYYY" FB_TEST_GROUPS="TEST_GROUP_NAME" # Runner folders CICD_ARTIFACTS_HOME="$CICD_HOME_DIR/$PROJECT_NAME/artifacts" CICD_CLONE_PATH="$CICD_HOME_DIR/clones/$PROJECT_NAME/" CICD_LINTER_HOME="$CICD_ARTIFACTS_HOME/swiftlint" CICD_LOGS_HOME="$CICD_ARTIFACTS_HOME/logs" CICD_ARCHIVES_LOCATION="$CICD_ARTIFACTS_HOME/archives/" CICD_RELEASE_NOTES_FILE_PATH="$CICD_ARCHIVES_LOCATION/release_notes" CICD_DERIVED_DATA_PATH="$CICD_ARTIFACTS_HOME/derived_data/" CICD_BUILD_RESULTS_PATH="$CICD_ARTIFACTS_HOME/build_results/$(date '+%Y-%m-%d_%H:%M:%S')/" CICD_TEST_RESULTS_PATH="$CICD_ARTIFACTS_HOME/test_results/$(date '+%Y-%m-%d_%H:%M:%S')/" CICD_LINTER_RESULTS_FILE="$CICD_LINTER_HOME/swiftlint_results" CICD_LINTER_LOCK_FILE="$CICD_LINTER_HOME/swiftlint.lock" CICD_IPA_ARCHIVE_NAME="$PROJECT_NAME.ipa" CICD_IPA_FULL_PATH="$CICD_ARCHIVES_LOCATION/$CICD_IPA_ARCHIVE_NAME" # Gitlab variables GIT_PROJECT_ID=XXXXXXXX # Message Agent for GitLab (Only for Premium or Ultimate Gitlab plans) CI_API_TOKEN="xxxxx-xxxxxxxxxxxxxxxxxxxx" # JIRA variables JIRA_URL="https://jira.ZZZZZZ.ru" JIRA_TICKET_URL="$JIRA_URL/browse" JIRA_HOST_NAME="$JIRA_URL/rest/api/latest" JIRA_SEARCH_URL="$JIRA_HOST_NAME/search" JIRA_API_KEY="yyyyyyyyyyyyyyyyyyyy"
Ниже предоставлю информацию, где можно получить данные для параметров выше:
Параметр |
Описание |
APP_CERTIFICATES_STORE |
Ссылка на репозиторий, где будут храниться все сертификаты. связанные с проектом. Репозитория заводился в шаге «Настройка [CERTS repo]« |
APP_BUNDLE_ID |
Bundle ID для проекта [PROJECT repo] |
XCODEPROJ |
Имя Xcodeproj файла с расширением |
XCWORKSPACE |
Имя Xcworkspace файла с расширением |
BUILD_SCHEME |
Имя схемы, в рамках которой будет проводить сборка |
TEST_SCHEME |
Имя схемы, в рамках которой будет проводиться Unit тестирование |
PROJECT_NAME |
Имя проекта, будет использоваться для создания директорий на сервере CI/CD, а также для обращения к env файлу |
FB_APP_KEY |
APP KEY от проекта в Firebase. Можем значение извлечь из «Firebase Console» -> «Project overview» -> «Project Settings» -> секция «Your apps» -> «App ID« |
FB_TEST_GROUPS |
Группы тестирования, заведенные через Firebase Console. Можем извлечь из «Firebase Console» -> Project Shortcuts «App Distribution» → «Testers & Groups» → Tester groups« |
GIT_PROJECT_ID |
ID проекта в Gitlab репозитории. Можем значение извлечь из настроек проекта «Settings» -> «General«, в секции «Naming, topics, avatar» значение «Project ID« |
CI_API_TOKEN |
Данный токен доступен только обладателем Premium или Ultimate подписки Gitlab. Может быть создан через настройки проекта «Settings» -> «Access Tokens» -> нажать «Add new token«. Обратите внимание, что имя токена будет отображаться в комментариях Gitlab MR |
JIRA_URL |
URL проекта в JIRA |
JIRA_API_KEY |
Токен пользователя из под которого будет идти обращение к JIRA API. Можно создать через профиль пользователя → «Персональные токены доступа» → «Создать токен«. |
После настройки этих двух файлов, считаем, что с настройкой CI/CD — закончено, остается лишь кислая вишенка на торте — добавить интеграцию с Discord.
Почему кислая? Потому что мы настроем оповещения на падения сборок
Интеграция с Discord
Переходим на наш Discord сервер, где предполагается будут находиться все заинтересованные в проекте. Для настройки интеграции необходимо только завести Webhook. Для этого необходим доступ администратора к каналу.
Клацаем «Edit Channel«
![Discord сервер. Выбираем необходимый канал и переходим в его настройки Discord сервер. Выбираем необходимый канал и переходим в его настройки](https://habrastorage.org/getpro/habr/upload_files/2ab/afc/566/2abafc56655bbcc6a4f82dc72565dcbb.png)
Нажимаем «New Webhook» и просто копируем его URL в появившимся поле нового элемента:
![Создание Webhook на канал в Discord сервере Создание Webhook на канал в Discord сервере](https://habrastorage.org/getpro/habr/upload_files/44c/d1a/24c/44cd1a24c3ec1af6c84030c604852784.png)
![Discord сервер, настройки Webhook, копируем Webhook URL Discord сервер, настройки Webhook, копируем Webhook URL](https://habrastorage.org/getpro/habr/upload_files/6f9/690/8c6/6f96908c63cf190bb58d7b138f685f68.png)
В проекте [PROJECT repo] переходим в «Settings» -> «Integrations» -> «Discord Notifications«. Заполняем следующие поля:
![](https://habrastorage.org/getpro/habr/upload_files/9cb/96b/0f2/9cb96b0f22e3bf8ec4aafcccbf01fbfb.png)
После этого, проект полностью подготовлен к запуску с использованием Gitlab CI/CD + Firebase + Fastlane + Jira и оповещением в Discord. Подводя итог, при условии правильной настройки CI/CD-сервера единственные шаги, необходимые для применения этого процесса CI/CD к новому проекту, следующие:
-
Создайте пустой частный репозиторий для его сертификатов.
-
Добавьте в свой проект
.gitlab-ci.yml
с ссылкой на.gitlab-ci-template.yml
. -
Добавьте переменные в проект GitLab.
-
Заполните переменные в .env.NewProject и соответствующем Appfile.
На все действия уйдет не больше часа
Надеюсь, что это руководство будет Вам полезным. Приятного вам кодинга!
Отдельное спасибо авторам следующих статей и моим коллегам.
Полезные ссылки
https://firebase.google.com/docs/app-distribution/authenticate-service-account?platform=ios
https://medium.com/google-cloud/gitlab-and-workload-identity-federation-on-google-cloud-a0795091e404
https://docs.gitlab.com/ee/user/project/integrations/discord_notifications.html
https://medium.com/@sky.tienyu/how-to-deploy-firebase-in-gitlab-ci-using-a-service-account-key-b2a459b63db9
https://about.gitlab.com/blog/2020/03/16/gitlab-ci-cd-with-firebase/
https://www.andrewhoog.com/post/how-to-export-ad-hoc-ios-ipa-xcode/#export-ipa
https://www.andrewhoog.com/post/how-to-build-an-ios-app-archive-via-command-line/
ссылка на оригинал статьи https://habr.com/ru/articles/821981/
Добавить комментарий