Skip to main content

安卓自动CI部署

在这个文件里面,我们将会介绍如何使用 Github Actions 来自动化部署安卓应用。

Google Play Console Setup

以下内容源自于Fastlane 的文档

1. 在 Google Play Console 中创建 Service Account

找到Account Details, 并且记录下Developer account id

Setup页面,找到API access,并且点击View in Google Cloud Platform

之后在这个页面创建Service account

之后输入Service account name,并且点击Next, 在第二步中选择Service account user,并且点击Next。第三步可以忽略,直接点击Done

note

步骤 1: 输入你的Service account name,并且点击Next

note

步骤 2: 选择Service account user,并且点击Next

note

步骤 3: 点击Done

2. 为 Service Account 生成 Key

在刚刚创建的Service account页面,点击Actions按钮

在下拉菜单中选择Manage keys

Manage keys页面,点击Add key

Add key页面,选择JSON,并且点击Create

这个时候系统会自动下载一个JSON文件,这个文件就是我们需要的Key

3. 给予 Service Account 权限

返回Google play console 并点击Refresh按钮,之后就能看到刚刚创建的Service account了。

点击Manage Play Console Permissions按钮,选择Admin 权限后保存

4. 将service.json文件放到 app 目录下

将刚刚下载的service.json文件放到app目录下。注意,这个文件名必须是service.json

service.json

Github Actions Setup

1. 创建 Github Secrets

我们需要准备几个文件把它们放到 Github Secrets 里面,因为我们不希望把这些敏感信息放到代码仓库里面。 要将文件放到 Github Secrets 里面,我们需要先将文件转换成 Base64 编码,然后再将 Base64 编码的文件放到 Github Secrets 里面,可以用下面这个脚本 来完成这个操作。

# given a list of path and convert them into base64 format and write back to the disk.
# for example, given a file name `a.key` will be converted into `a.key.b64`

import base64
from typing import List

def convert_to_b64(path: str):
with open(path, "rb") as f:
content = f.read()
b64_content = base64.b64encode(content)
output_path = path + ".b64"
with open(output_path, "wb") as f:
f.write(b64_content)

def main():
paths: List[str] = [
# for flutter
"./assets/.env.local",
# for android
"./android/local.properties",
"./android/keystore/amap-prod.jks",
"./android/keystore/debug-key.jks",
"./android/prod.key.properties",
"./android/debug.key.properties",
"./android/service.json",
# for ios
"./secrets/ios-distribution.p12",
"./secrets/profile.mobileprovision"
]
for path in paths:
print(f"converting {path} to base64")
convert_to_b64(path)

if __name__ == "__main__":
main()

将上面的脚本放到项目根目录下,然后执行python3 convert_to_b64.py,这个脚本会将上面列出的文件转换成 Base64 编码,并且将转换后的文件放到同一个目录下,文件名后面会多出一个.b64的后缀。

之后我们需要将这些文件放到 Github Secrets 里面,结果如下:

Github Secrets

2. 将 Base64 编码转换成原始文件

这个脚本可以将环境变量里的 Base64 编码转换成原始文件,其中ENV_B64是环境变量,我们会在Github Action Workflow里面从Github Secrets里面获取这个环境变量, 之后传入到Step里面的环境变量里面。

# Create the keystore directory for Android
echo -n "$ENV_B64" | base64 --decode > ./assets/.env.local

3. 创建 Github Action Workflow

build-android:
# Build the flutter app for android when a commit is pushed to main or a pull request is opened against main
name: Build flutter app for android
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.0"
bundler-cache: true
- name: Setup JDK
uses: actions/setup-java@v3
with:
java-version: 11
distribution: "temurin"
cache: "gradle"
- name: Setup Android SDK
uses: android-actions/setup-android@v2
- name: Setup Flutter SDK
uses: flutter-actions/setup-flutter@v2
with:
channel: stable
version: 3.7.4
- name: Run setup script
run: ./scripts/setup_android.sh
env:
ENV_B64: ${{ secrets.ENV_B64 }}
DEBUG_KEY_B64: ${{ secrets.DEBUG_KEY_B64 }}
DEBUG_KEY_PROPERTY_B64: ${{ secrets.DEBUG_KEY_PROPERTY_B64 }}
AMAQ_B64: ${{ secrets.AMAQ_B64 }}
LOCAL_PROPERTIE_B64: ${{ secrets.LOCAL_PROPERTIE_B64 }}
PROD_KEY_B64: ${{ secrets.PROD_KEY_B64 }}
- name: Install dependencies
run: flutter pub get
- name: Build app
run: flutter build apk --release
- name: Build appbundle
run: flutter build appbundle --release
- uses: actions/upload-artifact@v3
name: Upload APK
with:
name: apk
path: build/app/outputs/flutter-apk/app-release.apk
- uses: actions/upload-artifact@v3
name: Upload appbundle
with:
name: appbundle
path: build/app/outputs/bundle/release/app-release.aab

这个 Workflow 将会帮我们打包Flutter app成为APKApp-bundle。 其中Apk是为了直接用户下载使用,而App-bundle是为了发布到Google Play Console

Run setup script中,我们将Github Secrets里面的 Base64 编码转换成原始文件,然后将原始文件放到对应的目录下。

- name: Run setup script
run: ./scripts/setup_android.sh
env:
ENV_B64: ${{ secrets.ENV_B64 }}
DEBUG_KEY_B64: ${{ secrets.DEBUG_KEY_B64 }}
DEBUG_KEY_PROPERTY_B64: ${{ secrets.DEBUG_KEY_PROPERTY_B64 }}
AMAQ_B64: ${{ secrets.AMAQ_B64 }}
LOCAL_PROPERTIE_B64: ${{ secrets.LOCAL_PROPERTIE_B64 }}
PROD_KEY_B64: ${{ secrets.PROD_KEY_B64 }}

之后我们在Build Apk and Build App bundle后,会把打包好的APKApp-bundle上传到Github Actions里面,这个动作允许其他的Github Action来使用这个APKApp-bundle而不需要重新打包。

 - uses: actions/upload-artifact@v3
name: Upload APK
with:
name: apk
path: build/app/outputs/flutter-apk/app-release.apk

取决于存放keychain的方式,你可能还需要Keychain aliasKeychain password,这两个环境变量可以在Github Secrets里面设置以正确签署Android应用。

SIGN_KEYSTORE_PASSWORD: ${{ secrets.SIGN_KEYSTORE_PASSWORD }}
SIGN_KEYSTORE_ALIAS: ${{ secrets.SIGN_KEYSTORE_ALIAS }}
SIGN_PASSWORD: ${{ secrets.SIGN_PASSWORD }}

4. 自动发布版本

create-release:
# Creates a release when a commit is pushed to main
# This is done by the semantic-release-action
name: Create release
if: ${{ github.event.pusher.name != 'github action' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
needs: [build-android, build-ios]
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node JS
uses: actions/setup-node@v3
with:
node-version: 18
- name: Get next version
id: version
uses: cycjimmy/semantic-release-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
branch: main
dry_run: true
- name: Bump version
run: python3 scripts/update_version.py -v ${{ steps.version.outputs.new_release_version }} -b ${{github.run_number}}
- uses: EndBug/add-and-commit@v9
name: Add and commit version changed
with:
message: "Release ${{ steps.version.outputs.new_release_version }}"
push: false
if: ${{github.ref == 'refs/heads/main'}}
- name: Create Release
uses: cycjimmy/semantic-release-action@v3
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
branch: main
- uses: actions/download-artifact@v3
name: Download APK from artifact
with:
name: apk
if: ${{github.ref == 'refs/heads/main'}}
- uses: actions/download-artifact@v3
name: Download appbundle from artifact
with:
name: appbundle
if: ${{github.ref == 'refs/heads/main'}}
- uses: EndBug/add-and-commit@v9
name: Push release commit
with:
push: ${{ github.ref == 'refs/heads/main' }}
- name: Push release
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: main
force: true
if: ${{github.ref == 'refs/heads/main'}}
- name: Upload Release assets
uses: softprops/action-gh-release@v1
with:
files: app-release.apk,app-release.aab
tag_name: v${{ steps.version.outputs.new_release_version }}
if: ${{github.ref == 'refs/heads/main'}}

这个 Workflow 将会帮我们自动发布版本,当我们在main分支上提交代码时,Github Actions会自动帮我们打包Flutter app,并且自动发布版本。 当更新版本后,也同时会调用一个自定义脚本更新Pubspec.yaml中的版本号。这个脚本的内容如下:

# Read version and build number from command line
# update pubspec.yaml's version and build number
import argparse

def update_version(version: str, build_number: int):
with open("pubspec.yaml", "r") as read_file:
lines = read_file.readlines()
for i in range(len(lines)):
if "version:" in lines[i]:
new_version = f"version: {version}+{build_number}\n"
print(f"updating version to {new_version}")
lines[i] = new_version

with open("pubspec.yaml", "w") as write_file:
write_file.writelines(lines)



if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--version", type=str, help="version number", default="1.0.0")
parser.add_argument("-b", "--build-number", type=int, help="build number")
args = parser.parse_args()
update_version(args.version.replace("v", ""), args.build_number)

当自动脚本运行完毕后,我们可以在Github上看到自动发布的版本。

自动发布到 Google Play

1. 安装 Fastlane

gem install fastlane

2. 创建Fastfile

fastlane init

3. 配置Fastfile

打开你的fastlane/Fastfile,将下面的代码添加到Fastfile中。

note

注意,我们这里创建了三个lane,分别是internalbetadeploy,分别对应了Google Play上的三个渠道。

default_platform(:android)

platform :android do
desc "Runs all the tests"
desc "Submit a new internal Build"
lane :internal do
upload_to_play_store(track: 'internal', skip_upload_apk: true, aab: "../build/app/outputs/bundle/release/app-release.aab")
end

desc "Submit a new Beta Build"
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
print_command: false
)
upload_to_play_store(track: 'beta', skip_upload_apk: true, aab: "../build/app/outputs/bundle/release/app-release.aab")
end

desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease")
upload_to_play_store
end
end

4. 测试发布

danger

如果需要使用 API 发布,请至少用手动方式发布过一个在Closed Beta上。否则会报错。

fastlane internal

5. 自动发布

在这里我们创建了一个Github ActionsWorkflow,这个 CI 会自动在我们发布版本时,自动发布到Google Play上。

name: Publish app to Google Play Internal Testing
on:
release:
types:
- released

jobs:
bundle:
name: Generate App Bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter SDK
uses: flutter-actions/setup-flutter@v2
with:
channel: stable
version: 3.7.4
- name: Run setup script
run: ./scripts/setup_android.sh
env:
ENV_B64: ${{ secrets.ENV_B64 }}
DEBUG_KEY_B64: ${{ secrets.DEBUG_KEY_B64 }}
DEBUG_KEY_PROPERTY_B64: ${{ secrets.DEBUG_KEY_PROPERTY_B64 }}
AMAQ_B64: ${{ secrets.AMAQ_B64 }}
LOCAL_PROPERTIE_B64: ${{ secrets.LOCAL_PROPERTIE_B64 }}
PROD_KEY_B64: ${{ secrets.PROD_KEY_B64 }}
SERVICE_JSON_B64: ${{ secrets.SERVICE_JSON_B64 }}
- name: Install dependencies
run: flutter pub get
- name: Build appbundle
run: flutter build appbundle --release
- name: Publish to Google Play Internal Testing
run: fastlane internal
working-directory: ./android