Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 3
Всем привет. В преддверии старта курсов "iOS Developer. Basic" и "iOS Developer. Professional", публикуем заключительную часть статьи про интеграцию CI/CD для нескольких сред с Jenkins и Fastlane.
А также приглашаем вас на бесплатный демо-урок по теме: "Combine до iOS 13 и как добавить SwiftUI 2.0 в любое приложение"
Настройка Jenkins под разные среды
В предыдущей части нам удалось создать задачу Jenkins, загружающую наше приложение в Testflight для разных веток под разные фичи. Для достижения цели, намеченной в первой части, остается только реализовать возможность делать это для разных сред, например, для стейджа и тест продакшн среды. Разные среды эквивалентны разным конфигурациям Xcode (т. е. различным пользовательским схемам) с определенными настройками сборки под каждую. Если мы еще раз взглянем на три файла, написанные нами раньше: MyScipt.groovy
, Deploy.groovy
и Fastfile
, мы заметим, что там есть свойства, зависящие от конфигурации, а именно:
Идентификатор приложения
Конфигурация, которая соответствует уникальной схеме, созданной в Xcode
Имя профиля обеспечения (provisioning profile)
Эти свойства используются в разных местах, в основном внутри лейнов Fastlane. Следовательно, для достижения нашей цели мы должны передавать эти свойства в качестве параметров в лейнах и использовать разные значения для каждой конфигурации. Синтаксис лейна с параметрами следующий:
lane :build do |options|
а параметр может быть получен следующим образом:
parameter = options[:parameter_name]
В нашем случае нам нужны три разных параметра, описанных выше.
Теперь предположим, что у нас есть среды под названием Staging
и TestProduction
. Нам нужно будет реализовать в Jenkins два разных задания, таких же как задание, которое мы создавали ранее, и подключить их к двум разным скриптам, скажем, Stg.groovy
и TestProduction.groovy
. Эти скрипты будут идентичны, за исключением следующих трех параметров:
Stg-parameters.groovy
// идентификатор приложения
def getBundleId() {
return "com.our_project.stg"
}
// название схемы
def getConfiguration() {
return "Stg-Testflight"
}
// имя профиля обеспечения
def getProvisioningProfile() {
return "\'match AppStore com.our_project.stg'"
}
// идентификатор приложения
def getBundleId() {
return "com.our_project.test.production"
}
// название схемы
def getConfiguration() {
return "TestProduction-Testflight"
}
// имя профиля обеспечения
def getProvisioningProfile() {
return "\'match AppStore com.our_project.test.production'"
}
А получать к ним доступ внутри лейна сборки, мы будем следующим образом:
TestProduction-parameters.groovy
lane :build do |options|
bundle_id = options[:bundle_id]
configuration = options[:configuration]
provisioning_profile = options[:provisioning_profile]
.
.
.
end
Функция deploy внутри Deploy.script
будет получать эти параметры в сигнатуре метода:
def deployWith(bundle_id, configuration, provisioning_profile) {
.
.
.
}
Стадии, использующие эти параметры: Run Tests, Build и Upload to Testflight - это стадии, требующие параметризации. Остальные (Checkout repo, Install dependencies, Reset simulators и Cleanup в конце) останутся прежними.
Настройка стадии Run Tests
Мы реализуем Run tests, передавая configuration в качестве параметра, чтобы использовать его внутри соответствующего лейна:
stage('Run Tests') {
sh 'bundle exec fastlane test configuration:$configuration'
}
Теперь самая сложная часть - настроить лейн test внутри Fastfile, чтобы он использовал этот параметр. Получаем параметры следующим образом:
lane :a_lane do |options|
....
bundle_id = options[:bundle_id]
configuration = options[:configuration]
provisioning_profile = options[:provisioning_profile]
...
end
Итак, лейн test, который должен использовать только параметр configuration, теперь будет выглядеть так:
lane :test do |options|
configuration = options[:configuration]
scan(
clean: true,
devices: ["iPhone X"],
workspace: "our_project.xcworkspace",
scheme: configuration,
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: configuration,
jenkins: true,
ignore: [array_of_docs_to_ignore]
)
end
Обратите внимание, что в поле scheme мы используем параметр configuration, поскольку схема разнится от среды к среде.
Настройка стадии Build
Стадии Build необходимы все три параметра. Мы реализуем команду build Fastlane, передавая эти три параметра. Стадия Build, которая теперь внутри функции deployWith()
, будет выглядеть следующим образом:
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 bundle_id:$bundle_id configuration:$configuration provisioning_profile:$provisioning_profile'
}
}
}
Это означает, что лейн build останется точно таким же, как и раньше, за исключением команд которые используют эти три параметра:
lane :build do |options|
bundle_id = options[:bundle_id]
configuration = options[:configuration]
provisioning_profile = options[:provisioning_profile]
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: bundle_id,
force: true)
version = get_version_number(
xcodeproj: "our_project.xcodeproj",
target: "production_target"
)
build_number = latest_testflight_build_number(
version: version,
app_identifier: bundle_id,
initial_build_number: 0
)
increment_build_number({ build_number: build_number + 1 })
settings_to_override = {
:BUNDLE_IDENTIFIER => bundle_id,
:PROVISIONING_PROFILE_SPECIFIER => provisioning_profile,
:DEVELOPMENT_TEAM => "team_id"
}
export_options = {
iCloudContainerEnvironment: "Production",
provisioningProfiles: { bundle_id => provisioning_profile }
}
gym(
clean: true,
scheme: configuration,
configuration: configuration,
xcargs: settings_to_override,
export_method: "app-store",
include_bitcode: true,
include_symbols: true,
export_options: export_options
)
end
Легко заметить, что везде, где используются значения bundle_id
, configuration
и provisioning_profiles
, мы теперь используем значения этих параметров вместо захардкоженных.
Настройка стадии Upload To Testflight
Мы используем команду Fastlane upload_to_testflight
, передавая ей в качестве параметра bundle_id
. Стадия Upload to TestFlight теперь тоже находится внутри функции deployWith()
файла Deploy.groovy
и будет выглядеть, как показано ниже:
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 bundle_id:$bundle_id"
}
}
}
Соответствующий лейн внутри FastFile будет теперь использовать bundle_id
таким образом:
lane :upload_to_testflight do |options|
bundle_id = options[:bundle_id]
pilot(
ipa: "./build/WorkableApp.ipa",
skip_submission: true,
skip_waiting_for_build_processing: true,
app_identifier: bundle_id
)
end
И на этом все! Нам удалось использовать одни и те же Deploy.groovy
и Fastfile
для различных конфигураций. Но как мы собираемся разделять эти параметры для разных конфигураций?
В Jenkins вместо одной задачи для всех конфигураций мы теперь можем создавать разные задачи под каждую, которые будут идентичными, за исключением скрипта, который они используют. Поэтому, как я упоминал ранее, мы создаем задачу Stg
и используем скрипт Stg.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.deployWith(getBundleId(), getConfiguration(), getProvisioningProfile())
}
}
}
}
// идентификатор приложения
def getBundleId() {
return "com.our_project.stg"
}
//название схемы
def getConfiguration() {
return "Stg-Testflight"
}
// имя профиля обеспечения
def getProvisioningProfile() {
return "\'match AppStore com.our_project.stg'"
}
Точно так же для конфигурации TestProduction
, мы создаем новую идентичную задачу и используем скрипт TestProduction.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.deployWith(getBundleId(), getConfiguration(), getProvisioningProfile())
}
}
}
}
// идентификатор приложения
def getBundleId() {
return "com.our_project.test.production"
}
// название схемы
def getConfiguration() {
return "TestProduction-Testflight"
}
// имя профиля обеспечения
def getProvisioningProfile() {
return "\'match AppStore com.our_project.test.production'"
}
Обратите внимание, что все они используют один и тот же скрипт Deploy.groovy
и один и тот же файл Fastfile
, а именно:
def deployWith(bundle_id, configuration, provisioning_profile) {
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 configuration:$configuration'
}
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 bundle_id:$bundle_id configuration:$configuration provisioning_profile:$provisioning_profile'
}
}
}
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 bundle_id:$bundle_id"
}
}
}
stage('Cleanup') {
cleanWs notFailBuild: true
}
}
Fastfile_parameterized
fastlane_version "2.75.0"
default_platform :ios
lane :test do |options|
configuration = options[:configuration]
scan(
clean: true,
devices: ["iPhone X"],
workspace: "our_project.xcworkspace",
scheme: configuration,
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: configuration,
jenkins: true,
ignore: [array_of_docs_to_ignore]
)
end
lane :build do |options|
bundle_id = options[:bundle_id]
configuration = options[:configuration]
provisioning_profile = options[:provisioning_profile]
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: bundle_id,
force: true)
version = get_version_number(
xcodeproj: "our_project.xcodeproj",
target: "production_target"
)
build_number = latest_testflight_build_number(
version: version,
app_identifier: bundle_id,
initial_build_number: 0
)
increment_build_number({ build_number: build_number + 1 })
settings_to_override = {
:BUNDLE_IDENTIFIER => bundle_id,
:PROVISIONING_PROFILE_SPECIFIER => provisioning_profile,
:DEVELOPMENT_TEAM => "team_id"
}
export_options = {
iCloudContainerEnvironment: "Production",
provisioningProfiles: { bundle_id => provisioning_profile }
}
gym(
clean: true,
scheme: configuration,
configuration: 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 |options|
bundle_id = options[:bundle_id]
pilot(
ipa: "./build/WorkableApp.ipa",
skip_submission: true,
skip_waiting_for_build_processing: true,
app_identifier: bundle_id
)
end
Мы достигли нашей изначальной цели
Как я упоминал в предыдущем разделе, нашей изначальной целью было реализовать автоматическую загрузку билдов по нажатию одной кнопки в Testflight:
a) для разных веток под разные фичи
b) для нескольких конфигураций, соответствующих разным средам
Нам удалось достичь вышеупомянутое с помощью разных задач в Jenkins, которые могут быть настроены под разные ветки. Каждая задача соответствует своей конкретной конфигурации Xcode и, в добавку, своей конкретной среде. Все они используют одни и те же 2 основных файла (Deploy.groovy
и Fastlane
) и разные исходные файлы скриптов (Stg.groovy
и TestProduction.groovy
).
Конечно, вы можете распространить их сразу на несколько сред с помощью простого скрипта величиной всего в несколько строчек кода!
Спасибо, за ваше внимание ?, я надеюсь, что эта серия оказалась для вас полезной ?!
Особая благодарность Павлосу-Петросу Турнарису и Джорджу Цифрикасу за их ценные отзывы.
Twitter: @elenipapanikolo
Узнать подробнее о курсах: