CI/CD заказывали? Или простое, но подробное руководство по настройке CI/CD под несколько iOS проектов

от автора

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

Мы настроим CI/CD для iOS проекта с репозиторием на GitLab с использованием Fastlane. Сборки будем отправлять в TestFlight и в Firebase, если он у вас настроен. Полный код решения находится здесь.

Что нам потребуется: 

  1. 3 Gitlab репозитория:

    1. репозиторий с проектом, для которого мы настраиваем CI/CD (PROJECT repo).
      Предполагается, что на проекте настроен линтер, однако, его отсутствие не критично. Также для тестирования проекта будут использоваться Unit тесты.

    2. нужно создать 

      1. репозиторий для хранения сертификатов (CERTS repo)

      2. репозиторий с файлами CI/CD (Если вы работаете исключительно с одним проектом, то скрипты можно расположить и в репозитории с проектом. В рамках этой статьи будем считать, что вы работаете с несколькими проектами)

  2. MacOS машина, на которой будет работать CI/CD (CI/CD SERVER)

  3. Apple ID с доступом к проектам, от имени которого будет публиковаться приложение

  4. (Опционально) Firebase Service Account — для доступа к проектам. Авторизация CI/CD будет происходить от имени данного пользователя. Firebase здесь будет использоваться исключительно для предоставления сборок тестировщикам. 

  5. (Опционально) Gitlab (Premium or Ultimate) для использования Gitlab API запросов на отправку сообщений

  6. (Опционально) Discord сервер — стоит учесть, что на канале необходимы привилегии для создания вебхуков только в рамках настройки.

  7. (Опционально) Jira — так как в данном решении управление задачами осуществляется c помощью Jira, то потребуется аккаунт с доступом на чтение задач.

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

  1. Создание и настройка репозитория для хранения сертификатов

  2. Создание репозитория для скриптов CI/CD

  3. Настройка iOS проекта для работы с GitLab

  4. Настройка машины (хоста) для раннеров CI/CD

Введение

Рассмотрим следующий пайплайн:
При открытии мердж реквеста (MR) автоматически запускается сборка текущей ветки и прогон тестов. После их успешного завершения пайплайн ожидает ручного запуска следующего шага, чтобы разработчик мог при необходимости внести корректировки в код. На первом этапе пайплайна можно также прогнать линтер/форматтер. Если в MR вносятся правки, пайплайн запускается заново.

При запуске следующего этапа create_archive повышает версию приложения, генерирует .ipa-архив, а также release notes для Firebase. Затем этот архив будет отправлен в Firebase и TestFlight для тестирования.

В общем и целом, наш процесс CI/CD выглядит примерно так:

Выполнение начальных шагов пайплайна: выполнение сборки и запуск тестоРис 2. Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase

Выполнение начальных шагов пайплайна: выполнение сборки и запуск тестов
Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase

Выполнение заключительных шагов пайплайна: генерация архива и теплой в TestFlight и Firebase

Как я упомянул ранее, в нашей реализации на этапе сборки дополнительно выполняется проверка линтером файлов .swift, участвующих в MR, и если будут обнаружены какие-либо конфликты, соответствующие сообщения отправляются в MR. Хочу отметить, что даже если независимо от того, нашел ли линтер какие-либо проблемы или нет — пайплайн не блокируется. Если мы хотим, чтобы в MR на GitLab отображался статус проверки кода линтером, нам нужна подписка, иначе у нас не будет токена для API Gitlab.

Сообщения от линтера выглядят следующим образом:

Рис 3. Сообщения от линтера в MR

Рис 3. Сообщения от линтера в MR
Там, где возможно, линтер открывает тред в 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
Возможный пример исполнения release notes

Возможный пример исполнения release notes

Если же задача не будет найдена, в release notes будет представлена только информация о версии и номере сборки

Возможный пример исполнения release notes

Возможный пример исполнения release notes

Для архива, отправляющегося в TestFlight в рамках данного примера оповещения отключены.

Настройка CI/CD

Настало время приступить к настройке нашего процесса CI/CD

Настройка [CERTS repo] 

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

Настройка [CI/CD repo]

Мы создаем репозиторий и делаем его приватным. Файлы, перечисленные здесь, представлены только для справки. По окончанию настройки, репозиторий должен содержать следующие файлы, которые вы можете взять из репозитория.

Корень проекта репозитория со скриптами CI/CD

Корень проекта репозитория со скриптами CI/CD

где по пути fastlane/ располагается:

Содержимое каталога Fastlane

Содержимое каталога Fastlane

Обратите внимание на .gitlab-ci-template.yml — этот файл содержит необходимую информацию о нашем пайплайне и будет использоваться любым проектом с CI/CD. Как вы могли заметить, он довольно небольшой, и в нем не так много переменных — они должны быть объявлены позже в настройках CI/CD вашего проекта.

Вы можете скорректировать файл удобным для вас образом, например, добавить везде дополнительное условие на именование ветки, как это было сделано в шаге create_archive, либо убрать его вовсе:

Условие запускаcreate_archiveдля определенного эвента и имени ветки

Условие запускаcreate_archiveдля определенного эвента и имени ветки

Если вы не собираетесь внедрять Firebase в проект или использовать TestFlight для тестирования, удалите следующие строки:

Для Firebase:

Код задачи для деплоя сборки в Firebase

Код задачи для деплоя сборки в Firebase

Для TestFlight:

Код задачи для деплоя сборки в TestFlight

Код задачи для деплоя сборки в TestFlight

Настройка [PROJECT repo]

Допустим, у нас уже есть готовый проект с развернутым линтером, нам потребуется выполнить следующие шаги:

  1. Убедиться, что проект настроен корректно

  2. Завести несколько переменных

  3. Завести новый пайплайн

  4. Создать для проекта раннеры

  5. Настроить Appfile

1. Убеждаемся, что проект настроен корректно

Теперь проверим, что проект настроен корректно.

Настройка схем и таргетов

В проекте должны присутствовать кроме основного таргета еще таргет для тестирования с привязанной к нему схеме.
В данном гайде будет проект с 2 таргетами:

Таргеты проекта, для которого разворачиваем CI/CD

Таргеты проекта, для которого разворачиваем CI/CD

Как ранее было упомянуто, таргет для тестирования в нашем случае отвечает за unit тесты. Однако, если у Вас появляется желание или необходимость развернуть и UI тесты, дополнительно заводится таргет и схема под него.

Для работы CI/CD с нашей версией проекта необходимо создать как минимум первые 2 схемы:
1. Схема, с которой будет собираться проект
2. Схема, с которой будут проходить тесты
3. Схема, в которой ведется разработка
Таким образом, мы имеем 3 рабочих схемы, стоит убедиться, что для всех них стоит галочка на shared, в противном случае — схема видна будет только вам.

Схемы проекта, для которого разворачивается CI/CD

Схемы проекта, для которого разворачивается CI/CD

Интеграция с 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» для добавления тестовых групп

Перейдите во вкладку “Release & Monitor” и выберете “App Distribution”. В открывшемся окне, нажмите “Get started”. 

Настройка почти завершена.

Перейдите в таб “Testers & Groups” и добавьте группу для теста “Add group” (при желании можете добавить в нее себя).

На этом настройка тестовых групп завершена, скопируйте название группы — оно понадобится при настройке GitLab CI/CD

Теперь, перейдите в настройки проекта:

Переходим в "Project settings"

Переходим в «Project settings»

В табе “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

Переменная

Описание

Пример

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 в проекте

Устанавливать 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. Добавляем раннеры

Пока мы не можем найти нужные нам раннеры с тегами — создадим их, нажмем на “New project runner”. Необходимо указать теги для раннера.
В качестве названия раннеров можно, но не обязательно, использовать следующую нотацию для упрощения их идентификации: PROJECT_NAME/JOB_NAME/NUMBER.

Добавление Gitlab раннера в проект

Добавление Gitlab раннера в проект

После нажатия 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 в проект

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], defaultenv по умолчанию.
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:

'openssl base64 < path/to/key.p8 | tr -d '\n' | pbcopy'

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 сервер. Выбираем необходимый канал и переходим в его настройки

Нажимаем «New Webhook» и просто копируем его URL в появившимся поле нового элемента:

Создание Webhook на канал в Discord сервере

Создание Webhook на канал в Discord сервере
Discord сервер, настройки Webhook, копируем Webhook URL

Discord сервер, настройки Webhook, копируем Webhook URL

В проекте [PROJECT repo] переходим в «Settings» -> «Integrations» -> «Discord Notifications«. Заполняем следующие поля:

После этого, проект полностью подготовлен к запуску с использованием Gitlab CI/CD + Firebase + Fastlane + Jira и оповещением в Discord. Подводя итог, при условии правильной настройки CI/CD-сервера единственные шаги, необходимые для применения этого процесса CI/CD к новому проекту, следующие:

  1. Создайте пустой частный репозиторий для его сертификатов.

  2. Добавьте в свой проект.gitlab-ci.yml с ссылкой на .gitlab-ci-template.yml.

  3. Добавьте переменные в проект GitLab.

  4. Заполните переменные в .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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *