雲端 Mac 的 App Clip 呼叫鏈與宿主套件一致性驗證

雲端 Mac 的 App Clip 呼叫鏈與宿主套件一致性驗證

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 設定、四種租用週期與四個在售節點,再依任務需求完成部署。

選擇設定並下單