Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 072 / 100

CI/CD for Android: a Android & Kotlin guide for developers

Automate builds and releases.

Expona Admin6 min read

Why CI/CD for Android Matters

Manual testing and release processes don't scale. As your team grows, inconsistent builds, missed edge cases, and deployment delays become the norm rather than the exception. CI/CD for Android: a Android & Kotlin guide for developers starts with the recognition that automation isn't optional—it's the foundation of reliable, repeatable releases.

This guide walks you through setting up a production-ready CI/CD pipeline using tools your team likely already has access to: GitHub Actions, Firebase, and Google Play Console. We'll cover how to automate builds, run tests on every commit, sign APKs securely, distribute to testers, and push releases to Play Store tracks without touching a single local build command.

Build and Test with GitHub Actions

Every pull request should trigger automated checks. GitHub Actions gives you a free, integrated way to run unit tests, lint rules, and static analysis on every push.

Start with a workflow file in your repository at .github/workflows/ci.yml:

kotlin
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    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: Run lint
        run: ./gradlew lint
      
      - name: Run unit tests
        run: ./gradlew testDebugUnitTest
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: app/build/test-results/

This workflow runs every time you push to main or develop, and on every pull request. It sets up Java, runs your lint rules, executes unit tests, and uploads the results as artifacts so you can inspect them in the GitHub UI.

Add more granular checks as your project grows: Kotlin linter, code coverage thresholds, or integration tests against a mock API. The cost is minimal, and the feedback loop is immediate.

Signed Builds and Secure Key Storage

You cannot distribute or publish an unsigned APK. GitHub Actions must sign your build, but the signing key itself must never be committed to version control.

Generate your keystore locally (if you don't already have one):

kotlin
keytool -genkey -v -keystore release.keystore -alias release \
  -keyalg RSA -keysize 2048 -validity 10000

Convert it to base64:

kotlin
base64 release.keystore | tr -d '\n' | xclip -selection clipboard

In your GitHub repository settings, go to Secrets and Variables → Actions and create three secrets:

  • KEYSTORE_BASE64: the base64-encoded keystore
  • KEYSTORE_PASSWORD: the keystore password
  • KEY_ALIAS: the key alias (e.g., release)
  • KEY_PASSWORD: the key password

Update your workflow to decode and use the keystore:

kotlin
- name: Decode signing key
  run: |
    echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > release.keystore

- name: Build signed APK
  run: |
    ./gradlew assembleRelease \
      -Pandroid.injected.signing.store.file=release.keystore \
      -Pandroid.injected.signing.store.password="${{ secrets.KEYSTORE_PASSWORD }}" \
      -Pandroid.injected.signing.key.alias="${{ secrets.KEY_ALIAS }}" \
      -Pandroid.injected.signing.key.password="${{ secrets.KEY_PASSWORD }}"

Never log the secrets. GitHub masks them automatically in the workflow output, but be cautious with any echo statements that might expose them.

Distribute to Testers with Firebase App Distribution

Before pushing to Play Store, you need a staging gate. Firebase App Distribution lets you upload APKs and invite testers via email. They receive a link, install the app on their device, and report issues before production users see them.

Install the Firebase CLI and authenticate:

kotlin
npm install -g firebase-tools
firebase login

Add this step to your workflow after building the signed APK:

kotlin
- name: Upload to Firebase App Distribution
  run: |
    firebase appdistribution:distribute app/build/outputs/apk/release/*.apk \
      --app="${{ secrets.FIREBASE_APP_ID }}" \
      --release-notes="Testing build from commit ${{ github.sha }}" \
      --testers-file=testers.txt
  env:
    FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

Store your Firebase App ID and token in GitHub secrets. Create a testers.txt file (or generate it dynamically) with email addresses:

kotlin
qa-team@company.com
senior-qa@company.com
product-manager@company.com

Testers will receive an email invitation within minutes of the APK upload. You can iterate rapidly without the overhead of managing Play Store tracks or internal testing groups.

Publish to Google Play Store

When you're ready to release to production, automate the final step: uploading to Google Play. Two main tools exist: Gradle Play Publisher and Fastlane. Both work well; Gradle Play Publisher integrates more tightly with your Gradle build.

Set up Gradle Play Publisher in your app-level build.gradle.kts:

kotlin
plugins {
    id("com.android.application")
    id("com.github.triplet.play") version "3.8.0"
}

play {
    serviceAccountCredentials.set(file("play-console-key.json"))
    defaultToAppBundles.set(true)
}

Download a service account key from Google Play Console (Settings → API access → Create service account). Store it securely and reference it in CI:

kotlin
- name: Decode Play Console key
  run: |
    echo "${{ secrets.PLAY_CONSOLE_KEY_BASE64 }}" | base64 -d > play-console-key.json

- name: Build and publish to Play Store
  run: ./gradlew bundleRelease publishReleaseBundle \
    --artifact-type=bundle \
    -Dorg.gradle.jvmargs=-Xmx2048m

By default, this publishes to the internal track, which only visible to internal testers. Move to alpha, beta, or production once you've validated:

kotlin
play {
    track.set("beta")  // or "production" for general release
    releaseStatus.set(ReleaseStatus.COMPLETED)
}

If you prefer Fastlane, the setup is similar but offers more flexibility for complex release workflows:

kotlin
- name: Upload to Play Store with Fastlane
  run: |
    gem install fastlane
    fastlane supply --aab app/build/outputs/bundle/release/app-release.aab \
      --track beta \
      --json_key play-console-key.json

Common Mistakes

Committing the keystore file. Even in a private repository, version control is not a secure storage mechanism. Always use base64 encoding and GitHub secrets.

Hardcoding build variants and flavors. If your app has different product flavors (dev, staging, production), parameterize them in your workflow. Use environment variables or workflow inputs to avoid multiple nearly-identical workflow files.

Skipping versioning automation. Manually incrementing versionCode and versionName leads to conflicts and missed releases. Use a versioning strategy: increment versionCode with every build, and tie versionName to git tags.

No rollback strategy. Publishing directly to production without a staged rollout is risky. Always start with a small percentage in the Play Console's staged rollout feature, then increase to 100% once metrics look healthy.

Forgetting to test the CI workflow locally. Use act to simulate GitHub Actions locally before pushing:

kotlin
act -j test  # Runs the "test" job locally

Key Takeaways

  • Automate unit tests and lint on every push and pull request using GitHub Actions.
  • Store signing keys in secrets, never in version control; decode them at build time.
  • Use Firebase App Distribution as a staging gate before Play Store releases.
  • Publish to Google Play via Gradle Play Publisher or Fastlane, starting with internal or beta tracks.
  • Validate your CI workflow locally with act before committing.
  • Implement staged rollouts in Play Console to catch issues early in production.
  • Automate versioning to avoid manual conflicts and missed releases.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles