Android CI/CD with Jenkins and GitLab CI: From Build to Release

Introduction

In today’s fast-moving mobile development environment, continuous integration (CI) and continuous delivery (CD) have become essential parts of modern software development. For Android teams, building an efficient and reliable automated system for building, testing, and releasing apps can significantly improve development efficiency, reduce human error, and shorten product delivery cycles.

This article explores how to implement CI/CD for Android projects with two mainstream tools: Jenkins and GitLab CI. We will start with the basic concepts, then move gradually into advanced configuration and optimization techniques, covering the full process from build automation, test execution, code quality checks, and report generation to app release.

Whether you are new to CI/CD or an experienced developer looking to optimize an existing workflow, this guide provides practical guidance and deeper insight. Through detailed configuration examples, code snippets, and best practices, it will help you build a robust Android CI/CD pipeline.

Chapter 1: Continuous integration fundamentals and tool selection

1.1 Core concepts of continuous integration

Continuous integration is a software development practice that requires developers to merge code changes frequently into a shared mainline branch. Each integration is verified by automated builds and tests so integration errors can be found as early as possible.

Core value of continuous integration:

  • Fast feedback: developers receive build and test results immediately after committing code.
  • Early issue detection: integration problems and defects are found early in the development cycle.
  • Lower integration risk: long-lived branches and complex merge conflicts are avoided.
  • Deployable software: the team always maintains a deployable version of the software.

For Android development, continuous integration is especially important because:

  • Android apps must be built into APK or AAB files before they can run.
  • Testing is needed across multiple devices and API levels.
  • The release process is complex and includes signing, channel packaging, and related steps.

1.2 Comparing Jenkins and GitLab CI

When choosing a CI/CD tool, Jenkins and GitLab CI are two of the most popular options. The table below compares them in detail:

FeatureJenkinsGitLab CI
ArchitectureController-agent architecture; supports distributed executionRunner-based; supports distributed execution
Installation and maintenanceRequires an independent server; higher maintenance costIntegrated with GitLab; simpler maintenance
Configuration styleWeb UI or Groovy DSLYAML file (.gitlab-ci.yml)
ExtensibilityRich plugin ecosystem; highly extensibleMore focused feature set; moderate extensibility
IntegrationIntegrates with many tools but requires configurationDeep GitLab integration; other tools require configuration
Learning curveSteeperGentler
Community supportVery active, with extensive documentationActive, with good documentation
Best fitComplex projects that need heavy customizationGitLab users who want a simple CI/CD solution

1.3 Tool selection recommendations

Based on the comparison above, here are practical recommendations.

Choose Jenkins when:

  • The project is very complex and needs a highly customized build workflow.
  • The team already uses Jenkins and is familiar with it.
  • You need to integrate with many different tools and services.
  • The project is not hosted on GitLab.

Choose GitLab CI when:

  • The code is already hosted on GitLab.
  • You want simple configuration and low maintenance cost.
  • The project is relatively standard and does not need heavy customization.
  • The team is small and resources are limited.

In real projects, the two tools can also be combined to take advantage of each. For example, GitLab CI can handle the initial build and test steps after code submission, while Jenkins handles more complex release workflows and later-stage testing.

Chapter 2: Environment preparation and basic configuration

2.1 Hardware and software requirements

Before configuring a CI/CD workflow, make sure the environment is ready. These are the basic requirements for Android CI/CD:

Hardware requirements:

  • CPU: at least 4 cores; 8 cores or more recommended, especially when running multiple builds in parallel.
  • Memory: at least 8 GB; 16 GB recommended, and large projects may need 32 GB.
  • Storage: at least 100 GB SSD, because Android builds produce many cache files.
  • Network: stable, high-speed network connectivity for downloading dependencies and uploading build artifacts.

Software requirements:

  • Operating system: Linux, preferably Ubuntu LTS or CentOS; macOS and Windows are also possible.
  • Java Development Kit (JDK): Android development requires JDK 8 or 11; newer projects are generally better served by JDK 17. AdoptOpenJDK is recommended.
  • Android SDK: latest stable version, including the required platform tools and build tools.
  • Docker (optional): used to containerize the build environment and ensure consistency.
  • Version control system: Git.

2.2 Jenkins installation and initial configuration

2.2.1 Jenkins installation

Install Jenkins on Ubuntu with the following steps:

# 1. Add the Jenkins repository key
wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -

# 2. Add the Jenkins repository to the source list
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'

# 3. Update the package index
sudo apt-get update

# 4. Install Jenkins
sudo apt-get install jenkins

# 5. Start the Jenkins service
sudo systemctl start jenkins

# 6. Enable Jenkins on boot
sudo systemctl enable jenkins

After installation, Jenkins runs on port 8080 by default. Open http://your-server-ip:8080 in a browser and complete the initial setup wizard.

2.2.2 Initial security configuration

Get the initial administrator password from the log:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

After entering the password in the web UI, choose “Install suggested plugins” to install the recommended plugins.

  • Create the first administrator user.
  • Configure the instance URL; the default is usually fine.

2.2.3 Install required plugins

For Android CI/CD, install these key plugins:

  • Android Emulator Plugin: manages Android emulators.
  • Git Plugin: provides Git integration.
  • Gradle Plugin: supports Gradle builds.
  • Pipeline: defines build pipelines.
  • HTML Publisher: publishes HTML reports.
  • JUnit: processes JUnit test results.
  • JaCoCo: supports code coverage.
  • SonarQube Scanner (optional): analyzes code quality.
  • Google Play Android Publisher (optional): publishes apps to Google Play.

Plugin installation steps:

  1. Open “Manage Jenkins” > “Manage Plugins”.
  2. Select the “Available” tab.
  3. Search for the plugins above and select them.
  4. Click “Install without restart” or “Download now and install after restart.”

2.2.4 Configure global tools

In “Manage Jenkins” > “Global Tool Configuration”, configure:

  • JDK:
    • Name: jdk17
    • JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
  • Git:
    • Name: Default
    • Path to Git executable: git
  • Gradle:
    • Name: gradle-8.4
    • Select “Install automatically”
    • Version: 8.4
    • Leave other options at their defaults

2.3 GitLab CI Runner installation and configuration

2.3.1 Install GitLab Runner

Install GitLab Runner on Ubuntu:

# 1. Add the official repository
curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash

# 2. Install the latest GitLab Runner
sudo apt-get install gitlab-runner

# 3. Verify the installation
gitlab-runner --version

2.3.2 Register the Runner

In the GitLab project, go to “Settings” > “CI/CD” > “Runners”.

Find the URL and token in the “Set up a specific Runner manually” section.

Run the registration command on the server:

sudo gitlab-runner register

Enter the following when prompted:

GitLab instance URL: https://gitlab.xxx-host.com/

Registration token: get it from the GitLab UI.

Description: android-runner

Tags: android, docker (optional)

Executor: docker (recommended) or shell

If you choose the Docker executor, also specify a default Docker image, such as docker:stable.

2.3.3 Configure the Runner

Edit the Runner configuration file, usually /etc/gitlab-runner/config.toml, and make sure it contains the following configuration:

concurrent = 4
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "android-runner"
  url = "https://gitlab.com/"
  token = "YOUR_TOKEN"
  executor = "docker"
  [runners.docker]
    tls_verify = false
    image = "alpine:latest"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]

2.3.4 Install Docker (if using the Docker executor)

# 1. Install required dependencies
sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

# 2. Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

# 3. Add the Docker repository
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

# 4. Update the package index and install Docker
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

# 5. Add the current user to the docker group to avoid using sudo every time
sudo usermod -aG docker $USER
sudo usermod -aG docker gitlab-runner

# 6. Restart the Docker service
sudo systemctl restart docker

2.4 Android SDK configuration

Whether you use Jenkins or GitLab CI, the Android SDK must be configured correctly.

2.4.1 Install Android command-line tools

# 1. Create the Android SDK directory
mkdir -p ~/android-sdk/cmdline-tools
cd ~/android-sdk/cmdline-tools

# 2. Download command-line tools; the version may change, so check the latest release
wget https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip
unzip commandlinetools-linux-6858069_latest.zip
mv cmdline-tools latest

# 3. Add environment variables
echo 'export ANDROID_HOME=$HOME/android-sdk' >> ~/.bashrc
echo 'export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools' >> ~/.bashrc
source ~/.bashrc

# 4. Accept licenses
yes | sdkmanager --licenses

# 5. Install basic tools and platforms
sdkmanager "platform-tools" "platforms;android-30" "build-tools;30.0.3"

2.4.2 Configure Android SDK in Jenkins

Open “Manage Jenkins” > “Global Tool Configuration”:

  • Find the “Android SDK” section.
  • Click “Add Android SDK”.
  • Configure it as follows:
    • Name: android-sdk-latest
    • Clear “Install automatically”
    • Android SDK home: /home/jenkins/android-sdk (adjust according to the actual path)

2.4.3 Configure Android SDK in GitLab Runner

If you use the Docker executor, create a custom Docker image that includes the Android SDK:

# Dockerfile.android
FROM openjdk:17-jdk

# Install basic tools
RUN apt-get update && apt-get install -y \
    git \
    wget \
    unzip \
    && rm -rf /var/lib/apt/lists/*

# Set environment variables
ENV ANDROID_HOME /opt/android-sdk
ENV PATH ${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools

# Download and install Android SDK
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \
    cd ${ANDROID_HOME}/cmdline-tools && \
    wget https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -O cmdline-tools.zip && \
    unzip cmdline-tools.zip && \
    mv cmdline-tools latest && \
    rm cmdline-tools.zip

# Accept licenses and install components
RUN yes | sdkmanager --licenses && \
    sdkmanager "platform-tools" "platforms;android-30" "build-tools;30.0.3"

WORKDIR /app

Build and push the image:

docker build -t android-ci-image -f Dockerfile.android .
docker tag android-ci-image your-registry/android-ci-image:latest
docker push your-registry/android-ci-image:latest

Then use this image in .gitlab-ci.yml:

image: your-registry/android-ci-image:latest

Chapter 3: Basic build configuration

3.1 Android project structure review

Before configuring CI/CD, it is important to understand the standard Android project structure. A typical Android project contains these key parts:

my-android-app/
├── app/                # Main module
│   ├── build.gradle    # Module-level build configuration
│   ├── src/
│   │   ├── main/       # Main source code
│   │   ├── test/       # Unit tests
│   │   └── androidTest # Instrumented tests
├── build.gradle        # Project-level build configuration
├── settings.gradle     # Project settings
├── gradle.properties  # Gradle properties
└── gradlew             # Gradle wrapper script

3.2 Basic Jenkins build configuration

3.2.1 Create a freestyle project

  • In the Jenkins dashboard, click “New Item”.
  • Enter a project name, such as Android-CI.
  • Select “Freestyle project” and click “OK.”

3.2.2 Configure source code management

In the “Source Code Management” section:

  • Select “Git”.
  • Enter the Repository URL, such as a GitHub or GitLab repository URL.
  • Configure credentials as needed.
  • Specify the branch, such as */main or */develop.

3.2.3 Configure build triggers

In the “Build Triggers” section, select suitable triggers:

  • Poll SCM: periodically checks for code changes.
    • Schedule: H/5 * * * * (check every 5 minutes).
  • GitHub/GitLab hook: triggers when code is pushed; additional webhook configuration is required.

3.2.4 Configure the build environment

  • Select “Provide Configuration files” if needed.
  • Select “Use secret text(s) or file(s)” if secure credentials are needed.

In the “Build Environment” section, you can:

  • Select “Delete workspace before build starts” to clean the workspace.
  • Select “Add timestamps to the Console Output” to add timestamps to logs.

3.2.5 Configure build steps

In the “Build” section, click “Add build step”.

Select “Invoke Gradle script” and configure:

  • Gradle version: gradle-8.4 (configured earlier).
  • Tasks: clean assembleDebug or assembleRelease.
  • Select “Make gradlew executable” for the first build.

3.2.6 Configure post-build actions

In the “Post-build Actions” section, click “Add post-build action”:

  1. Select “Archive the artifacts”.
    • Files to archive: app/build/outputs/apk/debug/*.apk.
  2. Add “Publish JUnit test result”.
    • Test report XMLs: app/build/test-results/**/*.xml.
  3. Add “Record JaCoCo coverage report”.
    • Configure the coverage report paths.

3.2.7 Save and run the build

Click “Save”, then click “Build Now” to run the first build.

3.3 Basic GitLab CI configuration

GitLab CI defines the build workflow in a .gitlab-ci.yml file at the project root.

3.3.1 Create a basic configuration file

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy

variables:
  ANDROID_COMPILE_SDK: "30"
  ANDROID_BUILD_TOOLS: "30.0.3"
  ANDROID_SDK_TOOLS: "6858069"

# Cache configuration
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .gradle/
    - app/build/

build:
  stage: build
  tags:
    - android
  script:
    - export GRADLE_USER_HOME=$(pwd)/.gradle
    - chmod +x gradlew
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/*.apk
    expire_in: 1 week

3.3.2 Configuration notes

  • stages: defines the phases of the build workflow.
  • variables: sets environment variables for easier maintenance and changes.
  • cache: caches Gradle files and build outputs to speed up later builds.
  • build job:
    • stage: belongs to the build stage.
    • tags: specifies the runner tags used to run this job.
    • script: commands to execute.
    • artifacts: saves build outputs for later stages.

3.3.3 Advanced cache configuration

To use caching more effectively, optimize the configuration:

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .gradle/wrapper
    - .gradle/caches
    - app/build/intermediates/compile_only_not_namespaced_r_class_jar/debug
    - app/build/intermediates/bundle_manifest/debug
    - app/build/intermediates/merged_manifests/debug
    - app/build/intermediates/annotation_processor_list/debug
    - app/build/intermediates/compile_library_classes_jar/debug
    - app/build/intermediates/generated_proguard_file/debug
    - app/build/intermediates/incremental/mergeDebugResources
    - app/build/intermediates/incremental/packageDebugResources
    - app/build/intermediates/javac/debug
    - app/build/intermediates/processed_res/debug
    - app/build/intermediates/res/merged/debug
    - app/build/intermediates/symbols/debug
    - app/build/outputs
  policy: pull-push

3.3.4 Multi-module project configuration

For multi-module projects, extend the build configuration:

build:
  stage: build
  tags:
    - android
  script:
    - export GRADLE_USER_HOME=$(pwd)/.gradle
    - chmod +x gradlew
    - ./gradlew :app:assembleDebug :library:assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/*.apk
      - library/build/outputs/aar/*.aar
    expire_in: 1 week

3.4 Gradle build optimization

To speed up CI builds, add the following configuration to gradle.properties:

# Parallel build
org.gradle.parallel=true
# Enable build cache
org.gradle.caching=true
# Enable configuration cache (Gradle 6.6+)
org.gradle.unsafe.configuration-cache=true
# JVM memory configuration
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# Disable the Gradle daemon in CI environments
org.gradle.daemon=false

For Jenkins, add these parameters in the build step:

tasks: clean assembleDebug
switches: --build-cache --parallel --max-workers=4

For GitLab CI, add them to the script:

script:
  - ./gradlew assembleDebug --build-cache --parallel --max-workers=4

Chapter 4: Automated test integration

4.1 Android testing overview

Android app testing is usually divided into three layers:

  • Unit tests: test individual classes or methods and run on the JVM.
    • Location: module/src/test/java/.
    • Frameworks: JUnit, Mockito, Robolectric, and others.
  • Instrumented tests: tests that run on Android devices or emulators.
    • Location: module/src/androidTest/java/.
    • Frameworks: AndroidX Test, Espresso, UI Automator, and others.
  • UI tests: test user interface interactions.
    • Usually implemented as part of instrumented tests.

4.2 Configure unit tests

4.2.1 Jenkins configuration

Add the test task to the build step:

tasks: clean testDebugUnitTest

Add a post-build action to collect test results:

  • Add “Publish JUnit test result”.
  • Test report XMLs: app/build/test-results/testDebugUnitTest/**/*.xml.

Add a code coverage report.

Make sure JaCoCo is configured in build.gradle:

android {

    testOptions {

        unitTests.all {

            jacoco {

                includeNoLocationClasses = true

                excludes = ['jdk.internal.*']

            }

        }

    }

}

Add the build step:

tasks: jacocoTestReport

Add the “Record JaCoCo coverage report” post-build action.

4.2.2 GitLab CI configuration

unit_test:
  stage: test
  tags:
    - android
  script:
    - ./gradlew testDebugUnitTest jacocoTestReport
  artifacts:
    paths:
      - app/build/reports/tests/testDebugUnitTest/
      - app/build/reports/jacoco/jacocoTestReport/
    expire_in: 1 week
  coverage: '/Total.*?([0-9]{1,3})%/'

4.3 Configure instrumented tests

Instrumented tests must run on an Android device or emulator. In a CI environment, you can use:

  • Physical devices: real devices connected to the CI server.
  • Emulators: Android emulators started on the CI server.
  • Firebase Test Lab: a cloud testing service.
  • Third-party services: BrowserStack, Sauce Labs, and similar services.

4.3.1 Run instrumented tests with an emulator

Jenkins configuration:

  • Install Android Emulator Plugin.
  • Select “Android Emulator” in the build environment.
  • Configure the emulator:
    • Android SDK: select the configured Android SDK.
    • AVD name: ci-emulator.
    • System image: for example, system-images;android-30;google_apis;x86_64.
    • Screen density: 240.
    • Screen resolution: 1080x1920.
    • Device locale: en_US.
    • Device language: en.
    • Other options: adjust as needed.
  • Add a build step to run tests:
tasks: connectedDebugAndroidTest

Collect test results:

JUnit report path: app/build/outputs/androidTest-results/connected/**/*.xml

Coverage report: if JaCoCo is configured, the path is app/build/reports/coverage/androidTest/debug/

GitLab CI configuration:

Use Docker-in-Docker or privileged mode to run the emulator:

instrumented_test:
  stage: test
  tags:
    - android
  services:
    - docker:dind
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_HOST: tcp://docker:2375
  script:
    # Start the emulator container
    - docker run --detach --privileged --name emulator --publish 5554:5554 --publish 5555:5555 
      -e ADBKEY="$(cat ~/.android/adbkey)" android-emulator:30
    - adb wait-for-device
    - ./gradlew connectedDebugAndroidTest
  artifacts:
    paths:
      - app/build/reports/androidTests/connected/
      - app/build/outputs/androidTest-results/connected/
    expire_in: 1 week

4.3.2 Use Firebase Test Lab

For more comprehensive testing, use Firebase Test Lab:

  • Set up a Firebase project and enable Test Lab.
  • Create a service account and download the JSON key file.
  • Configure the key in CI.

Jenkins configuration:

  • Add the Firebase key to Jenkins as a secret file.

Add a build step:

withCredentials([file(credentialsId: 'firebase-key', variable: 'FIREBASE_KEY')]) {
    sh """
    export FIREBASE_KEY_PATH=\$(mktemp)
    cp \$FIREBASE_KEY \$FIREBASE_KEY_PATH
    gcloud auth activate-service-account --key-file=\$FIREBASE_KEY_PATH
    gcloud --quiet config set project your-project-id
    ./gradlew app:assembleDebug app:assembleDebugAndroidTest
    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=Pixel2,version=30,locale=en,orientation=portrait \\
        --timeout 20m
    rm \$FIREBASE_KEY_PATH
    """
}

GitLab CI configuration:

firebase_test:
  stage: test
  tags:
    - android
  before_script:
    - apt-get update && apt-get install -y curl
    - curl -sSL https://sdk.cloud.google.com | bash
    - source ~/.bashrc
    - echo $FIREBASE_KEY > /tmp/firebase-key.json
    - gcloud auth activate-service-account --key-file=/tmp/firebase-key.json
    - gcloud --quiet config set project your-project-id
  script:
    - ./gradlew app:assembleDebug app:assembleDebugAndroidTest
    - 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=Pixel2,version=30,locale=en,orientation=portrait
        --timeout 20m
  artifacts:
    reports:
      junit: app/build/outputs/androidTest-results/connected/**/*.xml

4.4 Test reports and visualization

4.4.1 Jenkins test reports

JUnit report:

  • Install JUnit Plugin.
  • Add “Publish JUnit test result” in “Post-build Actions”.
  • Specify the test result path, such as **/test-results/**/*.xml.

JaCoCo coverage report:

  • Install JaCoCo Plugin.
  • Add “Record JaCoCo coverage report” in “Post-build Actions”.
  • Configure include and exclude patterns.

HTML report:

  • Install HTML Publisher Plugin.
  • Add the “Publish HTML reports” post-build action.
  • Specify the HTML report directory, such as app/build/reports/.

4.4.2 GitLab test reports

GitLab automatically parses JUnit-formatted test reports:

artifacts:
  reports:
    junit: app/build/test-results/**/*.xml
    cobertura: app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml

Coverage visualization:

coverage: '/Total.*?([0-9]{1,3})%/'

4.4.3 Custom HTML reports

Create a custom test report:

Generate the HTML report in the build script.

Save it as an artifact.

generate_report:
  stage: test
  script:
    - ./gradlew generateTestReport
  artifacts:
    paths:
      - app/build/reports/custom-test-report.html

Chapter 5: Code quality checks

5.1 Static code analysis tools

Common static analysis tools in Android development:

  • Android Lint: the official static analysis tool, used to find potential issues and optimization suggestions.
  • Checkstyle: code style checks.
  • PMD: detects common programming defects.
  • FindBugs/SpotBugs: finds bug patterns in code.
  • Detekt (Kotlin projects): a Kotlin static analysis tool.
  • SonarQube: a comprehensive code quality platform.

5.2 Configure Android Lint

5.2.1 Gradle configuration

Add lint configuration in build.gradle:

android {
    lintOptions {
        abortOnError true
        warningsAsErrors true
        checkAllWarnings true
        htmlReport true
        htmlOutput file("${buildDir}/reports/lint/lint-report.html")
        xmlReport true
        xmlOutput file("${buildDir}/reports/lint/lint-report.xml")
        sarifReport true
        sarifOutput file("${buildDir}/reports/lint/lint-report.sarif")
    }
}

5.2.2 Jenkins integration

Add a build step:

tasks: lintDebug

Add a post-build action to publish reports:

HTML Publisher: app/build/reports/lint/lint-report.html

If the build should fail when Lint finds issues, add a script:

def lintTask = tasks.getByPath(':app:lintDebug')

if (lintTask.outputFile.text.contains("errors")) {

    error("Lint found errors")

}

5.2.3 GitLab CI integration

lint:
  stage: test
  script:
    - ./gradlew lintDebug
  artifacts:
    paths:
      - app/build/reports/lint/
    expire_in: 1 week
  allow_failure: true  # Set to true or false as needed

5.3 Configure Checkstyle

5.3.1 Add the Checkstyle plugin

In build.gradle:

plugins {
    id 'checkstyle'
}

checkstyle {
    toolVersion '8.42'
    configFile file("${project.rootDir}/config/checkstyle/checkstyle.xml")
    configProperties = ['checkstyle.cache.file': "${buildDir}/checkstyle.cache"]
    ignoreFailures false
    showViolations true
}

task checkstyle(type: Checkstyle) {
    source 'src'
    include '**/*.java'
    exclude '**/gen/**', '**/test/**', '**/androidTest/**'
    classpath = files()
}

5.3.2 Example configuration file

config/checkstyle/checkstyle.xml:

<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
        "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
        "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
    <property name="charset" value="UTF-8"/>
    <property name="severity" value="error"/>
    
    <module name="FileTabCharacter"/>
    <module name="TreeWalker">
        <module name="JavadocMethod"/>
        <module name="MethodName"/>
        <module name="ParameterNumber">
            <property name="max" value="5"/>
        </module>
    </module>
</module>

5.3.3 CI integration

Jenkins:

Add a build step:

tasks: checkstyle

Publish the HTML report:

Path: app/build/reports/checkstyle/checkstyle.html

GitLab CI:

checkstyle:
  stage: test
  script:
    - ./gradlew checkstyle
  artifacts:
    paths:
      - app/build/reports/checkstyle/
    expire_in: 1 week

5.4 Integrate SonarQube

5.4.1 Install SonarQube server

./bin/linux-x86-64/sonar.sh start

Open http://localhost:9000. The default account is admin/admin.

5.4.2 Gradle configuration

In build.gradle:

plugins {
    id "org.sonarqube" version "3.3"
}

sonarqube {
    properties {
        property "sonar.projectKey", "your-project-key"
        property "sonar.host.url", "http://your-sonar-server:9000"
        property "sonar.login", project.hasProperty('sonarToken') ? sonarToken : ""
        property "sonar.android.lint.report", "build/reports/lint/lint-report.xml"
        property "sonar.java.checkstyle.reportPaths", "build/reports/checkstyle/checkstyle.xml"
        property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml"
    }
}

5.4.3 CI integration

Jenkins:

Install the SonarQube Scanner plugin.

Configure the SonarQube server in “Manage Jenkins” > “Configure System”.

Add a build step:

tasks: sonarqube

-Dsonar.login=$SONAR_TOKEN

GitLab CI:

sonarqube:
  stage: test
  script:
    - ./gradlew sonarqube -Dsonar.login=$SONAR_TOKEN
  only:
    - master
    - develop

5.5 Quality gates and build blocking

Configure quality gates so builds are blocked when code quality does not meet the threshold:

5.5.1 Jenkins configuration

stage('Quality Gate') {
    steps {
        script {
            def qualityGate = waitForQualityGate()
            if (qualityGate.status != 'OK') {
                error "Quality gate failed: ${qualityGate.status}"
            }
        }
    }
}

5.5.2 GitLab CI configuration

quality_gate:
  stage: test
  script:
    - ./gradlew sonarqube -Dsonar.login=$SONAR_TOKEN
    - >
      curl --fail --user $SONAR_TOKEN:
      "$SONAR_HOST_URL/api/qualitygates/project_status?projectKey=$SONAR_PROJECT_KEY"
      | grep -q '"status":"OK"'
  allow_failure: false

Chapter 6: Automated release and deployment

6.1 Build variants and signing configuration

6.1.1 Configure build types and product flavors

In app/build.gradle:

android {
    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            debuggable true
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }
    
    flavorDimensions "environment"
    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
            versionNameSuffix "-dev"
        }
        prod {
            dimension "environment"
        }
    }
    
    signingConfigs {
        release {
            storeFile file("keystore.jks")
            storePassword System.getenv("STORE_PASSWORD")
            keyAlias System.getenv("KEY_ALIAS")
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }
}

6.1.2 Store signing information securely

Jenkins:

Add credentials in “Manage Jenkins” > “Manage Credentials”:

  • Kind: Secret text
  • Scope: Global
  • Secret: [your_store_password]
  • ID: STORE_PASSWORD

Use credentials in the build configuration:

withCredentials([string(credentialsId: 'STORE_PASSWORD', variable: 'STORE_PASSWORD'),

                string(credentialsId: 'KEY_ALIAS', variable: 'KEY_ALIAS'),

                string(credentialsId: 'KEY_PASSWORD', variable: 'KEY_PASSWORD')]) {

    sh './gradlew assembleRelease'

}

GitLab CI:

Add variables in “Settings” > “CI/CD” > “Variables”:

  • STORE_PASSWORD
  • KEY_ALIAS
  • KEY_PASSWORD
  • Select “Mask variable” and “Protect variable.”

In .gitlab-ci.yml:

build_release:

  stage: build

  script:

    - ./gradlew assembleRelease

  only:

    - tags

6.2 Release to internal channels

6.2.1 Release to an internal web server

Jenkins:

stage('Deploy Internal') {
    steps {
        sshagent(['web-server-credentials']) {
            sh """
            scp app/build/outputs/apk/release/app-release.apk \
                user@webserver:/var/www/downloads/app-${BUILD_NUMBER}.apk
            """
        }
    }
}

GitLab CI:

deploy_internal:
  stage: deploy
  script:
    - apt-get update && apt-get install -y openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - scp app/build/outputs/apk/release/app-release.apk user@webserver:/var/www/downloads/app-${CI_COMMIT_TAG}.apk
  only:
    - tags

6.2.2 Release to Firebase App Distribution

Jenkins:

stage('Firebase App Distribution') {
    steps {
        withCredentials([file(credentialsId: 'firebase-key', variable: 'FIREBASE_KEY')]) {
            sh """
            export FIREBASE_KEY_PATH=\$(mktemp)
            cp \$FIREBASE_KEY \$FIREBASE_KEY_PATH
            ./gradlew assembleRelease
            firebase appdistribution:distribute app/build/outputs/apk/release/app-release.apk \\
                --app 1:1234567890:android:abcdef1234567890 \\
                --groups "qa-team" \\
                --token $(cat \$FIREBASE_KEY_PATH | jq -r '.client_email')
            rm \$FIREBASE_KEY_PATH
            """
        }
    }
}

GitLab CI:

firebase_distribution:
  stage: deploy
  script:
    - curl -sL https://firebase.tools | bash
    - echo "$FIREBASE_KEY" > /tmp/firebase-key.json
    - ./gradlew assembleRelease
    - firebase appdistribution:distribute app/build/outputs/apk/release/app-release.apk
        --app 1:1234567890:android:abcdef1234567890
        --groups "qa-team"
        --token $(cat /tmp/firebase-key.json | jq -r '.client_email')
  only:
    - tags

6.3 Release to Google Play

6.3.1 Prepare Google Play API access

  • Create a service account in Google Play Console.
  • Download the JSON key file.
  • Configure the key in the CI system.

6.3.2 Jenkins configuration

  • Install the “Google Play Android Publisher” plugin.
  • Add credentials:
    • Kind: Google Service Account from private key
    • Upload the JSON key file.
  • Add a step to the build configuration:
stage('Deploy to Google Play') {

    steps {

        googlePlayUploader(

            applicationId: 'com.your.package',

            credentialsId: 'google-play-credentials',

            apkFiles: 'app/build/outputs/apk/release/app-release.apk',

            trackName: 'internal',

            rolloutPercentage: '100'

        )

    }

}

6.3.3 GitLab CI configuration

deploy_play_store:
  stage: deploy
  script:
    - mkdir -p ~/.android
    - echo "$GOOGLE_PLAY_KEY" > ~/.android/google-play-key.json
    - ./gradlew publishReleaseBundle
  only:
    - tags

Configure the publishing plugin in build.gradle:

plugins {
    id 'com.github.triplet.play' version '3.7.0'
}

play {
    serviceAccountCredentials = file("${System.getenv('HOME')}/.android/google-play-key.json")
    defaultToAppBundles = true
    track = 'internal'
}

6.4 Version management and changelogs

6.4.1 Automatic version number management

In build.gradle:

def getVersionCode = { ->
    def code = System.getenv("VERSION_CODE") ?: "1"
    return code.toInteger()
}

def getVersionName = { ->
    def name = System.getenv("VERSION_NAME") ?: "1.0.0"
    return name
}

android {
    defaultConfig {
        versionCode getVersionCode()
        versionName getVersionName()
    }
}

6.4.2 Automatically generate changelogs

Use git-chglog to generate changelogs:

generate_changelog:
  stage: deploy
  script:
    - curl -sSL https://github.com/git-chglog/git-chglog/releases/download/v0.15.0/git-chglog_linux_amd64 -o git-chglog
    - chmod +x git-chglog
    - ./git-chglog -o CHANGELOG.md ${CI_COMMIT_TAG}
  artifacts:
    paths:
      - CHANGELOG.md
  only:
    - tags

Chapter 7: Advanced topics and best practices

7.1 Build performance optimization

7.1.1 Build cache strategy

Gradle build cache:

In gradle.properties:

org.gradle.caching=true

Configure remote cache in CI:

buildCache {

    remote(HttpBuildCache) {

        url = 'https://your-cache-server/cache/'

        credentials {

            username = System.getenv('CACHE_USERNAME')

            password = System.getenv('CACHE_PASSWORD')

        }

    }

}

CI system cache:

  • Jenkins: use workspace/@libs shared libraries.
  • GitLab CI: optimize cache configuration.

7.1.2 Parallel builds

# gradle.properties
org.gradle.parallel=true
org.gradle.workers.max=4

Adjust the --max-workers parameter in CI according to the machine configuration.

7.1.3 Incremental builds

Make sure tasks configure inputs and outputs correctly so incremental builds can work:

task processTemplates(type: Copy) {
    inputs.property("version", project.version)
    from 'src/templates'
    into 'build/processed'
    expand(version: project.version)
}

7.2 Security best practices

Credential management:

  • Never commit sensitive information to the code repository.
  • Use the CI system’s secret-management features.
  • Restrict access to secrets.

Dependency verification:

dependencyVerification {

    verify = [

        'androidx.appcompat:appcompat:1.3.0': 'sha256:abcdef...',

        // Checksums for other dependencies

    ]

}

Principle of least privilege:

  • Run CI Runner/Agent under a dedicated user.
  • Restrict network access.
  • Rotate credentials regularly.

7.3 Monitoring and alerts

7.3.1 Build monitoring

Jenkins:

  • Install the Prometheus plugin.
  • Configure build health metrics.

GitLab CI:

  • Use built-in CI/CD analytics.
  • Integrate Prometheus monitoring.

7.3.2 Alert configuration

Build failure alerts:

  • Jenkins: install Email Extension Plugin and configure email notifications.
  • GitLab CI: configure webhooks or integrate Slack/Microsoft Teams.

Performance degradation alerts:

  • Monitor build duration.
  • Set thresholds to trigger alerts.

7.4 Disaster recovery

Backup strategy:

  • Back up Jenkins/GitLab configuration regularly.
  • Back up critical build artifacts.

Recovery process:

  • Document recovery steps.
  • Test the recovery process regularly.

High availability:

  • Consider a Jenkins controller-agent architecture.
  • Use GitLab Runner autoscaling.

Chapter 8: Case studies and practical examples

8.1 CI/CD configuration for small and medium teams

8.1.1 Jenkins Pipeline example

pipeline {
    agent any
    
    environment {
        ANDROID_HOME = '/opt/android-sdk'
        PATH = "${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}"
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        
        stage('Build') {
            steps {
                sh './gradlew assembleDebug'
            }
        }
        
        stage('Unit Test') {
            steps {
                sh './gradlew testDebugUnitTest jacocoTestReport'
            }
            post {
                always {
                    junit 'app/build/test-results/testDebugUnitTest/**/*.xml'
                    jacoco execPattern: 'app/build/jacoco/testDebugUnitTest.exec'
                }
            }
        }
        
        stage('Lint') {
            steps {
                sh './gradlew lintDebug'
            }
            post {
                always {
                    archiveArtifacts artifacts: 'app/build/reports/lint/lint-report.html', allowEmptyArchive: true
                }
            }
        }
        
        stage('Deploy to Internal') {
            when {
                branch 'develop'
            }
            steps {
                sshagent(['web-server-credentials']) {
                    sh """
                    scp app/build/outputs/apk/debug/app-debug.apk \
                        user@webserver:/var/www/downloads/app-${BUILD_NUMBER}.apk
                    """
                }
            }
        }
    }
    
    post {
        always {
            archiveArtifacts 'app/build/outputs/apk/debug/*.apk'
            cleanWs()
        }
        failure {
            emailext body: 'Build failed: ${BUILD_URL}', subject: 'Build failed: ${JOB_NAME}', to: 'team@example.com'
        }
    }
}

8.1.2 GitLab CI configuration example

image: android-ci-image:latest

variables:
  ANDROID_COMPILE_SDK: "30"
  ANDROID_BUILD_TOOLS: "30.0.3"
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"

stages:
  - build
  - test
  - deploy

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .gradle/
    - app/build/

build:
  stage: build
  script:
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/*.apk
    expire_in: 1 week

unit_test:
  stage: test
  script:
    - ./gradlew testDebugUnitTest jacocoTestReport
  artifacts:
    paths:
      - app/build/reports/tests/
      - app/build/reports/jacoco/
    reports:
      junit: app/build/test-results/testDebugUnitTest/**/*.xml
    expire_in: 1 week

lint:
  stage: test
  script:
    - ./gradlew lintDebug
  artifacts:
    paths:
      - app/build/reports/lint/
    expire_in: 1 week
  allow_failure: true

deploy_internal:
  stage: deploy
  script:
    - apt-get update && apt-get install -y openssh-client
    - mkdir -p ~/.ssh
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
    - chmod 600 ~/.ssh/id_rsa
    - scp app/build/outputs/apk/debug/app-debug.apk user@webserver:/var/www/downloads/app-${CI_COMMIT_SHORT_SHA}.apk
  only:
    - develop

8.2 Large enterprise configuration

8.2.1 Jenkins multibranch Pipeline

def androidBuildTools = '30.0.3'
def androidCompileSdk = '30'

pipeline {
    agent {
        label 'android-agent'
    }
    
    options {
        timeout(time: 30, unit: 'MINUTES')
        buildDiscarder(logRotator(numToKeepStr: '10'))
        disableConcurrentBuilds()
    }
    
    environment {
        ANDROID_HOME = '/opt/android-sdk'
        PATH = "${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}"
    }
    
    stages {
        stage('Checkout & Setup') {
            steps {
                checkout scm
                sh 'git submodule update --init --recursive'
            }
        }
        
        stage('Build') {
            parallel {
                stage('Debug Build') {
                    steps {
                        sh "./gradlew assembleDebug"
                    }
                }
                stage('Release Build') {
                    when {
                        anyOf {
                            branch 'main'
                            branch 'release/*'
                        }
                    }
                    steps {
                        withCredentials([...]) {
                            sh "./gradlew assembleRelease"
                        }
                    }
                }
            }
        }
        
        stage('Static Analysis') {
            parallel {
                stage('Lint') {
                    steps {
                        sh "./gradlew lintDebug"
                    }
                    post {
                        always {
                            archiveArtifacts artifacts: 'app/build/reports/lint/lint-report.html', allowEmptyArchive: true
                        }
                    }
                }
                stage('Checkstyle') {
                    steps {
                        sh "./gradlew checkstyle"
                    }
                    post {
                        always {
                            archiveArtifacts artifacts: 'app/build/reports/checkstyle/checkstyle.html', allowEmptyArchive: true
                        }
                    }
                }
                stage('SonarQube') {
                    steps {
                        withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_TOKEN')]) {
                            sh "./gradlew sonarqube -Dsonar.login=${SONAR_TOKEN}"
                        }
                    }
                }
            }
        }
        
        stage('Test') {
            parallel {
                stage('Unit Test') {
                    steps {
                        sh "./gradlew testDebugUnitTest jacocoTestReport"
                    }
                    post {
                        always {
                            junit 'app/build/test-results/testDebugUnitTest/**/*.xml'
                            jacoco execPattern: 'app/build/jacoco/testDebugUnitTest.exec'
                        }
                    }
                }
                stage('Instrumented Test') {
                    steps {
                        androidEmulator(
                            androidHome: env.ANDROID_HOME,
                            avdName: 'ci-emulator',
                            osVersion: '30',
                            arch: 'x86_64',
                            forceAvdCreation: false,
                            wipeData: false,
                            snapshot: false,
                            deleteAfterBuild: false
                        ) {
                            sh "./gradlew connectedDebugAndroidTest"
                        }
                    }
                    post {
                        always {
                            junit 'app/build/outputs/androidTest-results/connected/**/*.xml'
                        }
                    }
                }
            }
        }
        
        stage('Deploy') {
            when {
                anyOf {
                    branch 'main'
                    branch 'release/*'
                    tag '*'
                }
            }
            steps {
                script {
                    if (env.BRANCH_NAME == 'main' || env.BRANCH_NAME.startsWith('release/')) {
                        // Deploy to the test environment
                        firebaseAppDistribution(
                            appId: '1:1234567890:android:abcdef1234567890',
                            serviceCredentialsFile: 'firebase-key.json',
                            artifactPath: 'app/build/outputs/apk/release/app-release.apk',
                            groups: 'qa-team,dev-team'
                        )
                    }
                    
                    if (env.TAG_NAME != null) {
                        // Deploy to Google Play
                        googlePlayUploader(
                            applicationId: 'com.your.package',
                            credentialsId: 'google-play-credentials',
                            apkFiles: 'app/build/outputs/apk/release/app-release.apk',
                            trackName: 'production',
                            rolloutPercentage: '10'
                        )
                    }
                }
            }
        }
    }
    
    post {
        always {
            archiveArtifacts artifacts: 'app/build/outputs/**/*.apk', allowEmptyArchive: true
            cleanWs()
        }
        failure {
            slackSend color: 'danger', message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
        }
        success {
            slackSend color: 'good', message: "Build succeeded: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
        }
    }
}

8.2.2 Enterprise GitLab CI configuration

include:
  - template: 'Workflows/MergeRequest-Pipelines.gitlab-ci.yml'

variables:
  ANDROID_COMPILE_SDK: "30"
  ANDROID_BUILD_TOOLS: "30.0.3"
  GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=4 -Dorg.gradle.caching=true"
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""

stages:
  - build
  - test
  - security
  - deploy

.default_android:
  image: $CI_REGISTRY/android-ci-image:latest
  tags:
    - android
    - docker
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .gradle/
      - app/build/
    policy: pull-push
  before_script:
    - export GRADLE_USER_HOME=$(pwd)/.gradle
    - chmod +x gradlew

build:debug:
  extends: .default_android
  stage: build
  script:
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/*.apk
    expire_in: 1 week

build:release:
  extends: .default_android
  stage: build
  script:
    - ./gradlew assembleRelease
  artifacts:
    paths:
      - app/build/outputs/apk/release/*.apk
    expire_in: 1 week
  only:
    - main
    - release/*
    - tags

unit_test:
  extends: .default_android
  stage: test
  script:
    - ./gradlew testDebugUnitTest jacocoTestReport
  artifacts:
    paths:
      - app/build/reports/tests/
      - app/build/reports/jacoco/
    reports:
      junit: app/build/test-results/testDebugUnitTest/**/*.xml
    expire_in: 1 week

instrumented_test:
  extends: .default_android
  stage: test
  services:
    - docker:dind
  script:
    - docker run --detach --privileged --name emulator --publish 5554:5554 --publish 5555:5555
      -e ADBKEY="$(cat ~/.android/adbkey)" android-emulator:30
    - adb wait-for-device
    - ./gradlew connectedDebugAndroidTest
  artifacts:
    paths:
      - app/build/reports/androidTests/connected/
    reports:
      junit: app/build/outputs/androidTest-results/connected/**/*.xml
    expire_in: 1 week

lint:
  extends: .default_android
  stage: test
  script:
    - ./gradlew lintDebug
  artifacts:
    paths:
      - app/build/reports/lint/
    expire_in: 1 week
  allow_failure: true

sonarqube:
  extends: .default_android
  stage: security
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
  script:
    - ./gradlew sonarqube -Dsonar.login=$SONAR_TOKEN
  only:
    - main
    - merge_requests

dependency_scan:
  stage: security
  image: owasp/dependency-check:latest
  script:
    - dependency-check.sh --scan "$CI_PROJECT_DIR" --project "$CI_PROJECT_NAME"
      --out "$CI_PROJECT_DIR" --format ALL --disableAssembly
  artifacts:
    paths:
      - dependency-check-report.*
    expire_in: 1 week
  allow_failure: true

deploy:firebase:
  extends: .default_android
  stage: deploy
  script:
    - curl -sSL https://firebase.tools | bash
    - echo "$FIREBASE_KEY" > /tmp/firebase-key.json
    - firebase appdistribution:distribute app/build/outputs/apk/release/app-release.apk
        --app 1:1234567890:android:abcdef1234567890
        --groups "qa-team"
        --token $(cat /tmp/firebase-key.json | jq -r '.client_email')
  only:
    - main
    - release/*

deploy:play_store:
  extends: .default_android
  stage: deploy
  script:
    - mkdir -p ~/.android
    - echo "$GOOGLE_PLAY_KEY" > ~/.android/google-play-key.json
    - ./gradlew publishReleaseBundle
  only:
    - tags

Chapter 9: Common problems and solutions

9.1 Common causes of build failure

Dependency download failures:

Solution: configure mirror repositories or use an offline repository.

In build.gradle:

repositories {

    maven { url 'https://maven.aliyun.com/repository/public' }

    google()

    jcenter()

}

Insufficient memory:

Solution: increase Gradle memory.

In gradle.properties:

org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m

Incorrect signing configuration:

Solution: verify signing configuration and credentials.

  • Make sure the key file path is correct.
  • Verify passwords and aliases.

Emulator startup failure:

Solution: make sure KVM is enabled.

Example check command:

grep -c vmx /proc/cpuinfo

Flaky tests:

Solution: add a retry mechanism.

In build.gradle:

android {

    testOptions {

        execution 'ANDROIDX_TEST_ORCHESTRATOR'

        animationsDisabled = true

        unitTests {

            all {

                testLogging {

                    events "failed"

                    exceptionFormat "full"

                }

                maxParallelForks = Runtime.runtime.availableProcessors() / 2

                forkEvery = 100

                retry {

                    maxRetries = 3

                    maxFailures = 20

                }

            }

        }

    }

}

9.3 Performance optimization problems

Slow builds:

Solution:

  • Enable build cache.
  • Configure suitable parallelism.
  • Use incremental builds.

Ineffective cache:

Solution: verify the cache strategy.

  • Make sure the cache key includes all inputs that affect the build.

Sensitive information leakage:

Solution:

  • Use the CI system’s secret management.
  • Avoid printing sensitive information in logs.
  • Rotate credentials regularly.

Dependency security vulnerabilities:

Solution:

  • Use OWASP Dependency-Check.
  • Update dependencies regularly.

Faster build technologies:

  • Better incremental builds.
  • Distributed build cache.
  • Cloud-native build systems.

Smarter testing:

  • Change-based test selection.
  • Machine-learning-optimized test suites.

Tighter DevOps integration:

  • Infrastructure as code.
  • Automated canary releases.
  • Feature flag management.

Shift-left security:

  • Earlier security scanning.
  • Automated compliance checks.

10.2 Tool evolution

Jenkins:

  • Jenkins X focuses on cloud-native CI/CD.
  • Configuration as code continues to expand.
  • Better Kubernetes integration.

GitLab CI:

  • More powerful Auto DevOps capabilities.
  • More granular permission control.
  • Improved test report visualization.

10.3 Summary and recommendations

Building an efficient Android CI/CD workflow requires balancing team size, project complexity, and tool preference. Key recommendations include:

  • Start small and expand gradually: begin with basic build and test workflows, then add more complex processes.
  • Monitor and optimize: continuously monitor build performance and identify bottlenecks.
  • Document the process: make sure team members understand the CI/CD workflow.
  • Put security first: consider security from the beginning and avoid later rework.
  • Stay current: regularly evaluate and adopt new tools and practices.

Whether you choose Jenkins or GitLab CI, the key is to establish a reliable, repeatable automated workflow so the team can focus on building high-quality apps instead of spending time on manual build and deployment work.

Further reading