The build succeeds. You drag the file into Play Console and get:
You uploaded an APK or Android App Bundle that was signed in
debug mode. You need to sign your APK or Android App Bundle
in release mode.
The confusing part is that you did run a release build. Gradle does not fail when a release build has no signing config. It signs with the debug key instead. Nothing in the build output flags it, so the first thing that notices is Play Console.
Confirm it in ten seconds
Do not guess at this. Read the signature off the artifact you actually uploaded:
keytool -printcert -jarfile app-release.aab
Look at the Owner line. A debug-signed build says exactly this, on every machine in the world:
Owner: CN=Android Debug, O=Android, C=US
If you see that, the diagnosis is done. If you see your own name or organisation instead, the upload was signed with a real key and your problem is a different one, usually the wrong key rather than a debug key.
Why a release build uses the debug key
Gradle picks the debug signing config for any build type that has no signing config of its own. That is a convenience for local debug builds and a trap for release builds. The three usual versions of it:
No release signing config exists. The signingConfigs block was never added, or a release config was added but never attached to the release build type. Defining it is not enough. The buildTypes.release block has to reference it.
The keystore file is not on the runner. This is the CI-specific one. The config is correct, the .jks is gitignored (correctly), and it was never written to the machine doing the build. Locally it works, in CI it silently produces a debug-signed bundle.
You built the wrong task. assembleDebug and bundleDebug produce debug artifacts by definition. Check that the task in your pipeline is bundleRelease, and that you are uploading the file from build/outputs/bundle/release/, not the debug folder next to it.
The fix
Keep the passwords out of build.gradle. Put them in a properties file that is gitignored, and read it:
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
signingConfigs {
release {
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
In CI, base64 the keystore into a secret and write it back out before the Gradle step runs:
- run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/release.jks
Make it fail loudly next time
The real problem here is the silence. A release build that quietly signs itself with a throwaway key is worse than one that stops. Add the guard:
if (!keystorePropertiesFile.exists()) {
throw new GradleException("No keystore.properties. Release build would be debug-signed.")
}
Ten seconds of setup, and the failure moves from Play Console back into the build log where it belongs.
One thing this is not
Play App Signing does not remove the requirement. Google re-signs your app with the app signing key it holds, but you still sign the upload artifact yourself with your upload key, and that key can never be the debug key. The debug certificate is self-signed, generated automatically, and insecure by design. Play also requires an upload key valid past 22 October 2033, which the debug certificate will not satisfy in any case.
Top comments (0)