前言
你是否还在通过手动点击 Android Studio 的 Generate Signed Bundle 按钮,等待构建完成后再打开 Play Console 拖拽上传 AAB 来发布 Android 应用?
我也这样做了一段时间,但随着发布频率增加,遇到了以下问题:
- 本地环境差异导致构建失败(JDK 版本不匹配、Gradle 缓存问题)
- Keystore 管理孤岛化(只存在于某个开发者的 Mac 上)
- 发布流程依赖文档,容易产生人为失误
为了一次性解决这些问题,我使用 GitHub Actions 自动化了整个流程——构建、签名和上传到 Google Play Console。以下是具体步骤。
整体流程
最终构建的流水线如下所示:
flowchart TD
A["推送 git tag v1.0.0"]
B["触发 GitHub Actions"]
C["配置 JDK 并恢复 Gradle 缓存"]
D["从 Secrets 中恢复 Keystore(即上传密钥)"]
E["通过 ./gradlew bundleRelease 构建并签名 AAB"]
F["通过 r0adkll/upload-google-play 发布到内部测试"]
G["Google 接收后用应用签名密钥重新签名并分发给用户"]
A --> B --> C --> D --> E --> F --> G"AAB"和"上传密钥"等术语将在下一节详细说明。
需要提前了解的 3 个核心概念
在进入具体步骤之前,先梳理本文涉及的关键术语。如果对这些概念模糊,后面的 Play App Signing 部分可能会让人困惑。
APK 与 AAB 的区别
| APK (Android Package Kit) | AAB (Android App Bundle) | |
|---|---|---|
| 内容 | 将所有 ABI、屏幕密度和语言资源打包成一个通用文件 | 针对特定设备优化之前的构建产物 |
| 文件大小 | 因包含所有资源而偏大 | Google Play 生成 Split APK 进行分发,用户下载大小平均减少约 15% |
| 主要用途 | 直接分发 / 内部分发 / 第三方应用商店 | Google Play Store 分发(2021 年 8 月后的新应用为必须) |
| Gradle 任务 | ./gradlew assembleRelease |
./gradlew bundleRelease |
简而言之:"APK = 可直接安装到用户设备的成品","AAB = 提交给 Google Play 的设计蓝图"。Google Play 接收 AAB 后,针对每个用户的设备动态生成对应的 APK 进行分发。
本文目标是 Play Store 分发,因此使用 AAB(bundleRelease)。如需用于内部分发等场景,也可以用 assembleRelease 构建 APK。
上传密钥与应用签名密钥
Play App Signing 中存在两种签名密钥:
| 密钥类型 | 由谁管理? | 用途 |
|---|---|---|
| 上传密钥(Upload Key) | 开发者 | 上传 AAB 到 Play Console 时使用的签名 |
| 应用签名密钥(App Signing Key) | 从 Play Store 分发给用户的最终 APK 的签名 |
也就是说,Play Console 会验证"该 AAB 是否使用了正确的上传密钥进行签名",因此开发者需要使用自己持有的上传密钥 = Keystore 进行签名。Google 代劳的只是在用户分发时用应用签名密钥重新签名这一步。
flowchart TD
Dev["开发者"] -->|"用上传密钥签名"| AAB["AAB"]
AAB --> Play["Play Console"]
Play --> Verify["Google 验证上传密钥"]
Verify --> Resign["Google 用应用签名密钥重新签名"]
Resign --> User["用户"]即使使用了 Play App Signing,"开发者仍需自行创建 Keystore"这一事实并未改变。改变的只是"上传密钥丢失后可申请重置"这一安全保障。
调试签名与 Release 签名的区别
第一次在 Android Studio 中点击 Run 按钮时,无需任何配置就能构建 APK 并在设备上运行,这是因为 AGP(Android Gradle Plugin)自动生成了 ~/.android/debug.keystore 并自动对调试构建进行签名。
| 调试签名 | Release 签名 | |
|---|---|---|
| Keystore | ~/.android/debug.keystore(AGP 自动生成) |
自行创建和管理(本文中的 release.keystore) |
| 密码 | 固定为 android |
自行设置 |
| 别名 | 固定为 androiddebugkey |
自行设置 |
| 有效期 | 约 30 年 | 自行设置(本文约 27 年) |
| 用途 | 开发调试 / ./gradlew assembleDebug |
Play Store 分发 / ./gradlew bundleRelease |
| Git 管理 | 不需要(每台机器上有即可) | 需妥善保管(Play App Signing 下丢失可申请重置) |
有两个重要特性:
- Play Console 绝对不接受使用调试签名的 AAB
→ 因此 CI 中需要单独准备release.keystore - 调试签名与 Release 签名完全独立
→ 为 CI 添加 Release 签名配置对本地的./gradlew assembleDebug没有任何影响
第 5、6 节中对 signingConfigs.create("release") 进行条件判断,正是利用了这两个特性——实现了没有密钥的开发者也能正常运行调试构建的状态。
前提条件
- Android 应用(使用
build.gradle或build.gradle.kts) - GitHub 仓库
- Google Play Console 开发者账号
- 目标应用已在 Play Console 上手动完成首次发布(无法通过 API 进行首次创建)
- 目标应用已启用 Play App Signing
2021 年 8 月后创建的新应用会自动启用 Play App Signing,无需额外操作。此前使用旧方式创建的应用,需要先迁移到 Play App Signing(详见第 7-4 节)。
1. 创建上传密钥(Keystore)
在本地生成用于签名 AAB 的 Keystore。在 Play App Signing 配置中,该密钥作为"上传密钥"使用(即向 Play Console 提交 AAB 时的身份凭证)。最终分发给用户时的签名由 Google 持有的"应用签名密钥"完成,因此即使丢失此密钥,也可向 Play Console 申请重置(详见 7-3)。
不过,日常运维中若丢失密钥,在重置完成前将无法发布,因此还是要妥善保管。
# macOS / Linux
keytool -genkey -v \
-keystore release.keystore \
-alias my-release-key \
-keyalg RSA \
-keysize 2048 \
-validity 10000
Windows(PowerShell / 命令提示符)不支持反斜杠换行,请在一行中执行,或在 PowerShell 中使用反引号 ` 换行。
keytool -genkey -v `
-keystore release.keystore `
-alias my-release-key `
-keyalg RSA `
-keysize 2048 `
-validity 10000
按提示交互式输入密码、姓名、组织名称等信息。
2. 将 Keystore 进行 Base64 编码
GitHub Secrets 不能直接存储二进制文件,需将其转换为 Base64 字符串。工作流在解码时会用 tr -d '\n\r' 去除换行符,但输出时不含换行更为安全。
macOS
base64 -i release.keystore -o release.keystore.base64
cat release.keystore.base64 | pbcopy
Linux
base64 -w 0 release.keystore > release.keystore.base64
cat release.keystore.base64 | xclip -selection clipboard
Windows(PowerShell)
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
| Set-Clipboard
若要写入文件:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) `
| Out-File -Encoding ascii -NoNewline release.keystore.base64
Windows(命令提示符)
可以使用 certutil,但会附带头部、尾部和换行符,需要手动去除。
certutil -encode release.keystore release.keystore.base64
删除输出文件中的 -----BEGIN CERTIFICATE----- 行和 -----END CERTIFICATE----- 行以及所有换行符后再使用。建议直接使用 PowerShell。
将复制到剪贴板的字符串粘贴到下一步的 GitHub Secrets 中。
3. 为 Google Play Console 创建服务账号
向 Play Console 自动上传需要 Google Cloud 服务账号。
3-1. 启用 Google Play Android Developer API
首先在 GCP 侧启用 API。跳过此步骤将在后续遇到 403 错误,请优先完成。
3-2. 在 Google Cloud Console 中创建服务账号
- 访问 Google Cloud Console
- 创建项目(或选择已有项目)
- 在 IAM 和管理 → 服务账号 中新建服务账号。不要分配 IAM 角色(权限在 Play Console 侧管理)
- 创建后,选择添加密钥 → 创建新密钥 → JSON,下载 JSON 文件
3-3. 在 Play Console 中授予权限
- 打开 Play Console
- 打开用户和权限,点击邀请新用户
- 输入第 3-2 步创建的服务账号邮箱地址
- 在应用权限选项卡中添加目标应用,并授予以下权限:
- 发布到测试轨道 — 发布到内部测试轨道所需
- 查看应用信息和下载批量报告 — 读取权限
4. 在 GitHub Secrets 中注册相关值
在仓库的 Settings → Secrets and variables → Actions 中注册以下内容:
| Secret 名称 | 内容 |
|---|---|
KEYSTORE_BASE64 |
第 2 步获取的 Base64 字符串 |
KEYSTORE_PASSWORD |
Keystore 密码 |
KEY_ALIAS |
例:my-release-key |
KEY_PASSWORD |
密钥密码 |
SERVICE_ACCOUNT_JSON |
直接粘贴第 3-2 步下载的 JSON 文件内容 |
5. 在 build.gradle 中配置从环境变量读取签名信息
配置方式:CI 通过环境变量读取,本地开发通过 keystore.properties 读取,并且即使没有 Keystore 的环境也能正常运行调试构建。
按如下方式编辑 app/build.gradle.kts:
import java.util.Properties
import java.io.FileInputStream
// --- 解析签名信息 ---
// 优先级:环境变量(CI)> keystore.properties(本地)> 无(仅调试构建)
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties().apply {
if (keystorePropertiesFile.exists()) {
load(FileInputStream(keystorePropertiesFile))
}
}
fun resolveSigning(key: String, envKey: String): String? =
System.getenv(envKey) ?: keystoreProperties.getProperty(key)
val releaseStoreFile = resolveSigning("storeFile", "KEYSTORE_FILE")
val releaseStorePassword = resolveSigning("storePassword", "KEYSTORE_PASSWORD")
val releaseKeyAlias = resolveSigning("keyAlias", "KEY_ALIAS")
val releaseKeyPassword = resolveSigning("keyPassword", "KEY_PASSWORD")
val hasReleaseSigning = listOf(
releaseStoreFile, releaseStorePassword, releaseKeyAlias, releaseKeyPassword
).all { !it.isNullOrBlank() }
android {
signingConfigs {
if (hasReleaseSigning) {
create("release") {
storeFile = file(releaseStoreFile!!)
storePassword = releaseStorePassword
keyAlias = releaseKeyAlias
keyPassword = releaseKeyPassword
}
}
}
buildTypes {
getByName("debug") {
// 添加 applicationIdSuffix,让 release 版和 debug 版
// 可以在同一设备上共存
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
// signingConfig 使用 AGP 自动生成的 debug.keystore
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// 仅在有签名信息时分配 release 用的 signingConfig
//(= 没有密钥的开发者仍可正常运行 assembleDebug)
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
}
}
}
代码要点如下:
- 对
signingConfigs.create("release")进行了守卫判断,没有签名信息的环境不会创建 release 配置 debug构建类型使用 AGP 自动生成的debug.keystore,完全不受 release 签名设置影响- 环境变量优先级最高,CI 上无需放置
keystore.properties文件
6. 避免破坏本地开发环境的额外配置
"为 CI 修改了 build.gradle 后,本地调试构建无法通过"是 Android × CI 中常见的事故。为防止此类情况,需要进行以下配置。
6-1. 创建 keystore.properties(仅本地使用)
local.properties 本来是用来写 SDK 路径的文件,将签名信息混入其中会使文件职责不清。为签名信息单独创建 keystore.properties 是 Android 官方文档推荐的方式。
在项目根目录(app/ 的上一级)创建 keystore.properties:
storeFile=/Users/yourname/keys/release.keystore
storePassword=your-store-password
keyAlias=my-release-key
keyPassword=your-key-password
此文件绝对不能提交到 Git,请添加到 .gitignore(见后文)。
6-2. 需要添加到 .gitignore 的内容
Android Studio 生成的 .gitignore 中不包含签名相关条目,很容易被遗漏。请明确添加:
# 签名相关 - 绝对不要提交
*.keystore
*.jks
keystore.properties
release.keystore.base64
# 服务账号 JSON
*-service-account*.json
play-publisher.json
# google-github-actions/auth 生成的临时认证文件(使用 WIF 时)
gha-creds-*.json
# Android Studio 会创建,但以防万一也加上
local.properties
local.properties 通常由 Android Studio 自动添加,但旧项目中可能遗漏,请确认。
6-3. 通过 applicationIdSuffix 让 release 版和 debug 版共存
上面 build.gradle.kts 中的
debug {
applicationIdSuffix = ".debug"
}
这样设置,是为了让设备上可以同时安装 Play Store 上的 release 版(已发布版本)和本地构建的 debug 版。没有这个设置,在开发机上安装 debug 构建的瞬间就会覆盖 Play Store 版本。
虽然与 CI 没有直接关系,但一旦开始自动化发布,在设备上保留 Play Store 版本的同时进行开发的机会会增多,强烈建议一并配置。
6-4. 使用 ~/.gradle/gradle.properties 的选项
另一种选择是将签名信息写入主目录的 Gradle 属性文件:
# macOS/Linux: ~/.gradle/gradle.properties
# Windows: %USERPROFILE%\.gradle\gradle.properties
MYAPP_KEYSTORE_FILE=/Users/yourname/keys/release.keystore
MYAPP_KEYSTORE_PASSWORD=xxxx
MYAPP_KEY_ALIAS=my-release-key
MYAPP_KEY_PASSWORD=xxxx
在 build.gradle.kts 中使用 findProperty("MYAPP_KEYSTORE_FILE") as String? 读取。
优点是项目目录中完全不存放敏感信息。缺点是向新开发者传达配置步骤时略显麻烦。个人建议:个人开发使用 ~/.gradle/gradle.properties,团队开发使用 .gitignore 保护的 keystore.properties。
6-5. 在 Android Studio 中测试未签名的 Release 构建
./gradlew assembleDebug 随时可以运行,但有时也需要无签名运行 ./gradlew assembleRelease(例如检查 release 构建的 ProGuard 设置)。
此时,当 hasReleaseSigning 为 false 时,release 构建类型不会有 signingConfig,运行 assembleRelease 会生成未签名的 APK(无法安装到设备,但可检查大小和 mapping.txt)。
7. Play App Signing 运维补充说明
通过第 1~6 步,实际上 Play App Signing 的配置已经完成。第 1 步创建的 release.keystore 在 Play App Signing 的语境中作为上传密钥使用。
本节整理了运维过程中有用的补充知识。
7-1. 首次上传时会发生什么
对于新应用,首次将 AAB 上传到 Play Console 时:
- 该 AAB 使用的证书会被自动注册为"上传密钥证书"
- Google 自动生成新的"应用签名密钥"
- Play App Signing 被激活
因此无需额外的 Play Console 配置,按本文步骤操作即可完成 Play App Signing 配置。
flowchart TD
CI["CI"] -->|"用 release.keystore(即上传密钥)签名"| AAB["AAB"]
AAB --> Play["Play Console"]
Play --> Verify["Google 验证上传密钥证书"]
Verify --> Resign["Google 用应用签名密钥重新签名"]
Resign --> Device["用户设备"]7-2. 进一步提升安全性:创建专用上传密钥
即使是新应用,也可以在 Play Console 中选择"单独注册上传密钥"的配置方式。
- 首次上传时,在 Play Console 中选择"使用 Google 生成的应用签名密钥"
- 在同一界面单独上传上传密钥证书
此时,release.keystore 中存放的是"完全专用于上传的密钥"。
| 标准方式(7-1) | 分离密钥方式(7-2) | |
|---|---|---|
release.keystore 的内容 |
上传密钥 = 首次签名 AAB 的密钥 | 纯粹的上传专用密钥 |
| 万一泄露 | 可通过重置申请恢复 | 可通过重置申请恢复 |
| 安全级别 | 足够 | 更严格 |
两种方式的 CI 步骤完全相同,只是 release.keystore 的创建方式不同。
7-3. 上传密钥丢失怎么办
这是 Play App Signing 最大的优势。可以从 Play Console 申请重置。
-
在本地创建新的 Keystore
-
以 PEM 格式导出证书
keytool -export -rfc \ -keystore release.keystore \ -alias my-release-key \ -file upload_certificate.pem -
在 Play Console → 设置 → 应用完整性 → 应用签名 → 申请重置上传密钥 中上传证书
-
Google 支持团队审核(通常 1~2 个工作日)
-
审批后,使用新密钥签名的 AAB 即可被接受
旧方式中"密钥丢失意味着永远无法更新同一包名的应用"——这是无法挽救的绝境。仅凭这一救援机制,Play App Signing 就值得使用。
7-4. 迁移现有(旧方式)应用
对于 2021 年 8 月之前创建且尚未迁移到 Play App Signing 的应用,需要将当前使用的签名密钥交给 Google:
-
Play Console → 目标应用 → 设置 → 应用完整性 → 应用签名
-
下载 Google 提供的 PEPK(Play Encrypt Private Key)工具
-
使用 PEPK 工具以加密形式导出当前 Keystore
java -jar pepk.jar \ --keystore=existing-release.keystore \ --alias=my-release-key \ --output=encrypted-key.zip \ --include-cert \ --rsa-aes-encryption \ --encryption-key-path=public-key.pem -
将输出文件(
encrypted-key.zip)上传到 Play Console -
迁移后,根据需要创建并注册新的上传密钥(也可继续使用现有密钥作为上传密钥)
public-key.pem 是保存了 Play Console 迁移页面上显示的公钥的文件。
迁移完成后,即可直接使用本文第 1~6 步的操作。
8. 编写 GitHub Actions 工作流
终于到了正题。创建 .github/workflows/release.yml:
name: Android Release
on:
push:
tags:
- 'v*'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > ${{ github.workspace }}/release.keystore
- name: Build Release AAB
env:
KEYSTORE_FILE: ${{ github.workspace }}/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: ./gradlew bundleRelease --no-daemon
- name: Upload AAB as artifact
uses: actions/upload-artifact@v4
with:
name: release-aab
path: app/build/outputs/bundle/release/app-release.aab
- name: Deploy to Play Store (Internal Track)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
tracks: internal
status: completed
whatsNewDirectory: distribution/whatsnew
要点说明
- Tag 触发器(
v*)防止每次推送 main 分支时意外触发部署 - Gradle 缓存缩短构建时间(首次约 6 分钟,之后约 2 分钟)
- Artifact 上传作为备份——即使 Play Console 上传失败,也能从 Actions 界面手动下载 AAB
tracks: internal可改为alpha、beta、production等whatsNewDirectory是按语言存放发布说明的目录,例如distribution/whatsnew/whatsnew-ja-JP
8-2. 更安全的配置:使用 Workload Identity Federation
相比在 GitHub Secrets 中存储长期有效的 JSON 密钥,Workload Identity Federation(WIF) 允许 GitHub Actions 获取临时凭证来访问 Play Console。这消除了 JSON 密钥泄露的风险,在正式环境中推荐使用 WIF。
前期准备(GCP 侧)
WIF 的配置只需进行一次。参照 google-github-actions/auth 的文档进行配置。采用此方式不需要为服务账号生成 JSON 密钥,因此第 3-2 节的第 4 步(创建并下载密钥)以及第 4 节中注册 SERVICE_ACCOUNT_JSON 的步骤均不再需要。
使用以下 gcloud 命令进行配置,将占位符替换为实际值:
| 占位符 | 说明 |
|---|---|
${PROJECT_ID} |
GCP 项目 ID |
${GITHUB_ORG} |
GitHub 组织名或用户名 |
${REPO} |
org/repo 格式的仓库名称 |
${SERVICE_ACCOUNT} |
第 3-2 步创建的服务账号名称(邮箱 @ 左侧部分) |
${WORKLOAD_IDENTITY_POOL_ID} |
第 2 步获取的 Pool 完整 ID |
# 1. 创建 Workload Identity Pool
gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="GitHub Actions Pool"
# 2. 获取 Pool 的完整 ID
gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \
--location="global" \
--format="value(name)"
# 例:projects/123456789/locations/global/workloadIdentityPools/github
# 3. 创建 Workload Identity Provider
# --attribute-condition 限制只允许来自该组织的 Token 进入 Pool
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition="assertion.repository_owner == '${GITHUB_ORG}'"
# 4. 为服务账号授予 roles/iam.workloadIdentityUser
# 通过 principalSet 的 attribute.repository 限制为特定仓库
# ${WORKLOAD_IDENTITY_POOL_ID} = 第 2 步获取的完整 ID
gcloud iam service-accounts add-iam-policy-binding \
"${SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" \
--project="${PROJECT_ID}" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
配置完成后,记录 Provider 的完整 ID(projects/.../providers/...),在 GitHub Actions YAML 的 workload_identity_provider 字段中使用。
工作流 YAML(WIF 版本)
在仓库中添加 id-token: write 权限和 google-github-actions/auth 步骤:
name: Android Release
on:
push:
tags:
- 'v*'
permissions:
contents: read
id-token: write # WIF Token 获取所需
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle packages
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > ${{ github.workspace }}/release.keystore
- name: Build Release AAB
env:
KEYSTORE_FILE: ${{ github.workspace }}/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: ./gradlew bundleRelease --no-daemon
- name: Upload AAB as artifact
uses: actions/upload-artifact@v4
with:
name: release-aab
path: app/build/outputs/bundle/release/app-release.aab
- name: Authenticate to Google Cloud (WIF)
uses: google-github-actions/auth@v3
id: auth
with:
workload_identity_provider: projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID
service_account: SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com
- name: Deploy to Play Store (Internal Track)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
tracks: internal
status: completed
whatsNewDirectory: distribution/whatsnew
workload_identity_provider 和 service_account 请替换为 WIF 配置后在 GCP 控制台确认的值。与 JSON 密钥版本相比,唯一的区别是将凭证文件路径传递给 serviceAccountJson 而非 serviceAccountJsonPlainText,其他步骤完全相同。
9. 创建发布说明目录
在仓库根目录创建 distribution/whatsnew/,放置以下文件:
distribution/whatsnew/
├── whatsnew-en-US
└── whatsnew-ja-JP
每个文件包含对应语言的发布说明(500 字符以内)。
10. 运行测试
到这里,只需推送 Tag 即可:
git tag v1.0.0
git push origin v1.0.0
打开 GitHub 的 Actions 标签页,工作流会开始运行。成功后,Play Console 的内部测试中会出现新的发布版本。
11. 常见陷阱及解决方案
分享几个实际运维中踩过的坑:
忘记递增 versionCode
Play Console 不允许重复的 versionCode。使用 GitHub Actions 的运行编号(GITHUB_RUN_NUMBER)自动生成序号可以避免此类事故。versionName 使用去掉 v 前缀的 Tag 名称。
android {
defaultConfig {
// CI 中使用 GITHUB_RUN_NUMBER 作为 versionCode(本地回退到 1)
versionCode = (System.getenv("GITHUB_RUN_NUMBER")?.toInt() ?: 1) + 1000
// CI 中使用 git tag(v1.0.0 → 1.0.0)作为 versionName
versionName = System.getenv("GITHUB_REF_NAME")?.removePrefix("v") ?: "1.0.0-local"
}
}
服务账号权限生效延迟
授权后立即执行 Actions 可能出现 403 The caller does not have permission 错误。配置变更需要一些时间生效,第一次运行时请耐心等待。
Base64 解码时混入换行符
base64 命令在不同操作系统上的行为不同:
- macOS:每 76 个字符自动换行 → 使用
-i/-o获得完整输出 - Linux:默认添加换行 → 使用
-w 0指定无换行 - Windows(
certutil):包含头部、尾部和换行 → 需要后处理;推荐使用 PowerShell 的[Convert]::ToBase64String
粘贴到 GitHub Secrets 之前,务必确认没有混入换行符。 在 CI 解码时添加 tr -d '\n\r' 是标准做法。
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | tr -d '\n\r' | base64 --decode > release.keystore
结语
引入 GitHub Actions 后,发布工作变为:
"打 Tag,推送即可"
即使增加发布频率,运维负担几乎为零。
对于多人开发的团队,仅凭解决**"只有那台 Mac 才能发布"的问题**,就已经值得引入。如果你在 Android 应用开发中遇到同样的烦恼,不妨试试看。
参考链接
Android / 签名相关
- 为应用签名 | Android Developers
- 关于 Android App Bundle | Android Developers
- APK 与 AAB 对比 | Android Developers
- 配置构建 | Android Developers
- Gradle 提示和诀窍(从 Git 中排除签名信息)| Android Developers
Google Play Console
- 使用 Play 应用签名 | Play Console 帮助
- Google Play Developer API
- Google Play Developer API 入门 | Android Developers
GitHub Actions
- actions/checkout
- actions/setup-java
- actions/cache
- actions/upload-artifact
- r0adkll/upload-google-play
- 加密密钥 | GitHub 文档
