流水線顯示執行成功,但下載到的 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 完整的結束狀態與輸出,同時避免印出敏感的環境資訊。提交支援請求前,只擷取相關的時間範圍,並移除儲存庫權杖、簽章材料路徑中的內部識別資訊及連線憑證。
交付前檢查清單
在流水線標記為成功前,逐項確認:
- 封存檔中的 App 已通過嚴格的程式碼簽章驗證。
- IPA 是由當次工作的封存檔匯出,而非沿用舊目錄。
GIT_COMMIT是完整的提交雜湊,且與觸發記錄一致。SHA256SUMS已分別在暫存目錄與最終目錄各驗證一次。- 最終目錄以唯一的工作識別碼命名,不會被重試覆寫。
- 取用端只讀取已發布的目錄,或帶有完成標記的物件。
- 日誌足以定位失敗階段,但不包含憑證與簽章材料內容。
這套流程無法消除所有建置失敗,卻能把「編譯是否成功」與「交付物是否可信」拆分成兩個可驗證的問題。當封存、校驗、建置脈絡與發布邊界固定後,重試與回復舊版本都只需處理明確的物件,不再依賴持續變動的共用目錄。
常見問題
只有 SHA-256 校驗值,能證明 iOS 產物來自可信任的建置機嗎?
不能。SHA-256 只能證明檔案在兩個檢查點之間沒有改變。還要驗證 App 程式碼簽章,並一併保存提交版本、Xcode 版本、工作識別碼與校驗清單。
為什麼要先寫入暫存目錄,再重新命名為發布目錄?
同一檔案系統內的重新命名可讓讀取端一次看到完整版本,避免取得複製到一半的檔案。跨磁碟時應先複製並重新校驗,再於目的磁碟內重新命名。
專用雲端 Mac
在獨享實體節點上執行建置任務
按天、週、月或季選擇 MiniRents M4 與 MiniRents M4 Pro,並依團隊與程式碼儲存庫位置選擇節點。設備為獨享實體機,並非虛擬機。