Putting a maven version label on a bamboo build

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:

Screenshot showing the maven version number labelled against a bamboo build (plan view and individual build view). The maven version number labelled above is “0.0.4-SNAPSHOT“, which creates the label “maven-0_0_4-snapshot“.

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

The Maven plugin

The source code for the plugin looks like this:

LabelBambooMojo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
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;
    }
 
}

The references to ‘vmaint’ here refer to my internal virtual machine maintenance scripts, which I intend to publish on the net at some stage. You can track the progress (or lack thereof) of this project here.

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]:

Install it into your repository manager (if you have one) with this command-line:

C:> c:\java\prog\apache-maven-3.0.4\bin\mvn deploy:deploy-file 
  -Durl=http://nexus.dev.randomnoun:8082/nexus/content/repositories/releases/ 
  -DrepositoryId=releases -Dfile=vmaint-maven-plugin-1.1.0.jar 
  -DgroupId=com.randomnoun.maven.plugins -DartifactId=vmaint-maven-plugin 
  -Dversion=1.1.0 -Dpackaging=jar -DgeneratePom=false

C:> c:\java\prog\apache-maven-3.0.4\bin\mvn deploy:deploy-file 
  -Durl=http://nexus.dev.randomnoun:8082/nexus/content/repositories/releases/ 
  -DrepositoryId=releases -Dfile=vmaint-maven-plugin-1.0.0-sources.jar 
  -DgroupId=com.randomnoun.maven.plugins -DartifactId=vmaint-maven-plugin 
  -Dversion=1.1.0 -Dpackaging=java-source -DgeneratePom=false

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 bamboo maven task goal settings

-B  vmaint:label-bamboo clean deploy 
"-DbambooBuildKey=${bamboo.buildKey}" 
"-DbambooBuildNumber=${bamboo.buildNumber}" 
"-DbambooBuildPlanName=${bamboo.buildPlanName}" 

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.

The other parameters pass bamboo build information through to the maven plugin using the bamboo variable substitution mechanism.

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

Update 2021-01-29: Source is on github

[*] – except for its rubbish data model

[**] – 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.

Tags:,

Add a Comment

Your email address will not be published. Required fields are marked *