Continuous Delivery-ready Gradle project with Artifactory and Sonarqube integration

When playing around with the concepts of Continuous Integration/Delivery/Deployment I struggled over Gradle builds and how to integrate them in these concepts. Gradle is getting more and more important in build tool landscape and so I created a template or archetype about how to use it.
I choose a scenario that I saw already implemented with Maven and other tools:

  1. pushing a VCS change to central VCS
  2. tagging that change to a specific id and pushing it back to central VCS
  3. executing the build with unit tests
  4. executing the sonar analysis
  5. executing the artifactory upload

This chain gives you the ability of continuously having every change explicitely tagged, unit-tested, analyzed and uploaded to your artifact repository, ready for deployment. In short, this is (from my point of view) the spirit of Continuous Delivery.

Environment:

  • Gradle 3.5 (via project’s gradle-wrapper)
  • Artifactory: 5.2.1-OSS, running at http://localhost:8081/artifactory
  • Sonarqube: 6.3.1, running at http://localhost:9000

Note: In this scenario Sonarqube and Artifactory will be used by admin user. In a real production environment this has to be changed to a more secure way 🙂


First of all I added one local (m2-compatible) and two remote repositories in Artifactory:

  • „libs-release-local“ is meant to hold my own artifacts (that’s why it’s called „local“)
  • „plugin-gradle-org“ pointing to remote repo „https://plugins.gradle.org/m2/“ (this is required for gradle plugins)
  • „repo1-maven-org“ pointing to remote repo „http://repo1.maven.org/maven2/“ (this is required for all other dependencies of the project)

Additionally I created a virtual repo „libs-release“ containing all 3 mentionend repos. That makes it easier to obtain all released libs from one location.

The project itself doesn’t really matter for this post. I assume it’s a standard java project, that follows the conventions of Maven/Gradle. The more interesting parts are the Gradle files:

  • gradle/wrapper/gradle-wrapper.properties
  • build.gradle
  • credentials.properties
  • gradle.properties
  • settings.gradle

Most of the properties of a Gradle build are obtained automatically from gradle.properties file, so that’s the correct place for configuration:

projectGroupId=eu.christophburmeister.playground
projectArtifactId=example-java-gradle 
projectVersion=2.0.0 
sonarqubeBaseUrl=http\://localhost\:9000 artifactoryBaseUrl=http\://localhost\:8081/artifactory 
artifactoryUploadRepo=libs-release-local 

The settings.gradle is for global settings. Normally the name of the artifact is retrieved from the root directory name, but I find it more safe to explicitely hold the name in that file:

rootProject.name = projectArtifactId

Having these properties configured, we can use following buildscript:

apply plugin: 'java'
apply plugin: 'maven'
apply plugin: "org.sonarqube"

buildscript {
    repositories {
        maven {
            url "${artifactoryBaseUrl}/libs-release"
        }
    }
    dependencies {
        classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.3"
    }
}

group = projectGroupId // obtained from gradle.properties, can be referenced by project.group
// artifact id obtained from settings.gradle, can be referenced by project.name
version = projectVersion // obtained from gradle.properties, can be referenced by project.version

// get the credentials for uploadArtifact tasks etc. (should be only available on build server)
def credentialsFile = new File('./credentials.properties')
def credentials = new Properties()
if (credentialsFile.exists()) {
    credentialsFile.withInputStream { credentials.load(it) }
} else {
    println 'credentials.properties not found. this could affect several tasks.'
}

// defines the remote repo where to pull dependencies from
repositories {
    maven {
        url "${artifactoryBaseUrl}/libs-release"
    }
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '3.8.1'
}

// run with ./gradlew build sonarqube
sonarqube {
    properties {
        property 'sonar.host.url', project.sonarqubeBaseUrl
        property 'sonar.login', credentials.sonarqubeUsername
        property 'sonar.password', credentials.sonarqubePassword
        property 'sonar.projectKey', "${project.group}:${project.name}"
        property 'sonar.projectName', project.name
        property 'sonar.projectVersion', project.version
    }
}

// run with ./gradlew build uploadArchives
uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: "${artifactoryBaseUrl}/${artifactoryUploadRepo}") {
                authentication(
                        userName: credentials.artifactoryUsername,
                        password: credentials.artifactoryPassword
                )
            }
            pom.version = project.version
            pom.artifactId = project.name
            pom.groupId = project.group
        }
    }
}

task incrementVersion {
    description = 'increments the property "projectVersion" in gradle.properties file by passing "-Pincrement=<major|minor|patch>" where "minor" is default'
    group = 'util'

    doLast {
        def incrementPart = 'minor' // default
        if (project.hasProperty('increment')) {
            if (project.increment == 'major') {
                incrementPart = 'major'
            }
            if (project.increment == 'patch') {
                incrementPart = 'patch'
            }
        }

        println "incrementing '${incrementPart}' of project version"
        def projectProps = new Properties()
        def projectPropsFile = new File('gradle.properties')
        projectPropsFile.withInputStream { projectProps.load(it) }

        def currentProjectVersion = projectProps.get('projectVersion')
        println "current project version: ${currentProjectVersion}"

        def currentProjectVersionParts = currentProjectVersion.tokenize('.')
        def currentMajorVersion = currentProjectVersionParts[0] as int
        def currentMinorVersion = currentProjectVersionParts[1] as int
        def currentPatchVersion = currentProjectVersionParts[2] as int

        def nextProjectVersion, nextMajorVersion, nextMinorVersion, nextPatchVersion
        if (incrementPart == 'major') {
            nextMajorVersion = currentMajorVersion + 1
            nextMinorVersion = 0
            nextPatchVersion = 0
        } else if (incrementPart == 'minor') {
            nextMajorVersion = currentMajorVersion
            nextMinorVersion = currentMinorVersion + 1
            nextPatchVersion = 0
        } else {
            nextMajorVersion = currentMajorVersion
            nextMinorVersion = currentMinorVersion
            nextPatchVersion = currentPatchVersion + 1
        }

        nextProjectVersion = "${nextMajorVersion}.${nextMinorVersion}.${nextPatchVersion}"
        println "nextProjectVersion: ${nextProjectVersion}"

        projectProps.setProperty('projectVersion', nextProjectVersion)
        projectProps.store(projectPropsFile.newWriter(), 'properties written by incrementVersion task')
    }
}

There are several things to mention about this script:

  • First, it will look for a file called „credentials.properties“ in order to provide the credentials-properties object that is used in sonarqube and uploadArchive task:
    artifactoryUsername=admin
    artifactoryPassword=Test_123
    sonarqubeUsername=admin
    sonarqubePassword=Test_123
    

    As the default build task does not require credentials, this file shall be available only on build server.

  • The buildscript-part in build.gradle is only configured to enable download of sonarqube-gradle-plugin from Artifactory.
  • The „sonarqube“ task configures the communication with the Sonarqube instance where several sonar.XXX properties can be manipulated.
  • The „uploadArchive“ task configures the target artifact repository in Artifactory. Also here you can pass additional properties in order to create a maven conform pom.xml while upload.
  • The last task „incrementVersion“ is a preparation for continuous delivery. It simply updates the key projectVersion in gradle.properties according to given parameters.

With this solution you can have following scenario:
Your developers can implement their features and bugfixes without taking care of project version. As soon as a push goes to the source repository, a Jenkins (or Bamboo or whatever) job starts that simply executes

./gradlew incrementVersion -Pincrement=minor

and pushes and tags the result back to the source repository. Then the id of the tag is forwarded to another job that executes

./gradlew build sonarqube uploadArchive

Dividing the tag from the build action makes the queue for the tag job very small. That means, nearly every push will be tagged to a releasable version while the longer running second job can process one tag after another including the Sonarqube analysis and the upload to Artifactory (which transforms the „Continuous Integration“ into „Continuous Delivery“).
At this point there is also no need anymore for something like „SNAPSHOT“ artifacts that we used in Maven context.

And last but not least: The properties of the gradle wrapper. This file shall also be added to VCS as this is the recommended way for repeatable builds:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip

A good recommendation is also to put the gradle-3.5-all.zip to your own Artifactory in order to get it from cache and not from gradle.org.