Ship prebuilt iOS XCTest agent artifacts and re-sign them locally instead of building per device
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 25/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- ios, react-native, typescript
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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.
- Lenguaje dominante
- TypeScript
- Estrellas
- 333
- Forks
- 20
- Merge medio
- 1 d 19 h
- PR fusionados (30 d)
- 4
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de callstackincubator/react-native-harness
-
Android runner can't start on a Windows host: SDK binaries are looked up without .exe or .batAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
-
Windows Android SDK path not foundPosiblemente ocupada @V3RON la tomó hace 34 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
-
Android startup retries do not restore lost adb reverse mappingsPosiblemente ocupada @l2hyunwoo la tomó hoy. Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 35/100
Todos los issues de callstackincubator/react-native-harness
Issues similares
-
bug priority: P2
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 73/100
mattermost/compass-design#106 ·
Los mantenedores suelen responder en 1 día
-
clawsweeper:needs-maintainer-review clawsweeper:needs-product-decision clawsweeper:no-new-fix-pr clawsweeper:source-repro impact:other issue-rating: 🦞 diamond lobster P2
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
openclaw/openclaw#176671 · 1 comentario · 1 reacción ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
nordcraftengine/nordcraft#1400 ·
Los mantenedores suelen responder en 1 día