Building an application for the AppStore. We use Jenkins, Fastlane, TestFlight

Large tutorial on setting up a CI/CD pipeline using Jenkins and Fastlane.

Muhammadier Rasulov

TeamLead IOS in YuSMP Group, author of the material

Incorporating CI/CD into the iOS app creation process allows developers to focus on innovation and improving app functionality while routine processes are performed automatically. Jenkins and Fastlane are capable of providing the necessary automation and flexibility in development. Helps maintain a high standard of quality with faster turnaround times, ultimately resulting in a better product for the users.

I will be using Jenkins along with Fastlane to upload the application to TestFlight and submit my application to the AppStore. You can also use Fastlane to upload your apps to AppCenter.

Content

What is CI/CD?

Continuous Integration (CI) is the practice of regularly merging code changes into a common repository, occurring multiple times throughout the day. This process involves automating the creation of builds and their testing.

Continuous Delivery (CD) is an extension of continuous integration, supplementing it with additional stages that allow you to release the application to customers after each change or update.

What is Jenkins?

Jenkins is an open source automation server that allows developers around the world to reliably build, test, and deploy their software. Jenkins is a free tool that allows you to manage your entire CI/CD process.

What is Fastlane?

Fastlane is an open source platform aimed at making it easier to deploy apps for Android and iOS. Fastlane allows you to automate every aspect of your development and release workflow.

Automating assembly creation – general process

Installing Jenkins on macOS

To install Jenkins on our macOS machine, I will use the following command. You can also see the commands to start, stop, restart and update Jenkins:

brew install jenkins-lts

brew services start jenkins-lts

brew services stop jenkins-lts

brew services restart jenkins-lts

brew upgrade jenkins-lts

Once Jenkins is installed, you can start with the restart command given above. After that, go to the address http://localhost:8080/and you will see that Jenkins is running.

To get past this screen, you need to copy the path shown in red and paste it into the terminal with the open command. You will see the password, copy it and paste it into the Jenkins page.

This will begin installing the necessary files and plugins for you. Once the installation is complete, you may want to create your own administrator or skip this step.

You can also change the localhost port:

The Jenkins installation is complete, we will now create our first task and then use Fastlane to upload our project to TestFlight. Let's install Fastlane and then continue with creating a task in Jenkins.

Fastlane installation

You can install Fastlane using the brew command.

brew install fastlane

Open a terminal and go to your project folder in the terminal. Run the fastlane command below.

fastlane init

Fastlane will be initialized in your project directory and we will update the Fastfile with the necessary information to load our project. While running the command you will be asked for what purpose you will use fastlane, you can answer by writing 4.

Build and submit the application to TestFlight

Open Fastfile in the fastlane folder and copy the code below, updating it with your information. You need to create a p8 file to upload your app to the AppStore. The p8 file is sensitive so we don't want to place it in the project folder. When creating a task in Jenkins, we will upload the p8 file there and read it from there. You also need to declare the export method and signature options with your profile.

# Этот 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

Integrating Jenkins with Fastlane

Let's go to Jenkins and create New Item. I named it MyProject and created it as Pipeline.

After this we need to configure our project. Let's add source control. I was storing the project on Gitlab, so I provided the project URL.

Then we will create a user token, which contains the username and password of the user from GitLab.

Select the created token

Now we can get the project from Gitlab. Let's add an option to select which branch we will build the application from. To add parameterized Git, we need to add a new plugin to Jenkins. Go to Manage Jenkins -> Plugins -> Avaliable. Find Git Parameter and install it.

After installing the plugin, we will continue setting up our project. Select a project and click Configure. In chapter General select “This project is parameterized»

Add a Git option and select branch as the option type. Update the parameter name to ${BRANCH_NAME}.

Add two more parameters:

Now we've added two more parameters: one for the build version and another for the build number. These options will appear in TestFlight.

In this field you must indicate the name of the branch at the root of which the Jenkinsfileso that Jenkins can build from this branch.

By checking the box here, we will activate the function to cancel parallel builds. This means that if two builds start running at the same time, Jenkins will automatically cancel the first running pipeline, allowing for more efficient resource management.

Now in the section “Build with parameters” we see the following fields: the first field is the build version, the second field is the build number, and the third is the name of the branch from which the build will be built. This represents a very convenient build method, allowing the team to quickly respond in situations where the developer is unavailable, and If a project manager or QA specialist urgently needs a build to test a certain feature, all they need to do is select the necessary parameters and run the build with one click.

Now that we have configured the ability to manually run the pipeline through the Jenkins interface, the next step is to set up an automatic build that does not require the intervention of team members. This method allows you to automatically build new builds when changes are made to a specific branch. For example, when a developer completes a feature and pushes it to a master branch such as 'develop', triggers are fired and Jenkins automatically initiates a pipeline to build the latest version without the need for developer input. This ensures continuity of the development process and allows the team to quickly receive builds ready for testing.

Setting up automatic builds using webhooks

Smee.io is a tool that allows you to test webhooks locally. It creates a public URL that redirects incoming requests to your local server. This is especially useful when developing and testing integrations with external services, such as GitLab or GitHub, that use webhooks to notify about events, such as pushes to a repository.

Setting up Smee.io:

To configure a webhook in GitLab, go to your project settings, select the “Webhooks” section, enter the URL of your channel from Smee.io in the “URL” field, select the events at which the webhook should be triggered, such as “Push events” or “Merge Request events” and click “Add webhook” to activate a webhook that will send notifications to your local server via Smee.io when the selected events occur. Next, we launch the Smee.io client locally

First of all, to implement automatic build, we need to install a new plugin in the Jenkins plugins section. The plugin is called Generic Webhook Trigger. This plugin allows Jenkins to respond to webhooks sent from various sources, such as version control systems or other external services. With its help, we can configure automatic triggering of pipelines upon certain events, for example, when pushing changes to a repository branch. This enables continuous integration and automation of the build process, making it more efficient and reducing the need to manually run builds.

In chapter “Build Triggers“activate”Generic Webhook Trigger” and fill in the fields “Token”, “Post content parameters” and “Cause” to filter and process incoming webhooks, which will allow our Jenkins to automatically launch builds based on events from GitLab or other services.

Defining a build pipeline in Jenkinsfile

In the final step of setting up our CI/CD pipeline, we will add a Jenkinsfile to the root of our project. This file will contain a pipeline definition that describes all the stages of building, testing and deploying our application. The Jenkinsfile allows us to store the pipeline configuration along with the project source code, making collaboration and versioning easier. An example Jenkinsfile content might look like this:

#!/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

}

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *