整合 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 的使用情况
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
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
- gradle GitHub Action
- r0adkll/upload-google-play
- AKJAW/kotlin-multiplatform-github-actions
- gradle/setup-gradle
- 工具
- apksigner
- KeyStore Explorer
- 使用 Firebase CLI 将 Android 应用程式发布给测试人员