Ship prebuilt iOS XCTest agent artifacts and re-sign them locally instead of building per device
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 5/5
- 预计耗时
- 一周以上
- 新手友好度
- 25/100
- Issue 类型
- 功能
- 描述清晰度
- 描述清楚
- 活跃度
- 活跃
- 技术栈
- ios, react-native, typescript
调研方向
Start with the existing xctest build flow, external .xctestrun handling through HARNESS_IOS_XCTESTRUN_FILE, simulator cache manifest, and release CI; the issue identifies these as the integration points. First verify cross-Xcode artifact reuse and re-signed installation on a physical iPhone, then work through the listed command, publishing, resolver, and fallback tasks. Done means a fresh checkout can launch a verified re-signed artifact without building, with fallback on mismatch or verification failure.
由索引模型根据 Issue 内容生成。
描述
Summary
Harness builds the iOS XCTest agent from source on every run. On physical devices this costs a full
xcodebuild build-for-testing per machine, per run, and it requires a working signing setup on the
runner (including live Developer Portal access via -allowProvisioningUpdates).
Research suggests both costs are avoidable: publish prebuilt, unsigned agent artifacts and let
consumers re-sign them locally with their own certificate and provisioning profile. This is the
distribution model Appium already ships for WebDriverAgent, and it matches the intent already
documented for xctest build:
Unsigned device builds are allowed intentionally for workflows that upload the
iphoneosartifacts
and re-sign them elsewhere. —website/src/docs/api/cli.mdx
This issue is the umbrella for that work. Findings below are measured on this repo (Xcode 27.0,
build 27A266a); the one material gap is listed under Unverified.
Observed findings
The artifact is fully relocatable
build-for-testing against generic/platform=iOS emits a .xctestrun that uses only the
__TESTROOT__ / __TESTHOST__ relocation macros. Copying Build/Products and the .xctestrun to an
unrelated path and running test-without-building works end to end:
AGENT HEALTH OK after ~16s
{"permissions":{"autoAcceptPermissions":false},"status":"ok"}
[HarnessXCTestAgent] HTTP server started on port 7099
The only absolute paths in the file are SourceFilesCommonPathPrefix and
ClangProfileDataDirectoryPath — coverage/instrumentation metadata, not read by coverage-ios.
getExternalXCTestConfiguration() already consumes an external .xctestrun via
HARNESS_IOS_XCTESTRUN_FILE, so the reuse path is largely wired.
No device coupling is baked in
- No UDID appears anywhere in the artifacts (grepped).
- No embedded
Frameworks/. Binaries resolve@rpath/XCTest.framework/XCTestfrom device-local
rpaths (/System/Developer/Library/Frameworks,/Developer/usr/lib). This is what lets one
artifact serve many devices and OS versions — the device supplies its own XCTest. - XCTest is linked with compatibility version
0.0.0, i.e. satisfied by any version.
Re-signing works and reproduces Xcode's own output
An unsigned device build leaves the .xctest plugin completely unsigned and the runner host app
carrying Apple's own com.apple.XCTRunner signature (TeamID 59GAB85EFG, copied from Apple's
XCTRunner.app template). Re-signing inside-out (plugin → runner + embedded profile → app) with plain
codesign produces:
valid on disk
satisfies its Designated Requirement # --verify --deep --strict
with entitlements identical to a fresh Xcode-signed build (application-identifier,
com.apple.developer.team-identifier, get-task-allow=true, plus keychain-access-groups on the
runner; the .xctest plugin legitimately carries none).
Bundle-ID remapping also works without a rebuild — rewrite CFBundleIdentifier in all three
Info.plists plus TestHostBundleIdentifier and the crash-report IDs in the .xctestrun, re-sign,
launch. This is the escape hatch for accounts whose profile cannot cover com.callstackincubator.*.
Note: the re-signed binary is not byte-identical to Xcode's signed one (differs at byte 3889;
Xcode links its signature differently). Equivalence is established by launch + healthy agent, not by hash.
Cost comparison
| Path | Wall time | Needs network | Needs Xcode |
|---|---|---|---|
build-for-testing (clean) |
10–22 s | yes, with automatic signing | yes |
| Re-sign a prebuilt artifact | 0.9 s | no | yes (codesign only) |
Artifact size
Compressed, after stripping *.dSYM and *.swiftmodule (~1.9 MB of dead weight):
- device (
iphoneos, arm64): 582 KB - simulator (
iphonesimulator, arm64 + x86_64 universal): 712 KB
Current-state gaps
- Device artifacts land in
.harness/xctest-agent/device, i.e. outside.harness/cache/, so
action.yml(which globsxctest-agent-simulator-*) never caches them. Every physical-device run
pays the build. - No re-sign command exists.
- The strict
cache.jsoncompatibility manifest from #108 is applied to simulator only. SimulatorXCTestAgentCacheManifestkeys onhostArchitecture, but simulator binaries are built
universal (x86_64 arm64), so that axis is a no-op and can be dropped.
How many artifacts do we need?
iOS version is not a multiplier. Binaries do not link against a specific iOS; minos comes from
our IPHONEOS_DEPLOYMENT_TARGET and is a floor, not a version key; the .xctestrun filename encodes
the SDK, not the OS. Device-vs-simulator and Xcode version are the only real multipliers.
Assuming we support Xcode 16.4 through current (see version note below):
| Axis | Multiplies? | Notes |
|---|---|---|
| Device vs simulator | yes, ×2 | the only structural multiplier |
| Xcode version | yes | one set per supported Xcode minor |
| iOS version (15/16/17…27) | no | device-local XCTest + 0.0.0 compat version |
| Device model / UDID | no | only the profile must cover the device |
| iPhone vs iPad | no | UIDeviceFamily = [1, 2] (TARGETED_DEVICE_FAMILY = "1,2") |
| Host Mac arch | no | simulator build is universal; device build ignores host |
| User team / profile | no | resolved at re-sign time (0.9 s) |
Version-range note (this is the trap)
Xcode 17 through 25 do not exist. Apple jumped straight from Xcode 16.4 to Xcode 26, then to 27,
to align with the year-based OS naming. So "one version before Xcode 26" cannot mean a 25.x line —
both plausible readings land on the 16.x line:
| Reading | Xcodes in scope | Artifact sets (device + sim) |
|---|---|---|
| Release immediately preceding 26 → 16.4 | 16.4, 26.6, 27.1 (latest per line) | 3 → 6 |
| Any 16.x → Xcode 16.0–16.4 | 16.0, 16.1, 16.2, 16.3, 16.4, 26.x, 27.x | up to 7 → 14 |
Recommendation: key artifacts on the Xcode minor, publish only the minors CI actually pins plus
one current per line, and fall back to build-for-testing on a miss. That is 3 sets / 6 assets
(~3.9 MB total) as of today, retiring superseded pairs.
Highest-leverage open question: if an artifact built with the oldest supported Xcode also runs
under newer xcodebuild test-without-building sessions, we ship 2 artifacts total instead of 6.
The .xctestrun is FormatVersion 2 on both, which is encouraging but not proof. See task 0.
Two consequences of the 16.4 floor, from Apple's system requirements
| Xcode | macOS required | On-device debugging | Simulator |
|---|---|---|---|
| 16.4 | Sequoia 15.3 – Tahoe 26.1.x | iOS 15+ | iOS 15+ |
| 26.x | Sequoia 15.6 – Tahoe 26.x | iOS 15+ | iOS 15+ |
| 27 | Tahoe 26.6+ | iOS 17+ | iOS 17+ |
- The minimum testable iOS version is set by the user's Xcode, not by us. Xcode 27 dropped
iOS 15/16 device support entirely. This matches what we measured independently: the Xcode 27
XCTRunner.apptemplate carriesMinimumOSVersion = 17.0, overriding our15.0deployment
target. Harness should emit a clear diagnostic in this case instead of an opaque XCTest failure. - The publish matrix needs two macOS runner images. Xcode 16.4 and 26.x both run on macOS 15
(16.4 needs ≥ 15.3, somacos-14is too old); Xcode 27 needs macOS 26.6+. Note our e2e workflow
currently pins26.4.1.
Suggested behaviour
- Publish artifacts as GitHub Release assets, not inside the npm tarball. Keep shipping the
.xcodeprojsource as the correctness floor. Key each asset on
{agentVersion, platform, arch, xcodeVersion, sdkVersion}with a checksum. - Move device artifacts under
.harness/cache/and give them the same manifest-keyed validation
the simulator path already has. - Add
xctest resignwith--teamId/--signingIdentity/--provisioningProfile/ optional
--bundleIdPrefix. Sign inside-out, embed the profile, apply the entitlements validated above,
and gate oncodesign --verify --deep --strict. - Reuse the existing launch path untouched: fetch → resign → set
HARNESS_IOS_XCTESTRUN_FILE/HARNESS_IOS_XCTEST_DERIVED_DATA_PATH→ the external-xctestrun
branch handles the rest. No changes needed inensureStarted. - Fall back to
build-for-testingon artifact miss, Xcode/SDK mismatch, or verification failure. - Document that a dev profile with
get-task-allow=trueonly runs on provisioned devices, and
that test bundles must be reinstalled every session regardless — re-signing saves compilation,
not installation.
Recommended implementation order
- 0. Prove or disprove cross-Xcode reuse. Build with the oldest supported Xcode, launch under
the newesttest-without-building. If it holds, the artifact count drops from 6 to 2 and most
of the matrix work disappears. Everything downstream is sized by this answer. - 1. Physical-device verification (see Unverified) — install and launch a re-signed device
bundle on a real iPhone. This is the one blocking unknown. - 2.
xctest resigncommand + unit tests, with a--dryRun/inspect mode. - 3. Publish pipeline in release CI for the supported Xcode matrix (two macOS images).
- 4. Artifact resolver — fetch, checksum, manifest validation, cache placement, device
artifacts under the cache root. - 5. Wire into
prepare()ahead ofbuildXCTestAgent, with fallback. - 6. Diagnostics for unsupported Xcode/iOS combinations and profile gaps.
- 7. Docs: artifact model, re-sign flow, iOS 17 floor under Xcode 27, free-account and
bundle-ID-remap path.
Success criteria
- A fresh checkout with a valid profile reaches a running agent without
build-for-testing. - Re-signing completes in ~1 s offline and passes strict code-sign verification.
- Xcode/SDK mismatch rejects the artifact and rebuilds rather than launching a bad binary.
- Physical-device runs no longer depend on Developer Portal reachability at run time.
Non-goals
- Do not embed artifacts in the npm package.
- Do not attempt to support iOS below the floor imposed by the user's Xcode.
- Do not distribute signing identities, private keys, or provisioning profiles.
- Do not remove
build-for-testingor the checked-in project.
Unverified (blocking)
- No physical-device test yet. Both available devices reported
unavailabletodevicectland no
re-sign tooling (resigner,pymobiledevice3,go-ios,ios-deploy) was installed. Device-side
install and launch of a re-signed bundle is currently inferred from Appium's production practice,
not observed. Simulator reuse and re-signing are verified; the device path is not. - Cross-Xcode artifact reuse (task 0) is untested — only one Xcode was installed.
- Forward compatibility of an older-SDK artifact running on the newest iOS is unproven; low risk given
weak-linked Swift runtime libraries and0.0.0XCTest compatibility, but worth a smoke test.
Prior art
Appium ships CODE_SIGNING_ALLOWED=NO "generic" WebDriverAgent builds on GitHub Releases, recommends
signing them with a wildcard profile, and provides download-wda / sign-wda. Free accounts, which
cannot mint wildcard profiles, are directed to bundle-ID remapping via resigner — the same escape
hatch measured above. Maestro, by contrast, still builds on the user's machine.
Related
- #108 closed with explicit non-goals "Do not cache device XCTest artifacts yet" and "Do not
prebundle simulator binaries into the npm package" — this umbrella takes on both, on top of the
cache layout and strict-manifest machinery that issue landed.
- 主要语言
- TypeScript
- 星标
- 333
- 派生
- 20
- 平均合并
- 14 小时
- 30 天内合并 PR
- 30
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
callstackincubator/react-native-harness 的其他 Issue
-
--metroPort is documented, but the CLI rejects it as an unrecognized option可能已有人在做 @YevheniiKotyrlo 于 1 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 72/100
callstackincubator/react-native-harness#199 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
callstackincubator/react-native-harness#128 ·
维护者通常 1 天内回复
-
iOS screenshot implementation disagrees with the generated nullable bounds contract可能已有人在做 @mrousavy 于 1 天前认领。 未关闭
难度 4/5 3-5 天 新手友好度 25/100
callstackincubator/react-native-harness#253 ·
维护者通常 1 天内回复
-
enhancement
难度 5/5 一周以上 新手友好度 15/100
callstackincubator/react-native-harness#247 · 1 个 reaction ·
维护者通常 1 天内回复
-
iOS simulator runs fail on Xcode 27 when the app is not installed可能已有人在做 @DaveyEke 于 2 天前认领。 未关闭
难度 2/5 1-3 小时 新手友好度 35/100
callstackincubator/react-native-harness#223 ·
维护者通常 1 天内回复
查看 callstackincubator/react-native-harness 的全部 Issue
相似的 Issue
-
bug P3 UI/UX
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 78/100
StellarState/SS-backend#712 ·
维护者通常 1 天内回复
-
area: backend bug
难度 2/5 1-3 小时 新手友好度 78/100
snapotter-hq/SnapOtter#3473 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
gitroomhq/postiz-app#2304 ·
维护者通常 1 天内回复
-
harlan-agent-ready-to-implement routine:ci-review
难度 2/5 1-3 小时 新手友好度 72/100
harlan-zw/unlighthouse#454 · 2 条评论 ·
维护者通常 1 天内回复