packages/android/templates/*.kt.template is never compiled by anything in this repository. The only workflow that touches Android is mobile-e2e.yml, which is workflow_dispatch-only and whose Android job is guarded by if [ -x "packages/android/gradlew" ] — a file that does not exist. packages/android has no Gradle project at all: it is README.md, build.ts, package.json, src/ and templates/.
So every edit to CraftBridge.kt, CraftNative.kt and the six other templates is verified by reading it, and by nothing else.
What that cost, concretely
#156 rewrote 58 evaluateJavascript call sites to route through a new helper:
private fun jsQuote(value: String?): String = JSONObject.quote(value ?: "null")Three of those sites do not pass a String:
| site | expression | actual type |
|---|---|---|
addContact | ContentUris.parseId(results[0].uri!!) | Long |
downloadFile | downloadManager.enqueue(request) | Long |
authenticate | onAuthenticationError(code, errString) | CharSequence |
String interpolation accepted all three; a String? parameter does not. The template would not compile, every check on the PR was green, and it merged.
Fixed in the follow-up by widening the parameter to Any? — which is what interpolation always was — but the fix is not the point. The point is that a whole language in this repository has no compiler behind it, and the bug that found it was a mechanical rewrite of the sort that will happen again.
What would catch it
A Gradle project under packages/android that substitutes the template placeholders and compiles the result. It needs the Android SDK, which android-actions/setup-android@v3 already provides and mobile-e2e.yml already uses, and it needs and the other placeholders filled with something — the same substitution build.ts does when it generates an app.
assembleDebug on a generated app is the honest check, since that is what a user's build does. compileDebugKotlin alone is cheaper and catches this entire class.
Why this is not filed as a patch
It adds an Android SDK download and a Gradle build to CI, which is minutes on every run, and where it goes — its own workflow, a job in ci.yml, or a nightly — is a call about what the project wants to pay. Worth deciding deliberately.
The interim guard
android_reply_escaping_test.zig now asserts the signature is Any? and says why, so narrowing it back fails a test rather than a user's build. That covers exactly one regression and nothing else — it is a note, not a substitute.