前言:當建置一直紅燈
CI/CD pipeline 亮紅燈是開發日常,但連續遇到三個不同層面的問題,從 iOS codesigning、Android Gradle、到 Google Play API,這就值得記錄下來了。
這篇文章記錄我在同一天內遇到的三個建置失敗,以及逐步排除的過程。每個問題都有其獨特的根因,但也反映出 CI/CD 環境的複雜性。
問題一:iOS Keychain 解鎖失敗
症狀
Jenkins 建置在 iOS 階段失敗,錯誤訊息:
[!] Error unlocking keychain at path: fastlane_keychain
Command failed with exit status 51
macOS Keychain 運作機制
在深入問題之前,先了解 macOS Keychain 的運作方式:
關鍵概念:
- macOS 可以有多個 Keychain,每個都有獨立密碼
- 憑證必須在「已解鎖」的 Keychain 中才能被
codesign使用 - CI 環境通常會建立專用的 Keychain,避免影響系統 Keychain
Fastlane Match 與 Keychain 的互動流程
調查過程
Exit status 51 代表「密碼錯誤」。SSH 進 Jenkins Mac mini 確認:
# 確認 keychain 存在
security list-keychains
# 輸出包含 fastlane_keychain-db
# 確認憑證存在
security find-identity -v -p codesigning fastlane_keychain
# 輸出:Apple Distribution: XXXXXXX Inc. (XXXXXXXX)
問題出在 Fastfile 的邏輯:
unlock_keychain(
path: "fastlane_keychain",
password: ENV['KEYCHAIN_PASSWORD'] || "temp_password",
set_default: false
)
根本原因
密碼來源的優先順序問題:
Keychain 實際密碼是 temp_password,但 Jenkins Credential KEYCHAIN_PASSWORD 被設定成其他值。Ruby 的 || 運算子在 ENV['KEYCHAIN_PASSWORD'] 有值時不會 fallback。
解決方案
直接硬編碼密碼,繞過可能設定錯誤的環境變數:
unlock_keychain(
path: "fastlane_keychain",
password: "temp_password", # 直接使用,避免 credential 設定錯誤
set_default: false
)
同時修復 unsetup_ci 函式,避免刪除持久的 keychain:
def unsetup_ci
# 只刪除 setup_ci 創建的臨時 keychain
temp_keychain = "fastlane_tmp_keychain"
if File.exist?(File.expand_path("~/Library/Keychains/#{temp_keychain}-db"))
delete_keychain(name: temp_keychain)
end
end
教訓
CI 環境的 credential 管理要有單一真相來源。 當 keychain 密碼同時存在於 Jenkins Credentials 和程式碼中,不一致就會發生。
問題二:file_picker Gradle 相容性
症狀
iOS 建置成功後,Android 建置失敗:
A problem occurred evaluating project ':file_picker'.
> Cannot invoke method substring() on null object
Build file: file_picker-10.3.8/android/build.gradle line: 30
Flutter Plugin 版本解析流程
根本原因
這是 Flutter 版本相容性問題。file_picker 10.3.x 使用了 Flutter 3.27+ 才有的 Gradle 屬性,但 Jenkins 環境使用的是 Flutter 3.24.5。
解決方案
在 pubspec.yaml 中鎖定版本:
dependencies:
file_picker: 10.1.9 # 移除 ^,鎖定相容版本
教訓
版本約束要考慮 CI 環境的 Flutter 版本。 使用 ^ 語義版本時,要注意 minor version 升級可能帶來的相容性問題。
問題三:Google Play 草稿狀態限制
症狀
iOS 上傳 TestFlight 成功,Android AAB 也建置成功,但上傳 Google Play 失敗:
Google Api Error: Invalid request - Only releases with status draft
may be created on draft app.
Google Play Console App 狀態機
根本原因
STG app 在 Google Play Console 中尚未完成首次發布流程,還處於「草稿狀態」。在這個狀態下,Google Play API 不允許創建 completed 狀態的版本。
解決方案
將 release_status 改為 draft:
supply(
package_name: "com.example.app.stg",
aab: "../build/app/outputs/bundle/stagingRelease/app-staging-release.aab",
track: 'internal',
release_status: 'draft', # 草稿 app 只能用 draft
json_key_data: ENV['SUPPLY_JSON_KEY_DATA']
)
教訓
不同環境的 app 可能處於不同的 Play Console 狀態。 Production app 已經發布過,可以用 completed;但 STG/DEV app 可能還是草稿。
總結:CI/CD 的複雜性
一天內遇到三個不同層面的問題:
| 層面 | 問題 | 根因 |
|---|---|---|
| iOS Signing | Keychain 解鎖失敗 | Credential 與實際密碼不一致 |
| Android Build | Gradle 編譯錯誤 | Plugin 版本與 Flutter 版本不相容 |
| Play Store API | 上傳被拒絕 | App 狀態與 API 參數不匹配 |
這些問題的共同點是:CI 環境與本地開發環境的差異。
解決這類問題的關鍵是:
- 詳讀錯誤訊息:exit status 51、substring on null、draft app 都是明確的線索
- SSH 進 CI 環境驗證:不要猜測,直接進去看
- 理解背後的機制:Keychain 如何運作、Gradle 如何解析版本、Play Console 的狀態機
- 修復後驗證:每次只改一個東西,確認是否解決
