How to Set Up Jenkins for Android Projects

How to Set Up Jenkins for Android Projects

Android CI is one of the more forgiving mobile pipelines to build compared to iOS, mostly because you’re not fighting Apple’s hardware licensing – any Linux, macOS, or Windows machine with the Android SDK can build and test an APK. That doesn’t mean it’s trivial, though. Between Gradle build times, emulator instability, and Play Store signing, there’s plenty to get right. Here’s how I’ve set this up across a few different Android codebases.

Why Automate Android Builds with Jenkins

  • Consistent Gradle builds – eliminates “it builds on my machine” issues caused by different local SDK/NDK versions.
  • Automated instrumented and unit testing – catch regressions on every PR instead of relying on manual QA passes.
  • Automated signing and distribution – push builds straight to Firebase App Distribution or the Play Store internal testing track without manual APK uploads.
  • Lint and static analysis enforcement – fail builds automatically on new lint errors instead of letting them accumulate.

Architecture Overview

  • Jenkins Controller – runs anywhere, same as any other setup.
  • Build Agent – needs the Android SDK, a JDK matching your Gradle/AGP version, and enough RAM/CPU for Gradle builds (Gradle is notoriously resource-hungry).
  • Emulator or Physical Device Farm – for instrumented tests, either a headless emulator running on the agent (via avdmanager/emulator CLI) or a cloud device lab (Firebase Test Lab).
  • Signing – a keystore file plus credentials, managed securely through Jenkins Credentials rather than committed to the repo.

Step 1: Prepare the Jenkins Agent

The cleanest approach is a Docker image with the Android SDK pre-installed:

FROM eclipse-temurin:17-jdk

ENV ANDROID_SDK_ROOT=/opt/android-sdk
ENV PATH=$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools

RUN apt-get update && apt-get install -y wget unzip libgl1 && \
    mkdir -p $ANDROID_SDK_ROOT/cmdline-tools && \
    cd $ANDROID_SDK_ROOT/cmdline-tools && \
    wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O tools.zip && \
    unzip -q tools.zip && mv cmdline-tools latest && rm tools.zip

RUN yes | sdkmanager --licenses && \
    sdkmanager "platform-tools" "platforms;android-34" "build-tools;34.0.0"

Step 2: Install Required Jenkins Plugins

  • Git Plugin
  • Gradle Plugin
  • Android Lint Plugin (visualizes lint reports)
  • JUnit Plugin
  • HTML Publisher Plugin
java -jar jenkins-cli.jar -s http://localhost:8080/ install-plugin git gradle android-lint junit htmlpublisher -restart

Step 3: Write the Jenkinsfile

pipeline {
    agent {
        docker { image 'mycompany/android-build:latest' }
    }

    environment {
        GRADLE_OPTS = '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx4g"'
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/myorg/android-app.git'
            }
        }

        stage('Lint') {
            steps {
                sh './gradlew lintDebug'
            }
        }

        stage('Unit Tests') {
            steps {
                sh './gradlew testDebugUnitTest'
            }
        }

        stage('Build Debug APK') {
            steps {
                sh './gradlew assembleDebug'
            }
        }

        stage('Build Release APK') {
            when { branch 'main' }
            environment {
                KEYSTORE_PASSWORD = credentials('android-keystore-password')
                KEY_ALIAS = credentials('android-key-alias')
            }
            steps {
                sh './gradlew assembleRelease -Pandroid.injected.signing.store.password=$KEYSTORE_PASSWORD -Pandroid.injected.signing.key.alias=$KEY_ALIAS'
            }
        }
    }

    post {
        always {
            junit '**/build/test-results/**/*.xml'
            archiveArtifacts artifacts: '**/build/outputs/apk/**/*.apk', allowEmptyArchive: true
            publishHTML(target: [
                reportDir: 'app/build/reports/lint-results',
                reportFiles: 'lintDebug.html',
                reportName: 'Lint Report'
            ])
        }
    }
}

Step 4: Running Instrumented Tests with a Headless Emulator

Instrumented tests need an actual (or emulated) Android device. Here’s a stage that boots a headless emulator inside the pipeline:

stage('Instrumented Tests') {
    steps {
        sh '''
            echo "no" | avdmanager create avd -n test -k "system-images;android-34;google_apis;x86_64" --force
            $ANDROID_SDK_ROOT/emulator/emulator -avd test -no-window -no-audio -no-boot-anim &
            adb wait-for-device
            ./gradlew connectedDebugAndroidTest
        '''
    }
    post {
        always {
            sh 'adb emu kill || true'
        }
    }
}

Emulators are resource-heavy and slow to boot inside CI, so many teams instead offload this to Firebase Test Lab:

stage('Firebase Test Lab') {
    steps {
        sh './gradlew assembleDebugAndroidTest assembleDebug'
        sh '''
            gcloud firebase test android run \
              --type instrumentation \
              --app app/build/outputs/apk/debug/app-debug.apk \
              --test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
              --device model=Pixel7,version=34,locale=en,orientation=portrait
        '''
    }
}

This requires a GCP service account with Firebase Test Lab permissions, stored as a Jenkins credential and activated via gcloud auth activate-service-account.

Step 5: Managing the Signing Keystore

Never commit your release keystore to Git. Instead, upload it as a Secret file credential in Jenkins and reference it in the pipeline:

stage('Build Release APK') {
    steps {
        withCredentials([
            file(credentialsId: 'android-release-keystore', variable: 'KEYSTORE_FILE'),
            string(credentialsId: 'android-keystore-password', variable: 'KEYSTORE_PASSWORD')
        ]) {
            sh '''
                ./gradlew assembleRelease \
                  -Pandroid.injected.signing.store.file=$KEYSTORE_FILE \
                  -Pandroid.injected.signing.store.password=$KEYSTORE_PASSWORD \
                  -Pandroid.injected.signing.key.alias=$KEY_ALIAS \
                  -Pandroid.injected.signing.key.password=$KEYSTORE_PASSWORD
            '''
        }
    }
}

Step 6: Distributing Builds

Firebase App Distribution (great for internal QA builds):

stage('Distribute to Firebase') {
    steps {
        sh './gradlew appDistributionUploadRelease'
    }
}

Google Play (internal testing track) using the Gradle Play Publisher plugin:

stage('Publish to Play Store') {
    when { branch 'release' }
    steps {
        sh './gradlew publishReleaseBundle --track internal'
    }
}

Both require service account credentials with appropriate API access, stored securely in Jenkins.

Integrating with the Wider Toolchain

  • Git/GitHub – trigger builds on push and PR, and post lint/test results back as PR checks.
  • Docker – build inside a pinned Android SDK image so builds don’t drift as the SDK/NDK versions change over time.
  • Slack – notify QA channels when a new build lands in Firebase App Distribution, including release notes pulled from the latest commits.
  • Jira – transition tickets automatically when a build containing the relevant commit reaches the internal testing track.

Monitoring and Troubleshooting

  • Gradle build running out of memory – tune org.gradle.jvmargs in gradle.properties and disable the Gradle daemon in CI (-Dorg.gradle.daemon=false) to avoid daemon processes lingering across ephemeral containers.
  • Emulator fails to boot in CI – ensure hardware acceleration (KVM on Linux) is available to the container; without it, emulators fall back to painfully slow software rendering.
  • Flaky instrumented tests – common with emulators under CI resource constraints; consider Firebase Test Lab for more consistent, real-device-backed results.
  • Signing failures (“keystore was tampered with”) – almost always a wrong password or corrupted file transfer; verify the Secret file credential’s integrity with a checksum before debugging further.

Security Best Practices

  • Store the release keystore exclusively as a Jenkins Secret file credential, never in the repository, even encrypted.
  • Use separate service accounts with least-privilege scopes for Firebase Test Lab, Firebase App Distribution, and Play Store publishing.
  • Rotate the keystore password periodically if your organization’s policy requires it, and always keep an offline backup of the keystore itself (losing it means you can never update the app under the same signing identity).
  • Restrict who can trigger release-signed builds using Jenkins’ role-based authorization.

Best Practices

  • Enable Gradle build caching and configure a remote build cache if you have multiple agents, since Android builds are one of the more cache-friendly build systems available.
  • Run lint and unit tests on every PR; reserve full instrumented test suites and release builds for merges to protected branches to save CI time.
  • Keep separate Gradle build variants (debug/staging/release) clearly scoped in your Jenkinsfile so the right signing and configuration is used for each.
  • Archive APKs/AABs as build artifacts so any historical build can be re-downloaded without rebuilding.

FAQs

Do I need macOS to build Android apps? No, Android builds work fine on Linux, which is generally cheaper and faster to provision than macOS agents.

How long should a typical Android CI build take? With proper Gradle caching, a mid-sized app’s unit test and debug build stage should run in a few minutes; full instrumented test suites can add significantly more depending on emulator vs Firebase Test Lab usage.

Can I build both APKs and Android App Bundles (AAB) in the same pipeline? Yes, just add separate Gradle tasks (assembleRelease for APK, bundleRelease for AAB) as parallel or sequential stages depending on your distribution needs.

Is Firebase Test Lab better than a local emulator? For CI purposes, generally yes – it offers real device coverage and doesn’t compete for the same CPU/memory resources as the rest of your build, though it does add external cost and network dependency.

How do I handle multi-module Gradle projects? The same pipeline structure applies; just make sure your Gradle tasks target the correct modules (./gradlew :app:assembleRelease) and that your test/lint reporting steps glob across all module report directories.

Summary

Setting up Jenkins for Android comes down to a properly configured SDK-equipped build agent, a Jenkinsfile that runs lint, unit tests, and Gradle builds, secure keystore handling via Jenkins Credentials, and a distribution step to Firebase App Distribution or the Play Store. Instrumented testing is the trickiest part – decide early whether local emulators or Firebase Test Lab fits your team’s speed and budget needs. Once wired together, you get consistent, reproducible Android builds on every commit, with signed release artifacts ready to ship without anyone touching a keystore file by hand.

References

  • Android Gradle Plugin documentation: https://developer.android.com/build
  • Firebase Test Lab documentation: https://firebase.google.com/docs/test-lab
  • Firebase App Distribution documentation: https://firebase.google.com/docs/app-distribution
  • Gradle Play Publisher plugin: https://github.com/Triple-T/gradle-play-publisher
  • Jenkins Pipeline Syntax reference: https://www.jenkins.io/doc/book/pipeline/syntax/
Total
1
Shares

Leave a Reply

Previous Post
How to Build and Deploy a Docker Image with Jenkins

How to Build and Deploy a Docker Image with Jenkins

Next Post
How to Use Jenkins for iOS Continuous Integration

How to Use Jenkins for iOS Continuous Integration

Related Posts