Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
7375287
fix(deploy): preserve Android app data on native updates
dl-alexandre Aug 2, 2026
721edb1
fix(deploy): freeze Android update targets
dl-alexandre Aug 2, 2026
e8b9a05
fix(deploy): fail closed on empty native targets
dl-alexandre Aug 3, 2026
ebfc291
Merge pull request #1 from dl-alexandre/fix/native-empty-target-fail-…
dl-alexandre Aug 3, 2026
afbca65
fix deterministic plugin manifest generation
dl-alexandre Aug 3, 2026
0676249
Merge pull request #2 from dl-alexandre/agent/deterministic-plugin-ma…
dl-alexandre Aug 3, 2026
4ffc2c4
fix signing of Objective-C plugin sources
dl-alexandre Aug 3, 2026
baf77b0
Merge pull request #3 from dl-alexandre/agent/sign-objective-c-native…
dl-alexandre Aug 3, 2026
ac4bf89
version plugin signature payloads safely
dl-alexandre Aug 3, 2026
7d3bfac
Merge pull request #4 from dl-alexandre/agent/versioned-plugin-signat…
dl-alexandre Aug 3, 2026
b5cd016
add exact-set Android deploy lease recovery
dl-alexandre Aug 3, 2026
7bf64d4
Merge pull request #5 from dl-alexandre/agent/android-deploy-lock-202…
dl-alexandre Aug 3, 2026
659d5a9
fix fail-closed Android tombstone cleanup
dl-alexandre Aug 3, 2026
7d6277d
test exact nonrecursive lease tombstone cleanup
dl-alexandre Aug 3, 2026
c8d0b91
Merge pull request #6 from dl-alexandre/fix/android-lock-corrective-v…
dl-alexandre Aug 3, 2026
8886747
fence Android hot push as one transaction
dl-alexandre Aug 3, 2026
11ca8c8
fix hot push exact lease target dispatch
dl-alexandre Aug 3, 2026
faecca6
test exact HotPush lease cleanup sequence
dl-alexandre Aug 3, 2026
1b46d23
Merge pull request #7 from dl-alexandre/agent/android-hot-push-fence-…
dl-alexandre Aug 3, 2026
30bee0f
make Android deploy payloads authoritative
dl-alexandre Aug 3, 2026
4d7c8da
test exact Deployer lease cleanup sequence
dl-alexandre Aug 3, 2026
acb0669
fix immutable payload and retained filesystem failures
dl-alexandre Aug 3, 2026
b3ff343
Merge pull request #8 from dl-alexandre/agent/android-authoritative-d…
dl-alexandre Aug 3, 2026
50ffd6e
fix: require authoritative iOS restart success
dl-alexandre Aug 3, 2026
06d9fbd
fix: keep physical iOS restart app-scoped
dl-alexandre Aug 3, 2026
9eaf52f
Merge pull request #9 from dl-alexandre/agent/ios-restart-authority-v…
dl-alexandre Aug 3, 2026
4d51c07
serialize native Android authority before iOS
dl-alexandre Aug 3, 2026
d305890
fix native cleanup gate and canonical target order
dl-alexandre Aug 3, 2026
c9ae6f2
fix native prevalidation cleanup escape
dl-alexandre Aug 3, 2026
966a111
fix native primary failure and canonical runtime set
dl-alexandre Aug 3, 2026
148a658
fix authoritative native deploy status validation
dl-alexandre Aug 3, 2026
8a1d7e8
fix: fail closed on ambiguous native iOS deploys
dl-alexandre Aug 3, 2026
ed2335f
fix: freeze native iOS target before build
dl-alexandre Aug 3, 2026
6020a38
fix: require unique native iOS build target
dl-alexandre Aug 3, 2026
78c9c70
fix: reject malformed iOS simulator inventory
dl-alexandre Aug 3, 2026
96dccc4
Merge pull request #10 from dl-alexandre/agent/android-deploy-fail-cl…
dl-alexandre Aug 3, 2026
3f93dd4
fix: isolate Zigler staging per native build
dl-alexandre Aug 3, 2026
36ed329
Merge pull request #11 from dl-alexandre/agent/zigler-stage-isolation…
dl-alexandre Aug 3, 2026
2918035
fix(deploy): fail early for unmatched device platform
dl-alexandre Aug 3, 2026
3b61c98
fix(deploy): require authoritative device match
dl-alexandre Aug 3, 2026
d02e5f0
fix(deploy): reject ambiguous device matches
dl-alexandre Aug 3, 2026
c8b9295
fix(deploy): preserve WiFi ADB selectors
dl-alexandre Aug 3, 2026
228fa06
Merge pull request #12 from dl-alexandre/agent/deploy-device-prefligh…
dl-alexandre Aug 3, 2026
d099a22
fix(android): report partial native updates
dl-alexandre Aug 3, 2026
c282e66
Merge pull request #13 from dl-alexandre/agent/android-partial-update…
dl-alexandre Aug 3, 2026
d37fa44
fix(android): bound Elixir metadata verification
dl-alexandre Aug 5, 2026
f3f423d
Merge pull request #14 from dl-alexandre/fix/android-elixir-app-metad…
dl-alexandre Aug 5, 2026
a1bef7c
feat(android): fence native-ready lease recovery
dl-alexandre Aug 5, 2026
6da94cd
fix(android): retain ambiguous recovery authority
dl-alexandre Aug 5, 2026
ee037c8
feat(android): prove native-ready recovery before resume
dl-alexandre Aug 5, 2026
0ac970e
Merge pull request #15 from dl-alexandre/fix/android-proof-orchestrator
dl-alexandre Aug 5, 2026
b22556a
fix(android): classify recovery proof refusals safely
dl-alexandre Aug 5, 2026
20ed316
Merge pull request #16 from dl-alexandre/fix/android-recovery-refusal…
dl-alexandre Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 68 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,46 @@ A few helpers are public specifically to enable testing (the parsing and
narrowing functions). Don't make them private:

- `Discovery.Android.parse_devices_output/1`
- `Discovery.IOS.parse_simctl_json/1`, `parse_simctl_text/1`, `parse_runtime_version/1`
- `Discovery.IOS.parse_simctl_json/1`, `parse_simctl_text/1`, `parse_runtime_version/1`,
`build_simctl_launch_args/2`, `restart_app_physical/3`, and
`build_devicectl_launch_args/2`
- `OtpDownloader.valid_otp_dir?/2`, `ios_device_extras_present?/1`
- `PythonAppleSupport.valid_dir?/1`
- `NativeBuild.narrow_platforms_for_device/2`, `ios_toolchain_available?/0`, `read_sdk_dir/1`, `fallback_entitlements_plist/3`
- `NativeBuild.pythonx_in_project?/1`, `python_apple_support_env/2`
- `NativeBuild.build_all_with_outcome/1`, `build_outcome/1`, `build_outcome/2`,
`ios_phase_decision/3`, `resolve_android_update_targets/2`,
`install_android_updates/3`, `install_and_deliver_android/4`, and
`install_and_deliver_android_runtime/8`, `release_android_deploy_lock/2`,
`interpret_adb_update/2`, `android_otp_dir_from_abi_probe/4`,
`android_package_listed?/2`, `deliver_android_otp_release/7`, and
`push_otp_runas/6` (typed sequencing and update-only Android safety seams;
the deprecated direct mutators intentionally fail closed)
- `AndroidDeployLock.valid?/2`, `acquire/4`, `verify_owner/3`, `transition/4`,
`release/2`, `status/3`, and `cleanup_committed_tombstone/3` (the shared,
exact-target Android mutation lease and its bounded recovery surface)
- `HotPush.prepare/1`, `push_prepared/2`, `push_prepared/3`,
`validate_prepared_snapshot/1`, and `push_prepared_fenced/3` (immutable BEAM
snapshot and lease-fenced RPC seams; raw Android pushes intentionally reject)
- `Mix.Tasks.Mob.DeployLock.inspect_or_cleanup/4` (hermetic task decision seam;
production still requires an explicit exact `--device`)
- `Deployer.collect_android_beam_dirs/0`, `prepare_android_payload/2`,
`valid_android_payload?/2`, `cleanup_android_payload/1`, and
`deploy_all_with_lease/1`, `execute_ios_restart/1`, and
`interpret_ios_restart_result/1`, `restart_ios_simulator/4`, and
`restart_ios_physical/4` (immutable final-pass payload, shared-lease
integration, and authoritative iOS restart seams)
- `Deployer.select_canonical_android_devices/2`,
`classify_android_package_probe/2`, `deploy_android_device/4`,
`ensure_erts_on_device/3`, `verify_elixir_runtime_version_android/5`,
`setup_exqlite_android_runas/4`, `push_beams_android_runas/3`, and
`restart_android/3` (exact-target and per-mutation fencing seams; ordinary
`--device` matching remains user-friendly)
- `Mix.Tasks.Mob.Deploy.run/2`, `resolve_target_platforms!/4`, `execute_native_deploy!/6`,
`deploy_after_native_build!/3`, `deploy_after_native_build!/4`,
`deploy_after_native_build!/5`, `deploy_after_native_build!/6`,
`ensure_deploy_succeeded!/1`, and `report_deploy_result!/2` (typed
orchestration/result seams)
- `NativeBuild.__prune_plugin_artifacts__/2` (the plugin-removal prune; ledger-tracked per merge concern)
- `Enable.inject_pythonx_dep/1`, `inject_pythonx_uv_init_gate/2`, `python_paths_module_template/1`
- `Emulators.parse_simctl_json/1`, `find_emulator_binary/1`
Expand Down Expand Up @@ -164,9 +199,38 @@ needing the same fan-out behavior. Pin the headline guarantee in
each task's tests — "personal iPhone + dev emulators + `--all-devices`
must leave the iPhone alone."

**TODO:** apply this pattern to `mix mob.deploy` (today's `--all-devices`
deploy can push BEAMs to a personal phone). When that fan-out exists
or grows, factor `select_devices/3` plus the flag plumbing into a
Android **native** deploys resolve a non-empty connected serial set (narrowed
by `--device <id>` when supplied) and run only the data-preserving
`adb -s <serial> install -r <apk>` update path. They never force-stop first,
uninstall, or fall back to a clean install. A failed update must prevent the
final `MobDev.Deployer` pass. Before the first device mutation, freeze and hash
the APK, OTP archives, BEAM/priv payload, optional exqlite payload, restart
arguments, and exact canonical serial set. One phase-bound
`AndroidDeployLock` covers that complete set across native install/OTP work and
the final BEAM/restart pass. Prove the entire set immediately before every
mutation, halt later targets on the first failure, and retain the exact lease
on any ambiguous reply. Only a fully successful final pass may advance to a
committed phase and release it. Build-only APIs must remain artifact-only and
must never acquire a device lease or install an APK.

For a mixed native Android+iOS deploy, complete, commit, release, and clean the
entire Android transaction before beginning the iOS build or install. An exact
typed `:not_attempted` Android disposition may proceed to iOS; any malformed,
failed, retained, or ambiguous Android outcome suppresses iOS and fails closed.

Never recover by clearing app data, uninstalling, deleting an active lock, or
blindly retrying. `mix mob.deploy_lock --device <exact-serial>` is read-only;
`--cleanup-committed` may remove only one exact record-only tombstone already
in a committed phase and must prove the final clear state.

Fast Android BEAM deploys use an exact-set shared lease. Distribution is used
only when every frozen target is already connected; otherwise the entire set
uses the fenced filesystem/restart path rather than splitting authority.

**TODO:** apply the full physical-device *selection* pattern to the fast
`mix mob.deploy` BEAM fan-out. Its mutations are now exact-set fenced, but the
broad selector can still include a personal phone. When that fan-out grows,
factor `select_devices/3` plus the flag plumbing into a
shared `MobDev.TaskTargets` (or similar) module so the rules don't
drift between tasks.

Expand Down
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ end
| `mix mob.install` | First-run setup: download OTP runtime, generate icons, write `mob.exs` |
| `mix mob.deploy` | Compile and push BEAMs to all connected devices |
| `mix mob.deploy --native` | Also build and install the native APK/iOS app |
| `mix mob.deploy_lock --device ID` | Inspect one exact Android deploy lease; optionally clean only a verified committed tombstone |
| `mix mob.deploy --slim` | Same, but with the App Store strip pass applied (slow, lets you verify a slim build before TestFlight — see [`guides/slim_release.md`](guides/slim_release.md)) |
| `mix mob.release` | Build a signed `.ipa` / `.aab` for App Store / TestFlight / Play Store (slim by default) |
| `mix mob.release --security-gate` | Same, but runs `mix mob.security_scan` first and aborts on any critical/high/medium finding ([details](guides/security_scan.md)) |
Expand Down Expand Up @@ -89,10 +90,51 @@ Pushing 14 BEAM file(s) to 2 device(s)...
iPhone 15 Pro → pushing... ✓ (dist, no restart)
```

If dist is not reachable (first deploy, app not running), it falls back to `adb push` + restart. Mixed deploys work — one device can hot-push while another restarts.
If every frozen Android target is already reachable over distribution, the
whole exact set hot-pushes. Otherwise the whole Android set uses the fenced
`adb push` + restart path; Mob never splits one Android transaction across two
authorities. A mixed iOS/Android command handles each platform in its own
ordered, committed phase.

**Requirements:** The app must call `Mob.Dist.ensure_started/1` at startup, and the cookie must match the one in `mob.exs` (default `:mob_secret`).

### Android native updates preserve app data

`mix mob.deploy --native --android` is deliberately update-only. Every selected
device must already contain the configured package, and Mob uses only the
serial-scoped equivalent of `adb install -r`. It never clears app data,
uninstalls the package, or turns a rejected update into a fresh install.

Before the first device write, Mob snapshots and verifies the exact APK, OTP,
BEAM, `priv`, and optional exqlite payloads. A phase-bound lease covers the
sorted canonical device set so a concurrent deploy or hot push cannot change a
subset mid-transaction. The lease advances only after the native payload and
then the final authoritative BEAM/restart pass have each completed on every
target. Replayed, widened, stale, or wrong-phase work fails closed.

For a mixed Android+iOS native command, Android is deliberately serialized
first: it must commit, release its exact-set lease, and clean its immutable
staging before iOS build/install begins. A typed result that proves Android was
not attempted may continue to iOS; every failed, retained, malformed, or
ambiguous Android result suppresses iOS. Fast Android BEAM deploys are also
exact-set transactions.

If transport authority becomes ambiguous after a write, Mob intentionally
retains the device-side lease or release tombstone and stops later targets. Do
not recover by uninstalling the app or deleting its data. Inspect the bounded
lease status, resolve the interrupted operation, and remove only a verified
committed release tombstone; an active or malformed lease requires manual
diagnosis.

```sh
mix mob.deploy_lock --device <exact-adb-serial>
mix mob.deploy_lock --device <exact-adb-serial> --cleanup-committed
```

The first command is read-only. The second refuses every state except one exact,
record-only tombstone that already carries a committed phase, and proves the
device returned to a clear state after the single cleanup attempt.

## `mix mob.enable <feature>`

Wires up an optional Mob feature in one command — platform-manifest
Expand Down
Loading
Loading