CICD

Continuous Integration & Continuous Delivery

Kaylla Wen
Dev Chill

--

Fastlane

可以透過 fastlane 自動化所有測試、發佈跟憑證管理等等工作

以過去的架設 CICD 的經驗,一般大致可分為3個任務(視專案情況而定)

一個是 adhoc 環境的發佈(內部環境:給測試)

一個是 production 環境的發佈(線上環境:給所有人)

一個是 enterprise 環境的發佈(內部環境:給公司內部人員)

一般開發者帳號可以發佈 adhoc/production 的應用程式。而 enterprise 則是企業版帳號,可以用來發佈自己公司內部的應用程式,像是簽到系統、報表查看等等僅限於公司內部人員使用的 app。兩者的差別是,所有人都可以申請註冊開發者帳號並且付費,即可上架你自己的 app;而企業版帳號則需要公司的相關證明文件(公司名稱、電話、鄧白氏 … etc)才能申請通過企業版帳號。

這三種設定都包含以下固定的工作:

  • import certificate and provisioning profile
  • build app

其他差別只在於 build configuration 的不同以及上傳至的目的地有所不同,所以我們的 fastlane 要能依據我們所選取的任務,找到對應的 certificate 跟 provisioning profile,用它們來建置 app 並發佈。

Fastlane 起來

Setup — fastlane doc 官方文檔寫了詳細的步驟安裝 fastlane

首先身為開發者,我相信已經安裝好 gem 了(讓我們跳過這一步)

安裝 fastlane (使用 gem)

sudo gem install fastlane -NV

初始化 fastlane

cd 至你的專案資料夾然後執行

fastlane init
fastlane init

專案大都自己設定,所以這邊選擇 Maunal setup。接下來就是輸入 enter 繼續。完成後,專案資料夾底下會產生 GemfileGemfile.lock 以及fastlane/ 資料夾,資料夾底下有 AppfileFastfile 。為了方便管理,我會將 GemfileGemfile.lock 移到 fastlane/ 資料夾底下,與 Appfile 同層。

fastlane init finished

Gemfile

Gemfile 是 Ruby 套件管理程式 gem 的設定檔,描述了 project 所需要用到的程式,透過它來管理 fastlane 跟 cocoapods 的安裝,明確定義使用的 fastlane版本及其依賴性,並且加速使用 fastlane。

source “https://rubygems.org"gem "fastlane"
gem "cocoapods"

Appfile

Appfile 用來存放 app 相關的資訊,包含 identifier、apple id 及 team id …etc

# For more information about the Appfile, see:
# https://docs.fastlane.tools/advanced/#appfile
app_identifier "com.xxx.xxx" # The bundle identifier of your app
apple_id "xxx@xxx.com" # Your Apple email address
team_id "XXXXXXXXXX" # Developer Portal Team ID

Fastfile

fastfile 包含發布 app 所需的腳本,使用 lane 來撰寫腳本。以下是專案內使用到的其中一個 lane。

desc "Build AdHoc"
lane :build_adhoc do |options|
match(type: "adhoc", readonly: true)
cocoapods(
repo_update:true
)
increment_build_number_in_xcodeproj(
build_number:options[:build_number]
)
build_app(
export_method: "ad-hoc",
output_name: options[:OUTPUT_FILE_NAME],
configuration: "Adhoc",
output_directory: options[:OUTPUT_DIR],
scheme: "xxxxx",
workspace: "xxxxx.xcworkspace",
clean: true,
export_options: {
provisioningProfiles: {
"com.xxxx.xxxx" => "match AdHoc com.xxxx.xxxx",
}
})
end
  1. match:下載 adhoc 憑證,build 之前安裝最新憑證,確保憑證包含所有測試裝置。註:此專案使用 match 產生憑證的
  2. cocoapods:執行 pod install --repo-update ,安裝套件。
  3. increment_build_number:根據傳入的參數,設定 build 號,區別每一個 .ipa 檔案。
  4. build_app:建構 .ipa 及 dsym 檔案。

這邊說明 build_app使用到的參數:

export_method:Method used to export the archive. Valid values are: app-store, ad-hoc, package, enterprise, development and developer-id .

output_name:The name of the resulting ipa file.

configuration:The configuration to use when building the app. Defaults to Release .

output_directory:The directory in which the ipa file should be stored in.

scheme:The project’s scheme. Make sure it’s marked as Shared .

workspace:Path to the workspace file.

export_options:Path to an export options plist or a hash with export options. Use xcodebuild -help to print the full set of available options.

腳本撰寫完畢之後,你可以在 terminal 執行

fastlane build_adhoc

執行完畢後,會產生 .ipa 及 dsym 在你設定的 output_directory 。

以上就是基本使用 fastlane 建構 .ipa 的過程。

開發者憑證

fastlane 還有個好用的地方:統一管理開發者憑證。

iOS 開發者一定都有遇到憑證的各種問題,像是新人到職,平常也沒碰觸的憑證,要去找出來 export 給他;或者多人協助開發的時候,互踢憑證等等的問題。而你也已經使用了 fastlane 來完成 CICD 的工作,如果 build server 也可以自己更新憑證,那就更棒了!

關於如何設定開發者憑證,因為篇幅較多,所以我另外整理一篇。

請參考 使用 Fastlane 管理開發憑證

當開發憑證設定好之後,所有人都可以使用 fastlane 來下載開發憑證進行開發了。

在 fastfile 中的 lane 可以這樣寫

Refresh 更新憑證 (像是註冊新裝置時)

# =======================================
# ======= Refresh Certificates ==========
# =======================================
desc "Refresh Certificates And Profiles"
lane :refresh_profiles do
match(type: "development", force: true)
match(type: "adhoc", force: true)
match(type: "appstore")
end

Download 下載憑證

# =======================================
# ======= Download Certificates =========
# =======================================
desc "Download Certificates And Profiles"
lane :download_certificates do
match(type: "development", readonly: true)
match(type: "adhoc", readonly: true)
match(type: "appstore", readonly: true)
end

其他 fastlane 還有提供很多功能,大家可以玩一玩官網的文檔,依照自己的需求去增加更多的 lane。像是我自己多新增了幾個 lane

desc "Change marketing_version"
lane :new_version do |options|
increment_version_number_in_xcodeproj(
version_number: options[:VERSION] # Set a specific version number
)
end
desc "Unit test"
lane :tests do
run_tests(scheme: "xxxx",
device: "iPhone 11",
xcargs: "-UseModernBuildSystem=YES",
prelaunch_simulator: true,
clean: true
)
end

Jenkins

Jenkins Pipeline(或稱 “Pipeline”)是一個軟體,將持續性整合發佈實現集成到 Jenkins 中。Jenkins Pipeline 提供了一套可擴展的工具,它將「簡單到複雜」的流程用「程式碼」的方式實現持續性整合發佈。Jenkins Pipeline 的定義通常寫在 Jenkinsfile中,這份文件可被放入專案的版本控制。

Jenkinsfile 起來

在專案中新增名為 Jenkinsfile 的檔案,並修改 Jenkinsfilesh 指令,其中 sh 的指令要符合專案路徑。

Jenkinsfile 支持 Java Node.js/JavaScript Ruby Python PHP 語言。

我們用 Ruby 來執行 Jenkinsfile 吧。

// 範例程式碼pipeline {
agent { docker 'ruby' }
stages {
stage('build') {
steps {
sh 'ruby --version'
}
}
}
}

修改 sh 指令來符合我們專案。

pipeline {
agent { docker 'ruby' }
stages {
// update submodule
stage "Git submodule update"
sh 'git submodule sync'
sh 'git submodule update --init --recursive'
// load build config
stage "Build Env Setup"
load "ci/config.groovy"
// build
if ((env.GIT_BRANCH != 'master') {
stage "Build"
sh "sh ci/build.sh -t ${env.BUILD_TYPE} -d ${env.OUTPUT_DIR} -n ${env.IPA_NAME}"
stage "Deploy"
sh "sh ci/deploy.sh"
}
}
}

其中 config.groovybuild.shdeploy.sh 我放在自己建立 ci/ 資料夾底下統一管理。

Jenkinsfile 裡面的流程大致是這樣的:

  1. 如果專案有用到 submodule 那麼記得先 git submodule update 一下;
  2. 再來就是設定環境變數;
  3. 然後我們 git 採用 branch 區分版本,所以 merge 回 branch 觸發腳本,過濾 branch = master 的情況,就執行自動化打包的過程。

config.groovy

config.groovy設定環境變數。執行打包 schell 前,用來 export 環境變數,可以設定 BUILD_TYPE OUTPUT_DIR IPA_NAME …etc 任何你需要用到的變數。

《這個部分還在學習中,持續更新》

build.sh

build.sh 裡面可以做一些判斷,

  1. 像是:傳入的參數有沒有符合格式、有沒有指定 output dir、ipa 名稱之類的;
  2. 再執行我們在 fastfile 裡面寫好的 lane;
  3. 最後再把產出的 .ipa.dsym.zip 壓縮成 .tar 檔。

可以這樣寫:

echo "Build job\n"useage="-t <BUILD_TYPE> [-d <OUTPUT_DIR>] [-n <OUTPUT_FILE_NAME>]\n"
useage+="-t [enterprise|appstore|adhoc]\n"
useage+="-d \"the build dir\"\n"
useage+="-n \"file name of archive\"\n"
#设置超时
export FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT=120
############################
# parse args
#############################
parseArgs(){
case $1 in
"-t")
BUILD_TYPE=$2
;;
"-d")
OUTPUT_DIR=$2
;;
"-n")
OUTPUT_FILE_NAME=$2
;;
*)
echo "error: please exec like this:\n"
echo "${useage}"
exit 1
;;
esac
}
while (($#>0));
do
parseArgs $1 $2
shift 2
done
############################
# Compile
############################
VERSION_CODE=`git rev-list HEAD --count`
############################
# check args
#############################
if [ "${BUILD_TYPE}" == "" ]
then
echo "error: cannot find BUILD_TYPE\n"
echo "${useage}"
exit 1
fi
if [ "${OUTPUT_DIR}" == "" ]
then
OUTPUT_DIR="build"
fi
if [ "${OUTPUT_FILE_NAME}" == "" ]
then
GIT_COMMIT=`git rev-parse --short HEAD`
OUTPUT_FILE_NAME="${BUILD_TYPE}-${VERSION_CODE}-${GIT_COMMIT}.ipa"
fi
echo "=== BUILD_TYPE: ${BUILD_TYPE}"
echo "=== VERSION_CODE: ${VERSION_CODE}"
echo "=== OUTPUT_DIR: ${OUTPUT_DIR}"
echo "=== OUTPUT_FILE_NAME: ${OUTPUT_FILE_NAME}"
echo "rm ${OUTPUT_DIR}/* ..."
rm -rf ${OUTPUT_DIR}/*
fastlane build_$BUILD_TYPE VERSION_CODE:$VERSION_CODE OUTPUT_DIR:$OUTPUT_DIR OUTPUT_FILE_NAME:$OUTPUT_FILE_NAMEtarFile() {
# Archive .ipa and .dsym to tar
if tar -cvf $OUTPUT_DIR/${OUTPUT_FILE_NAME}.tar $OUTPUT_DIR/; then
echo "tar succeeded" >&2
else
echo "tar failed" >&2
exit 1
fi
}
if [ -f "${OUTPUT_DIR}/${OUTPUT_FILE_NAME}" ]
then
tarFile
else
echo "${OUTPUT_FILE_NAME} not found."
fi

deploy.sh

deploy.sh 這個部分就是用來上傳到管理 .ipa 的 server 啦。這個部分就看個人需求去撰寫內容啦。

可以這樣寫:

echo "Deploy job"# upload ipa to test
curl -X POST \ "http://xxx.xxx" \
-H 'cache-control: no-cache' \
-H 'content-type: multipart/form-data;' \
-F category="${BUILD_TYPE}" \
-F file=@"${OUTPUT_DIR}/${IPA_NAME}" \
-F description="${GIT_LOG}" \
-F userName=$UPLOAD_ID \
-F password=$UPLOAD_PW

目前這就是我專案裡使用 Fastlane + Jenkins 實現自動打包、自動發佈的過程。還在持續完善中,包括 Jenkins 的架設、Release 包的上傳、使用 fastlane 管理 certificates 跟 provision profiles。之後會慢慢地補上,如果內容太多,考慮再細分成不同的篇幅去詳細說明。

--

--

Kaylla Wen
Dev Chill

簡單的紀錄日常生活,以及在工程師開發上的遇到的事情。如果發現有問題,謝謝你告訴我一聲:)