工程實踐

雲端 Mac CI 的 Ruby 與 fastlane 版本隔離實作

雲端 Mac CI 的 Ruby 與 fastlane 版本隔離實作

在同一台雲端 Mac 上,開發者從終端機執行 fastlane 可以成功,CI 工作卻回報缺少 gem;修好 runner 後,登入終端機又解析到另一套 Ruby。這類問題通常不在 fastlane 本身,而是系統 Ruby、版本管理器、Bundler 設定與工作環境共同造成的路徑漂移。解法不是反覆安裝 gem,而是將整套 Ruby 工具鏈納入專案管理。

先確認實際執行的是哪套 Ruby

不要一開始就執行安裝指令。請分別在登入終端機與 CI 工作中記錄以下結果:

command -v ruby
ruby -v
command -v bundle
bundle -v
gem env home
bundle config list
printf '%s
' "$PATH"

重點是比較 rubybundle 是否來自相同的路徑前綴。若 Ruby 位於版本管理器目錄,而 Bundler 來自系統目錄,後續安裝很可能會寫入錯誤的 gem 路徑。也要檢查 CI 是否設定了 GEM_HOMEGEM_PATHBUNDLE_PATH;殘留的環境變數可能會覆寫專案設定。

能在互動式終端機中執行,不代表自動化工作擁有相同的環境。CI 應從明確的版本檔案與專案設定啟動,而不是依賴 shell 啟動指令碼碰巧修改 PATH。

建立一份診斷產物即可,不要在日誌中輸出權杖、憑證內容或完整的環境變數。路徑與版本資訊已足以判斷大多數解析問題。

固定 Ruby、Bundler 與 fastlane

儲存庫根目錄應提交 .ruby-versionGemfileGemfile.lock。Ruby 版本必須寫成可安裝的完整版本,而不是模糊的 3.3。先由團隊選定版本,再執行:

printf '%s
' '3.3.6' > .ruby-version
bundle init
bundle add fastlane --version '~> 2.0'
bundle lock

Gemfile 定義允許的版本範圍,Gemfile.lock 則記錄本次解析出的精確版本。CI 不應在每次建置前執行 bundle update,否則鎖定檔將失去約束作用。安裝完成後,所有指令都應透過 Bundler 進入點執行:

bundle exec fastlane --version
bundle exec fastlane ios ci

不要把全域 fastlane 當成捷徑。它可能來自另一套 Ruby,也可能在系統更新或人工除錯後改變。若專案需要特定的 Bundler 版本,可以從 Gemfile.lockBUNDLED WITH 區段讀取並安裝該版本,再執行 bundle install

將相依套件安裝限制在專案目錄

專案層級的設定應寫入儲存庫內的 .bundle/config,不要修改整台主機的全域 Bundler 設定:

bundle config set --local path vendor/bundle
bundle config set --local clean true
bundle install --jobs 4 --retry 2

vendor/bundle 通常不會提交到 Git,而是交由 CI 快取。如此既不會污染系統 gem,也能讓多個專案使用不同版本。安裝後可透過以下檢查確認完整的解析鏈:

test -f Gemfile.lock
test "$(ruby -e 'print RUBY_VERSION')" = "$(cat .ruby-version)"
bundle check
bundle exec ruby -e 'require "fastlane"; puts Fastlane::VERSION'

如果最後一步失敗,不要立刻刪除所有快取。先檢查 bundle config listbundle env,確認工作讀取的是目前儲存庫中的 Gemfile,而不是父目錄中的檔案,或由 BUNDLE_GEMFILE 指向的其他檔案。

模擬乾淨的非互動式環境

正式接入 runner 前,可以使用最小化環境進行一次驗收:

RUBY_BIN="$(dirname "$(command -v ruby)")"
env -i \
  HOME="$HOME" \
  PATH="$RUBY_BIN:/usr/bin:/bin:/usr/sbin:/sbin" \
  BUNDLE_GEMFILE="$PWD/Gemfile" \
  bundle exec fastlane --version

這一步可以提前暴露只寫在個人 shell 設定中的路徑、別名與環境變數。若指令失敗,應修正 runner 的明確初始化流程,而不是讓 CI 載入整份個人設定。

設計不會互相混用的快取鍵

含有原生擴充功能的 gem,不能只依據 Gemfile.lock 建立快取。Ruby 版本、CPU 架構與 macOS 主次版本只要不同,都可能導致已編譯的檔案無法載入。快取鍵至少應包含以下欄位:

欄位 取得方式 用途
Ruby 版本 ruby -e 'print RUBY_VERSION' 隔離 ABI 差異
CPU 架構 uname -m 區分不同的原生程式碼
macOS 版本 sw_vers -productVersion 避免系統程式庫差異
鎖定檔摘要 shasum -a 256 Gemfile.lock 相依套件變更時失效

可以產生一段穩定的指紋:

RUBY_VERSION_KEY="$(ruby -e 'print RUBY_VERSION')"
ARCH_KEY="$(uname -m)"
OS_KEY="$(sw_vers -productVersion | cut -d. -f1,2)"
LOCK_KEY="$(shasum -a 256 Gemfile.lock | cut -d' ' -f1)"
printf '%s-%s-%s-%s
' "$RUBY_VERSION_KEY" "$ARCH_KEY" "$OS_KEY" "$LOCK_KEY"

還原快取後仍須執行 bundle check。若檢查失敗,再執行 bundle install 補齊內容;不能把「快取成功解壓縮」視為相依套件可用的證明。

將失敗轉化為可定位的門禁檢查

最終工作應依序分成「環境驗收、相依套件驗收、業務 lane」三個階段。第一階段檢查版本與路徑,第二階段執行 bundle check,第三階段才呼叫 fastlane。任何階段失敗都應立即停止,避免錯誤在後續步驟中表現為難以理解的外掛程式缺失或常數載入異常。

建議將以下項目納入合併檢查:

  • .ruby-version 與 runner 實際使用的 Ruby 完全一致。
  • Gemfile.lock 已提交,且工作執行前後沒有變化。
  • fastlane 僅透過 bundle exec 呼叫。
  • .bundle/config 使用專案層級的相依套件目錄。
  • 快取鍵包含 Ruby、架構、系統版本與鎖定檔摘要。
  • 日誌只保留版本、路徑與驗證結果,不輸出敏感的環境值。
  • runner 與登入終端機使用同一套明確的初始化邏輯。

完成這些約束後,Ruby 工具鏈就會從「主機上曾經安裝過的一組指令」轉變為可稽核的建置輸入。日後升級 Ruby 或 fastlane 時,可以透過獨立提交更新版本檔案與鎖定檔,並使用同一套驗收步驟確認變更範圍,不必等到失敗後才猜測工作實際呼叫的是哪套環境。

常見問題

已提交 Gemfile.lock,為什麼 fastlane 版本仍可能改變?

若任務直接呼叫全域 fastlane,或在執行前重新產生鎖定檔,Gemfile.lock 就不會控制版本。應固定使用 bundle exec fastlane,並把鎖定檔視為不可變輸入。

vendor/bundle 能否直接在不同雲端 Mac 之間共用?

不能無條件共用。快取鍵至少要包含 Ruby 版本、CPU 架構、macOS 主次版本與 Gemfile.lock 摘要,否則原生擴充套件可能無法載入。

CI 可執行但登入終端失敗時應先查什麼?

先比較 command -v ruby、ruby -v、bundle config list 與環境變數,確認兩種工作階段是否載入不同 PATH、Ruby 管理器或 Bundler 設定。

獨享實體節點

在雲端持續執行你的 Mac mini 工作流程

選擇 RunAMac M4、租用週期與部署節點,將開發、建置或實驗工作放到一台獨享實體主機上。

選擇租用方案