Skip to content

Honour :ios_bundle_id everywhere; fail the deploy exit code; stamp the sim bundle id - #44

Merged
GenericJam merged 2 commits into
masterfrom
fix/deploy-bundle-id-and-exit-code
Sep 4, 2026
Merged

Honour :ios_bundle_id everywhere; fail the deploy exit code; stamp the sim bundle id#44
GenericJam merged 2 commits into
masterfrom
fix/deploy-bundle-id-and-exit-code

Conversation

@GenericJam

Copy link
Copy Markdown
Owner

Three bugs found and then verified fixed while deploying a real app to a physical iPhone.

1. :ios_bundle_id was silently ignored by the deployer

lib/mob_dev/deployer.ex had:

defpios_bundle_id,do: bundle_id()

— a stub discarding the setting entirely, while native_build.ex correctly used cfg[:ios_bundle_id] || cfg[:bundle_id].

Reproduced: with bundle_id: "com.example.mishka_mob" and ios_bundle_id: "com.genericjam.mishkamob", mix mob.deploy --native --device <udid> built, signed and installed the app as com.genericjam.mishkamob, then the BEAM push immediately failed with "App 'com.example.mishka_mob' is not installed on this device." The workaround was to clobber bundle_id, which then breaks Android.

This matters because the two often cannot be the same string: Apple forbids underscores in a bundle id (Android's applicationId allows them), and com.example.* is frequently already claimed by another Apple team.

Fixed with a single MobDev.Config.ios_bundle_id/0 resolver. connector.ex had the byte-identical stub; mob.provision and mob.battery_bench_ios are iOS-only paths that called generic bundle_id() — a profile provisioned for the wrong id doesn't cover the signed binary. The "not installed" hint now points at ios_bundle_id: instead of sending you to clobber bundle_id.

Device-verified: the exact configuration that previously failed now pushes cleanly.

2. mix mob.deploy exited 0 after a failed deploy

A run printing Failed on 1 device(s) and ✗ Kevin's iPhone still returned status 0, so CI and wrapper scripts read a failed deploy as success.

failure_message/3 is extracted as a tested pure function (per this repo's rule about extracting the decision kernel from I/O wrappers). Every device is still attempted and the full summary printed first — only the exit code changed. skipped stays non-fatal (it means "app not installed for that platform", the expected outcome of --ios with an Android phone plugged in, and the earlier fix that split skipped out of the failure tally is preserved). Partial success is fatal: an operator can read the summary, but a script has no way to notice one device missed out.

Device-verified:EXIT_CODE=1 with ** (Mix) Deploy failed on 1 device(s). Worth noting the failure only manifests when there is no live dist session to fall back on — with one, mob.deploy hot-pushes over distribution and never touches the bundle id.

3. Simulator and device builds disagreed about the bundle id

The sim build took the id verbatim from ios/Info.plist while the device build used the configured ios_bundle_id, so simctl launch <configured-id> failed and you had to guess which id a build used. I checked whether signing forces this: it does not — codesign_ios_device_app/3 reads the same cfg value, the sim path simply never stamped CFBundleIdentifier. Fixed rather than documented, and both paths now print the id they installed.

Verification

compile --warnings-as-errors, format --check-formatted, credo --strict all clean. mix test: 2127/2136 pass; the 9 failures are pre-existing and environment-only, confirmed by stashing and re-running on pristine origin/master for an identical result (3 × HotPushTest broken by a MIX_BUILD_PATH redirect, 6 × Mob.Adopt* needing mob_new templates). The five test files covering the changed code run 271 passed / 0 failed.

🤖 Generated with Claude Code

claudeand others added 2 commits September 4, 2026 16:39
Three defects found deploying one app to a physical iPhone + an Android
phone with `bundle_id: "com.example.mishka_mob"` (Android's applicationId)
and `ios_bundle_id: "com.genericjam.mishkamob"` in mob.exs.
1. `:ios_bundle_id` was silently ignored by the deployer.
`Deployer.ios_bundle_id/0` was a stub delegating to `bundle_id/0`, so
`mix mob.deploy --native --device <udid>` installed the app under the
configured id (NativeBuild honours it) and then pushed BEAMs at an id
that was never installed: "App 'com.example.mishka_mob' is not
installed on this device". Added `MobDev.Config.ios_bundle_id/0` and
routed every iOS consumer through it — Deployer, Connector (including
the sim `pgrep` sweep), `mob.provision` (it mints the profile that must
cover the signed id), `mob.battery_bench_ios`. Android keeps
`bundle_id/0`: the two ids often can't be the same string, since Apple
forbids `_` and `com.example.*` is frequently claimed by another team.
2. `mix mob.deploy` exited 0 after printing "Failed on 1 device(s)".
`failure_message/3` now drives a `Mix.raise` after the summary prints.
`skipped` (app not installed for that platform) stays non-fatal, per
the earlier fix that split it out of the Failed bucket; partial success
IS fatal — the fan-out and reporting are unchanged, but a script can't
see a missed device if the status code says all is well.
3. Sim and device builds disagreed about the bundle id. The sim bundle
never stamped CFBundleIdentifier (inheriting ios/Info.plist) while the
device bundle stamped the configured one. No signing step rewrites the
id, so there was no reason to diverge: the sim path now stamps it too
via the shared `NativeBuild.ios_bundle_id/1`, and both paths print the
id they installed so the next simctl/devicectl command can use it.
Rationale in decisions/2026-08-08-ios-bundle-id-single-source-and-deploy-exit-code.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An adversarial review of the rebased PR #44 found three blockers. All three are
verified in source, and two of them were introduced or worsened by the PR
itself.
The device build looked its provisioning profile up by `cfg[:bundle_id]` while
stamping and codesigning with `ios_bundle_id`. The PR changed `mob.provision` to
mint the profile for the iOS id, so provision and deploy actively disagreed
where before they were at least consistently wrong.
The release path ignored `:ios_bundle_id` entirely — including three env vars
named `MOB_IOS_*` reading the non-iOS setting, which stamps a submitted IPA with
the Android applicationId. For the id that motivated this work that is
`com.example.mishka_mob`, which App Store Connect rejects for the underscore.
The AGENTS.md rule the PR adds was violated by the repo the moment it was
written.
Making failures fatal turned an unrelated iPhone into a broken build. Android
reports "app not installed" as `:skipped`; iOS threw `{:error, ...}`, so a plain
`mix mob.deploy` on a Mac with any iPhone attached that lacked the app went from
exit 0 to exit 1. iOS now uses the same bucket. The requested-vs-incidental
distinction — `--android` asking for Android and getting nothing should fail —
is MOB-150 and deliberately not in here.
Also fixed from the same review: `mix mob.uninstall` targeted the Android id on
iOS and so removed nothing while reporting success; `mob.doctor` warned that
`bundle_id` was unset for a project correctly setting only `ios_bundle_id`; and
stamping `CFBundleIdentifier` crashed with a MatchError on an Info.plist lacking
the key, since PlistBuddy `Set` fails on a missing key.
Every headline fix in the original PR survived being reverted with the whole
suite green — it tested both new resolvers and none of their call sites, which
is where the bug lived both times. `Deployer.ios_bundle_id/0`,
`Connector.ios_bundle_id/0` and `Uninstaller.resolve_apps_for_device/3` are now
documented seams so the wiring is testable, and the paths that need a keychain
or a device are pinned by source assertion. Nine mutations checked against an
exact baseline, all caught, including the two the reviewer named: a skip
absorbing a failure, and deleting the `Mix.raise` block while leaving the
function that decides it fully covered.
Device-verified on a physical iPhone SE with divergent ids
(`bundle_id: com.example.mob_plugin_demo`, `ios_bundle_id:
com.genericjam.mobplugindemo`): the app installs under the iOS id and the BEAM
push targets the same id; and with the app uninstalled the run reports
`Skipped on 1 device(s)` and exits 0 rather than failing.
Worth recording that the original bug is worse than reported. On a machine that
still has an older build under the other id, the push does not fail with "not
installed" — it silently succeeds against the wrong app's container and prints a
tick. Both containers accepted a write on the test device, and that is how the
first attempt to reproduce the bug appeared to pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GenericJam
GenericJamforce-pushed the fix/deploy-bundle-id-and-exit-code branch from 8e987f9 to c64e9dcCompareSeptember 4, 2026 23:00
@GenericJam

Copy link
Copy Markdown
OwnerAuthor

Rebased onto current master (29 commits of drift, one trivial AGENTS.md conflict where both sides appended to the same list), then re-verified on a physical iPhone SE as asked. An adversarial review found three blockers, two of them introduced or worsened by this PR, so the second commit fixes them rather than merging as-is.

The device build looked its provisioning profile up by cfg[:bundle_id] while stamping and codesigning with ios_bundle_id. This PR changed mob.provision to mint the profile for the iOS id, so provision and deploy actively disagreed where before they were at least consistently wrong.

The release path ignored :ios_bundle_id entirely — including three env vars named MOB_IOS_* reading the non-iOS setting, which stamps a submitted IPA with the Android applicationId. For the motivating id that is com.example.mishka_mob, which App Store Connect rejects for the underscore. The AGENTS.md rule this PR adds was violated by the repo the moment it was written.

Making failures fatal broke an unrelated iPhone. Android reports "app not installed" as :skipped; iOS threw {:error, ...}. So a plain mix mob.deploy on a Mac with any iPhone attached that lacked the app went from exit 0 to exit 1. iOS now uses the same bucket. The requested-vs-incidental distinction — --android asking for Android and getting nothing should fail — is MOB-150 and deliberately not in here.

Also from the review: mix mob.uninstall targeted the Android id on iOS (removed nothing, reported success); mob.doctor warned bundle_id was unset for a project correctly setting only ios_bundle_id; and stamping CFBundleIdentifier crashed with a MatchError on an Info.plist lacking the key, since PlistBuddy Set fails on a missing key.

Every headline fix here survived being reverted with the suite green. The PR tested both new resolvers and none of their call sites — which is where the bug lived both times. Deployer.ios_bundle_id/0, Connector.ios_bundle_id/0 and Uninstaller.resolve_apps_for_device/3 are now documented seams so the wiring is testable; paths needing a keychain or a device are pinned by source assertion. Nine mutations checked against an exact baseline, all caught — including the two the reviewer predicted: a skip absorbing a failure, and deleting the Mix.raise block while leaving the function that decides it fully covered.

Device verification on a physical iPhone SE with divergent ids (bundle_id: com.example.mob_plugin_demo, ios_bundle_id: com.genericjam.mobplugindemo): the app installs under the iOS id and the BEAM push targets that same id; with the app uninstalled the run reports Skipped on 1 device(s) and exits 0 instead of failing.

One finding worth recording: the original bug is worse than this PR describes. On a machine that still holds an older build under the other id, the push does not fail with "not installed" — it silently succeeds against the wrong app's container and prints a tick. Both containers accepted a write on the test device, which is exactly why my first attempt to reproduce the bug appeared to pass. Silent success into the wrong app is the dangerous form of this.

2217 tests, credo and format clean, CI green (which had never run on this branch before — only GitGuardian had).

@GenericJam
GenericJam merged commit 36c10d4 into masterSep 4, 2026
3 checks passed
Sign up for freeto 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.

2 participants

@GenericJam@claude