整合 GitHub Action 在 Self Host Runner - Android 篇
CICD 建置 - 整合 GitHub Action - Android 篇
準備 Runner 環境
必要
- 安裝 Android Studio
- 設定 Android Studio SDK Tools
- 新增 Android SDK Command-line Tools
- (如果有) 更新 Android SDK Build-Tools
- 安裝 Firebase CLI
curl -sL firebase.tools | bash
- [可選擇] 目前 firebase cli 應該是 x64 的,所以如果可能需要裝 rosetta
softwareupdate --install-rosetta
- 增加設定環境變數 (在 mac 上編輯 .zshrc 檔案)
- 設定
JAVA_HOME
路徑與 Android Studio 的 GRADLE_LOCAL_JAVA_HOME 路徑相同export JAVA_HOME=/Applications/Android\ Studio.app/Contents/jbr/Contents/Home
- Android 2022.1.1 之後的路徑 = Reference: https://developer.android.com/build/jdks?hl=zh-tw#jdk-config-in-studio
- 設定
ANDROID_HOME
export ANDROID_HOME=~/Library/Android/sdk
- 增加 PATH
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools
- 設定
因為 Android 這邊使用的是 KMP 來開發,所以有額外的東西需要安裝,根據 Set
up an environment 有描述,或者可以照文件先安裝 kdoctor (brew install kdoctor
) 然後執行 kdoctor 來看缺少什麼東西要安裝
基本上就是
- Java
- Android Studio
- XCode
- Android Studio 的 Kotlin Multiplatform plugin
- Cocoapods
目前 kdoctor 有 bug ,對於新版的 Android Studio 的偵測支援有問題,就是即便安裝了 Kotlin Multiplatform Plogin 它也無法辨認,已經有人發 PR 修正了,需要等待新版的 kdoctor 釋出
可選
- 安裝 bundletoole [可選]
- 方式 1 : 從 Google 的 repo 下載
- 下載位置:bundletool
- 下載後可以把
bundletool.jar
放到/usr/local/bin/
下 - 打開 .zshrc 新增
alias bundletool="java -jar /usr/local/bin/bundletool.jar"
- 或者可以用 CLI :
echo 'alias bundletool="java -jar /usr/local/bin/bundletool.jar"' >> ~/.zshrc
- 或者可以用 CLI :
- 這樣可以直接在 terminal 中用
bundletool
來執行
- 方式 2 (在 MacOS) : 用 brew 安裝
brew install bundletool
- 安裝後預設就會幫你建立一個 bundletool 的捷徑(位於
/opt/homebrew/bin
)可以在 terminal 中用bundletool
來執行 - 注意
- 使用此方式會額外下載一個 openJDK
- 打開捷徑可以看到
- 會調整執行時候的
JAVA_HOME
變數export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home}"
- 執行 bundletool 是用
exec "${JAVA_HOME}/bin/java" -jar
來去執行,意思JAVA_HOME
這個變數的設定優先順序會改變用來執行這個 bundletool jsr 檔案的 java 版本
- 會調整執行時候的
- 方式 1 : 從 Google 的 repo 下載
準備及新增建置時候需要的 secrets 和 variable 到 GitHub 中
要能夠單純化 runner 建置的環境,避免要手動要放置各種檔案到特定位置後才可以建置,所以把必要的檔案、變數、密碼等等都放到 GitHub 當作環境變數,然後在 Flow 執行的過程中能夠動態自動的產生或取得
可以到 repo 的 Settings
中,左方的 Secrets and variables
點下後選 Actions
來管理 secrets 和
variable
secrets, variable 的名稱均可以自行討論決定
要取得及新增的項目如下
- 建置相關
- sign 用的 key store (.jks)
- Module name
- Google Play Package Name
- 上傳到 Firebase 的
- Firebase App Distritution Service Account Json
- Firebase 對應的 App ID
- 上傳到 Google Play 的
- Google Play API Service Account Json
- Google Play 的 ID
- Google Play 的 track
上述 Firebase 和 Google Play API 的 Service Account JSON 是兩個項目,但其實可以用同一個即可,只要權限設定好即可
下面分別就各項東西怎麼準備說明
sign 用的 key store (.jks)
參考 Generate an upload key and keystore 建立一個 keystore 檔案(.jks)
注意:此檔案不要進版控
新增 Secrets
KEY_STORE_BASE64
假設匯出的 key store 檔案名稱是 MyKeyStore.jks
使用指令
base64 -i MyKeyStore.jks | pbcopy
將檔案轉換成 base64 並且複製到記憶體後,貼在該 secrets 的內容
KEY_STORE_PASSWORD
步驟中建立 KeyStore 的密碼輸入進此 secret
KEY_ALIAS
步驟中建立 KeyStore 的 Key 的 alias 輸入此 secret
KEY_PASSWORD
步驟中建立 KeyStore 的 Key 的密碼輸入於此
注意:KeyStore 的密碼 和 KeyStore Key 的密碼要相同,在 Generate an upload key and keystore 裡面有提到有個 Known issue Error when using different passwords for key and keystore
Firebase App Distribution Service Account Json
取得步驟
參考這篇的操作 FIREBASE_TOKEN migration
大致上就是
- GCP Console
- 選擇自己的 project (若還沒有 project 則要建立)
- 點進去之後在左邊切換到
服務帳戶
- 建立或選擇一個 Firebase App Distribution 的 account
- 點進去 service account 後
- 切換上方到
金鑰
的頁籤 - 建立一把金鑰,並且保存好建立時候的 json 檔案,該檔案即是 service account json
此 json 檔案僅在建立時候可以下載,請妥善保存
新增 Secrets
FIREBASE_APP_DISTRIBUTION_CREDENTIAL_BASE64
假設下載的 service account json 檔案名稱叫做 firebase_credential.json
使用指令
base64 -i firebase_credential.json | pbcopy
將檔案轉換成 base64 並且複製到記憶體後,貼在該 secrets 的內容
避免直接把 json 文字直接貼到裡面,因為格式問題或者文字問題,可能會造成 GitHub Action 拿到內容存成檔案使用時會有類似 json 格式問題,導致 firebase 指令無法辨認 token 內容進而發生錯誤
Firebase 對應的 App ID
取得方式
- 打開 Firebase Console
- 進入自己的 App 的專案
- 點開左側功能表的最上方
專案總攬
右邊的設定 icon
進入專案設定
- 下方就可以看到
您的應用程式
- 點選會上傳的應用程式
- 畫面右邊就會有
應用程式ID
新增 Secrets
FIREBASE_PACK_APP_ID
把取得得應用程式ID
貼入 secrets
Google Play API Service Account Json
取得步驟
參考的操作:Google Play Credentials (Service Account JSON)
大致上就是
- GCP Console
- 選擇自己的 project (若還沒有 project 則要建立)
- 點進去之後在左邊切換到
服務帳戶
- 建立或選擇一個 Firebase App Distribution 的 account,建立的時候記得給予的權限要對,請參考上方連結
- 點進去 service account 後
- 切換上方到
金鑰
的頁籤 - 建立一把金鑰,並且保存好建立時候的 json 檔案,該檔案即是 service account json
新增 Secrets
GOOGLE_PLAY_API_CRDENTIAL_BASE64
假設下載的 service account json 檔案名稱叫做 google_play_credential.json
使用指令
base64 -i google_play_credential.json | pbcopy
將檔案轉換成 base64 並且複製到記憶體後,貼在該 secrets 的內容
避免直接把 json 文字直接貼到裡面,因為格式問題或者文字問題,可能會造成 GitHub Action 拿到內容存成檔案使用時會有類似 json 格式問題,導致 firebase 指令無法辨認 token 內容進而發生錯誤
其他
新增 variables
GOOGLE_PLAY_PACKAGE_NAME
上傳到 Google Play 對應的 package name
MAIN_PROJECT_MODULE_NAME
可以使用 ./gradlew project
列出來,是 android / compose project 的名稱
FIREBASE_DISTRIBUTION_GROUPS
請詢問相關人員是屬於哪一個 groups
準備 GitHub Action 要用的 YAML 檔案
新增 YAML 檔案
- 在 repo 的根目錄新增
.github/workflows
共 2 個資料夾 - 在
.github/workflows
的資料夾中新增{YOUR_WORKFLOW_NAME}.yaml
檔案
要讓 github action 有作用,需要在 default branch 新增這個 yaml 檔案,所以開發測試角度來說可以先建立一個空的,然後再開 branch 去調整
可以根據 Writing workflows 來寫 Workflow YAML,這邊提供 Android 的範例,可以再依照需求作調整
YAML 的內容
這邊以我用的專案使用的 YAML 舉例,說明 YAML 中哪些 step 對於建置 iOS App 時候哪些是必要的,以及額外補充說明一些用 Gtihub action 的心得技巧
name: "Build Android app"
env:
# The name of the main module repository
MAIN_PROJECT_MODULE_NAME: ${{vars.MAIN_PROJECT_MODULE_NAME}}
# Allow dory/test-reporter create and upload test results
permissions:
pull-requests: write
contents: write
statuses: write
checks: write
actions: write
on:
# allow manual trigger
workflow_dispatch:
# trigger push to main branch with tag start with "v"
push:
branches:
- main
- develop
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
- 'dev[0-9]+.[0-9]+.[0-9]+*'
# trigger pull request for all branches
pull_request:
types: [opened, synchronize, reopened]
branches:
- '*'
jobs:
lint:
runs-on: [self-hosted, macOS]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Lint
env:
INCLUDE_IOS: false
run: ./gradlew lint
- name: Check and report lint results
uses: hidakatsuya/action-report-android-lint@v1.2.2
with:
result-path: '${{ env.MAIN_PROJECT_MODULE_NAME }}/build/reports/lint-results.xml'
fail-on-warning: false
unit-test:
runs-on: [self-hosted, macOS]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Unit Test
env:
INCLUDE_IOS: false
run: ./gradlew test
- name: Create Test Report
uses: dorny/test-reporter@v1.9.1
if: success() || failure()
with:
name: Unit Test Results
path: '${{ env.MAIN_PROJECT_MODULE_NAME }}/build/test-results/testDevDebugUnitTest/**.xml'
reporter: java-junit
fail-on-error: false
- name: Upload Test Reports
uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: reports
path: ${{ env.MAIN_PROJECT_MODULE_NAME }}/build/test-results
build_and_upload:
needs: [unit-test, lint]
runs-on: [self-hosted, macOS]
env:
# 對應 build.gradle.kts 中的 signingConfigs 區段的變數名稱
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Get branch name (push branch)
if: github.ref_type == 'branch'
run: |
echo "CURRENT_BRANCH=${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}" >> $GITHUB_ENV
echo "CURRENT_TAG=" >> $GITHUB_ENV
- name: Get branch name (push tag)
if: github.ref_type == 'tag'
run: |
echo "CURRENT_TAG=${{ github.ref_name }}" >> $GITHUB_ENV
CURRENT_BRANCH=$(git branch -r --contains ${{ github.ref }} | grep -v HEAD | head -n 1 | tr -d ' ' | sed 's/origin\///')
echo "CURRENT_BRANCH=$CURRENT_BRANCH" >> $GITHUB_ENV
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Create keystore file
env:
KEY_STORE_BASE64: ${{ secrets.KEY_STORE_BASE64 }}
run: |
# 因為現在 sign 的步驟是寫在 build.gradle.kts 中,所以這邊要先將 keystore 寫入檔案
# 就不另外用 bundletool 來 sign 了
KEY_STORE_FILE_PATH=$RUNNER_TEMP/your_keystore.jks
echo "KEY_STORE_FILE_PATH=$KEY_STORE_FILE_PATH" >> $GITHUB_ENV
echo -n "$KEY_STORE_BASE64" | base64 --decode -o $KEY_STORE_FILE_PATH
- name: Build apk release project (APK)
run: ./gradlew assemblePackRelease
- name: Build app bundle release (AAB)
run: ./gradlew bundlePubRelease
- name: Set build artifacts path to environment variable
env:
UPLOAD_KEYSTORE: ${{ secrets.UPLOAD_KEYSTORE }}
run: |
PACK_APK_FILE_PATH=$(ls ${{ env.MAIN_PROJECT_MODULE_NAME }}/build/outputs/apk/pack/release/*.* | head -1 | sed 's/\.[^.]*$/.apk/')
PUB_AAB_FILE_PATH=$(ls ${{ env.MAIN_PROJECT_MODULE_NAME }}/build/outputs/bundle/pubRelease/*.* | head -1 | sed 's/\.[^.]*$/.aab/')
echo "PACK_APK_FILE_PATH=$PACK_APK_FILE_PATH" >> $GITHUB_ENV
echo "PUB_AAB_FILE_PATH=$PUB_AAB_FILE_PATH" >> $GITHUB_ENV
echo "Pack Apk Path: $PACK_APK_FILE_PATH, Pub Aab Path: $PUB_AAB_FILE_PATH"
- name: Check if need to upload to Firebase or Google Play
run: |
if [ $CURRENT_BRANCH == 'main' ] && echo "$CURRENT_TAG" | grep -Eq "^v[0-9]+\.[0-9]+\.[0-9]+$"; then
UPLOAD_TO='googleplay'
elif [ $CURRENT_BRANCH == 'develop' ] && echo "$CURRENT_TAG" | grep -Eq "^dev[0-9]+\.[0-9]+\.[0-9]+$"; then
UPLOAD_TO='firebase'
else
UPLOAD_TO='none'
fi
echo "Place to upload: $UPLOAD_TO"
echo "UPLOAD_TO=$UPLOAD_TO" >> $GITHUB_ENV
- name: Set Firebase App Distribution Credential
if: env.UPLOAD_TO == 'firebase' || env.UPLOAD_TO == 'googleplay'
env:
FIREBASE_APP_DISTRIBUTION_CREDENTIAL_BASE64: ${{ secrets.FIREBASE_APP_DISTRIBUTION_CREDENTIAL_BASE64 }}
run: |
FIREBASE_CREDENTIAL_FILE_PATH=$RUNNER_TEMP/firebase-credential.json
echo -n "$FIREBASE_APP_DISTRIBUTION_CREDENTIAL_BASE64" | base64 --decode -o $FIREBASE_CREDENTIAL_FILE_PATH
export GOOGLE_APPLICATION_CREDENTIALS=$FIREBASE_CREDENTIAL_FILE_PATH
echo "FIREBASE_CREDENTIAL_FILE_PATH=$FIREBASE_CREDENTIAL_FILE_PATH" >> $GITHUB_ENV
- name: Publish APK To Firebase App Distribution
if: github.env.UPLOAD_TO == 'firebase'
run: |
firebase appdistribution:distribute ${{ env.PACK_APK_FILE_PATH }} \
--app ${{ secrets.FIREBASE_PACK_APP_ID }} \
--groups "${{ vars.FIREBASE_DISTRIBUTION_GROUPS }}"
- name: Publish To Google Play for Internal Test
if: env.UPLOAD_TO == 'googleplay'
uses: r0adkll/upload-google-play@v1.1.3
with:
serviceAccountJson: ${{ env.FIREBASE_CREDENTIAL_FILE_PATH }}
packageName: ${{ vars.GOOGLE_PLAY_PACKAGE_NAME }}
releaseFiles: ${{ env.PUB_AAB_FILE_PATH }}
track: internal
通常在 build Android 時候會用的 actions/setup-java, actions/setup-gradle
如果跑在不是 self host runner 的情況下,在 job 裡面補上 setup-java, setup-gradle 的 step 是必要的,不然建置環境應該是無法建立的
actions/setup-java 的設定
distributions 可以的值 : Supported distributions
Setup JDK
一開始安裝 JDK 的時候是這麼寫的,裡面有 cache 的設定
- name: Set up JDK 17
uses: actions/setup-java@v4.3.0
with:
java-version: '17'
distribution: 'zulu'
cache: gradle
但發現開啟的話在最後面有個 Post Set Up JDK
的步驟會很久,在 GitHub 上面也有被開 Issue Post Setup JDK 17 takes very long time on local runner
解法很簡單就是把 cache
拿掉就好了,Issue 裡面有提到可能 Cache 的 Policy 可以調整,這個待有需要時再研究
Sign app bundle [可選]
根據關分文件的 Build an app bundle with Gradle 的建議有 3 個方式
- 增加
build.gradle.kts
檔案的方式在用 gradle 產生 app bundle 的時候就 sign ,這方式也可以搭配keystore.properties
把 keystore 的相關密碼等資訊放在keystore.properties
裡面避免資訊上到版控- 寫法可以不搭配
keystore.properties
可以在 build.gradle.kts 裡面透過環境變數
取得
- 寫法可以不搭配
jarsigner
來 signbundletool
來做 sign
看到有些文章會使用 r0adkll/sign-android-release 這個 action 來簡化
sign 的工作,看了一下它的原始碼發現它是用 apksigner
去 sign,apksigner
是跟著 Android Studio 會有版本問題,所以這個 action
會去找有安裝的最新版本 SDK 裡面的 apksigner
來用,但是!!
根據上面 Build an app bunlde with Gradle 的官方文件裡面寫
Note: You cannot use apksigner to sign your app bundle.
所以看起來是不可以用這個 action 來 sign aab
用 hidakatsuya/action-report-android-lint 上傳 lint 報告
用 hidakatsuya/action-report-android-lint 來產生比較好看的 lint rerport 而不僅把 lint result 上傳到 GitHub Action 中
asadmansr/android-test-report-action 使用限制
本來產生 unit test report 是採用這套第三方 action android-test-report-action
但這套只能支援
- Linux os
- 中文支援(非 ascii)有問題
- 應該是使用的 python 版本過舊
所以在 macos 上面不能使用,所以最後改使用 dorny/test-reporter
dorny/test-reporter 的使用情況
要打開 github action 的 permission
permissions:
pull-requests: write
contents: write
statuses: write
checks: write
actions: write
原因:"Error: HttpError: Resource not accessible by integration" during build process
Firebase CLI
我們是自己使用 Firebase CLI 上傳到 Firebase App Distribution,其中 app id 要給對,請參考上方的取得方式
使用 Firebase CLI 時可以使用多加參數 --debug
來看看詳細步驟是錯在那個步驟,可以知道比較清楚的錯誤訊息
wzieba/Firebase-Distribution-Github-Action
本來 YAML 是使用 Firebase App Distribution Github Action 來上傳到
但同上非 container 不能執行
mapping file
如果 App 有用 Shrink, obfuscate, and optimize your app 來編譯的話,要記得上傳 mapping file 到 Firebase,可以方便顯示錯誤的地方
Get readable crash reports in the Crashlytics dashboard 裡面也有提到,使用 Crashlytics 的話也要記得開啟上傳 mapping file
r0adkll/upload-google-play
裡面必要參數的 service account 的 JSON 有兩種給法,一個是 serviceAccountJsonPlainText
另一個是
serviceAccountJson
- 差異:
serviceAccountJsonPlainText
是直接給 JSON 文字,serviceAccountJson
是給 JSON 檔案路徑 - 使用經驗上用
serviceAccountJson
比較方便,給 JSON 文字到變數的時候,可能會遇到 Json 內容有一些被認為不合法字元的情況,看實做如果給 JSON 文字他還是會把他變成一個檔案,因為最終會 export GOOGLE_APPLICATION_CREDENTIALS 到環境變數中,而這個變數的值就是 JSON 檔案的路徑 - 這個 action 是使用 @googleapis/androidpublisher 這個 js 版本的 api 套件去上傳,看起來是 Google 官方提供的
TO-DO
參考資料
- automated-build-android-app-with-github-action
- Automating Success: GitHub Actions Workflow for Android App Deployment
- Sign your app
- Build your app from the command line
- Environment variables
- 設定 Runner 環境變數有哪些可以看這篇
- Android Studio 生成與導入 JKS 金鑰
- 示範使用
build.gradle.kts
與keystore.properties
的方式來建置專案時就簽署
- 示範使用
- Upload apk or bundle to Google Play via command line script
- Distribute Android apps to testers using the Firebase CLI
- 如何自動上傳
apk / aab 到 Google Play Console
- 這邊展示用 google play api python client 去使用 python 上傳
- lint
- Lint
- 官方文件說明 lint 裡面有哪一些 optino 可以用
- Android自定义Lint的二三事儿
- 使用Android Studio Lint静态分析(三)
- Lint
- 可用、參考的一些 Action
- 工具