This took a couple of hours to do the other day, so perhaps someone else out there might find this useful.
Bamboo‘s great[*], it allows you to continuously integrate your source trees so that you’ve got a reasonably good chance of knowing that your code builds and tests correctly.
Maven‘s great[**], it allows you to package your source code into libraries and maintain the dependencies between them
So anyway, bamboo’s got its build IDs, and maven’s got its version numbers. It would be nice if you could merge the two of them.
What I do is to label builds in bamboo with the version number of the project being built, which looks a bit like this:
This is surprisingly more difficult to do than it would at first appear.
To get this to work, I ended up:
Writing a maven plugin that is called from within the Bamboo build process
This plugin invokes the Bamboo REST API to label the build in which it is currently running
Configuring the settings.xml on the Bamboo server to contain the Bamboo connection settings used by the plugin, and to allow the plugin to be referred to by its short name
Added the maven goal provided by this plugin to every maven build in Bamboo
packagecom.randomnoun.maven.plugin.vmaint;/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
* BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
*/importorg.apache.commons.httpclient.HttpClient;importorg.apache.commons.httpclient.UsernamePasswordCredentials;importorg.apache.commons.httpclient.auth.AuthScope;importorg.apache.commons.httpclient.methods.PostMethod;importorg.apache.commons.httpclient.methods.StringRequestEntity;importorg.apache.maven.plugin.AbstractMojo;importorg.apache.maven.plugin.MojoExecutionException;importorg.apache.maven.project.MavenProject;importorg.apache.maven.settings.Settings;importorg.apache.maven.execution.MavenSession;importjava.net.MalformedURLException;importjava.net.URL;importjava.util.Properties;/**
* Maven goal which labels the current bamboo build with the version in that build's pom.xml file
* (in the form "<tt>maven-<i>{version}</i></tt>", eg "<tt>maven-0_0_1-snapshot</tt>").
*
* <p>You should probably run this goal before any others, so that even failed builds are labelled
* with the mvn version of the build.
*
* <p>The following maven properties are used:
*
* <p>The '<code>bamboo.rest.url</code>' maven property will be used to determine the URL of the bamboo REST API
* <p>The '<code>bamboo.rest.username</code>' and '<code>bamboo.rest.password</code>' properties will be used to authenticate to the
* Bamboo server. The password is in cleartext. Because I enjoy sanity.
*
* <p>These should be specified in the <code>/settings/profiles/profile/properties</code> element
* of your <code>.m2/settings.xml</code> file, but will also work if specified in your <code>pom.xml</code>'s project properties.
*
* <p>The current bamboo project and plan will be determined using the '<code>bambooBuildKey</code>',
* '<code>bambooBuildNumber</code>' and '<code>bambooBuildPlanName</code>' properties, which should be set using the following
* command-line arguments to the bamboo mvn task:
*
* <pre>
"-DbambooBuildKey=${bamboo.buildKey}"
"-DbambooBuildNumber=${bamboo.buildNumber}"
"-DbambooBuildPlanName=${bamboo.buildPlanName}"
</pre>
*
* <p>You probably already have this in your bamboo's project's plan's stage's job's task definition, if
* you're using a build.properties file in your project. That sentence will make sense, incidentally,
* if you think Atlassian creates intuitive build systems.
*
* <p>Bamboo unhelpfully lowercases labels, so the label "<tt>maven-0.0.1-SNAPSHOT</tt>" will appear as
* "<tt>maven-0_0_1-snapshot</tt>" .
*
* <p>Also, some maven build failure types (e.g. missing dependencies) will prevent the
* the failed build from being labelled in bamboo. There are some who would say that
* you could put the labelling goal into a separate bamboo task to prevent this, but they
* would be the sort of people who think that that would be a good idea.
*
* @goal label-bamboo
* @blog http://www.randomnoun.com/wp/2012/11/07/putting-a-maven-version-label-on-a-bamboo-build/
*/publicclass LabelBambooMojo
extends AbstractMojo
{// from http://grepcode.com/file_/repo1.maven.org/maven2/org.kuali.maven.plugins/maven-cloudfront-plugin/1.1.0/org/kuali/maven/mojo/s3/BaseMojo.java/?v=source/**
* The Maven project this plugin runs in.
*
* @parameter expression="${project}"
* @required
* @readonly
*/private MavenProject project;/**
* @parameter expression="${settings}"
* @required
* @since 1.0
* @readonly
*/private Settings settings;/**
* @parameter default-value="${session}"
* @required
* @readonly
*/private MavenSession mavenSession;/**
* @parameter expression="${label.bamboo.rest.url}"
*/privateURL bambooRestUrl;/**
* @parameter expression="${label.bamboo.rest.username}"
*/privateString bambooRestUsername;/**
* @parameter expression="${label.bamboo.rest.password}"
*/privateString bambooRestPassword;publicvoid execute()throws MojoExecutionException
{// the following line would use plugin properties, rather than project properties// Properties mavenProperties = getMavenSession().getExecutionProperties();// these can be set in a per-user settings.xml file (i.e. on the bamboo server)Properties mavenProperties = project.getProperties();String bambooRestUrlOverride = mavenProperties.getProperty("bamboo.rest.url");if(bambooRestUrlOverride!=null){try{
bambooRestUrl =newURL(bambooRestUrlOverride);}catch(MalformedURLException e){thrownew MojoExecutionException("Invalid bamboo.rest.url property", e);}}String bambooRestUsernameOverride = mavenProperties.getProperty("bamboo.rest.username");if(bambooRestUsernameOverride!=null){
bambooRestUsername = bambooRestUsernameOverride;}String bambooRestPasswordOverride = mavenProperties.getProperty("bamboo.rest.password");if(bambooRestPasswordOverride!=null){
bambooRestPassword = bambooRestPasswordOverride;}if(bambooRestUrl==null){thrownew MojoExecutionException("Missing bamboo.rest.url property");}// let's just get the bamboo build data from the System properties, since// that's an API that didn't take a floor full of software engineers to create.String bambooBuildKey =System.getProperty("bambooBuildKey");String bambooBuildNumber =System.getProperty("bambooBuildNumber");//String bambooBuildPlanName = System.getProperty("bambooBuildPlanName");if(bambooBuildKey==null){thrownew MojoExecutionException("Missing bambooBuildKey system property");}if(bambooBuildNumber==null){thrownew MojoExecutionException("Missing bambooBuildNumber system property");}//if (bambooBuildPlanName==null) { throw new MojoExecutionException("Missing bambooBuildPlanName system property"); }// these days, this seem to have the values:// bambooBuildKey=RANDOMNOUN-NSWEB-JOB1// bambooBuildNumber=60// bambooBuildPlanName=RANDOMNOUN - ns-web - Default Job// so I'm removing the last hyphenated component from the build key to get the plan key.String bambooPlanKey = bambooBuildKey.substring(0, bambooBuildKey.lastIndexOf("-"));// The following code is the equivalent of this:// /usr/bin/curl -v -X POST --user knoxg:supersecretpassword "bamboo.dev.randomnoun:8085/bamboo/rest/api/latest/result/RANDOMNOUN-NSWEB-60/label" \ // -d '{ "name" : "maven-0.0.3-SNAPSHOT" }' -H "Content-type: application/json"try{// if I was a maven developer, I'd be using wagons here. Probably.
HttpClient client =new HttpClient();if(bambooRestUsername!=null&& bambooRestPassword!=null){
getLog().debug("Authenticating with username '"+ bambooRestUsername +"'");
getLog().debug("Authenticating with password '"+ bambooRestPassword +"'");
client.getState().setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(bambooRestUsername, bambooRestPassword));
client.getParams().setAuthenticationPreemptive(true);}else{
getLog().warn("Not authenticating to bamboo");}String url = bambooRestUrl +"/api/latest/result/"+ bambooPlanKey +"-"+ bambooBuildNumber +"/label";String body ="{ \"name\" : \"maven-"+ project.getVersion()+"\" }";
getLog().info("POST "+ url);
getLog().info(" "+ body);
PostMethod postMethod =new PostMethod(url);
postMethod.setDoAuthentication(true);
postMethod.setRequestHeader("Content-type", "application/json");
postMethod.setRequestEntity(new StringRequestEntity(body));
client.executeMethod(postMethod);// normally returns 204 No Content, but I'm going to accept 200 as wellif(postMethod.getStatusCode()!=200&& postMethod.getStatusCode()!=204){thrownew MojoExecutionException("Bamboo return status code "+ postMethod.getStatusCode()+", body='"+ postMethod.getResponseBodyAsString()+"'");}}catch(Exception e){
getLog().info("Exception occurred labelling bamboo build", e);}}/**
* @return the project
*/public MavenProject getProject(){return project;}/**
* @param project
* the project to set
*/publicvoid setProject(final MavenProject project){this.project= project;}/**
* @return the settings
*/public Settings getSettings(){return settings;}/**
* @param settings
* the settings to set
*/publicvoid setSettings(final Settings settings){this.settings= settings;}/**
* @return the mavenSession
*/public MavenSession getMavenSession(){return mavenSession;}/**
* @param mavenSession
* the mavenSession to set
*/publicvoid setMavenSession(final MavenSession mavenSession){this.mavenSession= mavenSession;}}
package com.randomnoun.maven.plugin.vmaint;
/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
* BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
*/
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.apache.maven.execution.MavenSession;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Properties;
/**
* Maven goal which labels the current bamboo build with the version in that build's pom.xml file
* (in the form "<tt>maven-<i>{version}</i></tt>", eg "<tt>maven-0_0_1-snapshot</tt>").
*
* <p>You should probably run this goal before any others, so that even failed builds are labelled
* with the mvn version of the build.
*
* <p>The following maven properties are used:
*
* <p>The '<code>bamboo.rest.url</code>' maven property will be used to determine the URL of the bamboo REST API
* <p>The '<code>bamboo.rest.username</code>' and '<code>bamboo.rest.password</code>' properties will be used to authenticate to the
* Bamboo server. The password is in cleartext. Because I enjoy sanity.
*
* <p>These should be specified in the <code>/settings/profiles/profile/properties</code> element
* of your <code>.m2/settings.xml</code> file, but will also work if specified in your <code>pom.xml</code>'s project properties.
*
* <p>The current bamboo project and plan will be determined using the '<code>bambooBuildKey</code>',
* '<code>bambooBuildNumber</code>' and '<code>bambooBuildPlanName</code>' properties, which should be set using the following
* command-line arguments to the bamboo mvn task:
*
* <pre>
"-DbambooBuildKey=${bamboo.buildKey}"
"-DbambooBuildNumber=${bamboo.buildNumber}"
"-DbambooBuildPlanName=${bamboo.buildPlanName}"
</pre>
*
* <p>You probably already have this in your bamboo's project's plan's stage's job's task definition, if
* you're using a build.properties file in your project. That sentence will make sense, incidentally,
* if you think Atlassian creates intuitive build systems.
*
* <p>Bamboo unhelpfully lowercases labels, so the label "<tt>maven-0.0.1-SNAPSHOT</tt>" will appear as
* "<tt>maven-0_0_1-snapshot</tt>" .
*
* <p>Also, some maven build failure types (e.g. missing dependencies) will prevent the
* the failed build from being labelled in bamboo. There are some who would say that
* you could put the labelling goal into a separate bamboo task to prevent this, but they
* would be the sort of people who think that that would be a good idea.
*
* @goal label-bamboo
* @blog http://www.randomnoun.com/wp/2012/11/07/putting-a-maven-version-label-on-a-bamboo-build/
*/
public class LabelBambooMojo
extends AbstractMojo
{
// from http://grepcode.com/file_/repo1.maven.org/maven2/org.kuali.maven.plugins/maven-cloudfront-plugin/1.1.0/org/kuali/maven/mojo/s3/BaseMojo.java/?v=source
/**
* The Maven project this plugin runs in.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* @parameter expression="${settings}"
* @required
* @since 1.0
* @readonly
*/
private Settings settings;
/**
* @parameter default-value="${session}"
* @required
* @readonly
*/
private MavenSession mavenSession;
/**
* @parameter expression="${label.bamboo.rest.url}"
*/
private URL bambooRestUrl;
/**
* @parameter expression="${label.bamboo.rest.username}"
*/
private String bambooRestUsername;
/**
* @parameter expression="${label.bamboo.rest.password}"
*/
private String bambooRestPassword;
public void execute()
throws MojoExecutionException
{
// the following line would use plugin properties, rather than project properties
// Properties mavenProperties = getMavenSession().getExecutionProperties();
// these can be set in a per-user settings.xml file (i.e. on the bamboo server)
Properties mavenProperties = project.getProperties();
String bambooRestUrlOverride = mavenProperties.getProperty("bamboo.rest.url");
if (bambooRestUrlOverride!=null) {
try {
bambooRestUrl = new URL(bambooRestUrlOverride);
} catch (MalformedURLException e) {
throw new MojoExecutionException("Invalid bamboo.rest.url property", e);
}
}
String bambooRestUsernameOverride = mavenProperties.getProperty("bamboo.rest.username");
if (bambooRestUsernameOverride!=null) {
bambooRestUsername = bambooRestUsernameOverride;
}
String bambooRestPasswordOverride = mavenProperties.getProperty("bamboo.rest.password");
if (bambooRestPasswordOverride!=null) {
bambooRestPassword = bambooRestPasswordOverride;
}
if (bambooRestUrl==null) {
throw new MojoExecutionException("Missing bamboo.rest.url property");
}
// let's just get the bamboo build data from the System properties, since
// that's an API that didn't take a floor full of software engineers to create.
String bambooBuildKey = System.getProperty("bambooBuildKey");
String bambooBuildNumber = System.getProperty("bambooBuildNumber");
//String bambooBuildPlanName = System.getProperty("bambooBuildPlanName");
if (bambooBuildKey==null) { throw new MojoExecutionException("Missing bambooBuildKey system property"); }
if (bambooBuildNumber==null) { throw new MojoExecutionException("Missing bambooBuildNumber system property"); }
//if (bambooBuildPlanName==null) { throw new MojoExecutionException("Missing bambooBuildPlanName system property"); }
// these days, this seem to have the values:
// bambooBuildKey=RANDOMNOUN-NSWEB-JOB1
// bambooBuildNumber=60
// bambooBuildPlanName=RANDOMNOUN - ns-web - Default Job
// so I'm removing the last hyphenated component from the build key to get the plan key.
String bambooPlanKey = bambooBuildKey.substring(0, bambooBuildKey.lastIndexOf("-"));
// The following code is the equivalent of this:
// /usr/bin/curl -v -X POST --user knoxg:supersecretpassword "bamboo.dev.randomnoun:8085/bamboo/rest/api/latest/result/RANDOMNOUN-NSWEB-60/label" \
// -d '{ "name" : "maven-0.0.3-SNAPSHOT" }' -H "Content-type: application/json"
try {
// if I was a maven developer, I'd be using wagons here. Probably.
HttpClient client = new HttpClient();
if (bambooRestUsername!=null && bambooRestPassword!=null) {
getLog().debug("Authenticating with username '" + bambooRestUsername + "'");
getLog().debug("Authenticating with password '" + bambooRestPassword + "'");
client.getState().setCredentials(
AuthScope.ANY,
new UsernamePasswordCredentials(bambooRestUsername, bambooRestPassword)
);
client.getParams().setAuthenticationPreemptive(true);
} else {
getLog().warn("Not authenticating to bamboo");
}
String url = bambooRestUrl + "/api/latest/result/" + bambooPlanKey + "-" + bambooBuildNumber + "/label";
String body = "{ \"name\" : \"maven-" + project.getVersion() + "\" }";
getLog().info("POST " + url);
getLog().info(" " + body);
PostMethod postMethod = new PostMethod(url);
postMethod.setDoAuthentication(true);
postMethod.setRequestHeader("Content-type", "application/json");
postMethod.setRequestEntity(new StringRequestEntity(body));
client.executeMethod(postMethod);
// normally returns 204 No Content, but I'm going to accept 200 as well
if (postMethod.getStatusCode()!=200 && postMethod.getStatusCode()!=204) {
throw new MojoExecutionException("Bamboo return status code " + postMethod.getStatusCode() +
", body='" + postMethod.getResponseBodyAsString() + "'");
}
} catch (Exception e) {
getLog().info("Exception occurred labelling bamboo build", e);
}
}
/**
* @return the project
*/
public MavenProject getProject() {
return project;
}
/**
* @param project
* the project to set
*/
public void setProject(final MavenProject project) {
this.project = project;
}
/**
* @return the settings
*/
public Settings getSettings() {
return settings;
}
/**
* @param settings
* the settings to set
*/
public void setSettings(final Settings settings) {
this.settings = settings;
}
/**
* @return the mavenSession
*/
public MavenSession getMavenSession() {
return mavenSession;
}
/**
* @param mavenSession
* the mavenSession to set
*/
public void setMavenSession(final MavenSession mavenSession) {
this.mavenSession = mavenSession;
}
}
Anyway, rather than compiling the code above, you can download the plugin JARs and its sources here if you like [Update: although this now shouldn’t be necessary as the artifact is in the maven central repository]:
replacing the path to mvn and the repository URL as necessary.
Configuring settings.xml
Two things need to be specified in the settings.xml on the bamboo server in order to use this plugin:
The connection settings to bamboo, including the username/password credentials used to authorise the labelling of the build using the REST API. This user needs to be configured within Bamboo and have write access to the build in which it runs
A plugin prefix declaration. This isn’t absolutely necessary, but it does let you run things like mvn vmaint:label-bamboo instead of mvn com.randomnoun.maven.plugins:vmaint-maven-plugin:label-bamboo:1.0.0
You could, if you like, set the bamboo connection settings within your project’s pom.xml file but this could get repetitive over a large number of projects, and would expose the username/password in a public source-controlled file, which may not be something that you want.
Instead, find the settings.xml file that your Bamboo instance is using (on a Bamboo instance running as a Windows service, this is under C:\.m2\settings.xml ), and update these sections:
<settings>
<pluginGroups>
<!-- this plugin group allows us to run
mvn vmaint:label-bamboo
rather than
mvn com.randomnoun.maven.plugins:vmaint-maven-plugin:label-bamboo
-->
<pluginGroup>com.randomnoun.maven.plugins</pluginGroup>
</pluginGroups>
<!-- mirrors section here -->
<profiles>
<profile>
<id>default</id>
<!-- reposities section here -->
<!-- plugin repositories section here -->
<properties>
<!-- we use the dot-separated maven property naming convention here -->
<bamboo.rest.url>http://my-bamboo-server/bamboo/rest</bamboo.rest.url>
<bamboo.rest.username>my-bamboo-username</bamboo.rest.username>
<bamboo.rest.password>my-bamboo-password</bamboo.rest.password>
</properties>
</profile>
<!-- repeat properties section in any other profiles -->
</profiles>
<activeProfiles>
<activeProfile>default</activeProfile>
</activeProfiles>
<!-- servers section here -->
</settings>
Configuring the tasks in Bamboo
So each project in bamboo has plans, and each plan has stages, and each stage has jobs, and each job has tasks. If you’re anything like me, then there’s only one stage with one job with one task, which is something like ‘Maven build’.
What you want in your Maven bamboo task is the following goal settings:
The -B denotes a batch (non-interactive) build; this prevents maven from displaying progress percentages when uploading files, which can otherwise fill the build log files.
I sometimes put -U here as well to force maven to update any referenced snapshot artifacts from the artifact repository, but don’t have it on this build for some reason.
The vmaint:label-bamboo goal triggers the bamboo label, and the clean and deploy goals perform a standard maven clean, compile, build, test, install and deploy cycle.
Anyway; after possibly restarting bamboo to pick up the new settings.xml settings, try running a build and, with luck, you’ll have your maven version number labelled against your build ! Exciting !
Update 2013-09-25: Reference the plugin using the maven co-ordinates com.randomnoun.maven.plugins:vmaint-maven-plugin
[**] – except for its speed, choice of XML representation, nonstandard terminology, and various other idiosyncracies and inconsistencies, which I’m sure I’ll complain about at a later juncture.