整合 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_HOMEexport 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 应用程式发布给测试人员
 
 

	    	微信扫一扫打赏
	    
	    	支付宝扫一扫打赏