App Signing and the Upload Key

Android installs only signed APKs, and an update must be signed by the same key as the installed app. With Play App Signing (Play App Signing) the key you hold is an upload key, which only proves to Google that an upload comes from you. Create it with Release Signing's keytool command; here it became ~/keys/booknest-kt-upload.jks, alias upload, with its password in a chmod 600 file outside the project. (JDK 17's keytool signed its certificate with SHA256withRSA, JDK 21's with SHA384withRSA.)

The Kotlin DSL version of the signing config reads the three values as Gradle 19,597 properties, which may come from ~/.gradle/gradle.properties on your machine or from environment variables named ORG_GRADLE_PROJECT_<name> on a CI runner (Signing and Versioning used the same mechanism):

app/build.gradle.kts: an upload key that never enters the repositoryGroovy
signingConfigs {
    val store = providers.gradleProperty("BOOKNEST_UPLOAD_STORE_FILE").orNull
    if (store != null) create("upload") {      // 5.38.2 the upload key, from outside the repo
        storeFile = file(store)
        storePassword = providers.gradleProperty("BOOKNEST_UPLOAD_PASSWORD").get()
        keyAlias = providers.gradleProperty("BOOKNEST_UPLOAD_KEY_ALIAS").get()
        keyPassword = providers.gradleProperty("BOOKNEST_UPLOAD_PASSWORD").get()
    }
}
// and in buildTypes.release, after the R8 lines of Section 5.37.1:
            signingConfig = signingConfigs.findByName("upload")    // 5.38.2 when the key is given
                ?: signingConfigs.getByName("debug")               // otherwise a local test build

Without the properties, assembleRelease still works and signs with the debug key, which keeps Performance's benchmarks and every teammate's local release build running; Play rejects such an upload, so nothing debug-signed can reach users by accident. Android Studio 234 's Build > Generate Signed App Bundle / APK wizard does the same interactively; only the build file is repeatable in CI.