工程實踐

在雲端 Mac CI 驗收 iOS 與 watchOS 伴隨套件

在雲端 Mac CI 驗收 iOS 與 watchOS 伴隨套件

同時包含 iOS App 與 watchOS App 的封存檔,即使主應用程式編譯成功,仍可能無法交付:手錶端未正確嵌入、組建編號落後一版、延伸功能仍指向舊的 Bundle ID,或某個內層元件未正確簽章。這類問題不應等到匯出或提交階段才被發現。更穩妥的做法,是讓雲端 Mac 在每次產生 xcarchive 後直接檢查最終套件結構,並將結果設為流水線門禁。

為什麼要檢查最終封存檔

專案檔案描述的是預期結果,xcarchive 才是實際交付物。不同的設定檔、組建指令碼和環境變數都可能覆寫版本號或產品識別碼。只讀取專案設定,無法證明 watchOS App 已經出現在主應用程式的 Watch 目錄中,也無法證明延伸功能與外層應用程式之間的關係正確。

建議將驗收對象固定為 Release 封存檔,而不是 DerivedData 中的中間目錄。先使用與正式流水線相同的工作區、Scheme 和設定產生建置產物:

set -euo pipefail

ROOT="$PWD"
OUT="$ROOT/out"
ARCHIVE="$OUT/Client.xcarchive"

rm -rf "$ARCHIVE"
mkdir -p "$OUT"

xcodebuild \
  -workspace Client.xcworkspace \
  -scheme Client \
  -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$ARCHIVE" \
  clean archive

clean archive 適合用於基準驗收;日常流水線可依快取策略調整,但驗收指令碼必須始終讀取本次工作產生的封存檔,不能退回使用某個「最近產生」的目錄。

門禁應回答「這次準備交付的套件裡有什麼」,而不是「專案理論上應該產生什麼」。

從套件結構建立元件清單

典型路徑為 Products/Applications/*.app,其中的 Watch 目錄包含 watchOS App,而手錶應用程式的 PlugIns 目錄再包含延伸功能。不要在指令碼中寫死產品名稱;Target 重新命名後,固定路徑很容易失效。

IOS_APP=$(find "$ARCHIVE/Products/Applications" \
  -maxdepth 1 -type d -name '*.app' -print -quit)

test -n "${IOS_APP:-}" || {
  printf '%s
' "iOS app not found"
  exit 1
}

WATCH_APP=$(find "$IOS_APP/Watch" \
  -maxdepth 1 -type d -name '*.app' -print -quit)

test -n "${WATCH_APP:-}" || {
  printf '%s
' "watchOS app not found"
  exit 1
}

WATCH_EXTENSION=$(find "$WATCH_APP/PlugIns" \
  -maxdepth 1 -type d -name '*.appex' -print -quit)

printf 'ios=%s
watch=%s
extension=%s
' \
  "$IOS_APP" "$WATCH_APP" "${WATCH_EXTENSION:-embedded}"

部分較新的專案沒有獨立的 .appex 層,因此找不到延伸功能時不能直接判定失敗。門禁應依據儲存庫中宣告的產品結構作出判斷:舊式結構要求三層元件,較新的單一 Target 結構只要求 iOS App 與 watchOS App。可以在儲存庫中保存一份簡短的預期清單,而不是根據目錄結構猜測專案類型。

驗證版本與識別碼關係

先對每個 Info.plist 執行 plutil -lint,再讀取 CFBundleIdentifierCFBundleShortVersionStringCFBundleVersion。同一次發佈通常應讓 iOS 與 watchOS 使用完全相同的行銷版本與組建編號。如果團隊採用各自獨立的組建編號,也應在指令碼中明確定義允許的規則。

檢查項目 建議規則 失敗風險
行銷版本 iOS 與 watchOS 相同 商店版本關係不一致
組建編號 同一發佈工作中相同 手錶端被識別為舊組建
伴隨識別碼 指向 iOS 主應用程式 ID 安裝後無法建立伴隨關係
延伸功能歸屬 指向目前的 watchOS App ID 延伸功能與外層應用程式不相符
read_plist() {
  /usr/libexec/PlistBuddy -c "Print :$2" "$1"
}

IOS_PLIST="$IOS_APP/Info.plist"
WATCH_PLIST="$WATCH_APP/Info.plist"

plutil -lint "$IOS_PLIST" "$WATCH_PLIST"

IOS_ID=$(read_plist "$IOS_PLIST" CFBundleIdentifier)
WATCH_ID=$(read_plist "$WATCH_PLIST" CFBundleIdentifier)
IOS_VERSION=$(read_plist "$IOS_PLIST" CFBundleShortVersionString)
WATCH_VERSION=$(read_plist "$WATCH_PLIST" CFBundleShortVersionString)
IOS_BUILD=$(read_plist "$IOS_PLIST" CFBundleVersion)
WATCH_BUILD=$(read_plist "$WATCH_PLIST" CFBundleVersion)
COMPANION_ID=$(read_plist "$WATCH_PLIST" WKCompanionAppBundleIdentifier)

test "$IOS_VERSION" = "$WATCH_VERSION"
test "$IOS_BUILD" = "$WATCH_BUILD"
test "$COMPANION_ID" = "$IOS_ID"

不要以「watchOS Bundle ID 必須以 iOS Bundle ID 開頭」取代精確比對。團隊可能使用明確指定的識別碼,前綴相似並不能證明綁定關係正確。應從發佈設定讀取預期值,並驗證 WKCompanionAppBundleIdentifier 的完整內容。

相容包含延伸功能的結構

如果封存檔中存在 .appex,請繼續讀取其中的 WKAppBundleIdentifier,並要求該值等於目前 watchOS App 的 Bundle ID。同時記錄延伸功能本身的版本與組建編號,避免某個 Target 漏掉統一版本指令碼。

檢查簽章與可執行檔架構

外層應用程式成功簽章,不代表每個巢狀元件都能通過驗證。與其只對主應用程式執行遞迴檢查,不如逐一列舉實際元件,讓日誌明確指出驗證失敗的對象:

verify_component() {
  local item="$1"
  codesign --verify --strict --verbose=2 "$item"
  codesign -d --entitlements :- "$item" >/dev/null
}

verify_component "$IOS_APP"
verify_component "$WATCH_APP"

if test -n "${WATCH_EXTENSION:-}"; then
  verify_component "$WATCH_EXTENSION"
fi

接著根據各自的 CFBundleExecutable 找出二進位檔,並使用 filelipo -archs 記錄架構。門禁不必永久寫死某一組架構,因為工具鏈和部署目標都可能改變;更可靠的規則是拒絕空白的可執行檔、拒絕非預期的模擬器產物,並保存目前的架構清單以供檢閱。

此外,也要檢查巢狀元件中是否有符號連結越界、重複的 Bundle ID,或額外且未宣告的延伸功能。這些問題通常源自複製指令碼使用了範圍過大的萬用字元。

將驗收結果轉化為穩定門禁

指令碼的結束碼只負責阻擋流程,報告則負責說明原因。每次工作都應保存元件的相對路徑、Bundle ID、版本、組建編號、架構、簽章驗證狀態和封存檔校驗和。報告中不要收集憑證私鑰、完整的環境變數內容或完整的使用者目錄路徑。

建議將流程拆分為四個步驟:

  1. 產生唯一的封存目錄,禁止重複使用上一次的產物。
  2. 探索元件,並與儲存庫中的預期結構比較。
  3. 驗證中繼資料、關聯欄位、簽章和可執行檔架構。
  4. 輸出文字或 JSON 報告,再執行匯出工作。

首次啟用門禁時,可以先執行數次僅記錄、不阻擋的檢查,以確認新舊專案結構都已涵蓋;規則穩定後,再將元件缺失、版本不相符和簽章失敗設為硬性錯誤。如果同一台雲端 Mac 同時執行多個封存工作,必須為每個工作分配獨立的 archivePath 與報告目錄,避免其中一個工作讀取到另一個工作的結果。

最終目標不是增加一輪形式化檢查,而是讓流水線能夠證明:主應用程式、手錶應用程式及其延伸功能確實來自同一次發佈情境,彼此關係明確,而且最終封存檔可以繼續進入匯出階段。

常見問題

只核對 Xcode 專案設定,能取代 xcarchive 驗收嗎?

不能。專案設定只代表預期值,實際封存產物還會受到建置腳本、設定覆寫與簽章流程影響。

iOS App 與 watchOS App 的版本是否必須一致?

同一次發佈通常應讓行銷版本與建置編號一致;若採獨立策略,CI 必須明確定義並驗證允許的對應關係。

獨享實體節點

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

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

選擇租用方案