From 83c90ee9b45847695edfa129571c7c9baafae44f Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Thu, 16 Jul 2026 11:32:10 -0700 Subject: [PATCH] feat(mobile): add external release signing mode for central APK Signer pipelines (#1972) Signed-off-by: npub1qq582hwclq7jnux44a2xul8nhe2ue2zk9z49ehzngznnmmk5ka5sprvchv <0028755dd8f83d29f0d5af546e7cf3be55cca85628aa5cdc5340a73deed4b769@sprout-oss.stage.blox.sqprod.co> Co-authored-by: npub1qq582hwclq7jnux44a2xul8nhe2ue2zk9z49ehzngznnmmk5ka5sprvchv <0028755dd8f83d29f0d5af546e7cf3be55cca85628aa5cdc5340a73deed4b769@sprout-oss.stage.blox.sqprod.co> --- mobile/README.md | 5 +++++ mobile/android/app/build.gradle.kts | 31 ++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/mobile/README.md b/mobile/README.md index 1a6ae19ab..d96c0b1a1 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -42,6 +42,11 @@ environment: The keystore path must be absolute, and the keystore must remain outside the repository. Development and debug builds do not require these variables. +Release pipelines that sign through the central APK Signer service instead of +a local upload keystore must set `BUZZ_ANDROID_RELEASE_SIGNING=external`. That +mode produces an unsigned release bundle and refuses to run if any +`BUZZ_ANDROID_UPLOAD_*` value is also set. + ## Architecture ``` diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index c29158e50..f361b57a1 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -19,6 +19,28 @@ val uploadSigningValues = val missingUploadSigningValues = uploadSigningValues.filterValues { it.isNullOrBlank() }.keys val hasUploadSigning = missingUploadSigningValues.isEmpty() +// Release signing modes: +// - "upload-keystore" (default): sign with the CI-vended upload keystore; +// release builds fail loudly when any credential is missing. +// - "external": deliberately produce an UNSIGNED release bundle for a +// pipeline that signs through the central APK Signer service (Cashkite, +// BOT-1234). No keystore material may be present in this mode. +val releaseSigningMode = + providers.environmentVariable("BUZZ_ANDROID_RELEASE_SIGNING").orNull ?: "upload-keystore" +val externalReleaseSigning = releaseSigningMode == "external" +if (releaseSigningMode !in setOf("upload-keystore", "external")) { + throw GradleException( + "BUZZ_ANDROID_RELEASE_SIGNING must be \"upload-keystore\" or \"external\", got: " + + releaseSigningMode, + ) +} +if (externalReleaseSigning && uploadSigningValues.values.any { !it.isNullOrBlank() }) { + throw GradleException( + "BUZZ_ANDROID_RELEASE_SIGNING=external must not be combined with " + + "BUZZ_ANDROID_UPLOAD_* credentials; unset one of them.", + ) +} + android { namespace = "xyz.block.buzz.mobile" compileSdk = flutter.compileSdkVersion @@ -67,10 +89,17 @@ gradle.taskGraph.whenReady { val buildsRelease = allTasks.any { task -> task.project == project && task.name in setOf("assembleRelease", "bundleRelease") } + if (buildsRelease && externalReleaseSigning) { + // External signing: the unsigned bundle goes to the central APK + // Signer. All keystore checks are intentionally skipped; the + // guard above already rejected any BUZZ_ANDROID_UPLOAD_* values. + return@whenReady + } if (buildsRelease && !hasUploadSigning) { throw GradleException( "Release builds require Android upload signing credentials. Missing: " + - missingUploadSigningValues.sorted().joinToString(", "), + missingUploadSigningValues.sorted().joinToString(", ") + + ". For central APK Signer pipelines set BUZZ_ANDROID_RELEASE_SIGNING=external.", ) } if (buildsRelease) {