Build and release a Capacitor Android app with GitHub Actions
Automate your Capacitor Android builds: a GitHub Actions workflow that assembles a signed AAB, handles the keystore securely, and ships web-layer updates with OtaKit.
Android builds are the friendlier half of Capacitor CI — no macOS runner, no provisioning profiles, just a keystore and Gradle. This guide sets up a GitHub Actions workflow that builds a signed Android App Bundle (AAB) for a Capacitor app, and shows how OTA keeps most of your releases off this pipeline entirely.
Mental model: this pipeline is for store builds (native changes). Web-layer changes ship over the air with a much simpler OTA workflow.
What you need in secrets
- Your signing keystore, base64-encoded (
ANDROID_KEYSTORE). - The keystore password, key alias, and key password.
The workflow
Runs on Ubuntu, builds the web app, syncs Capacitor, and assembles a signed AAB:
# .github/workflows/android.yml
name: Android build
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- uses: actions/setup-java@v4
with: { distribution: temurin, java-version: 21 }
- run: npm ci
- run: npm run build # your web build
- run: npx cap sync android
- name: Decode keystore
env:
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
run: echo "$KEYSTORE" | base64 --decode > android/app/release.keystore
- name: Build signed AAB
working-directory: android
env:
KS_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
./gradlew bundleRelease \
-Pandroid.injected.signing.store.file=release.keystore \
-Pandroid.injected.signing.store.password=$KS_PASSWORD \
-Pandroid.injected.signing.key.alias=$KEY_ALIAS \
-Pandroid.injected.signing.key.password=$KEY_PASSWORD
- uses: actions/upload-artifact@v4
with:
name: app-release-aab
path: android/app/build/outputs/bundle/release/app-release.aabFrom here you can download the AAB and upload manually, or add a step using the Google Play Developer API to publish to a testing track automatically.
Where OTA changes the picture
This runs only when native code changes. Your day-to-day releases — UI, logic, fixes — ship over the air with no Gradle, no keystore, no waiting:
npm run build otakit upload --release
Pattern: native Android builds on version tags (this workflow), OTA releases on every merge (the OTA workflow). When you do cut a native build, bump runtimeVersion so later OTA bundles target the new shell — see versioning.
Keep your keystore backed up somewhere safe outside CI. Lose it and you can't update the app on Google Play under the same signing identity.
Where to go next
See the CI automation docs, and the iOS build guide for the other platform.