工程指南

雲端 Mac 的 iOS 建置產物完整性校驗與原子發布

雲端 Mac 的 iOS 建置產物完整性校驗與原子發布

流水線顯示執行成功,但下載到的 IPA 卻無法對應觸發該次工作的提交。另一種更危險的情況是,上傳中斷後,後續工作取走了只寫入一半的檔案。雲端 Mac 上的編譯、匯出與上傳通常由不同指令碼負責;如果只把「指令結束碼為 0」視為交付標準,這兩類問題很難在當次工作中被發現。

解法不是再加一道壓縮指令,而是把建置產物定義成不可拆分的集合:IPA、SHA-256 清單、建置脈絡與驗證結果必須一併產生,且只有在所有檢查都通過後才能對外發布。

先定義可交付的建置產物

每次工作都使用唯一的 BUILD_ID。交付目錄至少應包含以下內容:

檔案 用途 驗收方式
App.ipa 安裝與散布用建置產物 雜湊一致,且匯出指令執行成功
SHA256SUMS 內容完整性清單 shasum -a 256 -c 成功結束
build-context.tsv 追溯建置來源 提交、Xcode 與工作識別碼皆非空白
export.log 定位匯出失敗原因 保留完整輸出並限制存取權限

建置脈絡應取自流水線已確定的變數,而不是在指令碼中臨時猜測分支。至少要記錄完整的提交雜湊、工作識別碼、Scheme、Xcode 版本與產生時間。時間只用來關聯日誌;判定建置產物身分時,仍應以提交雜湊與校驗值為準。

雜湊可以證明檔案在兩個檢查點之間未被變更,卻無法證明檔案最初來自可信任的流程。程式碼簽章驗證與建置脈絡缺一不可。

匯出前驗證封存檔

先確認封存檔存在,再找出其中的 .app 並驗證程式碼簽章。不要使用 --deep 重新簽署;這裡的目的只是檢查既有的簽章結構。若應用程式包含擴充功能,驗證指令應涵蓋巢狀程式碼,且任何失敗都必須阻斷流程。

封存檔、匯出目錄與最終目錄應位於同一個工作磁碟區。如此一來,最後的目錄重新命名才具有原子性。在 MiniRent 雲端 Mac 上執行時,也應為每個並行工作配置獨立的根目錄,避免兩個工作共用 out/latest 之類的固定路徑。

可直接改造的指令碼

set -euo pipefail

: "${BUILD_ID:?BUILD_ID is required}"
: "${GIT_COMMIT:?GIT_COMMIT is required}"
: "${SCHEME:?SCHEME is required}"

ROOT="${ARTIFACT_ROOT:-$PWD/out}"
ARCHIVE="$ROOT/input/App.xcarchive"
EXPORT_OPTIONS="$ROOT/input/ExportOptions.plist"
STAGE="$ROOT/.stage-$BUILD_ID"
FINAL="$ROOT/releases/$BUILD_ID"

rm -rf "$STAGE"
mkdir -p "$STAGE" "$(dirname "$FINAL")"
trap 'rm -rf "$STAGE"' EXIT

test -d "$ARCHIVE"
test -f "$EXPORT_OPTIONS"
test ! -e "$FINAL"

APP_PATH="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$APP_PATH"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"

xcodebuild -exportArchive \
  -archivePath "$ARCHIVE" \
  -exportPath "$STAGE/export" \
  -exportOptionsPlist "$EXPORT_OPTIONS" \
  >"$STAGE/export.log" 2>&1

IPA_PATH="$(find "$STAGE/export" -maxdepth 1 -name '*.ipa' -print -quit)"
test -n "$IPA_PATH"
cp "$IPA_PATH" "$STAGE/App.ipa"

DIGEST="$(shasum -a 256 "$STAGE/App.ipa" | awk '{print $1}')"
printf '%s  %s
' "$DIGEST" "App.ipa" >"$STAGE/SHA256SUMS"

XCODE_VERSION="$(xcodebuild -version | paste -sd ' ' -)"
printf 'build_id	%s
commit	%s
scheme	%s
xcode	%s
' \
  "$BUILD_ID" "$GIT_COMMIT" "$SCHEME" "$XCODE_VERSION" \
  >"$STAGE/build-context.tsv"

(
  cd "$STAGE"
  shasum -a 256 -c SHA256SUMS
)

mv "$STAGE" "$FINAL"

(
  cd "$FINAL"
  shasum -a 256 -c SHA256SUMS
)

指令碼中的 ExportOptions.plist 應由儲存庫內受版本控管的設定提供。不要把密碼、私密金鑰、暫時性權杖或完整的環境變數集合寫入日誌或脈絡檔案。

以原子發布隔離未完成產物

關鍵在於先於隱藏的暫存目錄內完成所有寫入與校驗,再透過一次 mv 公開最終目錄。讀取端只掃描 releases/,不存取 .stage-*,因此不會取得仍在產生中的檔案。

這項保證只在同一個檔案系統內成立。如果發布目錄掛載於另一個磁碟區,mv 可能退化成複製後刪除。正確做法是在目標磁碟區建立暫存目錄,複製所有檔案,在目標端重新執行雜湊檢查,最後於目標磁碟區內重新命名。若物件儲存空間或製品服務不具備目錄重新命名語意,可以先上傳帶有工作識別碼的暫存鍵,完成校驗後再寫入一個很小的完成標記;取用端必須先檢查標記,再讀取建置產物。

不要覆寫 latest

latest 很容易讓重試工作覆寫較新的成功結果。穩定的做法是以不可變的 BUILD_ID 保存建置產物,再另外維護一個只包含目標工作識別碼的指標檔案。更新指標前,應先確認目標目錄已通過校驗;回復舊版本時也只變更指標,不修改歷史建置產物。

讓失敗停在正確階段

簽章錯誤應在匯出前停止流程,匯出錯誤應留在暫存目錄,雜湊錯誤則應在發布前阻斷。上傳後再次校驗,可用來發現傳輸、磁碟或指令碼選錯檔案所造成的變更。

常見誤區包括從匯出目錄取用「第一個檔案」卻未限制副檔名、多個 Scheme 共用同一個輸出路徑,以及重試時沿用上一次留下的暫存目錄。指令碼應在開始執行時清理本次工作的暫存目錄,但不得使用範圍過廣的萬用字元刪除其他工作的目錄。

日誌也必須劃定界線。保留 xcodebuild 完整的結束狀態與輸出,同時避免印出敏感的環境資訊。提交支援請求前,只擷取相關的時間範圍,並移除儲存庫權杖、簽章材料路徑中的內部識別資訊及連線憑證。

交付前檢查清單

在流水線標記為成功前,逐項確認:

  1. 封存檔中的 App 已通過嚴格的程式碼簽章驗證。
  2. IPA 是由當次工作的封存檔匯出,而非沿用舊目錄。
  3. GIT_COMMIT 是完整的提交雜湊,且與觸發記錄一致。
  4. SHA256SUMS 已分別在暫存目錄與最終目錄各驗證一次。
  5. 最終目錄以唯一的工作識別碼命名,不會被重試覆寫。
  6. 取用端只讀取已發布的目錄,或帶有完成標記的物件。
  7. 日誌足以定位失敗階段,但不包含憑證與簽章材料內容。

這套流程無法消除所有建置失敗,卻能把「編譯是否成功」與「交付物是否可信」拆分成兩個可驗證的問題。當封存、校驗、建置脈絡與發布邊界固定後,重試與回復舊版本都只需處理明確的物件,不再依賴持續變動的共用目錄。

常見問題

只有 SHA-256 校驗值,能證明 iOS 產物來自可信任的建置機嗎?

不能。SHA-256 只能證明檔案在兩個檢查點之間沒有改變。還要驗證 App 程式碼簽章,並一併保存提交版本、Xcode 版本、工作識別碼與校驗清單。

為什麼要先寫入暫存目錄,再重新命名為發布目錄?

同一檔案系統內的重新命名可讓讀取端一次看到完整版本,避免取得複製到一半的檔案。跨磁碟時應先複製並重新校驗,再於目的磁碟內重新命名。

專用雲端 Mac

在獨享實體節點上執行建置任務

按天、週、月或季選擇 MiniRents M4 與 MiniRents M4 Pro,並依團隊與程式碼儲存庫位置選擇節點。設備為獨享實體機,並非虛擬機。

選擇機型並訂購