[GitHub Action] 整合 GitHub Action 在 Self Host Runner - Androi

整合 GitHub Action 在 Self Host Runner - Android 篇

準备 Runner 环境

CICD 建置 - 整合 GitHub Action - Android 篇

準备 Runner 环境

必要

  1. 安装 Android Studio
  2. 设定 Android Studio SDK Tools
    • 新增 Android SDK Command-line Tools
    • (如果有) 更新 Android SDK Build-Tools
  3. 安装 Firebase CLI
    • curl -sL firebase.tools | bash
    • [可选择] 目前 firebase cli 应该是 x64 的,所以如果可能需要装 rosetta softwareupdate --install-rosetta
  4. 增加设定环境变数 (在 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 释出

可选

  1. 安装 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
      • 这样可以直接在 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 版本

準备及新增建置时候需要的 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

大致上就是

  1. GCP Console
  2. 选择自己的 project (若还没有 project 则要建立)
  3. 点进去之后在左边切换到 服务帐户
  4. 建立或选择一个 Firebase App Distribution 的 account
  5. 点进去 service account 后
  6. 切换上方到 金钥 的页籤
  7. 建立一把金钥,并且保存好建立时候的 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

取得方式

  1. 打开 Firebase Console
  2. 进入自己的 App 的专案
  3. 点开左侧功能表的最上方 专案总揽 右边的 设定 icon 进入 专案设定
  4. 下方就可以看到 您的应用程式
  5. 点选会上传的应用程式
  6. 画面右边就会有 应用程式ID

新增 Secrets

FIREBASE_PACK_APP_ID

把取得得应用程式ID贴入 secrets

Google Play API Service Account Json

取得步骤

参考的操作:Google Play Credentials (Service Account JSON)

大致上就是

  1. GCP Console
  2. 选择自己的 project (若还没有 project 则要建立)
  3. 点进去之后在左边切换到 服务帐户
  4. 建立或选择一个 Firebase App Distribution 的 account,建立的时候记得给予的权限要对,请参考上方连结
  5. 点进去 service account 后
  6. 切换上方到 金钥 的页籤
  7. 建立一把金钥,并且保存好建立时候的 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 档案

  1. 在 repo 的根目录新增 .github/workflows 共 2 个资料夹
  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 个方式

  1. 增加 build.gradle.kts 档案的方式在用 gradle 产生 app bundle 的时候就 sign ,这方式也可以搭配 keystore.properties 把 keystore 的相关密码等资讯放在 keystore.properties 里面避免资讯上到版控
    • 写法可以不搭配 keystore.properties 可以在 build.gradle.kts 里面透过环境变数取得
  2. jarsigner 来 sign
  3. bundletool 来做 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.ktskeystore.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静态分析(三)
  • 可用、参考的一些 Action
    • gradle GitHub Action
    • r0adkll/upload-google-play
    • AKJAW/kotlin-multiplatform-github-actions
    • gradle/setup-gradle
  • 工具
    • apksigner
    • KeyStore Explorer
    • 使用 Firebase CLI 将 Android 应用程式发布给测试人员

关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章

5 点赞(415) 阅读(67)