Pull to refresh
827.91
OTUS
Цифровые навыки от ведущих экспертов

Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 2

Reading time 6 min
Views 3.5K
Original author: Eleni Papanikolopoulou

В преддверии старта курса "iOS Developer. Basic" продолжаем публиковать серию полезных переводов, а также приглашаем записаться на бесплатный демо-урок по теме: "Result Type".


Читать первую часть


5. Сборка билда

stage('Build') {
        withEnv(["FASTLANE_USER=fastlane_user_email_address"]) {
            withCredentials([
                    string([
                      credentialsId:'match_password_id', 
                      variable: 'MATCH_PASSWORD'
                    ]),
                    string([
                      credentialsId: 'fastlane_password_id',
                      variable: 'FASTLANE_PASSWORD']),
                    ]) {
                       sh 'bundle exec fastlane build'
                    }
        }
  }

 

На этом этапе мы устанавливаем необходимые переменные среды с помощью функции Jenkins Pipeline withEnv как указано здесь, в соответствии с документацией Fastlane. Итак, мы устанавливаем переменную окружения FASTLANE_USER. После этого мы устанавливаем еще две переменные среды, MATCH_PASSWORD и FASTLANE_PASSWORD, которые нельзя получить без учетных данных. Они по очевидным причинам хранятся в зашифрованном виде внутри Jenkins в пункте меню «Credential» дашборда Jenkins в формате secret_text, и их можно получить, предоставив credentialsId.

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

lane :build do
     match(
        git_branch: "the_branch_of_the_repo_with_the_prov_profile", 
        username: "github_username", 
        git_url: "github_repo_with_prov_profiles", 
        type: "appstore", 
        app_identifier: "production_app_identifier", 
        force: true)
  
     version = get_version_number(
                       xcodeproj: "our_project.xcodeproj", 
                       target: "production_target"
               )
     build_number = latest_testflight_build_number(
                       version: version,   
                       app_identifier: "production_app_identifier",
                       initial_build_number: 0
                     )
    
     increment_build_number({ build_number: build_number + 1 })
     settings_to_override = {
      :BUNDLE_IDENTIFIER => "production_bundle_id",
      :PROVISIONING_PROFILE_SPECIFIER => "production_prov_profile",
      :DEVELOPMENT_TEAM => "team_id"
     }
   
     export_options = {
       iCloudContainerEnvironment: "Production",
       provisioningProfiles: { "production_bundle_id": "production_prov_profile" }
     }
    
     gym(
       clean: true,
       scheme: "production_scheme",
       configuration: "production_configuration",
       xcargs: settings_to_override,
       export_method: "app-store",
       include_bitcode: true,
       include_symbols: true,
       export_options: export_options
     )
  end

Теперь давайте разберем его шаг за шагом.

Мы начинаем с использования экшена Fastlane match. Match по сути создает все необходимые сертификаты и профили обеспечения, которые хранятся в отдельном git-репозитории, т.е. он по сути автоматизирует процесс подписания кода. Это означает, что перед запуском match мы должны были создать другой Github-репозиторий, где мы хранили бы наши профили обеспечения. В качестве альтернативы, если мы не хотим использовать match для подписи кода, мы можем использовать экшены sigh и cert.

А теперь самое интересное. Что бы мы хотели автоматизировать, так это увеличение номер версии билда для одного и того же релиза, чтобы не делать это каждый раз вручную через настройки билда Xcode. Все мы знаем о том, что для того, чтобы загрузить билд в Testflight несколько раз для одного и того же релиза и автоматически не словить ошибку, мы должны повышать версию сборки каждый раз, т.е. мы должны переходить к настройкам проекта или. plist, делать это вручную, а затем пробовать повторно загрузить его. В коде, приведенном выше, нам удалось автоматизировать эту процедуру, выполнив 3 следующих шага:

  1. get_version_number: Получить версию проекта, загруженного в настоящее время 

  2. latest_testflight_build_number: Получить текущий номер билда для версии, котрую мы получили в предыдущем шаге

  3. increment_build_number: Инкремент номера билда с указанным шагом (здесь на единицу).

Наконец, мы продолжаем вызовом экшена gym, который производит фактическую сборку и упаковку приложения. Он настраивается с несколькими аргументами, такими как configuration, scheme и xcargs, где мы можем указать bundle_identifier, export_options и т. д.

6. Загрузка в Testflight

stage('Upload to TestFlight') {
  withEnv(["FASTLANE_USER=fastlane_user_email_address"]) {
    withCredentials([
      string([
           credentialsId: 'fastlane_password_id', 
           variable: 'FASTLANE_PASSWORD']),
       ]) {
         sh "bundle exec fastlane upload_to_testflight"
       }
  }
}

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

lane :upload_to_testflight do
    pilot(
      ipa: "./build/our_project.ipa",
      skip_submission: true,
      skip_waiting_for_build_processing: true,
      app_identifier: "production_app_identifier"
    )
end

Мы используем команду Fastlane pilot, которая загружает сгенерированный на предыдущем шаге файл .ipa в Testflight. Этим действием мы также можем указать лог изменений. Мы также можем пропустить отправку двоичного файла, что означает, что файл .ipa будет только загружен, но не распространен среди тестировщиков.

7. Очистка 

И последнее, но не менее важное: на этом этапе мы выполняем очистку рабочей области с помощью плагина Jenkins cleanup.

stage('Cleanup') {
    cleanWs notFailBuild: true
}

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

Подводя итог, вот как функция deploy() выглядит внутри созданного скрипта Deploy.script.

def deploy() {

    stage('Checkout') {
        checkout scm
    }

    stage('Install dependencies') {
       sh 'gem install bundler'
       sh 'bundle update'
       sh 'bundle exec pod repo update'
       sh 'bundle exec pod install'
    }

    stage('Reset Simulators') {
       sh 'bundle exec fastlane snapshot reset_simulators --force'
    }

    stage('Run Tests') {
       sh 'bundle exec fastlane test'
    }
    
    stage('Build') {
        withEnv(["FASTLANE_USER=fastlane_user_email_address"]) {
            withCredentials([
                 string([
                      credentialsId:'match_password_id', 
                      variable: 'MATCH_PASSWORD'
                 ]),
                 string([
                      credentialsId: 'fastlane_password_id',
                      variable: 'FASTLANE_PASSWORD']),
                 ]) {
                      sh 'bundle exec fastlane build'
                 }
        }
    }
    
    stage('Upload to TestFlight') {
        withEnv(["FASTLANE_USER=fastlane_user_email_address"]) {
            withCredentials([
                 string([
                      credentialsId: 'fastlane_password_id', 
                      variable: 'FASTLANE_PASSWORD']),
                  ]) {
                      sh "bundle exec fastlane upload_to_testflight"
                 }
       }
    }
    
    stage('Cleanup') {
        cleanWs notFailBuild: true
    }
}

Теперь наш Fastfile выглядит вот так:

fastlane_version "2.75.0"

default_platform :ios

lane :test do
    scan(
        clean: true,
        devices: ["iPhone X"],
        workspace: "our_project.xcworkspace",
        scheme: "production_scheme",
        code_coverage: true,
        output_directory: "./test_output",
        output_types: "html,junit"
    )
    slather(
        cobertura_xml: true,
        proj: "our_project.xcodeproj",
        workspace: "our_project.xcworkspace",
        output_directory: "./test_output",
        scheme: "production_scheme",
        jenkins: true,
        ignore: [array_of_docs_to_ignore]
    )
end

lane :build do
     match(
        git_branch: "the_branch_of_the_repo_with_the_prov_profile", 
        username: "github_username", 
        git_url: "github_repo_with_prov_profiles", 
        type: "appstore", 
        app_identifier: "production_app_identifier", 
        force: true)
  
     version = get_version_number(
                       xcodeproj: "our_project.xcodeproj", 
                       target: "production_target"
               )
     build_number = latest_testflight_build_number(
                       version: version,   
                       app_identifier: "production_app_identifier",
                       initial_build_number: 0
                     )
    
     increment_build_number({ build_number: build_number + 1 })
    
     settings_to_override = {
      :BUNDLE_IDENTIFIER => "production_bundle_id",
      :PROVISIONING_PROFILE_SPECIFIER => "production_prov_profile",
      :DEVELOPMENT_TEAM => "team_id"
     }
    
     export_options = {
       iCloudContainerEnvironment: "Production",
       provisioningProfiles: { "production_bundle_id": "production_prov_profile" }
     }
   
     gym(
       clean: true,
       scheme: "production_scheme",
       configuration: "production_configuration",
       xcargs: settings_to_override,
       export_method: "app-store",
       include_bitcode: true,
       include_symbols: true,
       export_options: export_options
     )
end

lane :upload_to_testflight do
    pilot(
      ipa: "./build/our_project.ipa",
      skip_submission: true,
      skip_waiting_for_build_processing: true,
      app_identifier: "production_app_identifier"
    )
end

Функция deploy() вызывается из скрипта, который мы определили в задаче - MyScript.groovy, и выглядит следующим образом:

node(label: 'ios') {
  
  def deploy;
  def utils;

  String RVM = "ruby-2.5.0"

  ansiColor('xterm') {
    withEnv(["LANG=en_US.UTF-8", "LANGUAGE=en_US.UTF-8", "LC_ALL=en_US.UTF-8"]) {
        
        deploy = load("jenkins/Deploy.groovy")
        utils = load("jenkins/utils.groovy")

        utils.withRvm(RVM) {
          deploy.deploy()
        }
    } 
  }
}

Мы загружаем скрипт Deploy.groovy и вызываем функцию deploy(), которая выполняет всю работу. Здесь мы можем заметить, что мы также загружаем скрипт utils.groovy, который помогает нам установить некоторые переменные среды перед запуском Jenkins.job. AnsiColor еще один плагин Jenkins который используется для того, чтобы раскрасить вывод этапов в пайплайне сборки. Наконец, мы можем заметить, что мы запускаем скрипт внутри

node(label: 'ios')

В Scripted Pipeline указанный выше node является важным первым шагом, поскольку он выделяет исполнителя и рабочее пространство для пайплайна.

И вот нам удалось создать задачу Jenkins, которая распространяет различные ветки нашего приложения, определяя ветку в качестве параметра в Jenkins.

Часть 3

В следующей части мы рассмотрим как можно настроить Jenkins , чтобы распространять наше приложение на различные среды для различных конфигураций Xcode.


Записаться на бесплатный демо-урок.


Tags:
Hubs:
+6
Comments 0
Comments Leave a comment

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS