很久沒發文,轉眼都一年了 XD 去年工作很忙,忙到健康出了點狀況,工作又突遭許多變動與意外(是不是要去改運一下…😭),實在無暇、也沒心情抽空整理文章。總之,這是躺在我的技術筆記堆裡許久的一篇文章,記錄了我用 Bitrise 搭建 React Native Apps (包含 Android 與 iOS)的 CI/CD(Continuous Integration/Continuous Delivery)的設定過程,最近有空來整理一下,讓它重見天日 XD
背景
由於審核機制、版本更新機制等先天因素,手機 App 的更新雖然不像 Web 這麼頻繁,但每次新增功能進 code 後,若是都要人工手動去跑單元測試、建置、發佈到 store 等繁複的工作也太煩人了,所以是該弄個自動化 CI/CD 流程來處理這些瑣事。
先前在工作上是使用公司自架的 Jenkins 搭配 HockeyApp 來處理 Apps 的 CI/CD 流程,當發 Pull Request 或是 merge 進 master branch 便會觸發(分別到 staging/production 等環境)、每日也會觸發進行 daily build,以便面對各種測試/發佈需求。
後來隨著 App 功能日漸複雜、產品線的增加,原本只靠一台舊款 Mac mini 搭建的 CI/CD 系統要應付這些快速成長的建置工作變得頗為吃力(光排隊等建置就飽了),於是便尋求其他的解決方案,而最終選擇的便是本文要介紹的 Bitrise。
App 是使用 React Native 進行開發的(沒有使用 expo),同時擁有 Android 與 iOS 版本(使用 CocoaPods 管理套件)。考慮到篇幅,本文並不會講到 React Native 本身的設定。
備註:我是個 Web 前端工程師,過去常使用 React Native 開發 Android 與 iOS App,但對 DevOps 與 App 原生開發的領域並不熟悉,若有講不對的地方請多見諒哦! ☺️
需求
以 前端找工作 App 為例,雖然 iOS App 目前並未上架,但有成功地透過 Bitrise 將 Android 與 iOS App 分別發佈到 Google Play Console 與 App Store Connect 上,我自己就是用 Bitrise 來處理 CI/CD 的(要我用 Jenkins 我還真不會XD)。
工商一下,目前 Android 版本增加了「求職天眼通」的查詢功能(順便讓我自己練習一下怎麼接 GraphQL…XD),可以在瀏覽前端工作之餘,也可以查一下公司、工作與面試的評論喔,歡迎下載!至於使用 iPhone 的同學… iOS 版我在努力了,Apple 一直不給審過啊 XD
好,總之本文擬定要達成的需求如下:
- 我希望在 CI/CD 上能分成 staging 環境 與 production 環境。前者是當開發者建立一個 pull request 時觸發,後者則是 merge 回 master branch 時觸發。
- staging 環境會發佈至 Bitrise OTA 上,供開發測試人員透過 Bitrise 的網頁安裝;而 production 環境則會發佈到 Google Play Console 與 App Store Connect 上,利用其本身的功能(Google Play 的測試版,或是 Apple 的 TestFlight)發佈給部分測試使用者安裝,更進一步也可以將其選擇為正式發佈版本。
- CI 過程中會進行 JavaScript 的 Unit Test(使用 Jest),並進行 react native 的 build,確保測試都有通過,以及能成功 build 完。
一樣考量到篇幅,本文 不會 講到關於產生 test coverage 報告(可以接 Codecov 這類服務,跑完機器人自動在 PR 下方貼上報告)、E2E test(例如 detox)、beta 版本要接 beta api、設定 cache(下次跑建置的時候不必再去拉一次 third-party 的 code 來安裝)與跑 code push 等比較進階的流程,有興趣研究的話可以去 Bitrise DevCenter 看文件。
設定 Bitrise
我覺得 Bitrise 的設定頗容易上手,其建置流程的 task 安排也都能夠透過 UI 介面操作完成。就來看看需要設定哪些東西吧。
建立新專案
首先得先註冊一個 Bitrise 的帳號,如果本文有幫上您,可以透過 我的 referral link 註冊。
Bitrise 提供給新註冊的使用者 14 天的試用期,並有比較長的最大建置時間,這段期間可以評估看看 Bitrise 是否合用再決定要不要付費,付費方案可以提高同時建置的數量、建置時間與機器等級等。
設好專案名稱後,必須 connect 到專案所在的 repo 提供商,可選擇 GitHub, Bitbucket 或是 GitLab 等各種平台。
選好專案 repo 之後,若沒有 private repo 的需求,可以跳過 SSH private key 的設定,接著選擇主要使用的 branch,Bitrise 會自動偵測專案類型,依本例就會辨識出 React Native,然後依實際情況選擇 Scheme name 等設定。
初次設定完後,Bitrise 會自動進行第一次的建置。不過將會馬上看到 build fail,因為這邊的設定都還未經過專案情況的調整。(例如我是透過 yarn 安裝,但預設使用的是 npm,卻找不到適當的 package-lock.json
版本鎖定檔案之類的)
Build 最終會出現這個結果,讓你知道每個 task 花費的時間以及建置狀況等。
也同時會收到建置結果的信件。(Failed 了 QQ)
別擔心,失敗是意料之內的事情,接下來會進行調整。
而我的習慣是會將 Android 與 iOS 拆成兩個 Bitrise 專案,一方面是希望讓建置時間縮短(Bitrise 在單一建置時間有限定上限時間,逾時就會強制中斷),另一方面是實際開發情況下,常會針對單一平台有建置測試需求,例如也許某個功能只在 Android 上會 fail,那此時便不需要在同一個建置中跑 iOS 的部份。
所以若有兩個平台的需求,就分別建兩個專案吧。以下會分別對 Android 與 iOS 進行 workflow 的設定。
Android
前置作業
這邊有個前置作業,請先在 Google Play Console 上建立好應用程式,當然,必須繳交一次性的 Google Play 開發人員 註冊費 25 美元給 Google(一次性費用比起 Apple 的開發者年費 99 美元還是親民許多)。
並設定 Google Play API access
。這是為了讓 Bitrise 能夠發佈到 Google Play Console 的設定之一。請參考 設定說明。注意:過程中會建立角色,一併建立金鑰時,記得選擇 JSON
格式,並切記要下載該 .json
檔案,稍後我們會上傳到 Bitrise 設定中。
上面設定完成、也設好給 Android 用的 Bitrise 專案後,在 Bitrise 專案頁面點上方的 Workflow 項目。
點進 Workflow Editor 後,先進到 Code Signing
頁籤設定簽署 apk 金鑰的部份。找到 ANDROID KEYSTORE FILE
這個區塊,請上傳 release 用的 keystore,並填入相應的 keystore 密碼、key alias 與 key 密碼。
密碼的部份可以在 Bitrise 的 Secrets
頁籤中設定,避免直接顯示在 Code signing
中。
關於怎麼產生 keystore 與在 android/app/build.gradle
中設定的方式,請參考 React Native 官方的這篇 Generating Signed APK 教學。
個人習慣是除了教學文中提到的 signingConfigs
> release
區段外,會再增加一組給 debug app 用的 nonrelease
設定,如:
release {
// 此處設定省略不列出
}
nonrelease {
storeFile file('xxx-dev-key.keystore')
storePassword 'xxx'
keyAlias 'xxx-dev'
keyPassword 'xxx'
}
buildTypes
這邊則新增 debug
區段:
buildTypes {
release {
// 省略
signingConfigs.release
}
debug {
signingConfig signingConfigs.nonrelease
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
debuggable true
}
}
以便能同時安裝 debug 版與正式版。
此外,也可參考這篇文章關於 Google Play App Signing 的說明,將 Google Play App Signing 一併設定完成。
keystore 設定完後,往下捲找到 GENERIC FILE STORAGE
區塊,這邊就是要上傳方才我們建立的 Google Play API access 角色的金鑰(.json
)。
Workflows
接下來是建置流程(workflow)的設定了,Bitrise 提供了一套所見即所得的操作介面,讓開發者可以透過點擊就完成大部分的流程設置,其背後會產出對應的 yaml 設定檔,當然,直接去編寫該 bitrise.yml
檔案內容也是可以的。
我們可以將 Workflow 分門別類管理,而且以 _
開頭的可視作 utility workflow,而未以底線開頭的則是主要 workflow。分類管理比較不會一大串落落長難以瀏覽。例如可以切分成:
_base
:從 repo 拉 code 下來、跑 yarn 安裝相關套件_test_node
:跑 jest unit test_build
:跑 Android 建置工作_deploy_to_bitrise
:佈署至 Bitrise_deploy_to_store
:佈署至 Google Play_send_to_slack
:將 Bitrise 佈署結果發佈至 Slack_send_to_slack_for_store
:將 Google Play 佈署結果發佈至 Slack
而主要 workflow 分為 staging
與 production
:
staging
:_base
_test_node
_build
_deploy_to_bitrise
_send_to_slack
production
:_base
_test_node
_build
_deploy_to_store
_send_to_slack_for_store
在 Bitrise Workflow Editor
> Triggers
頁籤中可以設定觸發建置的條件,例如 master 有被 push 時,會觸發 production
build、任何 pull request 發生時,會觸發 staging
build。這邊就依照團隊的開發流程需要自行設定囉。
若是要設定 daily build,則是在 Builds
頁籤設置。
現在一一介紹其中的設定,其中子項目代表的是 Bitrise 本身提供的 stpes。
_base
- Activate SSH key (RSA private key)
這應該預設就有了。設定 bitrise ssh key,為了抓取 repo 使用。 - Git Clone Repository
這一樣預設就有了。在建立專案時就會設好目標 repo 的位置。 - Yarn
跑yarn --frozen-lockfile
安裝相關套件並不要產生yarn.lock
。
_test_node
- Yarn
很單純,就是跑 yarn test。
_build
- Install missing Android SDK components
會依據專案的 Android Gradle plugin 版本安裝缺少的 components - Change Android versionCode and versionName
為了在建置時改變版本號。我是將 App 的版號後綴設為 build number,方便辨識。By the way, 可以透過 react-native-device-info 的getReadableVersion()
在 App 中顯示版本號。 - Set Android Manifest Values
這也是為了版本號,不過是針對AndroidManifest.xml
的修改,可以用上一個步驟輸出的全域變數$BITRISE_BUILD_NUMBER
帶入。 - Android Build
- Sign APK
簽署 APK,這邊會照前置設定中相關的 keystore 設定運行。
_deploy_to_bitrise
- Deploy to Bitrise.io – Apps, Logs, Artifacts
這是給 staging 用的。將簽署好的 APK 佈署至 Bitrise。開發測試者可以透過 Bitrise 的 OTA 安裝頁(此處是用開放的 public install page) 安裝該版本的 APK。 - Create install page QR code
產生 Bitrise public install page 的 QRCode,這樣可以讓不方便輸入網址的測試者掃描 QRCode 來進入安裝頁。
_deploy_to_store
- Google Play Deploy
這是給 production 用的。佈署至 Google Play。需要設定前置步驟提到的Service Account JSON key file path
。
_send_to_slack
- Send a Slack message
這是給 staging 用的。將 staging 建置訊息發送至指定的 Slack 頻道(透過 webhook)。範例如下:
可以看到我設定了要顯示建置環境、build number、workflow、branch name、platform 與安裝網址及 QRCode 等。
_send_to_slack_for_store
- Send a Slack message
這是給 production 用的。將 production 建置訊息發送至指定的 Slack 頻道(透過 webhook)。由於是到 Google Play(可能是 alpha 或 beta 版),所以此處不再提供安裝網址與 QRCode。
範例的 bitrise.yml
(Android) 提供參考如下:
以上就是 Bitrise 對 Android 設定的部份。
成功設定完後,可以在 Builds
頁籤手動觸發看看。可以看到有底線前綴的 utility workflow 在此都是反灰的,只有主要的 workflow 可被選擇。
建置成功的話,可在 Builds 列表看到代表成功的綠色。
若失敗了則是紅色,手動中斷(Abort)或逾時(Timeout)則是黃色。可點進特定建置工作觀看失敗的 log 來除錯。
iOS
前置作業
同樣地,一樣需要先成為 Apple Developer,繳交 99 美元的年費,才有機會將開發完的 iOS App 上架到 App Store(有機會?對,因為還要通過嚴苛的 Apple Review 審核機制啊)。
相關的流程可參考 一步一腳印的 iOS App 上架和更新流程。前置作業比起 Google Play 複雜嚴謹許多,參考文章說明的相當詳細,在此就不花費篇幅說明了。
在 Bitrise 這邊,需要準備 PROVISIONING PROFILE
(正式發佈用的 iOS Distribution
與開發期用的 iOS Development
provisioning profile 各一) 與 CODE SIGNING IDENTITY
(一樣是 distribution 與 development 用的 .p12
檔各一)。
貼心的 Bitrise 大概也知道匯出這些檔案的動作相當麻煩,所以提供了好用的 CLI 自動化工具 codesigndoc,能協助我們匯出相關的檔案,甚至直接幫我們上傳到 Bitrise 的專案設定中喔!請參考這篇文件。
不過,下面還是簡單記錄一下若採用手動方式要做的事情:
.p12
檔案的產生可參考此篇 iOS – Creating a Distribution Certificate and .p12 File。
檔案都準備好之後,到建立好的 Bitrise iOS 專案的 Workflow Editor
> Code Signing
上傳。完成後如下圖:
這前置作業真是頗複雜的,要我從頭再做一次可能還是要花不少時間 XD
Workflows
需要先提醒的是,本例使用 CocoaPods 作為 iOS 的原生套件管理,而為了縮短建置時間,是把 Pods 資料夾整個納入 git 版控中,如此便不用每次建置都要花費 pod install
拉 code 安裝的時間。此方法可能是旁門左道,請自行評估調整(縮短建置時間也可以用 Bitrise 的 cache 機制來達成,但本文不會提到這部份)。
也因為這樣,這邊 Bitrise iOS 的 Workflow 設定中,不會使用到 CocoaPods 的安裝 step。
如同 Android 那邊的設定,iOS 這邊我們也將 workflow 拆分成許多小的 utility workflows:
_base
:從 repo 拉 code 下來、跑 yarn 安裝相關套件_test_node
:跑 jest unit test_build_ios_release_without_pod_install
:跑 iOS 建置工作,且並不包含安裝 pod 的部份(因為 pods 已經進 git 了)_deploy_to_bitrise
:佈署至 Bitrise_deploy_to_store
:佈署至 Apple Connect Store_send_to_slack
:將 Bitrise 佈署結果發佈至 Slack_send_to_slack_for_store
:將 Apple Connect Store 佈署結果發佈至 Slack
而主要 workflow 分為 staging
與 production
:
staging
:_base
_test_node
_build_ios_release_without_pod_install
_deploy_to_bitrise
_send_to_slack
production
:_base
_test_node
_build_ios_release_without_pod_install
_deploy_to_store
_send_to_slack_for_store
分別一一介紹:
_base
這個跟 Android 一樣,就跳過囉。
_test_node
這個跟 Android 一樣,就跳過囉。
_build_ios_release_without_pod_install
- Certificate and profile installer
安裝 certificate 與 provisioning profile。 - Set Xcode Project Build Number
如同 Android 那邊一樣,希望小版號也是跟著 build number,這邊更改的是Info.plist
。 - Xcode Archive & Export for iOS
打包成 IPA 檔案。
_deploy_to_bitrise
- Deploy to Bitrise.io – Apps, Logs, Artifacts
這是給 staging 用的。將打包好的 IPA 佈署至 Bitrise。開發測試者可以透過 Bitrise 的 OTA 安裝頁(此處是用開放的 public install page) 安裝該版本,記得要用 Safari 開啟才有用,任何 in-app browser 都是不行的。注意:若測試者開啟網頁卻無法安裝,可能是裝置的 UDID 未被加入到使用的 provisioning profile 中。 - Create install page QR code
產生 Bitrise public install page 的 QRCode,這樣可以讓不方便輸入網址的測試者掃描 QRCode 來進入安裝頁。
_deploy_to_store
- Deploy to iTunes Connect – Application Loader
這是給 production 用的。佈署至 App Store Connect。注意:如果有開啟 Apple ID 兩階段驗證的話,需到 Apple ID 特別設置一組App 專用密碼
來給 Bitrise 使用(因為在建置時無法自動完成二階段驗證),此組帳號密碼可存放於Secrets
頁籤,並在這個 step 中讀取相應的變數即可。
_send_to_slack
這個跟 Android 一樣,就跳過囉。
_send_to_slack_for_store
這個跟 Android 一樣,就跳過囉。
範例的 bitrise.yml
(iOS) 提供參考如下:
以上就是 Bitrise 對 iOS 設定的部份。
都完成之後,可以試著手動觸發建置看看。
其他技巧
跳過建置
有時候我們發出的 PR 是明確不想觸發建置的,而觸發後去 bitrise dashboard 手動 abort 很沒效率,不如一開始就指定它不要觸發。
可以在不要觸發建置的 PR 的標題(commit message 亦可)上加上 [skip ci]
,那麼就不會觸發了。相關說明在此。
Bitrise CLI
在 Bitrise 網站上反覆測試調整 workflow 相當耗時,要等建置跑完才知道其中一步是否要調整,那是否有更有效率的方式呢?有的,Bitrise 提供了 CLI 工具給我們(而且是開源的)。透過 CLI 可以在本機端測試與調整 workflow,也不受 bitrise 最大建置時間的限制。
Bitrise CLI 的官網在這:Bitrise CLI
用 Mac 的同學可以透過 Homebrew 安裝:
brew update && brew install bitrise
然後把 Bitrise 上設定好的 bitrise.yml
下載回本地端,放到專案根目錄下。
以 Android 為例,我們還需要設定 Secrets
環境變數以便 app 的簽署,請將 Bitrise 上 Secrets
中的變數都一一設定到 .bitrise.secrets.yml
中吧。
例如:
envs:
- XXX_RELEASE_KEY_ALIAS: xxx-release-alias
opts:
is_expand: no
- XXX_RELEASE_STORE_PASSWORD: xxx
opts:
is_expand: no
- XXX_RELEASE_KEY_PASSWORD: xxx
opts:
is_expand: no
- BITRISEIO_ANDROID_KEYSTORE_PASSWORD: xxx
opts:
is_expand: no
- BITRISEIO_ANDROID_KEYSTORE_ALIAS: xxx-release-alias
opts:
is_expand: no
- BITRISEIO_ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD: xxx
opts:
is_expand: no
而執行的指令像這樣 bitrise run [workflow name]
,例如 bitrise run staging
,不過還有一些環境變數在本地端是不存在的,缺少則會引發錯誤,例如 BITRISE_BUILD_NUMBER
,所以我們得手動指定一下,例如本例就加上 BITRISE_BUILD_NUMBER
與 BITRISEIO_ANDROID_KEYSTORE_URL
這兩個:
BITRISE_BUILD_NUMBER=3 BITRISEIO_ANDROID_KEYSTORE_URL=file://$HOME/release-key.keystore bitrise run staging
當然,最後還是有可能失敗,例如在產生 QRCode 的那一步,會需要 Bitrise 的 public install page,本地端是沒有的,所以可以透過 workflow 的 run_if
指令來依條件決定是否執行,這邊在 create-install-page-qr-code
這個 step 區塊加上 run_if: .IsCI
便能在本地端執行時,跳過該 step。
在本地端執行完後,依然可以看到 Slack 出現了建置成功的訊息(但不會有安裝位置,因為只是想在本地端更有效率地測試 workflow steps 是否正常)。