DEV Community

Aleksandr Ilinskiy
Aleksandr Ilinskiy

Posted on

How to Set Up Android CI/CD with GitHub Actions — Firebase Distribution & Play Store

Setting up CI/CD for Android apps on GitHub Actions is straightforward once you know the gotchas. This guide covers everything: building signed APKs/AABs, caching Gradle, deploying to Firebase Distribution for testers, and publishing to Play Store.

Prerequisites

  • An Android project with Gradle (Groovy or Kotlin DSL)
  • A GitHub repository
  • For Firebase: a Firebase project with your app added
  • For Play Store: a Google Play Console account with your app set up

Step 1: Make Gradlew Executable

This trips up almost everyone on their first CI run. Your gradlew file might not be executable in Git:

git update-index --chmod=+x gradlew
git commit -m "Make gradlew executable"
Enter fullscreen mode Exit fullscreen mode

Or add this step to your workflow (we'll include it below).

Step 2: Create a Signing Keystore

If you don't have one yet:

keytool -genkeypair -v \
  -keystore release.jks \
  -keyalg RSA -keysize 2048 \
  -validity 10000 \
  -alias release \
  -storepass YOUR_STORE_PASSWORD \
  -keypass YOUR_KEY_PASSWORD \
  -dname "CN=Your Name, O=Your Org"
Enter fullscreen mode Exit fullscreen mode

Never commit the keystore to Git. Instead, base64-encode it:

base64 -i release.jks | pbcopy   # macOS
base64 -w 0 release.jks          # Linux
Enter fullscreen mode Exit fullscreen mode

Step 3: Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions and create:

Secret Value
KEYSTORE_BASE64 Base64-encoded .jks file
KEYSTORE_PASSWORD Your keystore password
KEY_ALIAS Your key alias (e.g. "release")
KEY_PASSWORD Your key password

For Firebase Distribution, also add:

Secret Value
FIREBASE_APP_ID Firebase Console → Project Settings → Your Android app ID
FIREBASE_SERVICE_ACCOUNT JSON content of a Firebase service account key

For Play Store, add:

Secret Value
PLAY_SERVICE_ACCOUNT_JSON Google Play Console → API access → Service account JSON

Step 4: The Workflow — Firebase Distribution

Create .github/workflows/android-firebase.yml:

name: Android — Build & Deploy to Firebase

on:
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    name: Build & Upload to Firebase
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Make gradlew executable
        run: chmod +x ./gradlew

      - name: Decode Keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.jks

      - name: Build Release APK
        run: ./gradlew assembleRelease
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

      - name: Upload to Firebase Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_APP_ID }}
          serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
          groups: testers
          file: app/build/outputs/apk/release/app-release.apk
Enter fullscreen mode Exit fullscreen mode

Step 5: The Workflow — Play Store

For Play Store deployment, the workflow is similar but uses AAB (Android App Bundle) instead of APK:

name: Android — Build & Publish to Play Store

on:
  push:
    tags:
      - 'v*'

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    name: Build & Publish to Play Store
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Make gradlew executable
        run: chmod +x ./gradlew

      - name: Decode Keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.jks

      - name: Build Release AAB
        run: ./gradlew bundleRelease
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

      - name: Upload to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
          packageName: com.yourcompany.yourapp
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          track: internal
Enter fullscreen mode Exit fullscreen mode

Note: Replace com.yourcompany.yourapp with your actual package name. The track can be internal, alpha, beta, or production.

Step 6: Configure build.gradle for CI Signing

Your app/build.gradle.kts needs to read signing config from environment variables:

android {
    signingConfigs {
        create("release") {
            storeFile = file("release.jks")
            storePassword = System.getenv("KEYSTORE_PASSWORD")
            keyAlias = System.getenv("KEY_ALIAS")
            keyPassword = System.getenv("KEY_PASSWORD")
        }
    }

    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
            isMinifyEnabled = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Issues

"Permission denied: gradlew" — Either run chmod +x ./gradlew in your workflow or fix it in Git with git update-index --chmod=+x gradlew.

"JAVA_HOME is not set" — Make sure the setup-java step comes before any Gradle commands.

Build takes too long — The Gradle cache step should cut build times significantly after the first run. First build downloads all dependencies and can take 5-10 minutes.

"No key with alias found in keystore" — Double-check your KEY_ALIAS secret matches exactly what you used when creating the keystore.

"Failed to read key from store" — Your base64 encoding might be corrupted. Re-encode the keystore and update the secret.

Pro Tips

Concurrency groups prevent wasted CI minutes. If you push twice quickly, the first run cancels automatically.

Tag-based triggers for Play Store are ideal — push a v1.0.0 tag when you're ready to release, and CI handles the rest.

Firebase Distribution is great for internal testing — testers get a notification with each new build, no Play Store review required.

The Easy Way

Don't want to write YAML by hand? I built Run Lane — a free visual configurator that generates GitHub Actions workflows for Android and iOS. Pick your platform, choose your distribution target, and download a ready-to-use workflow. No account needed.


Tags: android, github, cicd, kotlin

Top comments (0)