Release APKs are obfuscated since R8 was turned on, and nothing kept
the mapping that decodes their stack traces. The workflow now copies
`android/app/build/outputs/mapping/release/mapping.txt` to
`bulwark-mobile-<version>-<commit>-mapping.txt`, uploads it as a
workflow artifact and attaches it to the release next to the APK. The
in-app updater only picks `.apk` assets, so it ignores the new file.
If R8 is on (`android.enableMinifyInReleaseBuilds=true`) and no mapping
was written, the step fails instead of publishing an APK whose crashes
cannot be read. docs/android-release.md explains how to decode a trace
with `retrace`.
Every published APK up to 0.1.62 was signed with the React Native
template's debug keystore, whose key is public. Anyone could sign an
"update" that Android installs over the app, with access to its
SecureStore credentials and mail cache. The release build type fell
back to the debug signing config whenever no release key was passed,
and the workflow carried on silently when `ANDROID_KEYSTORE_BASE64`
was empty.
- `android/app/build.gradle` reads the release key from the
`BULWARK_RELEASE_*` gradle properties or environment variables and
has no fallback. Without them a release build fails in the new
`checkReleaseSigning` task. It only runs for release variants, so
debug builds and `expo run:android` are unchanged. A local test
build can opt in with `-PallowDebugSignedRelease=true`, which is
ignored whenever `CI` is set.
- The release workflow fails in its first step when a signing secret
is missing, checks the keystore, alias and password before the
build, and refuses a debug key uploaded as the release key.
- An optional `ANDROID_SIGNING_LINEAGE_BASE64` secret re-signs the APK
with APK Signature Scheme v3 key rotation from the debug key to the
release key, from API 28, so existing installs on Android 9+ can
update and stop trusting the debug key afterwards.
- Before anything is uploaded, `apksigner verify` has to show the
release key as the Android 9+ signer, never the debug certificate.
- `*.keystore` and `*.lineage` files are gitignored.
Audit item S2 in `docs/audit-2026-09.md`.
CI:
- .github/workflows/release-android.yml builds APK on release publish
and via workflow_dispatch, attaches it to the release.
- android/app/build.gradle: optional release signingConfig driven by
Gradle properties (ANDROID_KEYSTORE_BASE64 secret etc.); falls back
to the debug keystore when no secrets are set.
- .gitignore: untrack /android so the runner can build directly;
ignore build outputs (build/, .cxx/), Metro/Expo dev logs, scratch.
Track android/ sources including BulwarkFcmPackage.kt and
google-services.json (Firebase Android keys are public per Google's
docs and intended to ship in the APK).
JS sync: push notifications + background task, vacation responder
(api + store), contact activity component, search utilities, settings
polish, calendar/contacts/email store and screen tweaks.
Tooling: scripts/poll-reload.mjs — Windows polling-watcher that hits
Metro /reload on file change (workaround for unreliable fs.watch
without Watchman).