feat: implement production release configuration and signing for Play Store

This commit is contained in:
Torsten Schulz (local)
2026-05-29 16:39:59 +02:00
parent 696c50f0fc
commit f5045c3cf0
7 changed files with 119 additions and 4 deletions

View File

@@ -141,6 +141,11 @@ Kurz: Ziel ist eine native Android-App mit Kotlin + Jetpack Compose, die die Web
[ ] 22. Analytics: Firebase / Matomo Integration (je nach Datenschutz) [ ] 22. Analytics: Firebase / Matomo Integration (je nach Datenschutz)
[x] 23. Crash-Reporting: Sentry / Crashlytics integrieren [x] 23. Crash-Reporting: Sentry / Crashlytics integrieren
[ ] 24. Build/Release: App signing, Release-Notes, Play-Store-Metadaten vorbereiten [ ] 24. Build/Release: App signing, Release-Notes, Play-Store-Metadaten vorbereiten
- [x] Technische Release-Basis vorbereitet: `ANDROID_VERSION_CODE` und `ANDROID_VERSION_NAME` als Gradle-Properties eingeführt (`android-app/gradle.properties`) und im App-Gradle verdrahtet.
- [x] Production-Release-Flavor auf Produktiv-Backend parametrisierbar gemacht (`PRODUCTION_API_BASE_URL`, Default `https://harheimertc.de/`).
- [x] Release-Signing per sicheren Gradle-Properties vorbereitet (`RELEASE_STORE_FILE`, `RELEASE_STORE_PASSWORD`, `RELEASE_KEY_ALIAS`, `RELEASE_KEY_PASSWORD`) statt Hardcoding.
- [x] `:app:assembleProductionRelease` erfolgreich gebaut (Stand 2026-05-29).
- [ ] Offen: Finales Upload-Keystore + Credentials in CI/Build-Host hinterlegen, Play-Store-Release-Notes und Store-Metadaten pflegen.
[ ] 25. Dokumentation: `README-android.md` mit Setup, Architektur und Release-Anleitung [ ] 25. Dokumentation: `README-android.md` mit Setup, Architektur und Release-Anleitung
5) Kurzzeit-MVP (Priorität für erste Version) 5) Kurzzeit-MVP (Priorität für erste Version)

View File

@@ -8,9 +8,31 @@ plugins {
val localApiBaseUrl = providers.gradleProperty("LOCAL_API_BASE_URL") val localApiBaseUrl = providers.gradleProperty("LOCAL_API_BASE_URL")
.orElse("https://harheimertc.tsschulz.de/") .orElse("https://harheimertc.tsschulz.de/")
.get() .get()
val productionApiBaseUrl = providers.gradleProperty("PRODUCTION_API_BASE_URL")
.orElse("https://harheimertc.de/")
.get()
val sentryDsn = providers.gradleProperty("SENTRY_DSN") val sentryDsn = providers.gradleProperty("SENTRY_DSN")
.orElse("") .orElse("")
.get() .get()
val androidVersionCode = providers.gradleProperty("ANDROID_VERSION_CODE")
.orElse("2")
.get()
.toInt()
val androidVersionName = providers.gradleProperty("ANDROID_VERSION_NAME")
.orElse("1.0.0")
.get()
val releaseMinifyEnabled = providers.gradleProperty("RELEASE_MINIFY_ENABLED")
.orElse("true")
.get()
.toBoolean()
val releaseStoreFile = providers.gradleProperty("RELEASE_STORE_FILE").orNull
val releaseStorePassword = providers.gradleProperty("RELEASE_STORE_PASSWORD").orNull
val releaseKeyAlias = providers.gradleProperty("RELEASE_KEY_ALIAS").orNull
val releaseKeyPassword = providers.gradleProperty("RELEASE_KEY_PASSWORD").orNull
val hasReleaseSigning = !releaseStoreFile.isNullOrBlank() &&
!releaseStorePassword.isNullOrBlank() &&
!releaseKeyAlias.isNullOrBlank() &&
!releaseKeyPassword.isNullOrBlank()
android { android {
namespace = "de.harheimertc" namespace = "de.harheimertc"
@@ -19,9 +41,38 @@ android {
defaultConfig { defaultConfig {
applicationId = "de.harheimertc" applicationId = "de.harheimertc"
minSdk = 24 minSdk = 24
targetSdk = 34 targetSdk = 35
versionCode = 1 versionCode = androidVersionCode
versionName = "0.1.0" versionName = androidVersionName
}
signingConfigs {
create("release") {
if (hasReleaseSigning) {
storeFile = file(releaseStoreFile!!)
storePassword = releaseStorePassword
keyAlias = releaseKeyAlias
keyPassword = releaseKeyPassword
}
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = releaseMinifyEnabled
isShrinkResources = false
ndk {
// Generate a native debug symbols archive for Play Console uploads.
debugSymbolLevel = "SYMBOL_TABLE"
}
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
if (hasReleaseSigning) {
signingConfig = signingConfigs.getByName("release")
}
}
} }
flavorDimensions += "environment" flavorDimensions += "environment"
@@ -46,7 +97,7 @@ android {
} }
create("production") { create("production") {
dimension = "environment" dimension = "environment"
buildConfigField("String", "API_BASE_URL", "\"https://harheimertc.tsschulz.de/\"") buildConfigField("String", "API_BASE_URL", "\"$productionApiBaseUrl\"")
buildConfigField("String", "SENTRY_DSN", "\"$sentryDsn\"") buildConfigField("String", "SENTRY_DSN", "\"$sentryDsn\"")
buildConfigField("String", "ENVIRONMENT_NAME", "\"\"") buildConfigField("String", "ENVIRONMENT_NAME", "\"\"")
manifestPlaceholders["usesCleartextTraffic"] = "false" manifestPlaceholders["usesCleartextTraffic"] = "false"
@@ -72,6 +123,44 @@ android {
} }
} }
val packageNativeDebugSymbolsForProductionRelease = tasks.register<Zip>("packageNativeDebugSymbolsForProductionRelease") {
group = "distribution"
description = "Packages production release native libraries into a Play Console debug symbols zip."
dependsOn(":app:mergeProductionReleaseNativeLibs")
from(layout.buildDirectory.dir("intermediates/merged_native_libs/productionRelease/mergeProductionReleaseNativeLibs/out/lib"))
destinationDirectory.set(layout.buildDirectory.dir("outputs/native-debug-symbols/productionRelease"))
archiveFileName.set("native-debug-symbols.zip")
}
val collectPlayStoreArtifacts = tasks.register("collectPlayStoreArtifacts") {
group = "distribution"
description = "Builds production release artifacts and collects AAB, mapping, and native symbols for Play Console upload."
dependsOn(":app:bundleProductionRelease")
dependsOn(packageNativeDebugSymbolsForProductionRelease)
doLast {
val outputDir = layout.buildDirectory.dir("outputs/playstore/productionRelease").get().asFile
outputDir.mkdirs()
val bundleFile = layout.buildDirectory.file("outputs/bundle/productionRelease/app-production-release.aab").get().asFile
if (bundleFile.exists()) {
bundleFile.copyTo(outputDir.resolve(bundleFile.name), overwrite = true)
}
val mappingFile = layout.buildDirectory.file("outputs/mapping/productionRelease/mapping.txt").get().asFile
if (mappingFile.exists()) {
mappingFile.copyTo(outputDir.resolve("mapping.txt"), overwrite = true)
}
val nativeSymbolsZip = layout.buildDirectory.file("outputs/native-debug-symbols/productionRelease/native-debug-symbols.zip").get().asFile
if (nativeSymbolsZip.exists()) {
nativeSymbolsZip.copyTo(outputDir.resolve("native-debug-symbols.zip"), overwrite = true)
}
println("Play Store artifacts prepared in: ${outputDir.absolutePath}")
}
}
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)

Binary file not shown.

2
android-app/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,2 @@
# Project-specific R8/ProGuard rules for release builds.
# Keep this file intentionally minimal and add rules only when needed.

View File

@@ -3,3 +3,22 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8
org.gradle.workers.max=2 org.gradle.workers.max=2
# Local API base URL for running the app from Android Studio / Gradle # Local API base URL for running the app from Android Studio / Gradle
LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/
# Production backend for Play Store build variant
PRODUCTION_API_BASE_URL=https://harheimertc.de/
# Android app versioning for Play Store uploads
ANDROID_VERSION_CODE=3
ANDROID_VERSION_NAME=1.0.0
# Enable R8 for release by default so mapping.txt is generated for Play Console.
RELEASE_MINIFY_ENABLED=true
# Release signing (set in local, untracked gradle.properties or via CI secrets)
# RELEASE_STORE_FILE=/absolute/path/to/keystore.jks
# RELEASE_STORE_PASSWORD=***
# RELEASE_KEY_ALIAS=***
# RELEASE_KEY_PASSWORD=***
# Build and collect Play Store upload artifacts:
# ./gradlew :app:collectPlayStoreArtifacts