Skip to content

feat(android): CI build/sign/release + remote-first OTA for the NIETE app (bd-2553) - #182

Merged
hatafatif merged 1 commit into
developfrom
bd-2553-21241
Aug 12, 2026
Merged

feat(android): CI build/sign/release + remote-first OTA for the NIETE app (bd-2553)#182
hatafatif merged 1 commit into
developfrom
bd-2553-21241

Conversation

@hatafatif

Copy link
Copy Markdown
Collaborator

Why

The Android pipeline is entirely manual and off-repo — hand-typed vite build --mode app, a local gradle build, a manual Play upload. That process already shipped a broken release: versionCode 1206 went to the Play internal track carrying a WEB-mode bundle, resolveApiBaseUrl() threw at first render, React never mounted, and every launch was a white screen (bd-2551) — which needed a WhatsApp downtime notice to 80 coaches (bd-2552). The build that reached users couldn't be reproduced afterwards, because nothing recorded how it was made.

What

1. OTA — remote-first WebView. The app is a pure WebView wrap with zero native plugins (only @capacitor/android|core|cli), so the web bundle is the product and there's no native surface for it to disagree with. resolveOtaUrl() derives the origin from VITE_API_BASE_URL — the same value the app already trusts for its data, so code-host and data-host can't drift — and capacitor.config.ts sets server.url from it when NIETE_OTA=1.

Change Ships via Reaches users
Anything in portal/src Portal web deploy Next app launch
Capacitor/manifest/SDK/native Play release Days

Opt-in, so a build without the flag behaves exactly as today. Every failure path returns null (= use bundled assets) rather than throwing — throwing at native boot is precisely the white screen being eliminated.

2. CI. android-release.yml builds through build:app, signs with the inherited NIETE key from secrets, asserts the bundle carries the expected SHA-256 (an unsigned bundle is otherwise only caught after upload), and uploads to the Play internal track. android-debug.yml attaches a .debug APK to every portal PR, so a device test needs no local JDK/SDK. The release keystore is unreachable from PR builds.

3. Guardrails. build:app + android:{sync,debug,release} scripts make the mode flag impossible to forget, and CI greps the built bundle for the API host, failing before signing if it's web-mode.

Verification

  • Red-first: 20 failing across the two new suites before implementation → 52 passing after (incl. pre-existing app-target, release-identity, session-persistence).
  • The guard catches bd-2551, proven both ways locally: an app-mode bundle contains the API host; a web-mode bundle does not.
  • capacitor.config.ts verified in all 3 states: OTA off → no url (identical to today); OTA on → url set; OTA on with missing URL → falls back to bundled assets, no throw.
  • android:sync runs green, and the OTA URL lands in the generated native capacitor.config.json (which is gitignored, so no baked URL can be committed).
  • tests/setup/ is at baselinedevelop already fails 15 suites/30 tests for unrelated bot/schema/docs reasons; none of the new files are implicated.

Not addressed (flagged as open risk in ANDROID.md)

Key escrow. The .jks still lives in taleemabad-core with plaintext passwords in build.gradle. Play App Signing enrollment is asserted in a build.gradle comment but unverified in the Play Console. If it's not enrolled and the key is lost, the listing can never be updated again.

Required before the release workflow can run

Repo secrets: NIETE_KEYSTORE_B64, NIETE_KEYSTORE_PASSWORD, NIETE_KEY_ALIAS, NIETE_KEY_PASSWORD, NIETE_API_BASE_URL, PLAY_SERVICE_ACCOUNT_JSON. Repo variable: NIETE_STAGING_API_BASE_URL. The Play publisher service account does not exist yet.

Closes: bd-2551

🤖 Generated with Claude Code

… app (bd-2553)

The Android pipeline was entirely manual and off-repo: hand-typed
`vite build --mode app`, a local gradle build, and a manual Play upload.
That process already shipped a broken release — versionCode 1206 went to
the Play internal track carrying a WEB-mode bundle, so resolveApiBaseUrl()
threw at first render, React never mounted, and every launch was a white
screen (bd-2551), which then needed a WhatsApp downtime notice to 80
coaches (bd-2552). The build that reached users could not be reproduced
afterwards, because nothing recorded how it was made.

Three parts:

1. OTA — remote-first WebView. The app is a pure WebView wrap with ZERO
   native plugins (only @capacitor/android|core|cli), so the web bundle is
   the whole product and there is no native surface for it to disagree
   with. `resolveOtaUrl()` derives the origin from VITE_API_BASE_URL — the
   same value the app already trusts for its data, so code-host and
   data-host cannot drift — and capacitor.config.ts sets `server.url` from
   it when NIETE_OTA=1. A portal web deploy now updates every installed
   app on next launch; Play releases are reserved for genuinely native
   changes. Opt-in, so a build without the flag behaves exactly as today.

   Every failure path returns null (= use the bundled assets) rather than
   throwing: this runs at native boot, and throwing there is precisely the
   white screen being eliminated.

2. CI — android-release.yml builds through build:app, signs with the
   inherited NIETE key from secrets, asserts the resulting bundle carries
   the expected SHA-256 (an unsigned bundle is otherwise only caught after
   upload), and uploads to the Play internal track. android-debug.yml
   attaches a .debug APK to every portal PR so a device test needs no
   local JDK/SDK. The release keystore is unreachable from PR builds.

3. Guardrails — `build:app` and `android:{sync,debug,release}` scripts make
   the mode flag impossible to forget, and CI greps the built bundle for
   the API host, failing before signing if it is web-mode. Verified both
   ways locally: an app-mode bundle contains the host, a web-mode bundle
   does not, so this check would have caught 1206.

Tests written red-first: 20 failing across the two new suites before the
implementation, 52 passing after (incl. the pre-existing app-target,
release-identity and session-persistence suites). tests/setup/ conformance
is at baseline — develop already fails 15 suites/30 tests for unrelated
bot/schema/docs reasons; none of the new files are implicated.

Key escrow is explicitly NOT addressed and is flagged as an open risk in
ANDROID.md: the .jks still lives in taleemabad-core with plaintext
passwords, and Play App Signing enrollment is asserted in a build.gradle
comment but unverified in the Play Console.

Closes: bd-2551
@hatafatif
hatafatif merged commit 2ffeadd into develop Aug 12, 2026
1 of 2 checks passed
Sign up for free to 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.

1 participant