Guides10 min read

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.aab

From 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.

Related docs