阿猫的博客

阿猫的博客

Android 项目使用 Github Actions 实现自动打包发布

3545
2022-09-19

背景

朋友的安卓项目[1]想要实现更新代码(发布版本)后自动打包成apk并发布。同时,他有这么一个需求:由于他的app需要支持安装和升级(安装时,app需要签名;同时,如果需要升级,必须用同一个key签名,才能保留上一版本的数据),因此需要增加签名这一步骤。

本文假定你已经掌握Android app的开发流程,熟悉Gradle,熟悉安卓应用的打包发布方法,本文重点介绍利用GitHub Actions 进行Android 项目CI/CD 的过程。

基础知识和思路

如果不使用CI/CD ,你大概需要使用 Android Studio 的 Generate Signed Bundle/APK 功能[2],关于应用打包发布的具体过程,这篇官方的文章讲得已经非常详细,就不再叙述了。后续的操作假定你已经熟悉使用这个功能进行打包,同时已经生成了一个供签名的key。

同时,文章也假定你已经熟悉Gradle[3],这是一个构建工具,能够自动化构建流程。在打包发布(Release)版本的的apk时,只需要使用命令gradle assembleRelease 即可构建供发布的apk。

最后,我们需要给apk签名。你可以使用apksigner等,在此不多赘述。

因此,如果要使用一个自动化的流程替代我们手工的发布,需要完成以下几个步骤:

  1. 将新版本的代码构建成apk
  2. 将上一步生成的apk用我们的key签名
  3. 以某种方式将这个apk发布出去,供用户下载

经过一些资料的查,其实Github 的 Marketplace就提供了很多已经封装好的workflow,例如

  1. https://github.com/marketplace/actions/gradle-build-action 解决构建apk的问题
  2. https://github.com/marketplace/actions/sign-android-release 解决apk签名的问题
  3. https://github.com/marketplace/actions/create-release 解决发布的问题

所以我们要做的事情就是将这几个积木串起来就大功告成了。另外,我希望这个过程能够跟git 工作流程结合起来,我们并不需要每次push代码都构建发布一个版本,使用tag来进行版本的管理是比较理想的:每当一个tag被push到Github,就触发一次构建,发布一个版本。

走,实操走起

Talk is cheap, show me the code. 这里先把workflow的yaml贴出来,再慢慢解释。

name: Release
on:
  push:
    tags:
      - "v*"

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: temurin
          java-version: 11
      - uses: gradle/gradle-build-action@v2
        with:
          gradle-version: current
          arguments: assembleRelease
      - uses: r0adkll/sign-android-release@v1
        id: sign_app
        with:
          releaseDirectory: app/build/outputs/apk/release
          signingKeyBase64: ${{ secrets.SIGNING_KEY }}
          alias: ${{ secrets.ALIAS }}
          keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
          keyPassword: ${{ secrets.KEY_PASSWORD }}
      - run: mv ${{steps.sign_app.outputs.signedReleaseFile}} LittleGooseOffice_$GITHUB_REF_NAME.apk
      - uses: ncipollo/release-action@v1
        with:
          artifacts: "*.apk"
          token: ${{ github.token }}
          generateReleaseNotes: true

一个GitHub Action的yaml包括几个必须的元素:name、on和jobs。name就是这个action的名称,on就是这个action 的触发方式,jobs就是这个action要干的事情。

从这个on开始解释,前面说到,我们希望“每当一个tag被push到Github,就触发一次构建,发布一个版本”。我们的tag应当使用semantic versioning[4],例如v1.0.1-beta。因此,on这部分我们这样写:

on:
  push:
    tags:
      - "v*"

表示在有tag被push,且tag是以v开头时,运行这个action。

接下来到jobs,这里定义这个action要干的事情。我们要做的事情比较简单,就只用一个叫build的job就好啦。如果定义多个job,可以享受到一些例如复用job、job之间的依赖之类的功能[[https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow](https://docs.github.com/en/actions/using-jobs/using-jobs-in-a-workflow)],在这里就不多赘述啦。再往里看,首先是一个`runs-on`标签,表示这个job要在什么环境下运行。一般情况下我们使用`ubuntu-latest`就可以啦,有特殊需求的可以使用其他的系统[https://docs.github.com/en/actions/using-jobs/choosing-the-runner-for-a-job]。然后是一个permission标签,这里不多叙述,感兴趣可以查阅文档[5],后面的steps才是重点。

可以看到,steps下面是多个单独的项,每个项里面又有useswith 等标签。steps之间是串行运行的,意味着前一个step运行成功后才会进入下一个step,如果某一个step失败了,整个过程就会失败。uses表示使用某个模板,with则是传入模版的一些参数,模板的使用可以在marketplace的文档中找到。接下来我们逐个step往下看。

检出代码

- uses: actions/checkout@v3

这一步是基本上所有CI/CD流程必须的,把对应的代码从git仓库中检出,放到工作目录中。

Gradle打包

- uses: actions/setup-java@v3
  with:
    distribution: temurin
    java-version: 11
- uses: gradle/gradle-build-action@v2
  with:
    gradle-version: current
    arguments: assembleRelease

这里实际上有两步,第一步先配置好java环境,这一步就不多说了;第二步就是前面提到的将新版本的代码构建成apk。这里传入了两个参数(with),gradle-version表示要使用的gradle版本,这里选择current,就是当前最新的稳定版本。arguments表示gradle命令的参数,我们这里要Release,所以选assembleRelease。根据经验,这个未签名的apk会生成在app/build/outputs/apk/release,这里暂时用不到,但是我们先记下来,下一步有用。

给apk签名

- uses: r0adkll/sign-android-release@v1
  id: sign_app
  with:
    releaseDirectory: app/build/outputs/apk/release
    signingKeyBase64: ${{ secrets.SIGNING_KEY }}
    alias: ${{ secrets.ALIAS }}
    keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
    keyPassword: ${{ secrets.KEY_PASSWORD }}

这一步就是要给apk签名了。这里有一个陌生的id标签,主要用来标识这一步,后续的step可以使用这一步产生的一些变量。

这一步的with中也有一些新东西,就是以 ${{ xxx }}标识的变量。这里都是secrets[6],后续我们还会遇到一些环境变量和GitHub定义的变量。secrets的设置在仓库的Settings - Security - Secrets - Actions找到,例如${{ secrets.SIGNING_KEY }}我们需要对应创建一个SIGING_KEY的secret。

这一步的alias等就不赘述了,就是前面提到的签名的时候使用的几个参数。重点讲一下signingKeyBase64,前面提到,签名需要用到这个keystore,但是这属于不能公开的内容(否则谁都可以用你的key来给应用签名),因此这个key不能存在公开的仓库中。这个workflow的作者使用base64对key进行编码(base64[7]是一种可以将任意二进制数据转化成文字的编码方法),通过secrets传进workflow中,避免key的泄露。这个base64的生成方法如下:

> openssl base64 < some_signing_key.jks | tr -d '\n' | tee some_signing_key.jks.base64.txt

发布

- run: mv ${{steps.sign_app.outputs.signedReleaseFile}} LittleGooseOffice_$GITHUB_REF_NAME.apk
- uses: ncipollo/release-action@v1
  with:
    artifacts: "*.apk"
    token: ${{ github.token }}
    generateReleaseNotes: true

这里还是走了两步,第一步是将上一步生成的文件改一个名,第二部才是创建一个Release

第一步的run是一个新鲜东西,表示直接在工作区运行一个命令,这里使用一个mv命令来给生成的apk改个名,顺便移到最外面去。还记得上面提到的step的id吗,这里就用到了。${{steps.sign_app.outputs.signedReleaseFile}}表示sign_app这一步的outputs.signedReleaseFile这个变量,是生成的签名apk的目录。这里还有一个新玩意,就是$GITHUB_REF_NAME。这个是Github定义的变量,其值为“触发workflow的分支或tag名称”[8]。我们希望生成的apk文件命名为LittleGooseOffice_v1.0.2.apk,所以做这么一个改名的操作。

第二步也比较简单,创建一个Release,这里artifact表示要包含到Release Assets中的文件,*.apk就能搞定;token这里填${{ github.token }},这也是一个内置的变量,会使用一个临时的token来创建Release;最后的generateReleaseNotes能够自动生成一些变化列表之类的内容。

到这里,这个action就已经大功告成了。接下来我们体验一下如何用gitflow实现自动构建发布。

扬帆起航!

在每次完成一个版本的开发后,我们执行以下操作:

> git tag v1.0.0
> git push --tags

可以看到在Actions页签出现了一个workflow run,如图。(这里由于workflow已经完成,所以是绿色,如果是刚提交的,应该是黄色。)

等待workflow完成后,前往Release,应该就能看到如下。

这时候,用户点击Assets中的apk就可以下载到我们最新的包啦!


  1. https://github.com/MReP1/LittleGooseOffice ↩︎

  2. https://developer.android.com/studio/publish/app-signing ↩︎

  3. https://gradle.org/ ↩︎

  4. https://semver.org/lang/zh-CN/ ↩︎

  5. https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs ↩︎

  6. https://docs.github.com/en/actions/security-guides/encrypted-secrets ↩︎

  7. https://en.wikipedia.org/wiki/Base64 ↩︎

  8. https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables ↩︎