Ship prebuilt iOS XCTest agent artifacts and re-sign them locally instead of building per device
メンテナーはふだん 1 日以内に返信
まだ誰も着手していません。
評価
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 初心者へのやさしさ
- 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時間 55分
- マージ済み PR(30日)
- 44
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
callstackincubator/react-native-harness のほかの issue
-
--metroPort is documented, but the CLI rejects it as an unrecognized option対応中かも @YevheniiKotyrlo が 2 日前に担当しました。 オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
callstackincubator/react-native-harness#199 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
callstackincubator/react-native-harness#128 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
callstackincubator/react-native-harness#277 ·
メンテナーはふだん 1 日以内に返信
-
難易度 3/5 1〜2日 初心者へのやさしさ 72/100
callstackincubator/react-native-harness#273 ·
メンテナーはふだん 1 日以内に返信
-
Android Activity recreation during a UI test is not reported as a host lifecycle change対応中かも @mrousavy が 1 日前に担当しました。 オープン
難易度 4/5 3〜5日 初心者へのやさしさ 35/100
callstackincubator/react-native-harness#271 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
callstackincubator/react-native-harness の issue をすべて見る
似ている issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 1/5 1時間未満 初心者へのやさしさ 76/100
logto-io/logto#9837 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
難易度 1/5 1時間未満 初心者へのやさしさ 76/100
lingdojo/kana-dojo#32964 · コメント 1 件 · リアクション 5 件 ·
メンテナーはふだん 1 日以内に返信
-
Documentation status: needs triage
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
mastra-ai/mastra#28194 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
priority-low refactor-3
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
メンテナーはふだん 3 日以内に返信