Publishing Snapshots With Gradle's maven-publish Plugin

For a new project I decided to use Gradle. Here's how I set it up with the incubating maven-publish plugin to publish snapshots to Sonatype's repository.

I've recently started a new project with Gradle and decided to jump straight in - no Gradle experience, no clue about Groovy, no tutorials, just hammer on it until it works. That went surprisingly well until I decided to publish snapshots to Sonatype's Maven snapshot repository with the incubating maven-publish plugin - that took, ahh, a little convincing.

Caveat:

As I said, I'm a noob in both Groovy and Gradle, so don't believe anything I say. I write this down for myself as much as for you.

The final (but still partial) build.gradle file can be found here, the actual variant I used in my project here.

As a zeroth step make sure the project's group, id, and version are present. The first and last can usually be found in the build.gradle file, the project name doubles for its id and is defined in settings.gradle.

Activating maven-publish

Ok, lets go! First of all I activated the plugin:

apply plugin: 'maven-publish'

To start publishing things I need the following incantation:

publishing {
	publications {
		mavenJava(MavenPublication) {
			from components.java
			// more goes in here
		}
	}
	repositories {
		mavenLocal()
	}
}

As you see I begin by publishing to the local repo. And indeed, running gradle publish should now create a JAR and a rudimentary pom in some .m2 subfolder. From here on I can add more features step by step.

Filling the POM

What do I need to publish an artifact? A full Maven pom. Since I don't have a pom.xml, where do I get it? I create it with some Gradle XML API. Obviously. Why don't I use Maven to get the pom first hand? Damned if I know...

So inside the mavenJava thingy (what is it? a task, I guess?) I create the pom. It took me a moment of trying this and that before settling on the following syntax:

pom.withXml {
	asNode().with {
		appendNode('packaging', 'jar')
		appendNode('name', 'PROJECT_NAME')
		appendNode('description', 'PROJECT_DESCRIPTION')
		appendNode('url', 'PROJECT_URL')
		appendNode('scm').with {
			appendNode('url', 'SCM_URL_FOR_PEOPLE')
			appendNode('connection', 'SCM_URL_FOR_SCM')
		}
		appendNode('issueManagement').with {
			appendNode('url', 'ISSUE_TRACKER_URL')
			appendNode('system', 'ISSUE_TRACKER_NAME')
		}
		appendNode('licenses').with {
			appendNode('license').with {
				appendNode('name', 'LICENSE_NAME')
				appendNode('url', 'LICENSE_URL')
			}
		}
		appendNode('organization').with {
			appendNode('name', 'ORG_NAME')
			appendNode('url', 'ORG_URL')
		}
		appendNode('developers').with {
			appendNode('developer').with {
				appendNode('id', 'DEV_HANDLE')
				appendNode('name', 'DEV_NAME')
				appendNode('email', 'DEV_MAIL')
				appendNode('organization', 'ORG_NAME_AGAIN')
				appendNode('organizationUrl', 'ORG_URL_AGAIN')
				appendNode('timezone', 'UTC_OFFSET')
			}
		}
	}
}

Ok, there we go. So much better than that ugly XML, right? I read somewhere that there are more beautiful APIs I could use here but I didn't feel like going off on another tangent. Feel free to propose something.

You might've noticed that the project group, id, and version do not need to be repeated. Running gradle publish should now publish a JAR with a complete, albeit somewhat ugly pom.

License and More

I want to add the project's license to the JAR's META-INF folder, so inside mavenJava I tell Gradle to include the file in every JAR task (or at least that's how I read it):

tasks.withType(Jar) {
	from(project.projectDir) {
		include 'LICENSE.md'
		into 'META-INF'
	}
}

Looking good, gradle publish now creates a full pom and a JAR with the project's license.

Sources and Javadoc JARs

Most projects like to publish more than just the compiled .class files, though, namely sources and Javadoc. For this I add two tasks and reference them from mavenJava:

publishing {
	publications {
		mavenJava(MavenPublication) {
			// ...
			artifact sourceJar
			artifact javadocJar
		}
	}
	// ...
}

task sourceJar(type: Jar, dependsOn: classes) {
	classifier 'sources'
	from sourceSets.main.allSource
}

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

Nice, now I get a full pom, an artifact for the project's classes and license, and JARs for sources and Javadoc. Time to take the last step: publish to the snapshot repo!

Publish To Snapshot Repository

For that I'll replace mavenLocal() with the actual repository. Besides the URL I also need to specify my credentials:

repositories {
	maven {
		url 'https://oss.sonatype.org/content/repositories/snapshots/'
		credentials {
			username 'user'
			password '123456'
		}
	}
}

Of course I wasn't planning to commit my password to source control so I went looking for an alternative. I found one - not sure whether it's the best but, hey, it works.

You can define new project properties on the command line with the -P option. So given a command like this...

gradle publish -P snapshotRepoPass=123456

... I can then access project.snapshotRepoPass in the credentials:

credentials {
	username 'user'
	password project.snapshotRepoPass
}

Sweet.

Until I realized that now all other tasks fail because the credentials object is always created and thus requires the property snapshotRepoPass to exist. Something that is not the case for other tasks than publish because I see no reason to pass the repo password to, for example, a test run. Soooo, I decided to define the property in the build file if it was not already defined due to the command line option:

ext {
	// the password needs to be specified via command line
	snapshotRepoPass = project.hasProperty('snapshotRepoPass')
			? project.getProperty('snapshotRepoPass')
			: ''
	// it looks like the ternary operator can not actually be
	// split across lines; I do it here for artistic purposes
}

I could've put the same hasProperty/getProperty check into credentials but decided to create a separate spot where I implement this behavior.

With all of that done, I can indeed publish my project's current state to the Sonatype Maven snapshot repository. Wohoo!

Reflection

All in all it wasn't actually that bad. The documentation was a little sparse and building an XML file in an API that made that even more verbose felt ridiculous but other than that it reads fairly straight forward. It wasn't at the time but now it works so I should stop complaining.

Here's what I did:

  • Activate the plugin with apply plugin: 'maven-publish' and add a publishing node to build.gradle.
  • Fill the pom with those beautiful asNode.appendNode calls.
  • Include the license by appending the copy step to each JAR related task
  • Create tasks for source and Javadoc JARs and reference them from the publications node.
  • Specify the repository URL and add your credentials.

As I said before, you can check out two versions of the resulting build.gradle file: an exemplary one consisting of exactly what we build here and the real deal.

I also managed to set up Travis CI to publish each successful build and will soon try to publish actual versions. I'll write about both...