Dec 2, 2013

Open source releases: a piece of Gradle

Yennick Trevels wrote a great blog on how to set up a Gradle build to deploy to the Sonatype OSS Repository (and from there to maven central).
Here I will show how the Gradle release plugin can be configured to
  • streamline the release process
  • automatically provide download links to you latest release on Github

The standard release process

The principles of the Gradle release plugin originate from the Maven release plugin. The release steps are:
  • check release preconditions (no uncommitted files and no snapshot dependencies)
  • get the release version number
  • build (and test) the release
  • tag the release
  • update the version number and commit it

Configuration

Add the build dependency and apply the plugin:
buildscript {
    dependencies {
        classpath 'com.github.townsfolk:gradle-release:1.2'
  ...
    }
 ...
}

apply plugin: 'release'
The release plugin is not fully documented, but fortunately it is small and the code speaks for itself (its Gradle :)
In ReleasePluginConvention.groovy you find all the properties that can be specified. I changed the default versionPropertyFile (gradle.properties) to versions.txt, to make it obvious to find the project version. You can also see how to use complex version schemes.
release {
    versionPropertyFile = 'version.txt'

    //a map of [regular expression : increment closure],
    //if the version matches one of the regexps, the corresponding
    // closure is used to auto-increment the version
    versionPatterns = [
            // 1.2.3-groovy-1.8.4 -> next version: 1.2.4-groovy-1.8.4
            /(\d+)-groovy-(.+$)/: {matcher, project ->
                matcher.replaceAll("${(matcher[0][1] as int) + 1}-less-${matcher[0][2]}")
            }
    ]
}
The version needs to be read from the file:
//the release plugin expects the content to be 'version=xyz',
//without whitespace around the =
version = file('version.txt').text.split('=')[1].trim()

Build hooks

One of the most powerful features of Gradle is the ability to hook custom tasks about anywhere in the build cycle.
gradle tasks --all shows all the steps in the release task that we can hook into:

checkCommitNeeded - Checks to see if there are any added, modified, removed, or un-versioned files.
checkSnapshotDependencies - Checks to see if your project has any SNAPSHOT dependencies.
checkUpdateNeeded - Checks to see if there are any incoming or outgoing changes that haven't been applied locally.
commitNewVersion - Commits the version update to your SCM
confirmReleaseVersion - Prompts user for this release version. Allows for alpha or pre releases.
createReleaseTag - Creates a tag in SCM for the current (un-snapshotted) version. [uploadArchives]
initScmPlugin - Initializes the SCM plugin (based on hidden directories in your project's directory)
preTagCommit - Commits any changes made by the Release plugin - eg. If the unSnapshotVersion tas was executed
release - Verify project, release, and update version to next. [clean]
unSnapshotVersion - Removes "-SNAPSHOT" from your project's current version.
updateVersion - Prompts user for the next version. Does it's best to supply a smart default.

Hooking in is straightforward:
//always clean before building a release
release.dependsOn clean
//upload to sonatype before tagging the VCS
createReleaseTag.dependsOn uploadArchives
We should only upload release builds to the sonatype staging repository, so we tweak the 'uploadArchives' a little bit:
task uploadSnapshot(dependsOn: uploadArchives)

// the release task spawns a new GradleBuild that doesn't contain
// release itself, but it contains createReleaseTag
def sonatypeRelease = gradle.startParameter.taskNames.contains('createReleaseTag')
def sonatypeSnapshot = gradle.startParameter.taskNames.contains('uploadSnapshot')
if (sonatypeRelease) {
    //only sign releases
    signing {
        sign configurations.archives
    }
}
def sonatypeRepositoryUrl = sonatypeRelease ?
    'https://oss.sonatype.org/service/local/staging/deploy/maven2/' :
    'https://oss.sonatype.org/content/repositories/snapshots/'

uploadArchives {
    repositories {
        if (!(sonatypeRelease || sonatypeSnapshot)) {
            mavenLocal()
        } else {
            mavenDeployer {
                if (sonatypeRelease) {
                    beforeDeployment { deployment -> signing.signPom(deployment) }
                }

                repository(url: sonatypeRepositoryUrl) {
                    authentication(userName: sonatypeUsername,
                    password: sonatypePassword)
                }

                pom.project {
...

Provide download links

In the README.md we can include links to our artifacts on maven central:
You can download coolapp from the [maven central repository]
 (http://central.maven.org/maven2/org/awesome/coolapp/1.2.3-groovy-1.8.4/coolapp-1.2.3-groovy-1.8.4.zip)
With a few extra lines of Groovy, we can make the links point to the new release. The modified README can be committed to Github together with the new snapshot version:
task updateReadme << {
    File readme = file('README.md')
    //the release version is the version before the release minus 'SNAPSHOT'
    def releaseVersion = "${project['release.oldVersion']}".replaceAll('-SNAPSHOT', '')
    //replace all occurrences of x.y.z-groovy-a.b.c with the new release version
    readme.text = (readme.text =~ /\d\.\d(\.\d)?-groovy-\d\.\d(\.\d)?(-SNAPSHOT)?/)
                                                     .replaceAll("${releaseVersion}")
}

commitNewVersion.dependsOn updateReadme
And that's all there is to it. A working example can be found in the lesscss project.