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

Architecture Overview

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

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

Monitoring and Troubleshooting

Security Best Practices

Best Practices

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

Exit mobile version