Implement CI/CD Using GitHub Actions for Android

Every time you push code, wouldn't it be great if your Android app was automatically built, tested, and verified — without you lifting a finger? That's exactly what CI/CD with GitHub Actions gives you.

CI/CD stands for Continuous Integration / Continuous Delivery. With GitHub Actions, you define workflows in YAML files that run automatically on events like a push or pull request — catching bugs early, enforcing code quality, and keeping your main branch always in a deployable state.

In this guide you'll set up a complete Android CI/CD pipeline with GitHub Actions covering builds, tests, Gradle caching, secrets management, and artifact uploads.

CI/CD GitHub Actions Android

Photo by Richy Great on Unsplash


What is GitHub Actions?

GitHub Actions is GitHub's built-in automation platform. You define workflows — sequences of automated steps — in YAML files stored in .github/workflows/ in your repository. Workflows are triggered by events like pushes, pull requests, or scheduled times.

Concept What it is Example
Workflow A YAML file defining automation android-ci.yml
Trigger (on) Event that starts the workflow push, pull_request, schedule
Job A set of steps that run on one machine build, test, deploy
Step A single task within a job Checkout code, run Gradle
Action Reusable, pre-written step actions/checkout@v4
How GitHub Actions work

How GitHub Actions workflows are structured — Image Source: GitHub


Setting Up Your First Workflow

Step 1: Create the workflow file

In your Android project, create the directory .github/workflows/ and add a new file called android-ci.yml. GitHub automatically detects and runs all .yml files in this directory.

# Create the directory structure in your project root:
mkdir -p .github/workflows
touch .github/workflows/android-ci.yml

Then paste the workflow YAML below into android-ci.yml and commit it to your repository. That's all — GitHub will start running it automatically on the next push.

Implement GitHub Actions for Android

Photo by Sai Kiran Anagani on Unsplash

Step 2: Monitor the workflow and download artifacts

After pushing your code, here's where to find everything:

  1. Go to your GitHub repository → click the Actions tab
  2. You'll see all workflow runs listed with their status (✅ passed, ❌ failed, 🟡 in progress)
  3. Click any run to see individual step logs — useful for debugging failures
  4. Scroll to the bottom of a completed run to find the Artifacts section — click to download your APK or reports
💡 Tip: If a step fails, click on it to expand the full log output. The most useful error messages are usually at the bottom of the log, not the top.

Basic CI Workflow — Build and Test

This workflow triggers on every push to main and every pull request. It checks out the code, sets up JDK 17, caches Gradle dependencies, builds the debug APK, and runs unit tests:

# .github/workflows/android-ci.yml

name: Android CI

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

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # Step 1: Check out the repository code
      - name: Checkout code
        uses: actions/checkout@v4

      # Step 2: Set up JDK 17 (required for AGP 8+)
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'gradle' # Caches Gradle dependencies automatically

      # Step 3: Make gradlew executable
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # Step 4: Build the debug APK
      - name: Build debug APK
        run: ./gradlew assembleDebug

      # Step 5: Run unit tests
      - name: Run unit tests
        run: ./gradlew test

      # Step 6: Upload the APK as a downloadable artifact
      - name: Upload debug APK
        uses: actions/upload-artifact@v4
        with:
          name: debug-apk
          path: app/build/outputs/apk/debug/*.apk
          retention-days: 7

Gradle Dependency Caching

Without caching, every workflow run downloads all Gradle dependencies from scratch — adding 2–5 minutes to every run.

Which approach to use:
Use cache: 'gradle' (automatic) — the simplest option. Already included in the basic workflow above via setup-java@v4. Works for most projects with no extra configuration.

Use manual actions/cache@v4 — only needed if you have a monorepo, custom Gradle home location, or need fine-grained control over the cache key.

Here's the manual approach if you need it:

      # Manual Gradle cache — use only if automatic cache: 'gradle' doesn't work for your setup
      - name: Cache Gradle packages
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

Managing Secrets — API Keys and Keystore

Never hardcode API keys, keystore passwords, or any sensitive data in your workflow files. Use GitHub Secrets — encrypted environment variables stored securely in your repository settings.

Add a secret in GitHub:

  1. Go to your repository → Settings → Secrets and variables → Actions
  2. Click New repository secret
  3. Add your secret name and value (e.g. KEYSTORE_PASSWORD)

Use secrets in your workflow:

      # Access secrets via ${{ secrets.SECRET_NAME }}
      - name: Build release APK
        run: ./gradlew assembleRelease
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

Building a Signed Release APK

To build a signed release APK in CI, store your keystore as a base64-encoded secret and decode it at runtime:

name: Android Release

on:
  push:
    tags:
      - 'v*'  # Triggers on version tags like v1.0.0

jobs:
  release:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'gradle'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # Decode base64 keystore from secret and write to file
      - name: Decode keystore
        run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > app/keystore.jks

      # Build signed release APK
      - name: Build signed release APK
        run: ./gradlew assembleRelease
        env:
          SIGNING_STORE_FILE: keystore.jks
          SIGNING_STORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          SIGNING_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          SIGNING_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

      # Upload signed APK as artifact
      - name: Upload release APK
        uses: actions/upload-artifact@v4
        with:
          name: release-apk
          path: app/build/outputs/apk/release/*.apk

      # Clean up keystore file
      - name: Clean up keystore
        if: always()
        run: rm -f app/keystore.jks
⚠️ Encode your keystore to base64 — run this command locally to get the base64 string, then paste it as a GitHub Secret named KEYSTORE_BASE64:

macOS: base64 -i your-keystore.jks | pbcopy
Linux: base64 your-keystore.jks
Windows (PowerShell): [Convert]::ToBase64String([IO.File]::ReadAllBytes("your-keystore.jks")) | clip

Configure signing in build.gradle

The workflow passes the keystore details as environment variables — but you also need to tell Gradle how to read them. Add this signing config to your app/build.gradle:

// app/build.gradle
android {
    signingConfigs {
        release {
            storeFile file(System.getenv("SIGNING_STORE_FILE") ?: "keystore.jks")
            storePassword System.getenv("SIGNING_STORE_PASSWORD")
            keyAlias System.getenv("SIGNING_KEY_ALIAS")
            keyPassword System.getenv("SIGNING_KEY_PASSWORD")
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

This reads the signing credentials from environment variables set by the workflow. Locally, your own keystore config handles signing — these env vars only exist in the CI environment.

Pull Request Checks Workflow

Add a separate workflow specifically for PRs to run linting and tests before merging. Pair this with a branch protection rule that requires these checks to pass:

name: PR Checks

on:
  pull_request:
    branches: [ main, develop ]

jobs:
  checks:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'
          cache: 'gradle'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # Run Android Lint
      - name: Run Lint
        run: ./gradlew lint

      # Upload lint report
      - name: Upload lint results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: lint-results
          path: app/build/reports/lint-results*.html

      # Run unit tests
      - name: Run unit tests
        run: ./gradlew test

      # Upload test report
      - name: Upload test results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: test-results
          path: app/build/reports/tests/

Best Practices

  • Always cache Gradle dependencies — use cache: 'gradle' in setup-java@v4. This alone can cut your build time by 50–70% by reusing previously downloaded dependencies.
  • Use latest action versions — always pin to a major version like @v4 not @v1. Outdated actions may use deprecated Node.js versions and cause warnings or failures.
  • Never hardcode secrets — use GitHub Secrets for API keys, keystore passwords, and tokens. Secrets are masked in logs and never exposed in workflow output.
  • Run checks on PRs, not just main — catch bugs before they reach main by triggering lint and tests on every pull request. Combine with branch protection rules to enforce this.
  • Use if: always() for upload steps — always upload test and lint reports even when the step fails. This lets you diagnose failures without re-running the workflow.
  • Clean up sensitive files — if you decode a keystore to disk, always add a cleanup step with if: always() to remove it, even if the build fails.

Frequently Asked Questions

How much does GitHub Actions cost for Android CI/CD?
GitHub Actions is free for public repositories. For private repositories, the free tier includes 2,000 minutes/month on Linux runners. A typical Android debug build takes 3–5 minutes without caching, so you get roughly 400–600 free builds per month. With Gradle caching, builds drop to 1–2 minutes significantly increasing that count.

What JDK version should I use for Android builds?
Use JDK 17 with the temurin distribution for Android projects targeting AGP 8.0+. Set java-version: '17' and distribution: 'temurin' in the setup-java@v4 action. JDK 11 is needed for AGP 7.x.

How do I store my Android keystore securely?
Encode your keystore to base64 and store it as a GitHub Secret. In your workflow, decode it back to a file using the base64 command. Store the keystore password, key alias, and key password as separate secrets. Always delete the decoded file after the build using a cleanup step with if: always().

How do I speed up my Android GitHub Actions build?
The biggest gain comes from caching Gradle dependencies via cache: 'gradle' in setup-java@v4. Other improvements: use --build-cache in Gradle commands, split build and test into parallel jobs, and run only lint/unit tests on PRs while full builds run on main.

📝 Summary
  • Place workflow YAML files in .github/workflows/ — GitHub detects them automatically
  • Use actions/checkout@v4 and actions/setup-java@v4 — always use latest major versions
  • Enable Gradle caching with cache: 'gradle' — cuts build time by 50–70%
  • Store sensitive data in GitHub Secrets — never hardcode API keys or passwords
  • Encode keystores to base64 and decode at runtime for signed release builds
  • Use upload-artifact to save APKs and test reports as downloadable files
  • Trigger PR checks separately from main branch builds for faster feedback

Pragnesh Ghoda

A forward-thinking developer offering more than 8 years of experience building, integrating, and supporting android applications for mobile and tablet devices on the Android platform. Talks about #kotlin and #android

3 Comments

Please let us know about any concerns or query.

  1. Informative, nicely explained. Thank you for sharing.

    ReplyDelete
  2. Thank you for explaining GitHub Actions to me.

    ReplyDelete
Previous Post Next Post

Contact Form