AndroidGitHub ActionsGoogle PlayCI/CDKotlin

使用 GitHub Actions 构建、签名并自动部署 Android 应用到 Google Play Console

Sloth255
Sloth255
·4 min read·770 words

前言

你是否还在通过手动点击 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) Google 从 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 下丢失可申请重置)

有两个重要特性:

  1. Play Console 绝对不接受使用调试签名的 AAB
    → 因此 CI 中需要单独准备 release.keystore
  2. 调试签名与 Release 签名完全独立
    → 为 CI 添加 Release 签名配置对本地的 ./gradlew assembleDebug 没有任何影响

第 5、6 节中对 signingConfigs.create("release") 进行条件判断,正是利用了这两个特性——实现了没有密钥的开发者也能正常运行调试构建的状态。

前提条件

  • Android 应用(使用 build.gradlebuild.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 错误,请优先完成。

  1. 访问 Google Play Android Developer API
  2. 点击启用

3-2. 在 Google Cloud Console 中创建服务账号

  1. 访问 Google Cloud Console
  2. 创建项目(或选择已有项目)
  3. IAM 和管理 → 服务账号 中新建服务账号。不要分配 IAM 角色(权限在 Play Console 侧管理)
  4. 创建后,选择添加密钥 → 创建新密钥 → JSON,下载 JSON 文件

3-3. 在 Play Console 中授予权限

  1. 打开 Play Console
  2. 打开用户和权限,点击邀请新用户
  3. 输入第 3-2 步创建的服务账号邮箱地址
  4. 应用权限选项卡中添加目标应用,并授予以下权限:
    • 发布到测试轨道 — 发布到内部测试轨道所需
    • 查看应用信息和下载批量报告 — 读取权限

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 申请重置

  1. 在本地创建新的 Keystore

  2. 以 PEM 格式导出证书

    keytool -export -rfc \
      -keystore release.keystore \
      -alias my-release-key \
      -file upload_certificate.pem
    
  3. 在 Play Console → 设置 → 应用完整性 → 应用签名 → 申请重置上传密钥 中上传证书

  4. Google 支持团队审核(通常 1~2 个工作日)

  5. 审批后,使用新密钥签名的 AAB 即可被接受

旧方式中"密钥丢失意味着永远无法更新同一包名的应用"——这是无法挽救的绝境。仅凭这一救援机制,Play App Signing 就值得使用

7-4. 迁移现有(旧方式)应用

对于 2021 年 8 月之前创建且尚未迁移到 Play App Signing 的应用,需要将当前使用的签名密钥交给 Google

  1. Play Console → 目标应用 → 设置 → 应用完整性 → 应用签名

  2. 下载 Google 提供的 PEPK(Play Encrypt Private Key)工具

  3. 使用 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
    
  4. 将输出文件(encrypted-key.zip)上传到 Play Console

  5. 迁移后,根据需要创建并注册新的上传密钥(也可继续使用现有密钥作为上传密钥)

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 可改为 alphabetaproduction
  • 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_providerservice_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 / 签名相关

Google Play Console

GitHub Actions

Gradle