CI/CD for Android
Continuous Integration and Continuous Deployment (CI/CD) pipelines completely automate the process of building, testing, signing, and releasing Android artifacts (APKs and AABs) to Google Play.
1. Why use CI/CD in Android?​
- Consistency: Every build runs in an identical Linux environment (usually Ubuntu). Eliminates the "it works on my machine" problem.
- Safety: Pull Requests cannot be merged if unit tests or UI tests fail in the pipeline.
- Velocity: Automatic deployment to QA (via Firebase App Distribution) or Alpha/Beta tracks avoids manual clicking in Android Studio.
2. Common Tools​
- GitHub Actions / GitLab CI / Bitrise: The machines (runners) that execute the scripts. Bitrise is particularly popular in mobile because it handles macOS/Android environment setups perfectly out of the box.
- Fastlane: A Ruby-based tool acting as the brains of the operation. It interfaces directly with the Google Play Developer Console API to upload metadata, screenshots, and AAB files.
3. Basic Workflow​
A typical production Android CI/CD workflow:
A. On Pull Request (Continuous Integration)​
- Trigger event on PR creation.
- Checkout source code.
- Validate
ktlintordetekt(ensure coding standards). - Run standard JUnit local unit tests (
./gradlew testDebugUnitTest). - Optional: Run UI tests on a Firebase Test Lab matrix (costs money but is incredibly robust).
- Build a Debug APK to ensure nothing is fundamentally broken (
./gradlew assembleDebug).
B. On Merge to main (Continuous Delivery)​
- Run all PR checks again.
- Build Release App Bundle (
./gradlew bundleRelease). - Sign the AAB securely using a Keystore injected via GitHub Secrets.
- Upload to Firebase App Distribution via Fastlane so QA testers get an immediate email with the new version.
C. On Tagged Release (Continuous Deployment)​
- Trigger when a tag like
v1.2.0is pushed. - Fastlane downloads signing keys.
- Fastlane compiles
bundleRelease. - Fastlane uploads the AAB directly to Google Play Console's Internal Testing or Beta tracks.
4. Keystore Security​
Never commit your keystore.jks file to your public repository!
Encode your keystore into a Base64 string, save it in GitHub Secrets, and then decode it back into a file dynamically during the CI pipeline run before executing ./gradlew assembleRelease.
# GitHub Actions Example decoding keystore
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.2
with:
fileName: 'keystore.jks'
encodedString: ${{ secrets.KEYSTORE_BASE64 }}