前言:當建置一直紅燈

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 的運作方式:

Mermaid Diagram

關鍵概念:

  • macOS 可以有多個 Keychain,每個都有獨立密碼
  • 憑證必須在「已解鎖」的 Keychain 中才能被 codesign 使用
  • CI 環境通常會建立專用的 Keychain,避免影響系統 Keychain

Fastlane Match 與 Keychain 的互動流程

Mermaid Diagram

調查過程

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
)

根本原因

密碼來源的優先順序問題:

Mermaid Diagram

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 版本解析流程

Mermaid Diagram

根本原因

這是 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 狀態機

Mermaid Diagram

根本原因

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 SigningKeychain 解鎖失敗Credential 與實際密碼不一致
Android BuildGradle 編譯錯誤Plugin 版本與 Flutter 版本不相容
Play Store API上傳被拒絕App 狀態與 API 參數不匹配

這些問題的共同點是:CI 環境與本地開發環境的差異

解決這類問題的關鍵是:

  1. 詳讀錯誤訊息:exit status 51、substring on null、draft app 都是明確的線索
  2. SSH 進 CI 環境驗證:不要猜測,直接進去看
  3. 理解背後的機制:Keychain 如何運作、Gradle 如何解析版本、Play Console 的狀態機
  4. 修復後驗證:每次只改一個東西,確認是否解決

相關資源