Setting up CI/CD for Android raises more questions than it seems. A common situation: a developer pushes code, CI fails with a 'Keystore not found' error, or the build takes 15 minutes, and the release is delayed by a day. Recently, a team of 5 developers approached us: their build took 30 minutes, releases came out once every two weeks. After implementing CI/CD with Gradle caching and parallel builds, build time dropped to 10 minutes, release frequency increased to weekly. Reducing build time directly lowers CI runner costs and speeds up update delivery, saving the team's budget.
The main pitfalls: keystore in the repo, manual versioning, no CI caching, incorrect build variant configuration. Each of these mistakes can block a release or lead to secret leakage. Our CI/CD setup service includes a full audit of your current build, Gradle configuration following best practices, integration with a CI system, and deployment to Google Play. Investment in CI/CD setup pays off through faster release cycles and reduced manual work. We guarantee stable operation after implementation.
How to properly sign APKs in CI?
Storing the keystore in Git is a critical mistake, even in private repositories. The correct approach: 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") } } The keystore does not end up in artifacts — we delete it after the build: rm -f app/release.keystore.
The process consists of several steps:
- Upload the keystore to CI secrets as a Base64 string.
- Create a job to decode and build.
- Configure signingConfig in build.gradle.kts using environment variables.
- After the build, delete the keystore from the file system.
Why is automatic versioning the standard?
versionCode in Android must be unique for each build. A mistake is to update it manually. On CI — automate it:
// 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}" } } Comparison of methods:
| Method | Description | Monotonicity | CI dependency |
|---|---|---|---|
| CI_BUILD_NUMBER | Build number from provider | Yes, if not reset | Yes (problems when switching CI) |
| git commit count | Number of commits | Always monotonic | No (universal) |
We recommend git commit count — it is not tied to a CI system.
How to speed up Gradle builds?
Slow Gradle is the top complaint for Android CI. Several specific settings in gradle.properties:
-
org.gradle.parallel=true— parallel module building. -
org.gradle.configureondemand=true— configure only needed modules. -
org.gradle.caching=true— local task cache. When module code is unchanged, Gradle takes the result from cache without recompilation. On CI this works viaactions/cache(GitHub) with a key based on the hash of*.gradle*files. -
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC— increased heap. -
android.defaults.buildfeatures.buildconfig=false— generate BuildConfig only for active build.
More on cache configuration: see Gradle Build Cache. Comparison of build time with different configurations on ubuntu-latest runner:
| Configuration | Build time (minutes) |
|---|---|
| No optimizations | 10–15 |
| Parallelism + configureondemand | 6–8 |
| Full caching + JVM heap 4GB | 2–4 |
Proper caching reduces build time by 3-4 times. Use R8 instead of ProGuard — it's faster.
Which build variants should be configured?
Different variants for different environments is 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 for each variant:
- PR →
assembleDevDebug+ tests - merge to develop →
assembleDevRelease+ Firebase App Distribution - release tag →
bundleProdRelease+ Google Play
Uploading to Google Play 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) } Credentials JSON — a service account from Google Play Console with Release manager access. Pass via environment variable on CI, similar to the keystore.
What's included in CI/CD setup?
| Stage | Description | Timeline |
|---|---|---|
| Analysis | Audit of current build, infrastructure, and CI configuration | 1 day |
| Gradle configuration | Setting up signing, versioning, flavors, caching | 1–2 days |
| CI integration | Configuring workflows (GitHub Actions/GitLab CI/Bitrise), secrets, deployment | 1–2 days |
| Optimization | Speeding up builds, introducing tests, parallel tasks | 1–2 days |
| Documentation | Description of the process, guide for the team | 1 day |
Timeline
Basic CI/CD setup (signing, versioning, one CI) takes 2–4 days. Full configuration with build variants, deployment to Google Play, build optimization — 1–2 weeks. Cost is calculated individually.
If your build takes too long or releases keep failing, it's time to implement professional CI/CD. Contact us — we'll audit and set up the process so you can forget about release issues. We guarantee stable results.







