Question: How do you handle feature branches for maven multi project builds?
Jenkins builds and deploys these branches to keep build overhead on developers to a minimum but develop and feature branches cannot build the same maven version or we risk mismatch between artifacts and source.
We have a script to change parent version in child poms and version in root pom. While this segregates the branches in maven space, it results in extra work when merging.
I think nexus pro staging feature might help us avoid this requirement and make each feature branch use a specific repo which we easily drop after branch deletion/merge.
Again: how to handle the problem of multiple branches and maven?
How about the following approach:
- Use the
buildnumber-maven-plugin
to fetch information from git and populate specific Maven properties (we are interested specifically in the scmBranch
property (that is, the current git branch)
- Use the
build-helper-maven-plugin
to check whether we are in a feature branch or not (via a regex, excluding well-known branches like master
, develop
, etc.) and populate (or not) a new Maven property, say branch.classifier
- Use the
maven-jar-plugin
to set a classifier on the generated artifacts, based on what the previous step set, that is, using the new branch.classifier
property: if empty, no classifier will be applied (default behavior, applied to the develop
branch, for example); otherwise a classifier named after the current branch will be dynamically applied.
Here is a minimal example:
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>buildnumber-maven-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>create</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.10</version>
<executions>
<execution>
<id>regex-property</id>
<goals>
<goal>regex-property</goal>
</goals>
<configuration>
<name>branch.classifier</name>
<value>${scmBranch}</value>
<regex>(^develop)|(^master)|(^release.*)</regex>
<replacement></replacement>
<failIfNoMatch>false</failIfNoMatch>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<classifier>${branch.classifier}</classifier>
</configuration>
</plugin>
</plugins>
</build>
The snippet below is basically populating the scmBranch
property dynamically, then setting the branch.classifier
to its value only if different than develop
, master
or release*
, then setting it as a classifier.
Main advantages of this approach:
- No pom changes will be applied, hence no merging issues at all
- No clashes on Nexus working on the same version of the project but on different branches: the classified artifact will have different Maven coordinates, that is, the GAV (groupId, artifactId, version) becomes unique GAVC (+classifier)
- That's actually a meaningful usage of the
classifier
attribute of an artifact:
The classifier allows to distinguish artifacts that were built from the same POM but differ in their content.
- The generated artifact will be dynamically different in Nexus, according to its source branch, hence having implicit traceability: no intervention from developers (no error prone, implicit convention), no intervention from CI job (easier maintenance), completely transparent
- Using classifiers, will be easier to use the artifacts generated by a branch as a maven dependency (e.g. in case of library project): I want to use the dependency currently under development on branch xxx
Examples
Hence, you would have the following artifacts generated:
- When working on
develop
: e.g. project-1.0.0-SNAPSHOT.jar
(empty classifier, hence not applied, as handled by the regex)
- When working on
featureA
: e.g. project-1.0.0-SNAPSHOT-featureA.jar
- When working on
hotfix-JIRA123
: e.g. project-1.0.0-hotfix-JIRA123.jar
- When working on
release-sprint42
: that's up to you, I added this case to not apply the branch name, simply because in these cases I prefer to esplicitely set a special classifier, RC<number>
, for release candidates, but that's a matter of conventions/taste/habits, you can apply the same approach on this branch as well, as long as no clashes will be created on Nexus. Also note: when using JIRA/Stash/Git integration, the release branch name is normally something like release/v0.1.0
, where the /
character may cause issues in some OS (still something fixeable via further regex replacing though, if really required).
- When working on
master
: hey, no one should work on master
:) the case is there just as a double check, but that's actually not required
Warnings on this approach:
- As explained in the discussion below via comments, if the concerned Maven project is already using classifiers and even more via inter-modules dependencies (e.g. dependendies on test scope classes from another module), then this approach should be carefully tested, since it might have some drawbacks
- The publication of the
<artifactId>.pom
files containing branch classifier can get into conflicts with the mainline build (i.e. overriding it)
This does not work Results in warnings throughout the build and GC error when run from top parent.
Ideally, we want to use version as to differentiate feature branch from mainline because it is the normal maven way and classifier manipulation can result in all kinds of issues.
Since maven can use environment variables for properties and we already initialize build environment with a script (we also have git hook scripts that can set environment variables from branch names) we can use env to control the version.
<groupID>my.project</groupId>
<artifactID>database</artifactId>
<version>1.2.0${env.BRANCHMODIFIER}-SNAPSHOT</version>
If on develop our scripts set BRANCHMODIFIER to ""
If on feature/JIRA-30495 our scripts set BRANCHMODIFIER to ".30495"
How does this work in eclipse or Intellij? No clue as of yet.
We use a similar technique as Peter Kahn, modifying the version of the branch before building. We have three steps in our "Pre Steps":
- Execute shell:
echo VERSION=$(echo ${GIT_BRANCH} | sed 's_^.*\/__') > env.properties
- Inject environment variables:
env.properties
- Invoke top level Maven targets:
versions:set -DgenerateBackupPoms=false -DnewVersion=${VERSION}-SNAPSHOT
I am quite sure that this can be done with two or even one and only step as well, bit the principe behind it will be the same.
The reason why we don't change the version in the pom.xml files in the branch directly is indeed merging. With SVN this was possible (merging with --accept-mine-conflict
. With GIT this does not exist anymore, so we stopped changing versions and created this pre-build steps.