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"
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"
Never commit the keystore to Git. Instead, base64-encode it:
base64 -i release.jks | pbcopy # macOS
base64 -w 0 release.jks # Linux
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
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
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"
)
}
}
}
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)