Android 自動化上架:用 fastlane 把 AAB 自動上傳到 Google Play

這篇文章要解決一個很實際的問題:每次要發版,手動構建、上傳 AAB、填測試記錄,既重複又容易出錯。用 fastlane 把它自動化之後,本地一行指令、或是在 CI 推版自動觸發,就能把新版本送到 Google Play 的測試軌或正式版。

適用場景與核心目標

  • 適用:原生 Android、Flutter(build/app/outputs/bundle/release/...)、React Native 等任何能產出 AAB 的專案。
  • 核心目標:用 fastlane 把「構建 Release AAB → 上傳到 Google Play」這條路徑自動化,涵蓋本地執行與 GitLab CI/CD 兩種情境。

本文假設你已經有 Google Play Developer 帳號、在 Play Console 建立好 App,也能手動構出一個 Release AAB。如果 App 還是草稿狀態(商店資料、Data safety 沒填完),即使 automation 配好也可能發不出去,請先確認前置條件。

先看結論

整條流程長這樣:

1
2
3
1. Google Cloud Console:建 Project → 啟用「Google Play Android Developer API」→ 建 Service Account → 下載 JSON key
2. Google Play Console:邀請該 Service Account 帳號 → 給它最小必要權限
3. 本地 / CI:用 JSON key + fastlane 構建 AAB 並上傳到指定軌道

跟 iOS 用 App Store Connect 那套 API Key ID / Issuer ID / API Secret 不同,Google Play 這側只需要一個 Service Account JSON key,fastlane 直接讀整個檔案即可。

開始前確認你已經有:

  1. Google Play Developer 帳號
  2. 在 Play Console 建立好 App
  3. 包名(applicationId / package name)已確定
  4. 能構出 Release AAB
  5. 環境裝好 Ruby 與 fastlane

前置條件:先確認能手動構出 Release AAB

自動化之前,請先確保你能手動產出可用的 AAB。原生專案在專案根目錄執行:

1
./gradlew bundleRelease

成功後會產生(預設路徑):

1
app/build/outputs/bundle/release/app-release.aab

Flutter 則是:

1
build/app/outputs/bundle/release/app-release.aab

如果這一步就有問題(例如簽名、編譯錯誤),先把它修好再談自動化。

簽名:Release AAB 怎麼來

Release AAB 必須用你的私鑰簽過名。你需要一個 keystore 檔案(.jks)。如果還沒有,可以用 keytool 產生一支:

1
2
3
4
5
6
7
8
keytool -genkeypair -v \
-keystore release.jks \
-alias release-key \
-keyalg RSA \
-keysize 2048 \
-validity 36500 \
-storepass YOUR_STORE_PASSWORD \
-keypass YOUR_KEY_PASSWORD

接下來讓 Gradle 在構建 Release 時用它簽名。為了讓同一套設定能在本地與 CI 通用,建議用環境變數來讀簽名資訊,而不是硬編碼密碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/build.gradle
android {
...
signingConfigs {
release {
storeFile file(System.getenv('KEYSTORE_PATH') ?: "release.jks")
storePassword System.getenv('KEYSTORE_PASSWORD')
keyAlias System.getenv('KEY_ALIAS')
keyPassword System.getenv('KEY_PASSWORD')
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled false
}
}
}

這樣本地只要準備好 release.jks 並設定環境變數就能簽;CI 則在執行時再注入這些變數,密碼永遠不進 Git。

第一步:在 Google Cloud 建立專案與服務帳號

fastlane 要用 Service Account JSON key 來授權,這東西放在 Google Cloud Console:

  1. 開 Google Cloud Console,在頂部 Project selector 選 New project,命名例如 google-play-fastlane。
  2. 這個 Cloud Project 只用來承載 API 與服務帳號,不是你的 App。

啟用 Google Play Android Developer API

在 Cloud Console 搜尋 Google Play Android Developer API,進入頁面後點 Enable。

注意不要啟錯 API,你要的是 Google Play Android Developer API,不是 Android Management API、Google Maps API 或 Firebase API。沒啟用這個,後面就算 JSON 建好了 fastlane 也連不上。

建立 Service Account 並下載 JSON

進入 IAM & Admin → Service Accounts → Create service account:

1
2
Service account name: fastlane-supply
Description: Used by fastlane to upload Android releases to Google Play

建立後會得到一個帳號郵箱,格式類似:

1
fastlane-supply@your-project-id.iam.gserviceaccount.com

把它記下來,稍後要在 Play Console 邀請它。接著下載金鑰:

  1. 點進剛建立的 Service Account。
  2. Keys → Add key → Create new key → Key type: JSON → Create。
  3. 瀏覽器會下載一個 your-project-id-xxxxxx.json。

這個 JSON 裡已經包含 client_email、private_key、project_id 等認證資訊,fastlane 直接讀整個檔案。不要把裡面的 private_key 單獨拆出來,也不用去找額外的 api key。

第二步:在 Play Console 授權服務帳號

只在 Google Cloud 建好服務帳號還不夠,Play Console 必須顯式授權它,否則會報權限錯誤。

  1. 開 Google Play Console,選目標 App。
  2. Users and permissions → Invite new users,填入剛才的服務帳號郵箱。
  3. 按用途分配最小權限,常見幾種:
1
2
3
4
5
6
7
8
9
10
- 只上傳到測試軌(internal / closed / open):
View app information
Create and edit releases
Release apps to testing tracks

- 要發正式版:再加
Release apps to production

- 要管理商店頁文案、截圖、圖標:再加
Edit store listing

如果只是用 fastlane 上傳 AAB,不需要給 Edit store listing,更別開內購、訂單管理的權限。權限給太少會報這些錯:

1
2
3
The caller does not have permission
Insufficient permissions
Package not found

第三步:本地安裝與初始化 fastlane

如果還沒裝 fastlane:

1
gem install fastlane

團隊協建建議用 Bundler 固定版本。在專案根目錄:

1
bundle init

然後在 Gemfile 加入:

1
2
3
source "https://rubygems.org"

gem "fastlane"
1
bundle install

接著進入 Android 專案根目錄初始化:

1
2
cd your_android_project
fastlane init

它會偵測你的專案並產生 fastlane/ 資料夾,裡面有 Appfile 與 Fastfile。

第四步:設定 Appfile 與 Fastfile

Appfile 告訴 fastlane 包名與 JSON key 位置:

1
2
3
# fastlane/Appfile
package_name("com.yourcompany.yourapp")
json_key_file("/Users/yourname/.keys/play-store-credentials.json")

建議把 JSON key 放在專案外,例如 ~/.keys/play-store-credentials.json。如果一時必須放進專案,務必加進 .gitignore:

1
2
fastlane/*.json
*.jks

Fastfile 定義發布 lane。下面是一個上傳到內部測試軌的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# fastlane/Fastfile
default_platform(:android)

platform :android do
desc "Build signed release AAB and upload to internal testing"
lane :internal do
gradle(
task: "bundle",
build_type: "Release"
)
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
release_status: "draft"
)
end

desc "Build signed release AAB and upload to production"
lane :production do
gradle(task: "bundle", build_type: "Release")
upload_to_play_store(
track: "production",
release_status: "inProgress"
)
end
end

因為簽名設定在 build.gradle 的 signingConfigs.release,gradle bundle 構 Release 時就會自動簽名。如果已在 Appfile 寫入 json_key_file 與 package_name,這裡就不用重複寫。

Flutter 專案的 AAB 路徑不同(build/app/outputs/bundle/release/...),請在 lane 裡用 aab: 明確指定路徑。

執行(用 Bundler 時建議都加 bundle exec):

1
bundle exec fastlane android internal

驗證 JSON key 是否可用

上傳前可以先驗證格式與基礎認證沒問題:

1
fastlane run validate_play_store_json_key json_key:/Users/yourname/.keys/play-store-credentials.json

常用軌道與發布狀態

Google Play 常見軌道:

1
2
3
4
internal     內部測試
alpha 封閉測試(舊寫法)
beta 開放測試(舊寫法)
production 正式版

release_status 常見值:

1
2
3
4
draft       上傳後不會推給任何人,自己去 Play Console 檢查後再提交
inProgress 進入該軌道的審核/發布流程
completed 直接完成發布
halted 暫停

開發階段建議先用 draft,確認流程穩定後再考慮自動發布。正式環境剛接入 fastlane 時,completed 要謹慎使用。

versionCode 遞增:CI 必做的環節

Google Play 要求每個版本號(versionCode)必須唯一且遞增。手機時代你可能每次手動 +1,自動化後應該讓 CI 自動處理,否則會遇到 Version code has already been used。

最簡單的做法是在 workflow 裡讀取目前的 versionCode、加一後寫回:

1
2
3
4
CURRENT=$(grep -m1 'versionCode' app/build.gradle | grep -oE '[0-9]+' | head -1)
NEW=$((CURRENT + 1))
sed -i "s/versionCode ${CURRENT}/versionCode ${NEW}/" app/build.gradle
echo "新版本號:$NEW"

這個寫法假設 versionCode 定義在 app/build.gradle。如果你的版本號是用其他方式管理(例如 Flutter 在 pubspec.yaml,或用了 flavor),請改用對應的遞增方式;在 GitLab 中直接用上面那段 script 遞增即可。

安全:金鑰與 keystore 不要進 Git

這兩樣東西一旦提交到公開倉庫就等於暴露,務必小心:

  • JSON key:本地放 ~/.keys/;CI 裡整份內容存成倉庫 Secret,執行時再寫成檔案。
  • keystore 與密碼:keystore 轉 base64 存 Secret,簽名密碼也各存一個 Secret。

.gitignore 至少包含:

1
2
3
*.jks
fastlane/*.json
local.properties

CI/CD:用 GitLab CI/CD 自動發布

下面是一個完整範例:推 tag(例如 v1.2.0)時自動構建並上傳到正式版。把這個 .gitlab-ci.yml 放到專案根目錄即可。

GitLab 的機密資料放在 Settings → CI/CD → Variables,不是 GitHub 那套 Secrets。新增以下變數(建議勾選 Masked;keystore 與 JSON key 再勾選 Protected):

1
2
3
4
5
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON   # JSON key 整份內容
ANDROID_KEYSTORE_B64 # keystore 的 base64(請存成單行,不要換行)
ANDROID_KEYSTORE_PASSWORD # keystore 密碼
ANDROID_KEY_ALIAS # 鑰別名
ANDROID_KEY_PASSWORD # 私鑰密碼

Gemfile 與 Gemfile.lock 要提交到倉庫(fastlane 依賴):

1
2
3
# Gemfile
source "https://rubygems.org"
gem "fastlane"

CI job(.gitlab-ci.yml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
stages:
- deploy

deploy_play:
stage: deploy
# 需在專案 Settings → CI/CD → Runners 指定具有 JDK 17 + Ruby 的自管 runner,或在此加 runner tags
tags:
- android-runner
rules:
- if: '$CI_COMMIT_TAG'
script:
# 寫入 Google Play service account JSON key
- mkdir -p ~/.keys
- echo "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON" > ~/.keys/play-store-credentials.json

# 解碼 keystore(與 signingConfigs.release 的 KEYSTORE_PATH 對應)
- mkdir -p app
- echo "$ANDROID_KEYSTORE_B64" | base64 -d > app/release.jks

# 簽名環境變數(Gradle signingConfigs.release 會讀這些)
- export KEYSTORE_PATH=app/release.jks
- export KEYSTORE_PASSWORD="$ANDROID_KEYSTORE_PASSWORD"
- export KEY_ALIAS="$ANDROID_KEY_ALIAS"
- export KEY_PASSWORD="$ANDROID_KEY_PASSWORD"

# versionCode 自動遞增
- CURRENT=$(grep -m1 'versionCode' app/build.gradle | grep -oE '[0-9]+' | head -1)
- NEW=$((CURRENT + 1))
- sed -i "s/versionCode ${CURRENT}/versionCode ${NEW}/" app/build.gradle

# 構建並上傳 AAB
- bundle install
- bundle exec fastlane android production

幾個重點:

  • rules: if: '$CI_COMMIT_TAG' 表示只有推 tag 才觸發,等同 GitHub Actions 的 on: tags。要更精控(例如只在 main 分支發正式版、其他軌道另開 lane)可改用 when: $CI_COMMIT_BRANCH 等條件。
  • GitLab 變數用 $VAR 引用(跟 shell 一樣),不需要 ${{ secrets.X }} 這種語法。
  • keystore 解碼後放在 app/release.jks,對應前面 signingConfigs.release 的 KEYSTORE_PATH;存 base64 時請確保是單行,否則 base64 -d 會失敗。
  • 這個 job 假設 runner 已裝好 JDK 17 與 Ruby(自管 runner 最省事)。若想要容器化,可改用 image: 指定鏡像並自行安裝工具鏈,或寫一個 Dockerfile。

常見問題除錯

1. The caller does not have permission / Insufficient permissions
多半是 Play Console 沒邀請服務帳號,或沒給對應 App 的發布權限。回到 Users and permissions 檢查。

2. Package not found
檢查 package_name 是否寫錯、App 是否真的建立、服務帳號是否有該 App 的權限。

3. Version code has already been used
versionCode 重複了。CI 環境務必加上自動遞增那一步;本地手動發版時記得先 +1。

4. 上傳成功但 Play Console 看不到正式版
檢查:上傳到哪個 track、release_status 是 draft 還是 completed、Managed publishing 是否開啟、App dashboard 是否還有必填項未完成。

5. JSON key 驗證通過卻還是連不上
確認 Google Play Android Developer API 有啟用,且 Play Console 已邀請該帳號並授權目標 App。

上線前檢查清單

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Google Cloud:
[ ] 建立 Cloud Project
[ ] 啟用「Google Play Android Developer API」
[ ] 建立 Service Account
[ ] 下載 JSON key(放到 ~/.keys/,不要進 Git)

Play Console:
[ ] 邀請服務帳號郵箱
[ ] 給目標 App 最小必要權限

本地:
[ ] 能手構出簽名過的 Release AAB
[ ] signingConfig.release 可用環境變數讀到簽名
[ ] Appfile 設定 package_name 與 json_key_file
[ ] Fastfile 定義 internal / production lane
[ ] validate_play_store_json_key 通過

GitLab CI/CD:
[ ] 在 Settings → CI/CD → Variables 新增所有變數
[ ] Gemfile 與 Gemfile.lock 已提交
[ ] keystore 解碼路徑與 KEYSTORE_PATH 一致
[ ] versionCode 自動遞增
[ ] 推 tag 能觸發並成功上傳

參考資料