工程指南

雲端 Mac CI 的 iOS 建置編號治理實作

雲端 Mac CI 的 iOS 建置編號治理實作

兩條 iOS 封存流水線同時在雲端 Mac 上執行:一條修復線上問題,另一條準備例行發布。兩者都從專案檔案讀取相同的 CURRENT_PROJECT_VERSION,最後產生兩個建置編號相同、程式碼卻不同的產物。問題通常要到提交發布系統時才會浮現;此時若只查看檔名與提交記錄,很難確認應該保留哪一份。

建置編號治理的目標不是「自動加一」,而是讓每個可發布產物都能回答三個問題:編號由誰分配、對應哪次提交,以及封存檔中的實際值為何。

先區分版本號與建置編號

MARKETING_VERSION 是使用者看到的版本,例如 3.8.0CURRENT_PROJECT_VERSION 是建置編號,應為只包含數字的遞增值。兩者的生命週期不同,不要將提交標籤直接同時寫入這兩個欄位。

欄位 範例 變更時機 主要用途
MARKETING_VERSION 3.8.0 產品版本變更 識別功能版本
CURRENT_PROJECT_VERSION 18427 每次可發布封存 區分同版本產物
Git 提交 a1b2c3d 每次提交 定位原始碼
流水線編號 5821 每次工作執行 定位執行記錄

專案儲存庫可以保存穩定的版本號,但不應由多個執行器同時修改並提交建置編號。當並行工作各自執行「讀取、加一、寫回」時,即使全部成功,也可能取得相同結果。

將建置編號視為流水線輸入,而不是原始碼修改的結果。原始碼負責宣告如何使用編號,排程系統則負責確保編號唯一。

建立唯一的編號來源

選擇可單調遞增的序列

正式封存應從單一集中來源取得整數,例如流水線系統的全域執行序號,或由內部協調工作以原子方式分配的序列。編號必須符合三個條件:

  1. 在同一發布目標下不重複;
  2. 新編號大於所有已提交的編號;
  3. 可反查對應的提交、分支與工作執行記錄。

git rev-list --count HEAD 適合不會重寫歷史記錄的單一分支專案,但不適用於淺層複製、變基或多發布分支。不同分支可能得到相同計數,重寫歷史記錄也可能導致數字倒退。它可以用於內部除錯套件,但不能作為複雜發布流程的唯一真實來源。

在 OpsVM 的並行工作中,可以先由排程層產生 BUILD_SEQUENCE,再將其傳入實際的建置節點。節點只取用編號,不自行競爭或回寫。

在工作入口拒絕無效資料

#!/bin/zsh
set -euo pipefail

: "${BUILD_SEQUENCE:?BUILD_SEQUENCE is required}"
: "${RELEASE_VERSION:?RELEASE_VERSION is required}"

if [[ ! "$BUILD_SEQUENCE" =~ ^[0-9]+$ ]]; then
  print -u2 "BUILD_SEQUENCE must contain digits only"
  exit 64
fi

if [[ ! "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
  print -u2 "RELEASE_VERSION must use major.minor.patch"
  exit 64
fi

入口驗證應早於相依套件安裝與編譯。如此一來,缺少變數就不會在十幾分鐘後才變成無法發布的封存檔。

在封存指令中注入編號

不需要讓流水線修改 project.pbxproj。直接在 xcodebuild 指令末尾覆寫建置設定,既不會在儲存庫中產生未提交的變更,也方便根據日誌重現。

archive_path="$PWD/output/App.xcarchive"
result_path="$PWD/output/Archive.xcresult"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -destination "generic/platform=iOS" \
  -archivePath "$archive_path" \
  -resultBundlePath "$result_path" \
  MARKETING_VERSION="$RELEASE_VERSION" \
  CURRENT_PROJECT_VERSION="$BUILD_SEQUENCE" \
  clean archive

覆寫項目應放在同一次封存指令中,避免測試使用一個編號,封存時卻又讀取專案預設值。若專案包含擴充元件,還必須確認主應用程式與擴充功能是否繼承相同設定。除非發布規範明確要求,否則不要為每個 target 分別產生編號。

封存前可以先檢查解析後的設定:

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -showBuildSettings |
awk '/MARKETING_VERSION|CURRENT_PROJECT_VERSION/ { print }'

這個步驟主要用來找出指令碼、設定檔或 target 層級設定是否意外覆寫命令列參數。

不要只相信日誌,直接驗收封存檔

指令成功返回只代表封存完成,不表示目標值已寫入最終應用程式。驗收指令碼應讀取 .xcarchive 內主應用程式的 Info.plist,並與流水線輸入進行比對。

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

if [[ -z "$app_path" ]]; then
  print -u2 "Application bundle not found"
  exit 65
fi

plist="$app_path/Info.plist"
actual_version=$(/usr/libexec/PlistBuddy \
  -c "Print :CFBundleShortVersionString" "$plist")
actual_build=$(/usr/libexec/PlistBuddy \
  -c "Print :CFBundleVersion" "$plist")

[[ "$actual_version" == "$RELEASE_VERSION" ]] || exit 66
[[ "$actual_build" == "$BUILD_SEQUENCE" ]] || exit 67

驗收通過後,將版本號、建置編號、完整提交雜湊、封存檔校驗值與流水線執行識別碼寫入同一份純文字清單,並與產物一併保存。檔名可以方便人工閱讀,但不能取代清單與封存檔內部欄位。

處理重試、分支與並行執行

在流水線層固定重試規則

若失敗發生在編譯前,且尚未產生封存檔,可以沿用原編號重試同一工作。若封存檔已產生、上傳或進入後續處理,就應取得新編號。舊編號應保留在記錄中,不要為了讓數字連續而回收。

當多條發布分支共用同一發布目標時,也應共用同一編號空間。各分支分別從 1 開始看似整齊,合併後卻會發生衝突。分支名稱、提交雜湊與版本號負責表達來源;建置編號只負責保持唯一且遞增。

最小檢查清單

  • 編號由單一集中來源以原子方式分配;
  • 建置節點只能讀取編號,不修改專案檔案;
  • 封存指令明確傳入兩個版本欄位;
  • 已核對主應用程式與擴充功能的繼承關係;
  • 成功後讀取封存檔內部欄位進行比對;
  • 每份產物都保存提交、工作與校驗值的對應關係;
  • 已產生產物的失敗工作不回收編號;
  • 並行節點在主控台確認目前可選設定後,只負責建置,不負責分配編號。

完成這些約束後,建置編號就不再是發布前臨時修改的數字,而是貫穿原始碼、執行記錄與最終產物的穩定索引。發生回復版本或並行工作衝突時,團隊可以先透過編號定位封存檔,再回溯至唯一對應的提交與流水線執行現場。

常見問題

可以直接用 Git 提交數量當作 iOS 建置編號嗎?

不改寫歷史的單一分支可以使用;若有變基、淺層複製或多條發布分支,應使用 CI 集中配置的單調遞增整數。

流水線重試時應該沿用原本的建置編號嗎?

只有前一次執行尚未產生封存檔或發布產物時才適合沿用。若已有產物,應配置新編號並保留兩次執行與同一提交的對應關係。

獨享實體節點

在 OpsVM 部署雲端 Mac 工作流程

從三種 Apple Silicon 配置與 6 個在售節點中選擇,實際可用狀態以控制台即時回傳結果為準。

選擇配置並下單