Android Gradle Build: AGP, Build Variants, Signing and CI
How Android apps are built with Gradle and the Android Gradle Plugin, which files are involved, how to lint, scan, test and sign, and a complete GitHub Actions pipeline.
Posted
DevOps pipeline loop motif with the title Android Builds with Gradle and AGP
Android Gradle Build: AGP, Build Variants, Signing and CI
Android apps are built with Gradle and the Android Gradle Plugin (AGP). AGP adds Android specific steps to a normal JVM build: resource compilation, DEX conversion, code shrinking with R8, packaging into an APK or Android App Bundle (AAB) and signing. Kotlin is the default language, with Jetpack Compose for UI.
In CI, each stage of the generic build pipeline maps to a Android Gradle command, plugin or scanner. Press play, or step through it:
Source + build scripts: A commit triggers CI. settings.gradle.kts, the module build.gradle.kts files and libs.versions.toml define the app; gradlew pins Gradle.
Resolve dependencies: Gradle and the Android Gradle Plugin resolve dependencies from Google Maven and Maven Central; setup-gradle caches them.
Lint and format: ./gradlew lint runs Android Lint (including security checks), and ktlintCheck and detekt cover Kotlin style and smells.
Compile: ./gradlew assembleDebug compiles Kotlin and Java, merges resources and builds a debug APK.
Test and coverage: ./gradlew testDebugUnitTest runs the JVM unit tests (including Robolectric), and koverXmlReport records coverage.
Security scans in parallel: mobsfscan and the Android Lint security checks cover our code, ./gradlew dependencyCheckAnalyze checks dependencies, and Gitleaks scans for secrets.
Quality gate: Tests, coverage and every scanner are required checks, so a single failure stops the pipeline here.
Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
Build the app bundle (pass): ./gradlew bundleRelease builds the release .aab, shrunk by R8; keep mapping.txt to read crash reports.
Sign (pass): The bundle is signed with the upload keystore decoded from CI secrets; Play App Signing re-signs it with the app signing key.
Upload to Google Play (pass): The signed .aab is uploaded to Google Play, which signs and delivers optimised APKs to each device.
Build variants = build types (debug, release) × product flavors (free, paid, staging, prod). Every variant gets its own tasks, for example assembleProdRelease or bundleStagingDebug.
R8 removes unused code and obfuscates names in release builds; keep the generated mapping.txt to de-obfuscate crash reports.
App Bundles (.aab) are required for Google Play; Play generates optimised APKs per device and signs them with the app signing key (Play App Signing), while you sign uploads with an upload key.
Use Play App Signing: keep only the upload key in CI secrets, so a leaked key can be reset.
Never commit keystores or local.properties; read signing values from environment variables.
Enable R8 (isMinifyEnabled) for release and archive mapping.txt with every build.
Make Android Lint a gate (warningsAsErrors) and use a baseline file for legacy issues.
Derive versionCode from CI (run number) so every upload is unique.
Run instrumented tests selectively (emulators are slow on CI); rely on Robolectric and Compose tests for most coverage, and use Firebase Test Lab or a device farm for the rest.
Keep AGP, Kotlin and the Compose BOM in the version catalog and update them together.