Skip to content

fix(ios): make on-device Debug builds signable with App Groups - #480

Merged
RonenMars merged 1 commit into
land/integration-prepfrom
fix/ios-dev-signing
Aug 1, 2026
Merged

fix(ios): make on-device Debug builds signable with App Groups#480
RonenMars merged 1 commit into
land/integration-prepfrom
fix/ios-dev-signing

Conversation

@RonenMars

Copy link
Copy Markdown
Owner

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.

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
RonenMars merged commit 95a6998 into land/integration-prep Aug 1, 2026
10 checks passed
@RonenMars
RonenMars deleted the fix/ios-dev-signing branch August 1, 2026 12:40
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]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant