App Clip 能在開發用 Mac 上從 Scheme 啟動,不代表交付產物正確。最常見的漏檢並非編譯失敗,而是 Clip 未納入宿主封存檔、父子識別碼不一致,或呼叫 URL 只涵蓋理想路徑。將這些檢查納入雲端 Mac 的持續整合工作,才能確保每次提交都使用同一套 Xcode、模擬器與驗收指令碼。
先定義可驗證的交付結果
不要將「Build Succeeded」視為唯一結果。一次完整驗收至少應產生宿主 .xcarchive、App Clip 獨立建置記錄、解析後的權限檔案,以及呼叫情境記錄。建議將測試 URL 放在儲存庫設定中,而不是散落於 Scheme 的個人設定內。
| 檢查層級 | 輸入 | 通過條件 |
|---|---|---|
| 編譯 | App Clip Scheme | 可針對模擬器目標建置 |
| 嵌入 | 宿主封存檔 | AppClips 下存在 Clip |
| 關聯 | 兩個應用程式的識別碼與權限 | 父子關係一致 |
| 呼叫 | 基礎、參數化、錯誤 URL | 路由與回退行為符合預期 |
單獨啟動 Clip 驗證的是業務進入點;封存宿主後再檢查嵌入關係,驗證的才是可交付的套件結構。兩者無法互相取代。
固定建置目錄
雲端工作應使用獨立的 DerivedData,避免上一輪產物掩蓋複製階段的錯誤。先建置 App Clip 的模擬器版本,再封存宿主。Scheme 名稱透過環境變數傳入,便可讓指令碼重複用於不同分支。
set -euo pipefail
ROOT="$PWD"
OUT="$ROOT/.ci-artifacts"
DERIVED="$OUT/DerivedData"
ARCHIVE="$OUT/HostApp.xcarchive"
rm -rf "$OUT"
mkdir -p "$OUT"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$CLIP_SCHEME" \
-sdk iphonesimulator \
-destination "$SIMULATOR_DESTINATION" \
-derivedDataPath "$DERIVED" \
clean build | tee "$OUT/app-clip-build.log"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$HOST_SCHEME" \
-destination "generic/platform=iOS" \
-archivePath "$ARCHIVE" \
archive | tee "$OUT/host-archive.log"
SIMULATOR_DESTINATION 可固定為管線已安裝的裝置類型與系統版本。升級 Xcode 後,應先更新基準工作,不要讓一般功能提交順帶變更測試執行環境。
檢查宿主與 Clip 的套件關係
找出嵌入的產物
完成封存後,從 Products/Applications 找出宿主 .app,再確認其 AppClips 目錄中恰好存在預期目標。不要依賴固定的應用程式檔名;可依副檔名搜尋並檢查結果數量。
HOST_APP="$(find "$ARCHIVE/Products/Applications" -maxdepth 1 -name '*.app' -print -quit)"
CLIP_APP="$(find "$HOST_APP/AppClips" -maxdepth 1 -name '*.app' -print -quit)"
test -n "$HOST_APP"
test -n "$CLIP_APP"
HOST_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$HOST_APP/Info.plist")"
CLIP_ID="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$CLIP_APP/Info.plist")"
printf '%s
' "$HOST_ID" > "$OUT/host-bundle-id.txt"
printf '%s
' "$CLIP_ID" > "$OUT/clip-bundle-id.txt"
如果 AppClips 目錄不存在,應優先檢查宿主 Target 的嵌入階段、Clip Target 是否屬於目前的封存 Scheme,以及 Release 設定是否覆寫了 Debug 中有效的設定。
比較最終權限
專案檔案中的宣告不一定等同於簽入產物的最終結果。使用 codesign -d --entitlements :- 分別匯出宿主與 Clip 的權限,再由 plutil 轉換成穩定格式。門禁應確認 Clip 的父應用程式關聯值指向目前宿主,同時限制不應繼承的功能。
codesign -d --entitlements :- "$HOST_APP" > "$OUT/host-entitlements.plist"
codesign -d --entitlements :- "$CLIP_APP" > "$OUT/clip-entitlements.plist"
plutil -lint "$OUT/host-entitlements.plist"
plutil -lint "$OUT/clip-entitlements.plist"
不要只對整份檔案進行文字比較,因為 Xcode 版本變更可能會調整鍵值順序。應擷取需要保持穩定的鍵逐項比較,並將實際值寫入建置產物,以便失敗後複查。
使用三類 URL 驗證路由
呼叫測試至少應涵蓋基礎進入點、合法參數與異常輸入。例如,基礎進入點應開啟預設的輕量情境;帶有資源編號的 URL 應進入指定內容;缺少參數或參數無效時,則應回到安全頁面,而不是停留在空白狀態。
將 URL 作為測試輸入
在 App Clip Scheme 中使用 _XCAppClipURL 適合人工除錯,但持續整合不應依賴開發者本機的 Scheme 狀態。更穩妥的做法是讓路由解析器接收 URL,透過單元測試直接涵蓋各種輸入,再保留一項模擬器冒煙測試來驗證應用程式生命週期。
func testInvocationRoutes() throws {
let base = try XCTUnwrap(URL(string: invocationBaseURL))
XCTAssertEqual(router.route(for: base), .home)
let item = try XCTUnwrap(URL(string: invocationItemURL))
XCTAssertEqual(router.route(for: item), .item(id: "42"))
let malformed = try XCTUnwrap(URL(string: invocationMalformedURL))
XCTAssertEqual(router.route(for: malformed), .fallback)
}
如此即可將網域名稱解析、路徑比對、查詢參數驗證與介面啟動分開處理。路由單元測試負責完整分支,模擬器工作只確認實際程序能夠啟動並顯示第一個畫面,失敗時也更容易直接定位問題。
收緊門禁並保留失敗現場
門禁失敗時,至少應封存 xcodebuild 記錄、兩份 Info.plist、匯出的權限檔案、宿主與 Clip 識別碼,以及失敗的測試案例名稱。若記錄包含存取權杖、臨時憑證或本機路徑中的敏感片段,應先去識別化再上傳。
建議固定檢查順序:清理獨立建置目錄、建置 Clip、封存宿主、檢查嵌入、比較關鍵權限、執行路由測試、執行模擬器冒煙測試。若前置結構檢查失敗,應立即停止,避免繼續在錯誤的封存檔上耗用模擬器時間。
GPUMini 上的工作也應明確記錄 xcodebuild -version、所選 SDK 與模擬器執行階段。真正需要長期維護的並非某次成功的螢幕擷取畫面,而是一組輸入明確、產物可追溯,且失敗後能快速重現的檢查項目。
常見問題
只建置 App Clip 目標就能證明封存檔正確嗎?
不能。獨立建置只能發現編譯與資源問題,仍需封存宿主應用程式並檢查 AppClips 路徑、識別碼關係與最終權限。
呼叫 URL 只測試一個固定網址可以嗎?
不建議。至少準備基本路徑、帶參數路徑與錯誤參數,分別驗證正常路由、資料解析與安全回退。
本機可以啟動時,為什麼持續整合仍要檢查套件?
本機 Scheme 可能直接啟動 App Clip,略過宿主封存檔的嵌入流程,因此無法發現複製階段、識別碼或權限漂移。
為開發與建置任務選擇一台雲端 Mac
比較兩種 M4 設定、四種租用週期與四個在售節點,再依任務需求完成部署。