Skip to content
All posts
March 1, 20264 min read

CI/CD for Android Apps: A Solo Developer's Practical Setup

Continuous integration for Android doesn't require a DevOps team. Here's a lean CI/CD setup that runs tests automatically, builds release APKs/AABs, and deploys to Play Store — all from GitHub Actions.

AndroidCI/CDKotlin
Share:

Manual builds and releases are the enemy of consistency. One missed Gradle sync, one forgotten signing configuration, one skipped test run — and a broken build ships to production.

Here's how to automate the Android build pipeline without overengineering it.


What the Pipeline Does

  1. On every push/PR: run tests, lint, build debug APK
  2. On merge to main: build release AAB, run full test suite
  3. On tag (v1.x.x): sign AAB, upload to Play Store internal track

All of this runs in GitHub Actions. No external CI service required.


Step 1: The Base Workflow

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - 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: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
      
      - name: Run unit tests
        run: ./gradlew test
      
      - name: Run lint
        run: ./gradlew lint
      
      - name: Build debug APK
        run: ./gradlew assembleDebug
      
      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: '**/build/reports/tests/'

This is the minimum viable pipeline. Tests + lint + build, triggered on every PR.


Step 2: Storing Secrets Safely

Never commit signing credentials. Store them in GitHub Secrets:

  1. Go to repository Settings → Secrets and variables → Actions
  2. Add these secrets:
    • code
      KEYSTORE_BASE64
      — your keystore file as base64
    • code
      KEYSTORE_PASSWORD
      — keystore password
    • code
      KEY_ALIAS
      — key alias
    • code
      KEY_PASSWORD
      — key password

To encode your keystore:

bash
base64 -i release.jks | pbcopy  # macOS — copies to clipboard

Step 3: The Release Workflow

yaml
# .github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - 'v*.*.*'

jobs:
  release:
    runs-on: ubuntu-latest
    
    steps:
      - 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: gradle-${{ hashFiles('**/*.gradle*') }}
      
      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.jks
      
      - name: Build release AAB
        run: |
          ./gradlew bundleRelease \
            -Pandroid.injected.signing.store.file=release.jks \
            -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \
            -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \
            -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
      
      - name: Upload to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
          packageName: com.sudarshantechlabs.myapp
          releaseFiles: app/build/outputs/bundle/release/*.aab
          track: internal

[!WARNING] Never pass signing credentials as command-line arguments in scripts you commit to the repo. Use GitHub Secrets and pass them via

code
-P
flags or environment variables at CI runtime only.


Step 4: Uploading to Play Store

The

code
upload-google-play
action needs a service account. To set it up:

  1. Go to Google Play Console → Setup → API access
  2. Link to a Google Cloud project
  3. Create a service account with "Release Manager" permissions
  4. Download the JSON key
  5. Add the key contents as
    code
    SERVICE_ACCOUNT_JSON
    secret in GitHub

Step 5: Version Bumping

Automate version code from the git tag:

kotlin
// build.gradle.kts
val versionPropsFile = rootProject.file("config/version.properties")
val versionProps = Properties().apply {
    if (versionPropsFile.exists()) load(versionPropsFile.inputStream())
}

android {
    defaultConfig {
        versionCode = (versionProps["VERSION_CODE"] as String).toInt()
        versionName = versionProps["VERSION_MAJOR"] as String + "." +
                      versionProps["VERSION_MINOR"] as String + "." +
                      versionProps["VERSION_PATCH"] as String
    }
}

When you push a tag

code
v2.1.3
, the CI reads the version properties file. Bump the file with the
code
version-bump.sh
script before tagging.


Branch Protection Rules

Enforce your pipeline at the repo level:

  1. Settings → Branches → Add rule for
    code
    main
  2. Require status checks:
    code
    test
    (your CI job)
  3. Require branches to be up to date before merging

Now nothing merges to main without passing CI.


The Complete Pipeline Summary

TriggerActions
PR opened/updatedTests + lint + debug build
Merge to mainFull test suite + release build
Tag
code
v*.*.*
Sign AAB + upload to Play Store internal

Takeaways

  • GitHub Actions is free for open-source repos and generous for private ones — no external CI needed
  • Store all signing credentials in GitHub Secrets — never in the repo
  • Separate CI workflow (every PR) from release workflow (on tag)
  • Use branch protection to enforce CI before merging
  • The service account setup is the hardest part — do it once, then releases are one command
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this