Honour :ios_bundle_id everywhere; fail the deploy exit code; stamp the sim bundle id - #44
Conversation
9f6c43a to
8e987f9CompareThree 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>
8e987f9 to
c64e9dcCompareGenericJam
commented
Sep 4, 2026
Rebased onto current master (29 commits of drift, one trivial The device build looked its provisioning profile up by The release path ignored Making failures fatal broke an unrelated iPhone. Android reports "app not installed" as Also from the review: 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. Device verification on a physical iPhone SE with divergent ids ( 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). |
Uh oh!
There was an error while loading. Please reload this page.
Three bugs found and then verified fixed while deploying a real app to a physical iPhone.
1.
:ios_bundle_idwas silently ignored by the deployerlib/mob_dev/deployer.exhad:— a stub discarding the setting entirely, while
native_build.excorrectly usedcfg[:ios_bundle_id] || cfg[:bundle_id].Reproduced: with
bundle_id: "com.example.mishka_mob"andios_bundle_id: "com.genericjam.mishkamob",mix mob.deploy --native --device <udid>built, signed and installed the app ascom.genericjam.mishkamob, then the BEAM push immediately failed with "App 'com.example.mishka_mob' is not installed on this device." The workaround was to clobberbundle_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
applicationIdallows them), andcom.example.*is frequently already claimed by another Apple team.Fixed with a single
MobDev.Config.ios_bundle_id/0resolver.connector.exhad the byte-identical stub;mob.provisionandmob.battery_bench_iosare iOS-only paths that called genericbundle_id()— a profile provisioned for the wrong id doesn't cover the signed binary. The "not installed" hint now points atios_bundle_id:instead of sending you to clobberbundle_id.Device-verified: the exact configuration that previously failed now pushes cleanly.
2.
mix mob.deployexited 0 after a failed deployA run printing
Failed on 1 device(s)and✗ Kevin's iPhonestill returned status 0, so CI and wrapper scripts read a failed deploy as success.failure_message/3is 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.skippedstays non-fatal (it means "app not installed for that platform", the expected outcome of--ioswith an Android phone plugged in, and the earlier fix that splitskippedout 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=1with** (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.deployhot-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.plistwhile the device build used the configuredios_bundle_id, sosimctl 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/3reads the samecfgvalue, the sim path simply never stampedCFBundleIdentifier. Fixed rather than documented, and both paths now print the id they installed.Verification
compile --warnings-as-errors,format --check-formatted,credo --strictall clean.mix test: 2127/2136 pass; the 9 failures are pre-existing and environment-only, confirmed by stashing and re-running on pristineorigin/masterfor an identical result (3 ×HotPushTestbroken by aMIX_BUILD_PATHredirect, 6 ×Mob.Adopt*needing mob_new templates). The five test files covering the changed code run 271 passed / 0 failed.🤖 Generated with Claude Code