feat(android): CI build/sign/release + remote-first OTA for the NIETE app (bd-2553) - #182
Merged
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 fromVITE_API_BASE_URL— the same value the app already trusts for its data, so code-host and data-host can't drift — andcapacitor.config.tssetsserver.urlfrom it whenNIETE_OTA=1.portal/srcOpt-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.ymlbuilds throughbuild: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.ymlattaches a.debugAPK 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
app-target,release-identity,session-persistence).capacitor.config.tsverified in all 3 states: OTA off → nourl(identical to today); OTA on →urlset; OTA on with missing URL → falls back to bundled assets, no throw.android:syncruns green, and the OTA URL lands in the generated nativecapacitor.config.json(which is gitignored, so no baked URL can be committed).tests/setup/is at baseline —developalready 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
.jksstill lives intaleemabad-corewith plaintext passwords inbuild.gradle. Play App Signing enrollment is asserted in abuild.gradlecomment 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