Большой туториал настройки CI/CD пайплайна с использованием Jenkins и Fastlane.

Мухаммадиер Расулов

TeamLead IOS в YuSMP Group, автор материала

Внедрение CI/CD в процесс создания iOS-приложений  позволяет разработчикам сосредоточиться на инновациях и улучшении функциональности приложений, в то время как рутинные процессы выполняются автоматически. Jenkins и Fastlane способны обеспечивать необходимую автоматизацию и гибкость в разработке. Помогают поддерживать высокий стандарт качества при более быстром цикле, что в конечном итоге приводит к созданию лучшего продукта для пользователей.

Я буду использовать Jenkins вместе с Fastlane для загрузки приложения в TestFlight и отправки моего приложения в AppStore. Вы также можете использовать Fastlane для загрузки ваших приложений в AppCenter.

Содержание

Что такое CI/CD?

Непрерывная интеграция (CI) — это практика регулярного слияния изменений кода в общий репозиторий, происходящая многократно в течение дня. Этот процесс включает в себя автоматизацию создания сборок и их тестирования.

Непрерывная доставка (CD) является расширением непрерывной интеграции, дополняя её дополнительными этапами, которые позволяют выпускать приложение клиентам после каждого изменения или обновления.

Что такое Jenkins?

Jenkins — это сервер автоматизации с открытым исходным кодом, который позволяет разработчикам по всему миру надежно собирать, тестировать и развертывать свое программное обеспечение. Jenkins — это бесплатный инструмент, с помощью которого вы можете управлять всем процессом CI/CD.

Что такое Fastlane?

Fastlane — это платформа с открытым исходным кодом, направленная на упрощение развертывания приложений для Android и iOS. Fastlane позволяет автоматизировать каждый аспект вашего рабочего процесса разработки и выпуска.

Автоматизация создания сборки – общий процесс

  • Настройка проекта и создание репозитория в GitLab 

  • Подключение локального Jenkins к облаку GitLab для выгрузки ветки проекта (для сборки в формате IPA) 

  • Интеграция Jenkins и Fastlane для сборки и архивации проекта 

  • Перемещение архивированного IPA в TestFlight из Fastlane

Установка Jenkins на macOS

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

brew install jenkins-lts

brew services start jenkins-lts

brew services stop jenkins-lts

brew services restart jenkins-lts

brew upgrade jenkins-lts

После установки Jenkins вы можете начать с команды перезапуска, указанной выше. После этого перейдите по адресу http://localhost:8080/, и вы увидите, что Jenkins запущен.


Чтобы пройти этот экран, вам нужно скопировать путь, указанный красным, и вставить его в терминал с командой open. Вы увидите пароль, скопируйте его и вставьте на страницу Jenkins.

Это начнет установку необходимых файлов и плагинов для вас. После завершения установки вы можете захотеть создать своего администратора или пропустить этот шаг.

Также вы можете изменить порт локального хоста: 

Установка Jenkins завершена, теперь мы создадим нашу первую задачу, а затем применим Fastlane для загрузки нашего проекта в TestFlight. Давайте установим Fastlane и затем продолжим с созданием задачи в Jenkins. 

Установка Fastlane

Вы можете установить Fastlane с помощью команды brew.

brew install fastlane

Откройте терминал и перейдите в папку проекта в терминале. Выполните команду fastlane, указанную ниже.

fastlane init

Fastlane будет инициализирован в директории вашего проекта, и мы обновим Fastfile необходимой информацией для загрузки нашего проекта. Во время выполнения команды вас спросят, для каких целей вы будете использовать fastlane, вы можете ответить, написав 4.

Сборка и отправка приложения в TestFlight

Откройте Fastfile в папке fastlane и скопируйте код ниже, обновив его вашей информацией. Вам необходимо создать файл p8 для загрузки вашего приложения в AppStore. Файл p8 является чувствительным, поэтому мы не хотим размещать его в папке проекта. При создании задачи в Jenkins мы загрузим файл p8 туда и будем читать его оттуда. Вам также нужно объявить метод экспорта и опции для подписи с вашим профилем.

# Этот Fastfile автоматизирует процесс сборки и отправки новой бета-версии приложения в TestFlight

# Определение путей и ключевых переменных

KEY_FILE_PATH = "путь/к/вашему/файлу.p8" # Путь к файлу .p8 для аутентификации в App Store Connect
KEY_ID = "ваш_key_id" # ID ключа для App Store Connect
ISSUER_ID = "ваш_issuer_id" # Issuer ID для App Store Connect
WORKSPACE_PATH = "путь/к/вашему/workspace.xcworkspace" # Путь к рабочему пространству Xcode
XCODEPROJ_PATH = "путь/к/вашему/project.xcodeproj" # Путь к проекту Xcode
TARGET_SCHEME = "ЦелеваяСхема" # Целевая схема сборки в Xcode
CONFIG_APPSTORE = "Release" # Конфигурация для сборки App Store
OUTPUT_DIRECTORY = "./fastlane/builds" # Директория для сохранения собранных приложений

default_platform :ios

platform :ios do

  desc "Push a new beta build to TestFlight"
  lane :build_and_send_to_testflight do |options|
  
    version = options[:VERSION_NUMBER] || ENV['VERSION_NUMBER'] # Получение номера версии из параметров или переменных окружения
    if version.to_s.empty?
      UI.error("Переменная 'VERSION_NUMBER' пуста или имеет значение nil") # Проверка на пустую версию
    else
      increment_version_number_in_xcodeproj(
          version_number: version,
          xcodeproj: XCODEPROJ_PATH,
          target: TARGET_SCHEME
      ) # Увеличение номера версии в Xcode проекте
    end
    
    build_version = options[:BUILD_NUMBER] || ENV['BUILD_NUMBER'] # Получение номера сборки из параметров или переменных окружения
    if build_version.to_s.empty?
      UI.error("Переменная 'BUILD_NUMBER' пуста или имеет значение nil") # Проверка на пустой номер сборки
    else
      increment_build_number_in_xcodeproj(
          build_number: build_version,
          xcodeproj: XCODEPROJ_PATH,
          target: TARGET_SCHEME
      ) # Увеличение номера сборки в Xcode проекте
    end
  
    app_store_connect_api_key(
      key_id: KEY_ID,
      issuer_id: ISSUER_ID,
      key_filepath: KEY_FILE_PATH, # Использование файла аутентификации
      duration: 1200, # Длительность сессии (необязательно, максимум 1200 секунд)
      in_house: false # Флаг для внутреннего использования (необязательно, может быть необходим при использовании match/sigh)
    )
    build_app(
      workspace: WORKSPACE_PATH,
      scheme: TARGET_SCHEME,
      configuration: CONFIG_APPSTORE,
      export_method: "app-store",
      export_options: {
        provisioningProfiles: {
          "идентификатор_вашего_приложения" => "ПрофильРазвертыванияAppStore"
        }
      }, # Опции экспорта для подписи приложения
    )
    upload_to_testflight # Загрузка собранного приложения в TestFlight
  end
end

Интеграция Jenkins с Fastlane

Перейдем в Jenkins и создадим New Item. Я назвал его MyProject и создал как Pipeline.

После этого нам нужно настроить наш проект. Давайте добавим управление исходным кодом. Я хранил проект на Gitlab, поэтому я указал URL проекта.

Затем создадим токен пользователя, который содержит в себе имя и пароль пользователя с GitLab.

Выбираем созданный токен


Теперь мы можем получить проект из Gitlab. Давайте добавим опцию для выбора, с какой ветки мы будем собирать приложение. Чтобы добавить параметризованный Git, нам нужно добавить новый плагин в Jenkins. Перейдите в Manage Jenkins -> Plugins -> Avaliable. Найдите Git Parameter и установите его.

После установки плагина продолжим настройку нашего проекта. Выберите проект и нажмите Configure. В разделе General выберите «This project is parameterized»

Добавьте параметр Git и выберите ветку в качестве типа параметра. Обновите название параметра на ${BRANCH_NAME}.

Добавляем еще два параметра:

Теперь мы добавили ещё два параметра: один для версии сборки и другой для номера сборки. Эти параметры будут отображаться в TestFlight.

В данном поле необходимо указать название ветки, в корне которой находится Jenkinsfile, чтобы Jenkins мог собирать сборку из этой ветки.

Установив здесь галочку, мы активируем функцию отмены параллельных сборок. Это означает, что если две сборки начнут выполняться одновременно, Jenkins автоматически отменит первый запущенный пайплайн, обеспечивая тем самым более эффективное управление ресурсами.

Теперь в разделе "Build with parameters" мы видим следующие поля: первое поле - версия билда, второе поле - номер сборки, а третье - название ветки, из которой будет собираться сборка. Это представляет собой очень удобный метод сборки, позволяя команде быстро реагировать в ситуациях, когда разработчик недоступен, а проектному менеджеру или QA специалисту срочно требуется сборка для тестирования определенной функции. Все, что им нужно сделать, это выбрать необходимые параметры и запустить сборку одним кликом.

Теперь, когда мы настроили возможность ручного запуска пайплайна через интерфейс Jenkins, следующим шагом является настройка автоматической сборки, которая не требует вмешательства членов команды. Этот метод позволяет автоматически собирать новые сборки при внесении изменений в определённую ветку. Например, когда разработчик завершает работу над функционалом и отправляет его в главную ветку разработки, такую как 'develop', срабатывают триггеры, и Jenkins самостоятельно инициирует пайплайн для сборки актуальной версии без необходимости участия разработчиков. Это обеспечивает непрерывность процесса разработки и позволяет команде оперативно получать готовые к тестированию сборки.

Настройка автоматической сборки с помощью вебхуков

Smee.io — это инструмент, который позволяет локально тестировать вебхуки. Он создаёт публичный URL, который перенаправляет входящие запросы на ваш локальный сервер. Это особенно полезно при разработке и тестировании интеграций с внешними сервисами, такими как GitLab или GitHub, которые используют вебхуки для уведомления о событиях, таких как push в репозиторий.

Настройка Smee.io:

  • Откройте веб-браузер и перейдите на официальный сайт Smee.io(https://smee.io).

  • На главной странице сайта найдите и нажмите кнопку "Start a new channel" («Создать новый канал»). Это действие инициирует создание нового канала для перенаправления вебхуков.

  • После нажатия на кнопку, система автоматически сгенерирует уникальный URL-адрес для вашего нового канала. Этот URL будет использоваться как конечная точка для перенаправления вебхуков с вашего источника (например, с GitLab) на Jenkins.

Для настройки вебхука в GitLab, перейдите в настройки вашего проекта, выберите раздел "Webhooks", введите URL вашего канала из Smee.io в поле "URL", выберите события, при которых должен срабатывать вебхук, такие как "Push events" или "Merge Request events", и нажмите "Add webhook" для активации вебхука, который будет отправлять уведомления на ваш локальный сервер через Smee.io при наступлении выбранных событий. Далее, запускаем клиент Smee.io у себя локально

Прежде всего, для реализации автоматической сборки нам необходимо установить новый плагин в разделе плагинов Jenkins. Плагин называется Generic Webhook Trigger. Этот плагин позволяет Jenkins реагировать на веб-хуки, отправленные из различных источников, таких как системы контроля версий или другие внешние сервисы. С его помощью мы можем настроить автоматическое срабатывание пайплайнов при определённых событиях, например, при пуше изменений в ветку репозитория. Это обеспечивает непрерывную интеграцию и автоматизацию процесса сборки, делая его более эффективным и уменьшая необходимость вручную запускать сборки.

В разделе "Build Triggers" активируем "Generic Webhook Trigger" и заполним поля "Token", "Post content parameters" и "Cause" для фильтрации и обработки входящих вебхуков, что позволит нашему Jenkins автоматически запускать сборки на основе событий из GitLab или других сервисов.

Определение пайплайна сборки в Jenkinsfile

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

#!/bin/bash -l  

pipeline { 

    agent any  

    options {  # Блок опций пайплайна

        timeout(time: 1, unit: 'HOURS')  # Устанавливает ограничение времени выполнения пайплайна до 1 часа

        disableConcurrentBuilds(abortPrevious: true)  # Отменяет предыдущие параллельные сборки при запуске новой

    }

    parameters {

        string(name: 'VERSION_NUMBER', description: 'Версия сборки в TestFlight')  # Параметр для версии сборки в TestFlight

        string(name: 'BUILD_NUMBER', description: 'Номер сборки в TestFlight')  # Параметр для номера сборки в TestFlight

        string(name: 'BRANCH_NAME', description: 'Ветка с которой собирается сборка')  # Параметр для ветки сборки

    }

    environment {  # Блок переменных окружения

        VERSION_NUMBER = "${params.VERSION_NUMBER}"  # Присваивание значения параметра VERSION_NUMBER переменной окружения

        BUILD_NUMBER = "${params.BUILD_NUMBER}"  # Присваивание значения параметра BUILD_NUMBER переменной окружения

    }

    triggers {  # Блок триггеров пайплайна

        GenericTrigger(  # Определение общего триггера

        genericVariables: [  # Список переменных, извлекаемых из вебхука

        [key: 'ref', value: '$.ref'],  # Извлечение ветки из вебхука

        [key: 'before', value: '$.before'],  # Извлечение значения хэша коммита до события

        [key: 'after', value: '$.after'],  # Извлечение значения хэша коммита после события

        [key: 'repo_url', value: '$.repository.url'],  # Извлечение URL репозитория

        ],

        causeString: 'Triggered By Gitlab On $ref',  # Строка причины запуска

        token: 'REDACTED_TOKEN',  # Токен для аутентификации вебхука

        tokenCredentialId: '',  # Идентификатор учетных данных для токена

        regexpFilterText: '$after',  # Текст для фильтрации с помощью регулярного выражения

        regexpFilterExpression: '^(?!0000000000000000000000000000000000000000$).*$',  # Регулярное выражение для фильтрации

        printContributedVariables: true,  # Печать извлеченных переменных

        printPostContent: true,  # Печать содержимого вебхука

        silentResponse: false,  # Ответ на вебхук

        )

    }

    stages {  # Блок этапов пайплайна

        stage('Checkout') {  # Этап для получения исходного кода из репозитория

            steps { 

                script {  # Выполнение скрипта

                    def branchToBuild = env.ref ? env.ref.replaceAll("refs/heads/", "") : params.BRANCH_NAME  # Определение ветки для сборки

                    echo "Выбрана ветка для сборки: ${branchToBuild}"  # Вывод выбранной ветки

                    

                    sh ''' 

                    cd /path/to/project

                    '''

                    

                    checkout scm: [  # Выполнение операции checkout с использованием настроек SCM

                        $class: 'GitSCM',  # Указание на использование GitSCM

                        branches: [[name: "*/${branchToBuild}"]],  # Выбор ветки для сборки

                        userRemoteConfigs: [[  # Конфигурация удаленного репозитория

                            url: 'REDACTED_REPO_URL',  # URL репозитория

                            credentialsId: 'REDACTED_CREDENTIALS_ID',  # Идентификатор учетных данных

                            extensions: [[$class: 'CloneOption', timeout: 20]]  # Настройки клонирования

                        ]]

                    ]

                    

                    sh """

                        cd /path/to/project

                        git fetch --all

                        git checkout ${branchToBuild}

                        git pull origin ${branchToBuild}

                    """

                }

            }

        }

        

        stage('Install dependencies') {  # Этап установки зависимостей

            steps {

                sh ''' 

                source ~/.zshrc

                cd /path/to/project

                pod deintegrate

                pod install

                '''

            }

        }

        stage('Pre-build') {  # Предварительный этап сборки

            steps {

                script {

                    sh """ 

                        source ~/.zshrc

                        /opt/homebrew/opt/fastlane/bin/fastlane add_plugin versioning

                    """

                

                    def branchToBuild = getBranchToBuild()  # Получение ветки для сборки

                    sendMessage("⏳\nСборка начинается!\nВетка: ${branchToBuild}${getVersionText()}${getBuildText()}\n⏳")  # Отправка сообщения о начале сборки

                    

                    sh """  # Запуск дополнительных shell команд

                        echo n

                    """

                }

            }

        }

        stage('Build and send to TestFlight') {  # Этап сборки и отправки в TestFlight

            steps {

                sh '''  # Запуск shell команд

                    source ~/.zshrc

                    /opt/homebrew/opt/fastlane/bin/fastlane build_and_send_to_testflight VERSION_NUMBER:$VERSION_NUMBER BUILD_NUMBER:$BUILD_NUMBER

                '''

            }

        }

        stage('Send notification to Telegram') {  # Этап отправки уведомления в Telegram

            steps {

                sh """  # Запуск shell команд

                    echo n

                    echo n

               """

            }

        }

    }

    

    post {  # Блок post для выполнения действий после завершения пайплайна

        success {  # Действия в случае успешного завершения пайплайна

            script {

                sendMessage("✅\nСборка успешно завершена!\nВетка: ${getBranchToBuild()}${getVersionText()}${getBuildText()}\n✅")  # Отправка сообщения об успешном завершении сборки

            }

        }

    }

}

def getBranchToBuild() {  # Функция для получения ветки для сборки

    return env.ref ? env.ref.replaceAll("refs/heads/", "") : params.BRANCH_NAME

}

def getVersionText() {  # Функция для получения текста версии

    return params.VERSION_NUMBER ? "\nВерсия: ${params.VERSION_NUMBER}" : ""

}

def getBuildText() {  # Функция для получения текста номера сборки

    return params.BUILD_NUMBER ? "\nСборка: ${params.BUILD_NUMBER}" : ""

}

def sendMessage(String messageText) {  # Функция для отправки сообщения

    sh "curl -s -X POST https://api.telegram.org/botREDACTED_BOT_TOKEN/sendMessage -d chat_id=REDACTED_CHAT_ID -d text='${messageText}'"  # Отправка сообщения через API Telegram

}

Комментарии (0)