Skip to content
advanced Phase 13 · Play Store & Production

CI/CD

Automate builds, tests, and deployments with GitHub Actions, Bitrise, or Gradle tasks.

50m
0 problems
Topic Progress 0%

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?

Question 1 options

2. What is the benefit of using Gradle build scans in CI?

Question 2 options

3. How should you handle version codes in a CI/CD pipeline?

Question 3 options

4. What is the purpose of running unit tests and lint checks in CI before merging a pull request?

Question 4 options

Flashcards

Question

What Gradle task builds a release Android App Bundle?

Answer

./gradlew bundleRelease

Question

Where should signing credentials be stored in GitHub Actions?

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?

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?

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@v4 with JDK 17
  • Cache Gradle with gradle/actions/setup-gradle@v3
  • Store secrets in GitHub Secrets
  • Upload artifacts with actions/upload-artifact@v4

Pipeline Structure:

  1. Checkout code
  2. Setup Java/Gradle
  3. Run lint + tests (parallel)
  4. Build release AAB
  5. Sign and upload to Play Console

Version Code:

versionCode = System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull() ?: 1