Continuous Integration Pipeline
Why CI/CD for Android
Manual builds, testing, and uploading are slow and error-prone. A CI pipeline automates the full build-test-sign-deploy cycle, ensuring every commit is validated against the same standards. This catches regressions before they reach users.
GitHub Actions Workflow
A basic Android CI workflow runs on every pull request:
name: Android CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run lint checks
run: ./gradlew lint
- name: Run unit tests
run: ./gradlew testDebugUnitTest
- name: Build debug APK
run: ./gradlew assembleDebug
- name: Upload lint report
uses: actions/upload-artifact@v4
if: always()
with:
name: lint-report
path: app/build/reports/lint-results-debug.html
Gradle Task Dependencies
Understanding which tasks run for different operations:
# Full build pipeline
clean -> lint -> testDebugUnitTest -> assembleRelease
# Check code quality only
./gradlew check
# Run specific tests
./gradlew testDebugUnitTest --tests "com.example.LoginTest"
# Build without tests (use carefully)
./gradlew assembleRelease -x test
Caching in CI
Gradle builds are slow. Cache dependencies and build outputs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
with:
cache-read-only: ${{ github.ref != 'refs/heads/main' }}
The gradle/actions/setup-gradle action handles caching automatically, reducing build times by 30-60%.
Continuous Deployment Pipeline
Building and Signing Release APKs
For deployment, you need to sign the release build using the keystore:
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Build release AAB
run: ./gradlew bundleRelease
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
Store signing credentials as GitHub Secrets — never commit them to the repository.
Publishing to Play Console
Use the r0adkll/upload-google-play action or the google-play-cli tool:
- name: Upload to Play Console (Internal Track)
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_KEY }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: internal
status: completed
For the production track, use track: production and set status: inProgress for staged rollouts.
Automating Version Codes
Use the GitHub run number or a version management plugin:
// build.gradle.kts
android {
defaultConfig {
// Version code from environment variable, fallback to 1
val buildNum = System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull() ?: 1
versionCode = buildNum
versionName = "1.0.${buildNum}"
}
}
Branch-Based Deployment
- name: Deploy to Internal (develop branch)
if: github.ref == 'refs/heads/develop'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_KEY }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: internal
- name: Deploy to Production (main branch)
if: github.ref == 'refs/heads/main'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_KEY }}
packageName: com.example.myapp
releaseFiles: app/build/outputs/bundle/release/app-release.aab
track: production
status: inProgress
rolloutPercentage: 10
CI/CD Best Practices for Android
Parallelizing Tests
Run unit tests and instrumented tests in parallel to reduce pipeline time:
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- run: ./gradlew testDebugUnitTest
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- run: ./gradlew lint
Test Reporting
Publish test results as artifacts for easy debugging:
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: app/build/test-results/
Flaky Test Handling
Configure Gradle to retry flaky tests:
// build.gradle.kts
tasks.withType<Test> {
retry {
maxRetries = 2
maxFailures = 3
}
}
Gradle Build Scans
Enable build scans in CI for performance analysis:
- name: Build with scan
run: ./gradlew assembleRelease --scan
env:
GRADLE_OPTS: "-Dscan.enabled=true"
Pipeline Security
- Use GitHub Secrets for all credentials
- Never print secrets in logs
- Use
if: always()for artifact uploads so reports are available even on failure - Pin action versions to specific SHA commits for supply chain security
Monitoring Build Times
Track build duration over time. If builds exceed 10 minutes, investigate:
- Gradle configuration cache
- Module dependency graph complexity
- Test parallelization opportunities
- Hardware upgrade for self-hosted runners
Quiz
1. Why should you never commit keystore passwords to version control?
2. What is the benefit of using Gradle build scans in CI?
3. How should you handle version codes in a CI/CD pipeline?
4. What is the purpose of running unit tests and lint checks in CI before merging a pull request?
Flashcards
Question
What Gradle task builds a release Android App Bundle?
Click to reveal answer
Answer
./gradlew bundleRelease
Question
Where should signing credentials be stored in GitHub Actions?
Click to reveal answer
Answer
In GitHub Secrets (Settings > Secrets and variables > Actions), referenced as ${{ secrets.SECRET_NAME }}.
Question
What does the gradle/actions/setup-gradle action do in CI?
Click to reveal answer
Answer
It handles Gradle setup, caching, and dependency management, reducing build times by 30-60%.
Question
How do you deploy a staged rollout to Google Play from CI?
Click to reveal answer
Answer
Use an upload action with track: production and rolloutPercentage set to a low value (e.g., 10), then increase gradually.
Revision Notes
Key Takeaways
- 1. CI/CD automates the build-test-sign-deploy cycle, catching regressions before production
- 2. Always store signing credentials in secrets — never commit them to version control
- 3. Parallelize independent CI jobs (tests, lint) to reduce pipeline duration
- 4. Use Gradle caching and build scans to optimize slow builds
- 5. Branch-based deployment ensures only validated code reaches production
Interview Tips
- • Describe the full CI/CD pipeline for an Android app from commit to Play Store
- • Explain how you would handle signing credentials securely in CI
- • Discuss strategies for reducing build times in a large Android project
- • Know how to set up automated deployment to different Play Console tracks
Cheat Sheet
CI/CD Cheat Sheet
Key Gradle Tasks:
./gradlew lint # Code quality
./gradlew testDebugUnitTest # Unit tests
./gradlew bundleRelease # Release AAB
./gradlew assembleDebug # Debug APK
GitHub Actions Essentials:
- Use
actions/setup-java@v4with JDK 17 - Cache Gradle with
gradle/actions/setup-gradle@v3 - Store secrets in GitHub Secrets
- Upload artifacts with
actions/upload-artifact@v4
Pipeline Structure:
- Checkout code
- Setup Java/Gradle
- Run lint + tests (parallel)
- Build release AAB
- Sign and upload to Play Console
Version Code:
versionCode = System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull() ?: 1