Skip to content

v4.25 — Release-Build Safety Gates

CI now assembles and boots a minified/release-mode mobile build before merge — on both platforms — so a crash that only reproduces under R8/optimization can't ship undetected again.

Summary

Before this milestone, a production release-only startup crash had already shipped once: v4.3.0's Android build force-closed on launch because R8 minification stripped WorkManager's reflective WorkDatabase_Impl.<init> (OBJ-1899). The root cause was structural, not a one-off miss — every pre-merge mobile CI job built debug artifacts only, and the jobs that did assemble release/minified artifacts (assembleRelease, the tag-only fastlane iOS archive) only assembled or uploaded them, never booting the result. This entire class of bug — crashes and registration failures that only reproduce in an optimized build — was invisible to CI regardless of test count or quality, on either platform.

This milestone closed that gap with three pre-merge CI gates: a Capacitor native-registration drift gate (catching an npm-installed plugin that ships without its native registration regenerated — the same shape as OBJ-1899's companion drift), an Android release build-and-boot smoke test, and an iOS release build-and-boot smoke test paired with a separate plugin-registration check (a boot smoke can't catch a missing registration — it fails as UNIMPLEMENTED at call time, not a crash). All three shipped as independent, additive GitHub Actions workflows off a shared integration branch, with a fourth phase documenting them. The iOS registration check proved its own premise on landing: it caught a real live bug, WidgetBridgePlugin compiled into the App target but never registered with the Capacitor bridge.

The whole milestone is infrastructure-only — new .github/workflows/*.yml files and CI-side build config under ionic_frontend/. No Rails or Vue application code changed, and rollback for any gate is simply deleting or disabling its workflow.

Goal

Close the structural CI gap that let the OBJ-1899 crash ship: add pre-merge gates that assemble a minified/release-mode mobile build and boot it far enough to confirm no immediate startup crash, on both Android and iOS, plus a cap sync drift gate so an npm-installed Capacitor plugin can't silently ship without its native registration. A boot-and-check-no-crash smoke — not full E2E — is enough to have caught this bug class.

Scope — What Shipped

  • .github/workflows/cap-sync-drift.yml — on any PR (or push to master) touching ionic_frontend/package.json, package-lock.json, android/**, or ios/**, runs a fresh npx cap sync android && npx cap sync ios and diffs the result against the 3 git-tracked, cap-sync-generated files (android/capacitor.settings.gradle, android/app/capacitor.build.gradle, ios/App/CapApp-SPM/Package.swift). Deliberately triggers on the npm manifests, not just native paths — the bug class it targets can land with zero native-file changes.
  • .github/workflows/mobile-android-boot-smoke.yml — a proguard-keep-rules-guard job grepping proguard-rules.pro for the WorkManager/Room keep rules, plus a boot-smoke job that assembles a minified :app:assembleRelease (debug-signed), verifies an R8 mapping.txt was actually produced, and boots the APK on a reactivecircus/android-emulator-runner emulator, failing on a startup crash or ANR within a 10-second window.
  • .github/workflows/mobile-ios-boot-smoke.yml — two independent jobs: boot-smoke (3a) builds the App target in Release for an iOS Simulator (CODE_SIGNING_ALLOWED=NO), verifies the build wasn't de-optimized to pass (SWIFT_OPTIMIZATION_LEVEL must not resolve to -Onone), then boots and asserts no startup crash; plugin-registration-check (3b) runs ios/App/AppTests/PluginRegistrationTests.swift, which reflects over the App target's compiled classes plus the npm-side packageClassList and asserts every one of them actually registers with the live Capacitor bridge.
  • A same-milestone live bug fix: WidgetBridgePlugin compiled into the iOS App target but never registered — closed by adding bridge?.registerPluginInstance(WidgetBridgePlugin()) to MainViewController.capacitorDidLoad().
  • Documentation: docs/operations/mobile-builds.md's "Release-Build Safety Gates (v4.25)" section (all three gates — what each catches, how it works, local-reproduction commands), a new CONTRIBUTING.md § Capacitor Plugin Installs contributor rule, and a CHANGELOG.md [Unreleased] entry.

Phases

PhaseNameStatusPlansHighlights
1cap sync drift gateShipped1New cap-sync-drift.yml; triggers on npm manifest changes, not just native paths — closing the exact no-native-diff shape OBJ-1899's companion drift shipped with
2Android release build & boot smokeShipped1Minified :app:assembleRelease + emulator boot + ProGuard keep-rule guard; needed an Orion respec to a single hardened script after 3 fix-rounds
3iOS release boot smoke + plugin-registration checkShipped1Two separate gates (3a boot smoke, 3b registration reflection test); 3b caught a real live bug on landing — WidgetBridgePlugin unregistered
4DocumentationShipped1mobile-builds.md section, CONTRIBUTING.md rule, CHANGELOG.md entry; shipped via the milestone-merge PR after a wrong-base-ref false start

Key Decisions

  • Three separate CI gates instead of one combined check — the milestone's own root-cause analysis found two structurally distinct failure modes that a single check couldn't cover: (1) native-registration drift from an un-synced npm plugin install, invisible without diffing generated files against the dependency graph; (2) a release-only crash, which only a real boot-and-wait can catch; (3) a release-only registration gap, which surfaces as UNIMPLEMENTED at call time and is invisible to a boot smoke because the app doesn't crash — it just silently fails the specific call. Folding (2) and (3) into one iOS gate was explicitly rejected in the ROADMAP's locked decisions: a boot smoke proves the app started, not that every plugin it depends on actually works.
  • The drift gate triggers on package.json/package-lock.json, not native paths only — a native-only filter would have made the gate a no-op on the exact bug class it targets: PR #1542 (which added @capacitor/app-launcher) touched 38 files and zero under android/ or ios/. The boot-smoke gates (Phases 2–3) were allowed to keep a native-only path filter as a cost mitigation, since they don't share that blind spot.
  • Boot smoke, not full E2E — assemble minified/release → install → launch → assert the process is still alive and no crash dialog appeared, for a fixed window. Full E2E was explicitly scoped out; a boot-and-check-no-crash smoke is sufficient to have caught OBJ-1899.
  • The WidgetBridgePlugin fix that 3b's own build validated its premise — the plugin-registration check wasn't a speculative gate against a hypothetical future regression; it caught a live, already-shipped bug (WidgetBridgePlugin compiled but never calling registerPluginInstance) during the same phase that built the check. That's the strongest possible proof the check earns its place: it went red on a real defect, not a synthetic one.
  • Additive-only, no fix-first phase — the ROADMAP's baseline scoping found the WorkManager/Room ProGuard keep rules (PR #1877) were already present on master by kickoff, so this milestone added CI gates only; it never touched product code, and rollback for any phase is deleting or disabling its workflow.

Requirements Coverage

4 / 4 deliverables satisfied (quoted from the MILESTONE-AUDIT). This is a CI/infra-only milestone with no backend/API/data-model surface, so deliverables are tracked by the ROADMAP's four phase IDs (OBJ-1902–1905) rather than a numbered REQUIREMENTS.md.

CategoryCountStatus
OBJ-1902 (Phase 1 — drift gate)1Satisfied
OBJ-1903 (Phase 2 — Android boot smoke)1Satisfied
OBJ-1904 (Phase 3 — iOS boot smoke + registration check)1Satisfied
OBJ-1905 (Phase 4 — docs)1Satisfied

Full detail: v4.25-release-build-safety-gates-MILESTONE-AUDIT.md on GitHub.

Outcomes

A PR that reintroduces the OBJ-1899 bug class — a minified/release build that crashes on startup, or a Capacitor plugin that ships without native registration — now fails CI before merge, on both Android and iOS, instead of reaching production undetected. The three gates are proven against the failure modes they target: the drift gate is designed around the exact no-native-diff shape the original bug shipped with, the boot smokes exercise the real R8/optimization codepath (not a debug build), and the registration check already caught one live defect during its own construction. No production tag had been cut for this milestone as of ship — it rides the next scheduled Wednesday release train per normal (non-hotfix) cadence, but the gates themselves are live on master and protecting every PR from the moment they merged.

Tech Debt

None open. One candidate — OBJ-1906, docs debt split out of Phase 1 at ship time — was checked against master during the retroactive audit and appears fully covered by Phase 4's documentation (docs/operations/mobile-builds.md, CONTRIBUTING.md); see the MILESTONE-AUDIT for the caveat on that finding.

  • 30cddb29 — [Orion] v4.25: Release-Build Safety Gates — milestone merge to master (#1893)
  • 7ac2c0b9 — [Tess] ci(mobile-ios): add Release boot smoke + plugin-registration gate (OBJ-1904) (#1889)
  • 86d9cf1b — [Tess] ci(mobile-android): add minified release build & boot smoke gate (OBJ-1903) (#1888)
  • ca6a7e72 — [Tess] ci(cap-sync): add native drift gate (OBJ-1902) (#1885)

Last updated: 2026-08-04

Loading…