在 iOS 開發的世界,有個非常有趣、但也非常痛苦的地方,就是 iOS 的開發者需要的基本知識非常多。Cocoa framework 本身就涵蓋了前端的 UI 邏輯,與資料庫等等的後端邏輯,既要注意頁面跟頁面之間狀態的處理,也要小心記憶體的運用,有時候還要學貝茲曲線跟 3D 轉場。雖然每一樣都不可能像各領域的專家一樣精通,但也算是工程師裡武器相當多的種族了。今天,想和大家分享身為 iOS 工程師,你可能還可以多學的技術:Continuous Delivery!身為一個工程師,你一定或多或少聽說過 Continuous Intgration 跟 Continuous Delivery (CICD);但是實際生活中,除非是跟一個團隊一起開發,不然應該很少有機會碰到 CICD 的概念。
所謂的 CI,就是在開發的過程中,我們需要隨時隨地確保 code 主幹處在可以一個發佈的狀態。也就是說,不能因為正在開發一個新功能,我們的主幹程式就無法運作或是打包新版本。而 CD指的,是我們希望在開發的任何一個階段,都能夠自動化打包出版本給需要的人使用。誰會是需要的人?在開發的過程中,工程團隊想要手動測試 App 時,就會需要一個 build 來測試;而在開發完畢後,UI 測試人員也會需要一個 build 來做測試。最後,在 App 要上線時,理所當然也會需要一個 build 來送到 iTunesConnect 上以供審核。
所以在系列作拖搞將近三個月後,今天我們要來介紹一個非常實用的技巧:如何做出一個專供 Apple 生態系使用的 CD 系統,讓你可以每天都提早五分鐘下班 (不算很吸引)。
如果還沒看過系列作的前面幾集,歡迎參考我的部落格,這個系列記載了教科書沒有的現實生活開發技術。
簡介
我們會以下面的步驟來說明如何建立一個好用的 CD 系統,你隨時都可以直接跳到某個章節開始閱讀。
- 在這個 CD 系統中,我們選擇某種設計方式,而不選另一種的理由
- 任務簡介與基本的 Project 設定
- 手動化你的 code signing
- 用 Bundler 管理你的系統工具
- 如何設定 fastlane
- 如何設定 Jenkins
開始之前,不是必須,但最好先具備:
- fastlane 的基本知識
- iOS/ macOS code signing 的手動管理經驗
- Jenkins 的基本知識
我們開始吧!
不選某種設計方式的理由
在這個世界上,有成千上萬的公司,運行著成千上萬的工作流程,我們不可能設計出一套系統,同時通用到所有人的工作流程上。所以身為一個系統的建置者,你的任務就是要設計出一個系統,讓它能夠完美地整合進去公司或你個人既有的工作流程中。在這篇文章中,因為篇幅有限,我們不可能列出所有可能的設計方式,但是我們會讓你知道,我們每一個選擇的原因。這樣你在實作的時候,就能夠知道那些東西適合你,那些不適合。
為甚麼選擇 Fastlane 和 Jenkins
Fastlane 是一套熱門的 CI 軟體,它提供了許多好用的工具,還有直觀的腳本檔,讓你可以不用碰觸到很多系統的細節面,也能夠做到各種客制流程,而且它同時支援 iOS/ MacOS 和 Android 平台。大多數的設定你都可以透過 commandline 直接跟 xcode 互動做到;但相信我,你不會想要自己寫那些 script 的。目前有些線上的 CI 工具,像是 bitrise,讓你可以用點按的就能夠設定好 CI,但如果你有預算上的限制,並且希望任務也能夠在自己的電腦上運作,那 fastlane 就會是你的最佳選擇。
Jenkins 則是一個有網頁 UI 的自動化排程工具,讓你可以利用 GUI 設定工作內容,像是設定 project 參數等;也可以做到執行排程,像是每日定時任務等。因為有大量 plugin 的關係,它跟很多既有平台的整合也都不錯。其它的選擇也有像是雲端的 CI 系統如 BuddyBuild、CircleCI 等等可以使用,好處是設定更加簡單了,但相對地能客制的部份就比較少,並且因為機器不在我們手邊,遇到比較麻煩的問題也就只能求助 support。對 Jenkins 來說,你除了可以買一台 Mac mini 來架站之外,也可以架在自己的電腦上方便測試,所以這篇我們主要還是以 Jenkins 為主。
為甚麼不選擇 match 或 sigh/cert
你大概已經知道 (或者已經很熟悉),fastlane 提供了許多好用的工具,像是 match 或者是 certsigh,來幫我們管理 code signing,但我們這篇並不會使用這些工具,為甚麼呢?自動化的工具的確很方便,但對於大公司或是外包人員來說,首先,並不是所有開發者都有 developer portal 的權限的,很多開發者因為公司政策的關係,只能取得開發者的權限,要修改或是下載 certificate/ provisioning profile 都是不行的。這時候 matchsighcert 就完全派不上用場了。除此之外,大家應該都有跟 xcode 裡的 code signing 奮戰的經驗吧?我們希望在 code signing 這邊,越少黑盒子,流程越透明,未來遇到問題的時候,就越容易進入狀況並且找到解決方法。
我們的任務
假設我們現在正在開發一個App 叫 Brewer,它每天晚上都會準時釀啤酒。你可以在 github 上面找到它: GitHub – koromiko/Brewer: I brew beer everyday
我們的工作流程是:
- 每天一早,工程師要拿到一個最新的 build,來幫同事做簡易的手動測試 (Staging build)
- 每星期的一開始,也會產生一個 build,讓公司裡的 QA 人員做完整的測試 (Production build)
所以我們的 CD 系統,要能夠
- 針對不同任務切換不同的 build configuration (staging/ production)
- 針對不同任務使用不同的 code signing
- 把打包好的 build 送到發佈系統 (Crashlytics, testFlight, etc)
- 固定時間啟動任務
所以我們會這樣設計我們的 CD 系統:
在圖的最上面,是我們原本的工作流程,即是一般常見的開發、測試、發佈流程。從開發到測試大概需要一周。而中間 Delivery 那一列,就是我們的 CD 策略。在開發階段,每一天都會自動釋出一個 nightly build 供開發者做測試;在一週的結尾,則會釋出一個版本供 QA 人員做測試。最底下的 Env,指我們希望釋出的版本,是運行在怎樣的環境。對開發者來說,我們會傾向用運行在 Staging 環境的版本做測試,這樣可以一直開假帳號、亂買東西也不用擔心弄壞 production。而對 QA 人員來說,他們就會希望能夠使用跟使用者一樣的環境來做測試,所以會在 Production 環境底下建置。最後,在 QA 過後就是 Release 階段,因為 Release 發生的頻率相對不高,並且很有可能沒有固定的週期,所以我們會將它設定成手動發佈。在這篇文章中我們只會介紹前兩種設定方法,一旦知道要怎樣設定之後,後面的設定應該都可以得心應手。
接著我們要來了解一下,怎樣透過 Xcode 的 build configuration 做到環境的切換,而不用更改 code。詳細的內容可以參考這篇文章 Managing different environments in your Swift project with ease 的第 3 和 4 點。簡單來說,我們會透過 project 的 build configuration 來設定不同的 Flag,做到切換 Staging 版本跟 Production 的效果,我們的 build configuration 設定如下:
針對不同的設定,我們需要設定不一樣的 AppID,這樣的好處是我們可以在測試裝置上同時安裝不同的版本也不用擔心搞混,就發佈系統來說,也比較好管理。
接著我們要來針對不同的 Configuration 做不同的 code signing。
讓 certificate 跟 provisioning profile 回歸你的控制
寫 iOS/ macOS 的開發者,應該已經非常熟悉這個畫面了吧!沒錯,我們會第一時間把自動管理憑證關掉,不然你可能會進入自動更新的無間地獄。把自動化關掉之後,就要來準備好所有 code signing 所需要的檔案了,針對我們上面的環境設定,我們需要準備這些檔案:
- Provisioning profile: 針對不同 AppID 的 Ad Hoc distribution profile
- Certificate: Ad Hoc distribution certificate,並且匯出成 .p12
要注意的是,在 developer portal 下載的憑證檔是利用 DEM 加密的 .cer 檔,但 DEM 的檔案裡並沒有包括私鑰;也就是說如果你換電腦了,這張憑證就會因為找不到你的私鑰而失效。所以我們必需要把 DEM 輸出成包含私鑰的 P12 檔,輸出的方法可以參考這篇 stackoverflow。
最後我們把上述的檔案都下載下來,存到另外開的 codesigning 資料夾:
這個資料夾不能跟你的 code 放在一起,它可以放在 Jenkins 主機上,也可以做成一個 git repository 或者一個雲端共享資料夾,再利用 fastlane 將這些檔案下載下來使用,好處是不用每次移機時也要手動移動這些檔案。放遠端的做法基本上是安全的,如果你覺得放遠端安全性是有疑慮的,可以參考 match – fastlane docs。
用 Bundler 管理你的系統工具
如果你是 iOS/macOS 開發者,你應該已經很熟悉用 homebrew 來安裝像是 Cocoapods 或是 Carthage 等的套件,homebrew 讓你可以輕易地管理你的系統程式,讓你自己電腦裡面的環境保持一致。但是我們希望建置環境能夠越獨立越好,這樣才不會因為換了電腦建置就出錯,或者是還要再重覆套件安裝一樣的動作。也就是說我們希望可以製作出一個獨立的環境,可以完整地複製到不同電腦上,這時候我們就會需要 Bundler。Bundler 是一個 Ruby 的環境管理系統,它可以幫你設定出一個能夠被複製的虛擬環境,之後不管換到任何電腦,對在這個虛擬環境中的程式來說,執行的環境都是一模一樣的。
我們會透過 bundler 管理 fastlane 跟 Cocoapods 的安裝,所以請在 project folder 裡面新增一個 Gemfile 檔案,內容如下:
source "https://rubygems.org"
gem "fastlane"
gem "cocoapods"
Gemfile 是 Ruby 套件管理程式 gem 的設定檔,裡面描述了你這個 project 所需要用到的程式。Bundler 會透過 gem 來安裝程式,並且幫你設定好虛擬環境。
接著,我們就可以來安裝這兩隻程式:
bundle install --path vendor/bundler
--path vendor/bundler
表示我們希望把程式安裝在當前的目錄底下,而不是安裝到系統檔案夾如 /usrlocalbin 或 usrbin 裡面,這樣你的 project 就不會跟你的系統有任何掛勾了。
請記得將 vendor
這個資料夾加入 .gitignore 之中。
設定完 bundler 之後,未來我們想要在虛擬環境下執行程式的話,就需要下 bundler exec 的前綴,比方說,fastlane init 是初始化一個 fastlane 的 project,現在我們要改成 bundler exec fastlane init,表示我們要初始化一個 fastlane project,並且在剛剛設定的虛擬環境下執行這個指令。
來 fastlane 一下
fastlane docs 是一個好用的工具,幫助你自動化完成許多繁複的工作。就算你是一人團隊,你也可以透過 fastlane 自動化所有測試、發佈跟憑證管理等等工作。
我們打算利用 fastlane,加入兩個任務,一個是 staging 環境的發佈,一個是 production 環境的發佈。這兩種設定都包含以下固定的工作:
- 從檔案匯入 certificate 與 provisioning profile
- build app 並且上傳到 Crashlytics
兩種設定差別只在於 build configuration 的不同而已,所以我們的 fastlane 要能依據我們所選取的任務,找到對應的 certificate 跟 provisioning profile,用它們來建置 App 並發佈。
在這篇文章中,將以 swift 設定檔來當範例。選 swift 來撰寫設定檔,目前 fastlane swift 是基於 ruby 的 wrapping,並不是真的原生 swift,所以 fastlane plugin 是不被支援的,有需要用 plugin 的可能要考慮一下。另外,Swift 也無法 catch 任何 ruby 發出來的例外,如果有需要完整的例外處理,就還是請用 ruby 原生的 fastlane 吧。
安裝方法請參考 Setup – fastlane docs。
安裝完後,我們要先初始化我們的 fastlane project:
bundler exec fastlane init swift
接著,你就可以在 fastlane/Fastfile.swift
找到你的 fastlane 設定檔。在設定檔裡面,我們可以找到 Fastfile 這個 class,這個 class 裡面所定義的 method,只要是以 Lane 結尾的 method,都會被認定為是一個 “lane”,可以透過 fastlane 來執行。所以我們先新增兩個 lane,一個叫做 developerRelease
,另外一個叫做 qaRelease
:
class Fastfile: LaneFile {
func developerReleaseLane() {
desc("Create a developer release")
package(config: Staging())
crashlytics
}
func qaReleaseLane() {
desc("Create a qa release")
package(config: Production())
crashlytics
}
}
未來我們想要打包 release 的時候,都可以執行下面這樣的指令:
bundle exec fastlane developerRelease
# or
bundle exec fastlane qaRelease
想要打包給 developer 就執行上面的指令,想要打包給 QA 人員,就執行下面的指令。我們可以看到在這兩個 lane 裡面都會呼叫一個 method:package
,並且根據不同的 lane 給予不同的 config 參數,這個 package 的接口如下:
func package(config: Configuration) {
}
它的參數,其實是一個叫 Configuration 的 protocol:
protocol Configuration {
/// file name of the certificate
var certificate: String { get }
/// file name of the provisioning profile
var provisioningProfile: String { get }
/// configuration name in xcode project
var buildConfiguration: String { get }
/// the app id for this configuration
var appIdentifier: String { get }
/// export methods, such as "ad-doc" or "appstore"
var exportMethod: String { get }
}
所有的 configuration 都要 conform 這個 protocol,才能被傳入 package 裡面做打包。在這個範例裡面,我們設定了兩種 configuration:
struct Staging: Configuration {
var certificate = "ios_distribution"
var provisioningProfile = "Brewer_Staging"
var buildConfiguration = "Staging"
var appIdentifier = "works.sth.brewer.staging"
var exportMethod = "ad-hoc"
}
struct Production: Configuration {
var certificate = "ios_distribution"
var provisioningProfile = "Brewer_Production"
var buildConfiguration = "Production"
var appIdentifier = "works.sth.brewer.production"
var exportMethod = "ad-hoc"
}
根據實際的狀況,在這兩個 struct 裡面實作 protocol,這樣可以確保我們的 package 有一致的接口,同時又能符合各種不同的狀況。
接著,我們要開始來實作 package 了。還記得 package 的任務嗎?首先,它需要從檔案引入 certificate 跟 provisioning profile。關於 certificate,我們會使用 importCertificate 這個 action,來讀取系統中的 .p12 檔:
importCertificate(
keychainName: environmentVariable(get: "KEYCHAIN_NAME"),
keychainPassword: environmentVariable(get: "KEYCHAIN_PASSWORD"),
certificatePath: "\(ProjectSetting.codeSigningPath)/\(config.certificate).p12",
certificatePassword: ProjectSetting.certificatePassword
)
KeychainName 這個參數就填入你 keyChain 的名稱,keyChainPassword 通常就是你系統的密碼。
Fastlane 的設定檔通常都會跟著原始檔一起被 commit 到 git 裡,所以把密碼放在 code 裡面不是一個好主意;我們會用系統變數來取代固定的字串。在系統上,我們會存入系統變數:
export KEYCHAIN_NAME="KEYCHAIN_NAME";
export KEYCHAIN_PASSWORD="YOUR_PASSWORD";
而在 fastlane 裡面,就利用 environmentVariable(get:)
來讀取系統變數。這樣我們就可以避免把機密字串 commit 到 git 上,大大增加系統的安全性。 接著我們透過 certificatePath 指定 certificate .p12 檔的位置和檔案的密碼。在這裡,ProjectSetting 是一個 enum,存放著 project 相關的變數,我們定義了 codesigningPath 與 certificatePassword,代表所有 code signing 的相關檔案的位置跟密碼,而這些資訊一樣是透過系統變數給予的。
enum ProjectSetting {
static let codeSigningPath = environmentVariable(get: "CODESIGNING_PATH")
static let certificatePassword = environmentVariable(get: "CERTIFICATE_PASSWORD")
}
以上 certificate 的引入就設定完畢了!接下來是 provisioning profile 的設定,我們會利用 updateProjectProvisioning,根據不同的設定檔,來更新 project 的 provisioning profile:
updateProjectProvisioning(
xcodeproj: ProjectSetting.project,
profile: "\(ProjectSetting.codeSigningPath)/\(config.provisioningProfile).mobileprovision",
targetFilter: "^\(ProjectSetting.target)$",
buildConfiguration: config.buildConfiguration
)
config
就是我們一開始代入的 package 參數,是一個 conforms Configuration protocol 的 struct 或 class。在 profile
這個參數中,我們利用 codeSigningPath 跟 config.provisioningProfile 來指定 provisioning profile 的位置,我們同時也在 buildConfiguration
這個參數裡指定我們要修改的 build configuration,這樣 updateProjectProvisioning 就會把指定 provisioning profile 寫入指定 configuration 裡面。 注意:這個 action 會直接修改你的 .xcodeproj,對 Jenkis 來說,所有修改都是不會被 commit 的,所以在 CD 系統中這些修改都是沒有問題的;但如果你希望在你的 working directory 裡面執行 fastlane,就要特別留心 .xcodeproj 的修改問題。
設定完了 code signing 之後,我們就可以開始來 build 跟 export 了:
buildApp(
workspace: ProjectSetting.workspace,
scheme: ProjectSetting.scheme,
clean: true,
outputDirectory: "./",
outputName: "\(ProjectSetting.productName).ipa",
configuration: config.buildConfiguration,
silent: true,
exportMethod: config.exportMethod,
exportOptions: [
"signingStyle": "manual",
"provisioningProfiles": [config.appIdentifier: config.provisioningProfile] ],
sdk: ProjectSetting.sdk
)
buildApp 這個 action,能夠幫你 build project,並且輸出 ipa,供上傳或送審。大部份的參數都非常直觀,像 configuration 這裡,可以看出我們行用 config 物件來指定要使用的 build configuration。另外,在 exportOptions 這個參數:
exportOptions: [
"signingStyle": "manual",
"provisioningProfiles": [config.appIdentifier: config.provisioningProfile] ]
這個參數的型態是 dictionary,“signingStyle” 是指你希望使用的 code signing 方法,這邊必須要設定成 “manual”。再來是 “provisioningProfiles” 這個參數指定 app id 跟 provisioning profile 的 mapping,也就是哪一個 app id 該使用哪一個 provisioning profile 去 sign。它也是一個 dictionary,key 是 app id,而 value 就是 provisioning profile 的檔案名稱。在這邊我們使用 config.appIdentifier: config.provisioningProfile,來讓這個 mapping 可以由 config 物件決定。
以上我們就完成了從 code signing 到 build 跟 export 的設定了!
現在你可以透過
bundle exec fastlane qaRelease
或是
bundle exec fastlane developerRelease
來輸出針對不同環境建置的 App 了!
以上是利用 fastlane 打包的部份;但別忘了,我們還有定時打包的功能。這個任務,就要交給 Jenkins 來做了。
CICD 管理系統 – Jenkins
Jenkins 是一個 CI/ CD 的管理系統,幫助你自動並定期執行各種不同的任務。它有著憨厚的介面,讓你不用寫一堆 script 就能夠設定好自動化任務。如果你是一個大團隊,建議另外弄一台 mac mini 當做 Jenkins 的主機,所有發佈的任務都在那台主機上運行,這樣可以確保每次出去的版本都不會因為環境不同而出現不能預計的問題。如果你是一人團隊,那你就把 Jenkins 裝在自己的電腦,或者直接使用 fastlane 做打包就好,沒有 24 小時運行的主機的話,設定定期任務是沒有太大意義的。
Jenkins 在 mac 上安裝非常容易,可以直接從這裡透過 dmg 檔安裝。另外,我們會需要 git 的 plugin,讓 Jenkins 能夠支援 git 的操作。dmg 安裝會設定一個 mac 的 user: jenkins,它不能登入〉不能操作 commandline、不佔太多空間,純粹就是讓 Jenkins server 有獨立的權限控管。
安裝完之後,我們先來看看,Jenkins 在我們的 CD 中,扮演怎樣的角色:
從上圖可以看得出來,Jenkins 扮演著管家的角色,時間一到,它就會負責去 repository 抓最新的原始碼,抓下來之後,開始執行指定的任務,任務內容包括:
- 設定 environment variables
- 透過 bundler 安裝 dependency
- 執行 fastlane 的任務
了解了 Jenkins 的工作之後,我們就可以開始來新增我們的定期任務。先從 nightly build 開始,我們先新增一個 Jenkins freestyle project,並點擊 Configure
進到設定頁面。設定頁面裡有幾個重點我們需要注意:
第一個是 Source Code Management (SCM),在這邊我們會設定 project 的 repository URL,還有指定要 fetch 的 branch,如下圖:
你可以在 Repository URL 設定 github 或其它系統的 repository url。Credentials 則可以讓你設定 repository 的帳號跟密碼,這樣 Jenkins 才能通過授權下載原始碼 (如果是 private repository)。在 Branches to build,你可以設定目標 branch,以定期任務來說,通常都會設定成你的 default branch。
再往下,我們可以看到 Builder Trigger 區塊,這個區塊是要設定這個任務的觸發點,也就是要設定自動化啟動任務的條件。如果這邊都沒有設定任何 trigger,那任務就是手動觸發。我們的 trigger 設定如下:
我們啟動了一個 Poll SCM,意思是讓 Jenkins 定期向 source code repository 查看是否有更新資料,如果有的話,就啟動我們現在這個 task。設定是一串空白隔開的火星文:
H 0 * * 0-4
這是甚麼意思?讓我們先看一下,關於 Poll SCM 的說明:
This field follows the syntax of cron (with minor differences). Specifically, each line consists of 5 fields separated by TAB or whitespace:
MINUTE HOUR DOM MONTH DOW
MINUTE Minutes within the hour (0–59)
HOUR The hour of the day (0–23)
DOM The day of the month (1–31)
MONTH The month (1–12)
DOW The day of the week (0–7) where 0 and 7 are Sunday.
從說明可以看得出來,這一串字串總共有五個部份,由左到右依序是:
- 分鐘
- 小時
- 日期
- 月份
- 星期幾
我們可以直接指定數字,或者以 0–59
這樣的字串代表有效區間。使用 * 的話就表示所有可能的數值都會觸發,代表任意一個有效數字,這通常會被用在分鐘的設定上,目地是要把系統上每一個 task 都盡量分散在一小時內運行。所以回到我們剛剛的 schdule setting:
H 0 * * 0-4
表示我們希望任務可以在週日到週四、一年中所有的月份、每天的 0 點不指定分執行,這就是 nightly build 的 schedule。
最後,我們往下可以看到 Build 的區塊,在這裡我們就可以設定當時間一到,要讓 Jenkis 執行的程式。內容如下:
export LC_ALL=en_US.UTF-8;
export LANG=en_US.UTF-8;
export CODESIGNING_PATH="/path/to/cert";
export CERTIFICATE_PASSWORD="xxx";
export KEYCHAIN_NAME="XXXXXXXX";
export KEYCHAIN_PASSWORD="xxxxxxxxxxxxxx"
bundle install --path vendor/bundler
bundle exec fastlane developerRelease
前面幾行都是在設定 environment 變數,像是 LCALL 跟 LANG 主要是要確保 fastlane 能運行在正確的 locale 底下,而 KEYCHAINNAME、KEYCHAINPASSWORD 與 CERTIFICATEPASSWORD 就是我們在 fastlane 設定教學時使用的環境變數。CERT_PATH 是你放置在 Jenkins 主機上的 code signing 目錄的位置。這裡我們會建議使用絕對路徑,從我的經驗所得,有時候設定相對路徑在某些 action 上面是無法運作的。 最後兩行則是安裝 dependency 跟真正執行 fastlane 來 build 你的 project。這邊我們可以看得出來,nightly build task 會執行 developerRelease 這個 lane。
到這邊,我們終於把我們的 nightly build 建立起來了!你可以在 Jenkins 的 project 頁面點擊 build now 來看看你的任務是否有運行成功。
在底下的 Build history,你可以看到每一次 build 的時間與成功或失敗的記錄,點擊 build number 可以看到更詳細的狀態,也可以看到 console 的輸出。這些功能都可以讓你很方便地了解 build 的狀況,而且在發生問題的時候,更快地找到問題並解決。
為甚麼任務一直失敗?
只要是跟系統相關的問題,通常狀況都非常多且複雜,雖然大多的錯誤都可以在 google 上找到解答,但是有時候你得到的錯誤訊息並不是有用的或是相關的。以下有幾點小技巧,讓你在遇到問題時可以更快獲得解答。
解鎖 KeyChain
不管你想把 certificate 存在那一個 keychain 裡,都要記得解鎖它噢。
Keychain 產生時,既要避免動到預設 login keychain,還要減少 certificate 被從 keychain copy 出來的風險。想要了解更進階的 keychain 產生技巧,可以參考小弟的 github project,裡面的 fastfile 是自動產生 keychain,並且在使用過後刪除 keychain 的範例。
先確保能在 Jenkins 主機能夠 build & export
fastlane 其實是把 xcode commandline tool 打包成人類比較好理解的語言,所以通常如果不能 build,有很大的機會是因為 xcode 本身就 build 不過。如果 Jenkins 的任務不過,請先不要急著在 Jenkins 的介面上 debug;而應先在 Jenkins 主機上,透過 commandline build 你的 project,從而得到更多有用的資訊。
xcodebuild clean archive -workspace -scheme
你可以加上 —verbose
參數,讓輸出的訊息更完整。
fastlane 透過 parse 你的 build setting 來決定要怎樣執行任務,所以如果你想確定 build 設定是不是正確,可以使用以下指令來檢查:
xcodebuild -showBuildSettings -workspace -scheme -configuration
當你 achieve 完之後,也可以測試一下是否能夠 export 成功:
xcodebuild -exportArchive \
-exportFormat ipa \
-archivePath " \
-exportPath \
-exportProvisioningProfile ""
CD系統的設計原則
以 iOS/ macOS 的 CD 系統來說,減少 bug 並且提早下班 (或務實一點,準時下班),通常有幾個要點:
- 減少系統相依性:盡量不要依賴某個系統的程式,像是需要 ruby 的某個版本,或者呼叫某個在 usrlocal/ bin 的程式。
- 任務跟任務之間不能有相依性:每次的任務都是全新的開始,不能有下次任務用到上次任務產出的資料的情況發生。最好的做法就是執行完任務之後,就把中間產物刪除。
- 確保 CD 系統能夠被很快地複製:就算轉換到不同電腦上,應該也要能夠在裝完 Jenkins 後就直接被執行,像是把跟 project 無關的設定,如 path 跟 password,都移到 environment variables,這樣任務出錯時,我們就能夠把專注力放在我們的程式,而不是某個主機的設定上。
最後,你也可以參考 fastlane 的 troubleshooting,這裡面有許多有用的資訊:Troubleshooting – fastlane docs。 可以看到,大多數的問題,都是萬惡的 code signing 引起的, iOS 工程師通常會在一天的一早開始處理不能 build 的問題,並且在下班前五分鐘發現原來是用到了舊的 certificate,然後流著淚在公司加班。
總結
雖然 iOS/ macOS 工程師,有很大部份都是一人團隊,所以所謂的 delivery 也就是 archive 後,直接點 Crashlytics 的 distribute 就送出了。但是就算是一人團隊,透過一個自動化系統,就能夠把 build 任務分攤到別的主機,減少 build 時切換環境發生的錯誤,還是非常值得投資的。更不用說在多人團隊,跟 workflow 結合的 delivery system,能夠帶來的好處就更多了。如果你不喜歡寫 script 或跟系統打交道,目前 Apple 生態系的 CD 系統其實也非常蓬勃,包括個人覺得相當好用的 bitrise、很多人都在使用的 circleCI、還有被 Apple 買走的 Buddybuild,都是非常可以考慮的選項。這篇文章主要著眼在了解如何透過 fastlane 跟 jenkins 建置簡單的 CD 系統,如果你都了解的話,一定可以設計出一套符合你目前團隊工作流程的 CD 系統的!