CI/CD for Android Applications via Gradle
Gradle is Android's build system, and most CI/CD for Android projects revolves around key Gradle commands. But running ./gradlew assembleRelease and properly configuring CI/CD are different tasks. APK/AAB signing, versioning, build variants management, dependency caching—all require explicit configuration.
Signing in CI Without Keystore in Repository
Keystore in Git is critical error, even in private repository. Correct scheme: keystore Base64-encoded → CI secret → decode on-the-fly.
# GitHub Actions
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/release.keystore
- name: Build Release AAB
run: ./gradlew bundleRelease
env:
SIGNING_STORE_FILE: release.keystore
SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
In app/build.gradle.kts:
signingConfigs {
create("release") {
storeFile = file(System.getenv("SIGNING_STORE_FILE") ?: "debug.keystore")
storePassword = System.getenv("SIGNING_STORE_PASSWORD") ?: ""
keyAlias = System.getenv("SIGNING_KEY_ALIAS") ?: ""
keyPassword = System.getenv("SIGNING_KEY_PASSWORD") ?: ""
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
Keystore doesn't end up in artifacts—delete after build: rm -f app/release.keystore.
Versioning via CI
Android versionCode must be unique per build. Mistake—use 1, 2, 3 manually or forget to update. On CI—automatically:
// build.gradle.kts
val ciPipelineNumber = System.getenv("CI_BUILD_NUMBER")?.toIntOrNull() ?: 1
val gitCommitCount = "git rev-list --count HEAD".runCommand().trim().toIntOrNull() ?: 1
android {
defaultConfig {
versionCode = ciPipelineNumber.takeIf { it > 1 } ?: gitCommitCount
versionName = "2.4.${gitCommitCount}"
}
}
gitCommitCount as versionCode—reliable: monotonically increasing, independent of CI system.
Gradle Performance: Key Levers
Slow Gradle—common complaint. Several concrete settings in gradle.properties:
# Parallel module build
org.gradle.parallel=true
# Configure only needed modules
org.gradle.configureondemand=true
# Build cache
org.gradle.caching=true
# JVM heap
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
# Build config only for active build
android.defaults.buildfeatures.buildconfig=false
org.gradle.caching=true—local task cache. With unchanged module code, Gradle takes result from cache without recompilation. On CI this works through actions/cache (GitHub) with key by .gradle file hash.
Typical before/after with proper caching on ubuntu-latest runner:
- Without cache: ./gradlew assembleRelease—8–12 minutes
- With Gradle daemon + dependency cache: 2–4 minutes
Build Variants and CI
Different variants for different environments—standard:
flavorDimensions += "env"
productFlavors {
create("dev") {
applicationIdSuffix = ".dev"
buildConfigField("String", "API_URL", "\"https://api.dev.example.com\"")
}
create("prod") {
buildConfigField("String", "API_URL", "\"https://api.example.com\"")
}
}
On CI—separate jobs per variant:
- PR → assembleDevDebug + tests
- merge to develop → assembleDevRelease + Firebase App Distribution
- release tag → bundleProdRelease + Google Play
Google Play Upload via Gradle
gradle-play-publisher plugin:
// build.gradle.kts
plugins {
id("com.github.triplet.play") version "3.9.1"
}
play {
serviceAccountCredentials.set(file(System.getenv("PLAY_STORE_JSON") ?: "play-store-credentials.json"))
track.set("internal")
defaultToAppBundles.set(true)
}
./gradlew publishProdReleaseBundle
Credentials JSON—service account from Google Play Console with Release manager access. On CI—pass via environment variable or temporary file, like keystore.
Timeline
Gradle setup with signing, versioning, basic CI (GitHub Actions/GitLab CI): 2–4 days. Full configuration with build variants, Google Play deploy, Gradle performance optimization, test runners: 1–1.5 weeks. Cost calculated individually.







