All checks were successful
PR Checks / android-build (pull_request) Successful in 21m22s
Step 1 of M8 (docs/android/PLAN.md item 9). - compileSdk/targetSdk 36 (Play's floor since 2026-08-31). AGP 8.6.1 cannot build against 36, so AGP goes to 8.13.2 (the last 8.x) with the Gradle 8.14.3 wrapper. Kotlin, KSP and Hilt are unchanged. All three workflows install platforms;android-36; AGP 8.13 still defaults to build-tools 35.0.0. - release.yml also runs bundleRelease, signed with the same keystore, and attaches the .aab to the Gitea release beside the APK (the first bundle has to be uploaded by hand, and this is where it comes from). - A last step uploads the bundle to Play's internal track through the Play Developer API v3 edit flow. The OAuth token comes from a service-account JWT signed with openssl; the key is the new PLAY_SERVICE_ACCOUNT_JSON secret. While that secret is unset, the step warns and skips. The step runs after the Gitea release, so a Play failure never holds back the APK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
222 lines
8.9 KiB
Plaintext
222 lines
8.9 KiB
Plaintext
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import java.io.FileInputStream
|
|
import java.util.Properties
|
|
|
|
plugins {
|
|
alias(libs.plugins.android.application)
|
|
alias(libs.plugins.kotlin.android)
|
|
alias(libs.plugins.kotlin.compose)
|
|
alias(libs.plugins.kotlin.serialization)
|
|
alias(libs.plugins.ksp)
|
|
alias(libs.plugins.hilt)
|
|
jacoco
|
|
}
|
|
|
|
jacoco {
|
|
toolVersion = "0.8.12"
|
|
}
|
|
|
|
// Release signing material (PLAN.md §12) is never committed. It is read from, in
|
|
// order of precedence: a local gitignored `keystore.properties` at the repo root,
|
|
// then environment variables (how CI injects the decoded keystore + secrets). When
|
|
// none is present, the release build is simply left unsigned — `assembleDebug` and
|
|
// the PR gate are unaffected, so contributors without the keystore can still build.
|
|
val keystorePropsFile = rootProject.file("keystore.properties")
|
|
val keystoreProps = Properties().apply {
|
|
if (keystorePropsFile.exists()) FileInputStream(keystorePropsFile).use { load(it) }
|
|
}
|
|
fun signingValue(propKey: String, envKey: String): String? =
|
|
keystoreProps.getProperty(propKey) ?: System.getenv(envKey)
|
|
|
|
val ksStoreFilePath = signingValue("storeFile", "ANDROID_KEYSTORE_FILE")
|
|
val ksStorePassword = signingValue("storePassword", "ANDROID_KEYSTORE_PASSWORD")
|
|
val ksKeyAlias = signingValue("keyAlias", "ANDROID_KEY_ALIAS")
|
|
val ksKeyPassword = signingValue("keyPassword", "ANDROID_KEY_PASSWORD")
|
|
val hasReleaseSigning = ksStoreFilePath != null && ksStorePassword != null &&
|
|
ksKeyAlias != null && ksKeyPassword != null
|
|
|
|
android {
|
|
namespace = "com.runicgateway.app"
|
|
compileSdk = 36
|
|
|
|
defaultConfig {
|
|
// Target application id per docs/android/PLAN.md §13 (pending runicgateway.app domain).
|
|
applicationId = "com.runicgateway.app"
|
|
minSdk = 29
|
|
targetSdk = 36
|
|
// These committed defaults are the version source of truth (PLAN.md §10).
|
|
// release.yml's conventional-commit engine bumps versionName here and commits
|
|
// it on release; versionCode is derived from it (major*10000+minor*100+patch)
|
|
// so it stays monotonic. Both remain overridable via -P for local/manual builds.
|
|
versionCode = (project.findProperty("versionCode") as String?)?.toIntOrNull() ?: 1
|
|
versionName = (project.findProperty("versionName") as String?)?.takeIf { it.isNotBlank() } ?: "0.1.0"
|
|
|
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
|
|
// Android App Links host (docs/android/APP_LINKS.md). autoVerify needs a
|
|
// *literal* host at build time, so a single multi-tenant APK cannot verify
|
|
// open-ended shard domains: App Links are a build-time opt-in. Left empty for
|
|
// the generic build (custom scheme only); a white-label/first-party build
|
|
// bakes one host with `-PappLinkHost=play.myshard.com`.
|
|
// • BuildConfig.APP_LINK_HOST — SsoAuthManager reads it to pick the redirect.
|
|
// • manifestPlaceholder appLinkHost — substituted into the intent-filter host;
|
|
// empty falls back to the reserved `.invalid` sentinel so the autoVerify
|
|
// filter is inert (matches no real link, never verifies).
|
|
val appLinkHost = (project.findProperty("appLinkHost") as String?)?.trim().orEmpty()
|
|
buildConfigField("String", "APP_LINK_HOST", "\"$appLinkHost\"")
|
|
manifestPlaceholders["appLinkHost"] = appLinkHost.ifBlank { "runic-gateway.invalid" }
|
|
}
|
|
|
|
signingConfigs {
|
|
if (hasReleaseSigning) {
|
|
create("release") {
|
|
storeFile = file(ksStoreFilePath!!)
|
|
storePassword = ksStorePassword
|
|
keyAlias = ksKeyAlias
|
|
keyPassword = ksKeyPassword
|
|
// Sign with both v1 (JAR) and v2 (APK) schemes for broad compatibility.
|
|
enableV1Signing = true
|
|
enableV2Signing = true
|
|
}
|
|
}
|
|
}
|
|
|
|
buildTypes {
|
|
debug {
|
|
// Produce a JaCoCo .exec from JVM unit tests so SonarQube receives real
|
|
// coverage (§12.1). Debug-only: the scan analyses the debug variant.
|
|
enableUnitTestCoverage = true
|
|
}
|
|
release {
|
|
// R8 full-mode minify + resource shrink (§7: no offline cache, so a lean
|
|
// release APK). Keep rules live in proguard-rules.pro.
|
|
isMinifyEnabled = true
|
|
isShrinkResources = true
|
|
proguardFiles(
|
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
"proguard-rules.pro",
|
|
)
|
|
// Signed only when the keystore material is present (local or CI); an
|
|
// unsigned APK is produced otherwise. The direct-APK release (§10) runs
|
|
// through release.yml, which supplies the keystore from a Gitea secret.
|
|
if (hasReleaseSigning) {
|
|
signingConfig = signingConfigs.getByName("release")
|
|
}
|
|
}
|
|
}
|
|
|
|
compileOptions {
|
|
sourceCompatibility = JavaVersion.VERSION_17
|
|
targetCompatibility = JavaVersion.VERSION_17
|
|
}
|
|
|
|
kotlinOptions {
|
|
jvmTarget = "17"
|
|
}
|
|
|
|
buildFeatures {
|
|
compose = true
|
|
buildConfig = true
|
|
}
|
|
|
|
packaging {
|
|
resources {
|
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
}
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
// Core / lifecycle / activity
|
|
implementation(libs.androidx.core.ktx)
|
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
implementation(libs.androidx.lifecycle.runtime.compose)
|
|
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
|
implementation(libs.androidx.activity.compose)
|
|
|
|
// Compose (BOM-managed versions)
|
|
implementation(platform(libs.androidx.compose.bom))
|
|
implementation(libs.androidx.compose.ui)
|
|
implementation(libs.androidx.compose.ui.graphics)
|
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
|
implementation(libs.androidx.compose.material3)
|
|
implementation(libs.androidx.compose.material.icons.core)
|
|
implementation(libs.androidx.navigation.compose)
|
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
|
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
|
|
|
// DI
|
|
implementation(libs.hilt.android)
|
|
ksp(libs.hilt.compiler)
|
|
implementation(libs.androidx.hilt.navigation.compose)
|
|
|
|
// Networking (wired for M1+; declared now so the stack resolves)
|
|
implementation(libs.retrofit)
|
|
implementation(platform(libs.okhttp.bom))
|
|
implementation(libs.okhttp)
|
|
implementation(libs.okhttp.logging.interceptor)
|
|
implementation(libs.okhttp.sse)
|
|
implementation(libs.kotlinx.serialization.json)
|
|
implementation(libs.retrofit.kotlinx.serialization.converter)
|
|
|
|
// Storage
|
|
implementation(libs.androidx.datastore.preferences)
|
|
implementation(libs.androidx.security.crypto)
|
|
|
|
// Web hand-off (Chrome Custom Tabs) for register / invite / reset / SSO (§4.2)
|
|
implementation(libs.androidx.browser)
|
|
|
|
// Images
|
|
implementation(libs.coil.compose)
|
|
|
|
// Unit tests
|
|
testImplementation(libs.junit)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
|
|
// Instrumented tests
|
|
androidTestImplementation(libs.androidx.test.ext.junit)
|
|
androidTestImplementation(libs.androidx.espresso.core)
|
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
|
}
|
|
|
|
// StringVocabularyTest (PLAN.md M20) reads the string resources from disk. A reworded
|
|
// string changes no class, so without this input the test task would stay UP-TO-DATE
|
|
// and the guard would not run again.
|
|
tasks.withType<Test>().configureEach {
|
|
inputs.dir("src/main/res").withPathSensitivity(PathSensitivity.RELATIVE)
|
|
}
|
|
|
|
// JaCoCo XML coverage from the JVM unit tests, consumed by SonarQube (§12.1). Generated,
|
|
// DI (Hilt), and Compose-scaffold classes are excluded so they don't dilute the number;
|
|
// pure-@Composable UI is excluded on the Sonar side (sonar.coverage.exclusions) because
|
|
// JVM unit tests can't execute composable bodies without Robolectric.
|
|
tasks.register<JacocoReport>("jacocoTestReport") {
|
|
dependsOn("testDebugUnitTest")
|
|
group = "verification"
|
|
description = "Generates JaCoCo XML/HTML coverage for the debug unit tests."
|
|
|
|
reports {
|
|
xml.required.set(true)
|
|
html.required.set(true)
|
|
}
|
|
|
|
val coverageExcludes = listOf(
|
|
"**/R.class", "**/R$*.class", "**/BuildConfig.*", "**/Manifest*.*",
|
|
"**/*_Hilt*.*", "**/Hilt_*.*", "**/*_Factory*.*", "**/*_MembersInjector*.*",
|
|
"**/*_Impl*.*", "**/di/**", "**/*Module.*", "**/*Module$*.*",
|
|
"**/*ComposableSingletons*.*", "**/ComposableSingletons$*.*",
|
|
)
|
|
val buildDirFile = layout.buildDirectory.get().asFile
|
|
classDirectories.setFrom(
|
|
fileTree("$buildDirFile/tmp/kotlin-classes/debug") { exclude(coverageExcludes) },
|
|
)
|
|
sourceDirectories.setFrom(files("src/main/java", "src/main/kotlin"))
|
|
executionData.setFrom(
|
|
fileTree(buildDirFile) {
|
|
include("outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
|
|
},
|
|
)
|
|
}
|