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 APIGoogle Maps APIFirebase 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_emailprivate_keyproject_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/ 資料夾,裡面有 AppfileFastfile

第四步:設定 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.gradlesigningConfigs.releasegradle bundle 構 Release 時就會自動簽名。如果已在 Appfile 寫入 json_key_filepackage_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 # 私鑰密碼

GemfileGemfile.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.releaseKEYSTORE_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 能觸發並成功上傳

參考資料