安卓自动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
。
步骤 1: 输入你的Service account name
,并且点击Next
。
步骤 2: 选择Service account user
,并且点击Next
。
步骤 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
。
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 里面,结果如下:
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
成为APK
和 App-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
后,会把打包好的APK
和App-bundle
上传到Github Actions
里面,这个动作允许其他的Github Action
来使用这个APK
和App-bundle
而不需要重新打包。
- uses: actions/upload-artifact@v3
name: Upload APK
with:
name: apk
path: build/app/outputs/flutter-apk/app-release.apk
取决于存放keychain
的方式,你可能还需要Keychain alias
和Keychain 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
中。
注意,我们这里创建了三个lane
,分别是internal
、beta
、deploy
,分别对应了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. 测试发布
如果需要使用 API 发布,请至少用手动方式发布过一个在Closed Beta
上。否则会报错。
fastlane internal
5. 自动发布
在这里我们创建了一个Github Actions
的Workflow
,这个 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