fix(ios): make on-device Debug builds signable with App Groups - #480
Merged
Conversation
An on-device Debug build could not sign. Xcode's automatic signing generates an "iOS Team Provisioning Profile" that does not carry App Groups, and both Threadbase and ExpoWidgetsTarget declare group.com.ronenmars.threadbase, so xcodebuild failed with six errors before compiling anything. Enabling the capability on the App ID is not sufficient: automatic signing regenerates its own profile every build and ignores hand-made ones, so manual signing with an explicit profile per target is the only way through. That cannot be expressed on the xcodebuild command line, because command-line build settings apply to every target at once while the app and the widget need different profiles. withLiveActivityTarget now maps IOS_PROVISION_PROFILE_UUID and IOS_WIDGET_PROVISION_PROFILE_UUID per target for Debug as well as Release, and dev-device.sh feeds the UUIDs in through XCODE_XCCONFIG_FILE. Debug stays on CODE_SIGN_STYLE = Automatic in the committed project, so this is inert by default and a simulator build, a fresh clone, or CI is unaffected: an unset UUID leaves the specifier empty and automatic signing behaves exactly as before. A test pins that, so a later change cannot silently require provisioning profiles for every Debug build. dev-device.sh discovers the profiles rather than taking configuration, scanning installed profiles for a development one whose app-id matches and which grants App Groups, so no UUIDs are hardcoded and no per-machine setup is needed.
RonenMars
added a commit
that referenced
this pull request
Aug 1, 2026
## Problem `npm run dev:device` could not sign, failing during "Planning build" with six errors and `xcodebuild` exit 65: ``` Provisioning Profile "iOS Team Provisioning Profile: com.ronenmars.threadbase" does not support the App Groups capability. ❌ Threadbase → group.com.ronenmars.threadbase ❌ ExpoWidgetsTarget → group.com.ronenmars.threadbase ``` Release builds and TestFlight ships were unaffected — they use explicit distribution profiles that do carry App Groups. ## Why the obvious fixes don't work **Enabling the capability on the App ID is not enough.** It was already enabled on both bundle ids, the device was already registered, and a DEVELOPMENT certificate already existed. Xcode's *automatic* signing regenerates its own "iOS Team Provisioning Profile" on every build and ignores hand-made ones, so the profile it produces still lacks App Groups. Manual signing with an explicit profile per target is the only way through. **The profiles can't be passed on the command line.** `xcodebuild` applies command-line build settings to every target at once, and the app and the widget need *different* profiles. So the per-target mapping has to live in the project. ## Change `withLiveActivityTarget` already mapped `IOS_PROVISION_PROFILE_UUID` / `IOS_WIDGET_PROVISION_PROFILE_UUID` per target for Release. It now does the same for Debug, and `dev-device.sh` supplies the UUIDs through `XCODE_XCCONFIG_FILE` — the one channel that reaches a build `expo run:ios` invokes on our behalf. **Inert by default.** Debug stays on `CODE_SIGN_STYLE = Automatic` in the committed project. An unset UUID leaves the specifier empty and automatic signing behaves exactly as before, so a simulator build, a fresh clone, and CI are all unaffected. A test pins this, so a later change can't silently start requiring provisioning profiles for every Debug build. **Discovered, not configured.** `dev-device.sh` scans installed profiles for a development one (has `ProvisionedDevices`) whose app-id matches and which grants App Groups. No UUIDs in the repo, no per-machine setup. If none is found it says so and points at the troubleshooting entry rather than failing opaquely. ## Verification - Built and installed on a physical iPhone 17 Pro — **0 signing errors**, where the same command previously failed with 6. - Plugin suite: 11 tests pass, including 3 new ones covering Debug mapping, the automatic-signing guard, and idempotency across both configurations. - ESLint clean; `dev-device.sh` passes `bash -n`. - Profile discovery resolves both UUIDs correctly against the installed set. ## Note on the native diff `ios/Threadbase.xcodeproj/project.pbxproj` is **+2 lines**, hand-applied to match exactly what the plugin generates. A full `expo prebuild --no-clean` additionally re-quoted `PRODUCT_NAME`, added an `Assets.xcassets` reference, and rewrote three other native files; that churn is unrelated to this change and was reverted. The next full prebuild will not fight these two lines. Setting up the two development profiles is a one-time account action, documented in the new troubleshooting entry along with the API calls to check the prerequisites first.
RonenMars
added a commit
that referenced
this pull request
Aug 1, 2026
…it [skip-ci] The end-to-end check passed from `main` on a physical iPhone 17 Pro, but only after three false starts the Verification section did not warn about. A bare `npx expo run:ios --device` cannot sign this app on a physical device. Xcode's automatic signing regenerates its own Team Provisioning Profile every build and ignores hand-made ones, so the profile never carries App Groups and the build dies during "Planning build" with six errors and xcodebuild exit 65. That is exactly the failure #480 exists to solve, and it ships `scripts/dev-device.sh` for it — the six errors mean the wrong command was used, not that signing regressed. The existing block is right for a simulator, and only its `<simulator-udid>` placeholder said so. The other two faults arrived together and are independent, which matters because either can occur alone. The first is that a Metro already listening on 8081 captures the device. `expo run:ios` is non-interactive here, so it answers its own port-conflict prompt with `Skipping dev server`, and the device attaches to whatever already owns the port — on this run a server rooted at the repo root on `land/integration-prep` rather than the worktree under test. This is the dangerous one precisely because it is silent: with a healthy bundler it produces a working app showing another branch's code, and the log reports success. The remedy is to check the port owner before trusting the result and to confirm the bundle root in the Metro log. The second is that a long-lived Metro's resolver cache goes stale when its tree is rewritten underneath it. That server had been running for eight hours across the whole landing and could no longer resolve `react/jsx-runtime`, which was present on disk in both trees the entire time. This has nothing to do with which branch is served — it reproduces on the correct branch, with no mismatch to point at — so it is documented as its own failure with `--clear` as the fix. Both were visible in one red screen, and only the module path in the error revealed that the tree was also wrong. Recording them as one story would send the next reader hunting a branch-contamination bug that does not exist. [skip-ci]
RonenMars
added a commit
that referenced
this pull request
Aug 1, 2026
…it [skip-ci] The end-to-end check passed from `main` on a physical iPhone 17 Pro, but only after three false starts the Verification section did not warn about. A bare `npx expo run:ios --device` cannot sign this app on a physical device. Xcode's automatic signing regenerates its own Team Provisioning Profile every build and ignores hand-made ones, so the profile never carries App Groups and the build dies during "Planning build" with six errors and xcodebuild exit 65. That is exactly the failure #480 exists to solve, and it ships `scripts/dev-device.sh` for it — the six errors mean the wrong command was used, not that signing regressed. The existing block is right for a simulator, and only its `<simulator-udid>` placeholder said so. The other two faults arrived together and are independent, which matters because either can occur alone. The first is that a Metro already listening on 8081 captures the device. `expo run:ios` is non-interactive here, so it answers its own port-conflict prompt with `Skipping dev server`, and the device attaches to whatever already owns the port — on this run a server rooted at the repo root on `land/integration-prep` rather than the worktree under test. This is the dangerous one precisely because it is silent: with a healthy bundler it produces a working app showing another branch's code, and the log reports success. The remedy is to check the port owner before trusting the result and to confirm the bundle root in the Metro log. The second is that a long-lived Metro's resolver cache goes stale when its tree is rewritten underneath it. That server had been running for eight hours across the whole landing and could no longer resolve `react/jsx-runtime`, which was present on disk in both trees the entire time. This has nothing to do with which branch is served — it reproduces on the correct branch, with no mismatch to point at — so it is documented as its own failure with `--clear` as the fix. Both were visible in one red screen, and only the module path in the error revealed that the tree was also wrong. Recording them as one story would send the next reader hunting a branch-contamination bug that does not exist. [skip-ci]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
npm run dev:devicecould not sign, failing during "Planning build" with six errors andxcodebuildexit 65:Release builds and TestFlight ships were unaffected — they use explicit distribution profiles that do carry App Groups.
Why the obvious fixes don't work
Enabling the capability on the App ID is not enough. It was already enabled on both bundle ids, the device was already registered, and a DEVELOPMENT certificate already existed. Xcode's automatic signing regenerates its own "iOS Team Provisioning Profile" on every build and ignores hand-made ones, so the profile it produces still lacks App Groups. Manual signing with an explicit profile per target is the only way through.
The profiles can't be passed on the command line.
xcodebuildapplies command-line build settings to every target at once, and the app and the widget need different profiles. So the per-target mapping has to live in the project.Change
withLiveActivityTargetalready mappedIOS_PROVISION_PROFILE_UUID/IOS_WIDGET_PROVISION_PROFILE_UUIDper target for Release. It now does the same for Debug, anddev-device.shsupplies the UUIDs throughXCODE_XCCONFIG_FILE— the one channel that reaches a buildexpo run:iosinvokes on our behalf.Inert by default. Debug stays on
CODE_SIGN_STYLE = Automaticin the committed project. An unset UUID leaves the specifier empty and automatic signing behaves exactly as before, so a simulator build, a fresh clone, and CI are all unaffected. A test pins this, so a later change can't silently start requiring provisioning profiles for every Debug build.Discovered, not configured.
dev-device.shscans installed profiles for a development one (hasProvisionedDevices) whose app-id matches and which grants App Groups. No UUIDs in the repo, no per-machine setup. If none is found it says so and points at the troubleshooting entry rather than failing opaquely.Verification
dev-device.shpassesbash -n.Note on the native diff
ios/Threadbase.xcodeproj/project.pbxprojis +2 lines, hand-applied to match exactly what the plugin generates. A fullexpo prebuild --no-cleanadditionally re-quotedPRODUCT_NAME, added anAssets.xcassetsreference, and rewrote three other native files; that churn is unrelated to this change and was reverted. The next full prebuild will not fight these two lines.Setting up the two development profiles is a one-time account action, documented in the new troubleshooting entry along with the API calls to check the prerequisites first.