云端 Mac 上的 App Clip 调用链与宿主包一致性验收

云端 Mac 上的 App Clip 调用链与宿主包一致性验收

一个 App Clip 在开发机上能从 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 应该只测试一个固定地址吗?

不应该。至少准备基础路径、带参数路径和非法参数三类用例,分别验证正常路由、参数解析和安全回退。

为什么本地可以启动,持续集成却应继续检查包结构?

本地运行可能直接使用 App Clip Scheme,绕过宿主归档中的嵌入关系。包结构检查能发现复制阶段、标识和权限配置漂移。

独享物理节点

为开发与构建任务选择一台云端 Mac

比较两档 M4 配置、四种租用周期与四个在售节点,再按任务需要完成部署。

选择配置并下单