Skip to content

GitLab CI template for Gradle

This project implements a GitLab CI/CD template to build, test and analyse your Gradle-based projects.

Usage

This template can be used both as a CI/CD component or using the legacy include:project syntax.

Use as a CI/CD component

Add the following to your .gitlab-ci.yml:

include:
  # 1: include the component
  - component: $CI_SERVER_FQDN/to-be-continuous/gradle/gitlab-ci-gradle@2.8.0
    # 2: set/override component inputs
    inputs:
      image: "registry.hub.docker.com/library/gradle:jdk11" # ⚠ this is only an example

Use as a CI/CD template (legacy)

Add the following to your .gitlab-ci.yml:

include:
  # 1: include the template
  - project: 'to-be-continuous/gradle'
    ref: '2.8.0'
    file: '/templates/gitlab-ci-gradle.yml'

variables:
  # 2: set/override template variables
  GRADLE_IMAGE: "registry.hub.docker.com/library/gradle:jdk11" # ⚠ this is only an example

Global configuration

The Gradle template uses some global configuration used throughout all jobs.

Input / Variable Description Default value
image / GRADLE_IMAGE The Docker image used to run Gradle
⚠ set the version required by your project
registry.hub.docker.com/library/gradle:latest
cli-opts / GRADLE_CLI_OPTS Additional Gradle options used on the command line None
cli-bin / GRADLE_CLI_BIN The location of the gradle binary. If you prefer using a gradle wrapper you should override this (for e.g. gradlew) gradle
user-home / GRADLE_USER_HOME The gradle user home $CI_PROJECT_DIR/.gradle
daemon / GRADLE_DAEMON Whether to use or not gradle daemon false
project-dir / GRADLE_PROJECT_DIR Gradle project root directory .

As you can see the GRADLE_USER_HOME is set to a directory inside $CI_PROJECT_DIR. This will allow you to do some gradle caching (declared in all gradle jobs) but also to provide a custom gradle.properties file in your directory.

⚠ You don't need to provide proxy and daemon properties in your gradle.properties as they will be set by each gradle job (they won't be overwritten if you do so).

Template supports http_proxy, https_proxy and no_proxy that will be put in gradle.properties.

For example, if you provide:

variables:
    http_proxy: "http://the-proxy:8080"
    https_proxy: "http://the-proxy:8080"
    no_proxy: "localhost,my.neighbour.service"

and if you kept default GRADLE_DAEMON variable and did not specify proxy and daemon properties, the properties added to $GRADLE_USER_HOME/gradle.properties will be:

org.gradle.daemon=false
systemProp.http.proxyHost=the-proxy
systemProp.http.proxyPort=8080
systemProp.http.nonProxyHosts=localhost|my.neighbour.service
systemProp.https.proxyHost=the-proxy
systemProp.https.proxyPort=8080
systemProp.https.nonProxyHosts=localhost|my.neighbour.service

Jobs

Build jobs

The Gradle template features a single job gradle-build that performs build (including tests) at once. This stage is performed in a single job for optimization purpose (it saves time) and also for test jobs dependency reasons.

It uses the following variable:

Input / Variable Description Default value
build-args / GRADLE_BUILD_ARGS Gradle arguments for the build & test job build

About Code Coverage

You can in your build.gradle file apply some code coverage such as jacoco.

For example, by doing this in your build file (more information about the jacoco plugin ):

plugins {
    //...
    id 'jacoco'
    //...
 }
//...   
jacoco {
    toolVersion = "0.8.3"
}

jacocoTestReport {
    reports {
        // mandatory as it will be read by the pipeline
        csv.enabled true
        // optional for gitlab-ci pipeline yet required if you need sonar analysis
        xml.enabled true
    }
}

By adding the plugin, the template will automatically add jacocoTestReport and jacocoTestCoverageVerification tasks to the build phase.

The jacoco coverage display in gitlab uses the following variable

Input / Variable Description Default value
jacoco-csv-report / JACOCO_CSV_REPORT Name of report jacocoTestReport.csv

gradle-sonar job — SonarQube analysis

This job is disabled by default and performs a SonarQube analysis of your code.

The job is bound to the test stage and uses the following variables:

Input / Variable Description Default value
sonar-host-url / SONAR_HOST_URL SonarQube server url none (disabled)
🔒 SONAR_TOKEN SonarQube authentication token none
sonar-base-args / SONAR_BASE_ARGS SonarQube analysis arguments sonar -Dsonar.links.homepage=${CI_PROJECT_URL} -Dsonar.links.ci=${CI_PROJECT_URL}/-/pipelines -Dsonar.links.issue=${CI_PROJECT_URL}/-/issues
sonar-quality-gate-enabled / SONAR_QUALITY_GATE_ENABLED Set to true to enable SonarQube Quality Gate verification.
Uses sonar.qualitygate.wait parameter (see doc).
none (disabled)

Dependency-check

The Gradle template features a job gradle-dependency-check that performs a manual Dependency-Check analysis.

It is bound to the test stage and use the following variable :

Input / Variable Description Default value
GRADLE_DEPENDENCY_CHECK_DISABLED Set to true to disable this job none (enabled)
dependency-check-task / GRADLE_DEPENDENCY_CHECK_TASK Name of the gradle task launching the analysis dependencyCheckAnalyze

A Dependency-Check is a quite long operation and therefore the job is configured to be ran manually by default (overridable).

You need to configure your build.gradle to include the Dependency-Check module, the exemples below can help you kickstart the use of the plugin

Here is a generic configuration :

buildscript {
  repositories {
    mavenCentral()
    // ...
  }
  dependencies {
    // ...
    classpath 'org.owasp.dependency-check-gradle:5.3.2.1'
  }
}
apply plugin: 'org.owasp.dependencycheck'
dependencyCheck {
  formats=['HTML', 'JSON']
  analyzers {
    assemblyEnabled=false
    retirejs {
      enabled=false
    }
  }
}

Managing NVD API requests limit

Dependency-Check fetches its vulnerabilities database from the National vulnerability Database (NVD) API, that has requests limit. If nothing is done, fetching the database will be very slow.

There are two options to solve this:

  1. Use an NVD API key (this will increase the rate limit and therefore reduce the execution time of Dependency-Check)
    • Request an NVD API key,
    • Set the 🔒 NVD_API_KEY variable as a (masked) GitLab variable,
    • Configure the API key in your Gradle file:
      // ...
      dependencyCheck {
        // ...
        nvd {
          // NVD_API_KEY got from environment (set it in your GitLab CI/CD variables)
          apiKey = System.getenv("NVD_API_KEY")
        }
        // ...
      }
      
  2. Setup a local mirror of NVD Data files (this solution might be required if your GitLab runners cannot reach the NVD API).
    • You can use the vulnz tool to automate the sync of Data files from the NVD API. It fetches the vulnerabilities and store them as data feeds (the data feeds will be stored as JSON files with the NVD Vulnerability Data API version 2.0 schema).
      ℹ You should store those files in a place reachable by your GitLab runners.
    • Then simply configure this URL as a datafeedUrl in your Gradle file:
      // ...
      dependencyCheck {
        // ...
        nvd {
          datafeedUrl="https://URL-OF-LOCAL-CACHE-NVD-DATA-FEED"
        }
        // ...
      }
      

If you're using an Artifactory as a repository/mirror, you might need to add a repositories block and add an artifactory block in the job definition.

Here is an example (change the urls accordingly):

buildscript {
  // ...
  repositories {
    maven {
      url "https://artifactory.acme.host/artifactory/maven-virtual-repo"
    }
  }
}
// ...
dependencyCheck {
  // ...
  analyzers {
    artifactory {
      enabled=true
      url="https://artifactory.acme.host/artifactory"
    }
    // ...
  }
}

⚠ The injection of systemProp.http(s).nonProxyHosts (through either custom gradle.properties file or no_proxy environment variable) into gradle.properties crashes for version 5.3.x below 5.3.2.1 (bug) and is ignored for 5.3.2.1 version of plugin (bug) so if you need to specify exceptions for your proxy for this task, you should either :

  • use the 5.2.4 version or below
  • or removing injection for the task and add a proxy block like this :
dependencyCheck {
  // ...
  proxy {
    server=my.proxy.org
    port=8080
    nonProxyHosts=["127.0.0.1", "mycorp.com"]
  }
}

More info on how you can configure the gradle Dependency-Check plugin can be found in the official documentation

Software Bill Of Materials

This job generates a SBOM file listing all dependencies using cyclonedx-gradle-plugin.

It is bound to the test stage, and uses the following variables:

Input / Variable Description Default value
sbom-disabled / GRADLE_SBOM_DISABLED Set to true to disable this job none
sbom-version / GRADLE_SBOM_VERSION Version of the cyclonedx-gradle-plugin used for SBOM analysis
When unset, the latest version will be used
none
maven-plugin-url / GRADLE_MAVEN_PLUGIN_URL Maven Repository that is used to download the cyclonedx-gradle-plugin. No trailing slash. https://plugins.gradle.org/m2

This job injects cyclonedx plugin in your project. This can be disabled by defining the plugin in your build.gradle and setting $GRADLE_SBOM_VERSION variable to disabled.

plugins {
    id 'org.cyclonedx.bom' version '1.7.2'
}

Publish jobs

Currently the pipeline exposes two manual jobs of publication:

  • gradle-snapshot: launched on branches
  • gradle-release: launched on tags

Both jobs use the following variables

Input / Variable Description Default value
publish-version / GRADLE_PUBLISH_VERSION The value is propagated as gradle properties named version. It should be used in your publish task ${CI_COMMIT_REF_SLUG}-SNAPSHOT
publish-args / GRADLE_PUBLISH_ARGS The publish task that is invoked publish
no-publish / GRADLE_NO_PUBLISH Set this variable if you wish to disable publish phase None

If you keep default value for GRADLE_PUBLISH_VERSION, it will have the following values

  • for a snapshot: <branch name>-SNAPSHOT
  • for a release: <tag name>

Here is an example of a build.gradle publishing on Artifactory:

plugins {
    // ...
    id 'maven-publish'
}

def snapshotsRepoUrl = "https://artifactory.acme.host/artifactory/maven-snapshot-repo/"
def releasesRepoUrl = "https://artifactory.acme.host/artifactory/maven-release-repo/"

task sourceJar(type: Jar) {
    classifier "sources"
}

task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
    outputFormat = 'javadoc'
    outputDirectory = javadoc.destinationDir
    inputs.dir 'src/main/kotlin'
}

task dokkaJavadocJar(type: Jar, dependsOn: dokkaJavadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}


publishing {
    repositories {
        maven {
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
            authentication {
                basic(BasicAuthentication)
            }
            credentials(PasswordCredentials) {
                // Artifactory credentials got from environment (set it in your GitLab CI/CD variables)
                username System.getenv("ARTIFACTORY_USERNAME")
                password System.getenv("ARTIFACTORY_PASSWORD")
            }
        }
    }
    publications {
        maven(MavenPublication) {
            from components.kotlin
            artifact(sourceJar)
            artifact(dokkaJavadocJar)
        }
    }
}