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

Published: (February 19, 2026 at 04:35 PM EST)
6 min read
Source: Dev.to

Source: Dev.to

CI/CD for Android Apps on GitHub Actions

Setting up CI/CD for Android apps on GitHub Actions is straightforward once you know the gotchas. This guide covers everything:

  • Building signed APK / AAB files
  • Caching Gradle
  • Deploying to Firebase App Distribution for testers
  • Publishing to the Google Play Store

Prerequisites

RequirementDetails
Android projectGradle (Groovy or Kotlin DSL)
RepositoryGitHub repo
FirebaseFirebase project with your app added
Play StoreGoogle Play Console account with your app set up

Common Pitfall

The gradlew file may not be executable in Git. Fix it once in the repo:

git update-index --chmod=+x gradlew
git commit -m "Make gradlew executable"

Or add a step to your workflow (shown later).


Create a Signing Keystore (if you don’t have one)

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"

Never commit the keystore to Git. Instead, base‑64 encode it:

  • macOS

    base64 -i release.jks | pbcopy   # copies to clipboard
  • Linux

    base64 -w 0 release.jks

Add Secrets to GitHub

Navigate to Repo → Settings → Secrets and variables → Actions and create the following secrets.

General signing secrets

SecretValue
KEYSTORE_BASE64Base64‑encoded .jks file
KEYSTORE_PASSWORDYour keystore password
KEY_ALIASYour key alias (e.g., release)
KEY_PASSWORDYour key password

Firebase Distribution secrets

SecretValue
FIREBASE_APP_IDFirebase Console → Project Settings → Your Android app ID
FIREBASE_SERVICE_ACCOUNTJSON content of a Firebase service‑account key

Play Store secrets

SecretValue
PLAY_SERVICE_ACCOUNT_JSONGoogle Play Console → API access → Service account JSON

Workflow 1 – Build & Deploy to Firebase

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

Workflow 2 – Build & Publish to Play Store

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

name: Android — Build & Publish to Play Store
on:
  push:
    tags:
      - 'v*'   # e.g. v1.0.0

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   # ← replace with your package name
          releaseFiles: app/build/outputs/bundle/release/app-release.aab
          track: internal   # internal / alpha / beta / production

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


Gradle Signing Configuration

Add the following to app/build.gradle.kts (or the Groovy equivalent) so the build picks up the secrets from the environment:

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"
            )
        }
    }
}

Frequently Encountered Errors

ErrorFix
Permission denied: gradlewEnsure the workflow runs chmod +x ./gradlew or make the file executable in Git (git update-index --chmod=+x gradlew).
JAVA_HOME is not setThe setup-java step must appear before any Gradle commands.
Build takes too longThe Gradle cache step (actions/cache@v4) dramatically reduces build times.

You’re now ready to automate Android builds, testing, and releases with GitHub Actions! 🚀

Common CI/CD Issues & Tips for Android Projects

  • First run latency – The initial build downloads all dependencies and can take 5‑10 minutes.
  • “No key with alias found in keystore” – Double‑check that your KEY_ALIAS secret matches exactly the alias you used when creating the keystore.
  • “Failed to read key from store” – Your Base64‑encoded keystore may be corrupted. Re‑encode the keystore file and update the secret.

Concurrency & Triggers

  • Concurrency groups prevent wasted CI minutes. If you push twice quickly, the first run is automatically cancelled.
  • Tag‑based triggers for the Play Store are ideal – push a v1.0.0 tag when you’re ready to release, and the CI pipeline handles the rest.

Distribution Options

  • Firebase App Distribution – Great for internal testing. Testers receive a notification with each new build, and no Play Store review is required.

Visual Workflow Generator

  • Run Lane – A free visual configurator that generates GitHub Actions workflows for Android and iOS.
    1. Pick your platform.
    2. Choose your distribution target.
    3. Download a ready‑to‑use workflow.

No account needed.


Tags: android, github, cicd, kotlin

0 views
Back to Blog

Related posts

Read more »

Apex B. OpenClaw, Local Embeddings.

Local Embeddings para Private Memory Search Por default, el memory search de OpenClaw envía texto a un embedding API externo típicamente Anthropic u OpenAI par...