Customise the Maven Release process

Shipping a new release of software usually involves quite a few steps. Depending on the type of software, this may be something you rarely do. Thus, it often involves manual steps. This is not necessary! Maven has had its “Release Plugin” since approximately April 2007; yes, that’s over 12 years! It has served both the Maven project and many other software projects.

Release Strategies

Recently, the Apache Maven team released the first 3.0 milestone of the Release Plugins. One feature caught my attention: MRELEASE-956, or the “Release Strategy Interface”. This might not sound very exciting, but in fact, it is. It offers you the option to completely customise what the Release Plugin does at which stage. Many have loathed the default behaviour of the plugin in the past. That default is now no longer the only way to use the plugin.

In short, a “Release Strategy” describes which steps the plugin must execute at each stage of the release process. The most familiar stages are “preparing” and “performing” the release. Apart from those, the plugin offers different goals for different tasks. Those include creating a branch or bumping version numbers.

So how to enjoy this new flexible approach?

The first thing we need is a “Release Strategy”. As said, a strategy describes the steps - or phases - that the Release Plugin should execute for each of its goals. Such strategies should follow the Strategy interface.

The default strategy returns a pre-configured list of phases for each goal. It is flexible enough to configure it using an XML descriptor. The default mapping comes packaged with the Release Plugin. It describes DefaultStrategy as the default implementation of the Strategy interface. It also configures a list of phases for each goal. If you decide to take this approach, you need Plexus’ Component Metadata Maven Plugin. It combines all descriptor fragments to write the final XML descriptor.

For greater flexibility, we can supply our own implementation of the Strategy interface. Let’s have a look at a simple customisation. We’ll add a single step to the existing default release strategy. You can do this using XML descriptors only, but for illustration I am writing my own Strategy for this. I will let it end the “prepare” and “perform” stages with some ASCII art - because who doesn’t love ASCII art?

Creating a Custom Strategy

Define a Strategy by writing a pure Java class that follows the Strategy interface:

@Named("ascii-art-enhanced-strategy")
@Singleton
public class AsciiArtEnhancedStrategy implements Strategy {
}

The Strategy interface describes five methods. Those methods tell the Release Plugin which phases to execute from each goal of the plugin. In this example we’ll focus on the getPreparePhases() and getPerformPhases() methods. They correspond to the release:prepare and release:perform goals. Since we want to enhance existing behaviour, we will delegate most of the work to the default strategy. This way, we only deviate when needed.

To get hold of that default strategy, we use dependency injection:

@Inject
@Named("default")
Strategy defaultStrategy;

The runtime may be aware of more than one class that follows the Strategy interface. We use the hint parameter to specify which particular instance we’re interested in.

Implementing most methods is now a simple task:

@Override
public List<String> getRollbackPhases() {
    return defaultStrategy.getRollbackPhases();
}

Although implementing the prepare or perform phases isn’t particularly hard either:

@Override
public List<String> getPreparePhases() {
    final List<String> phases = new ArrayList<>(defaultStrategy.getPreparePhases());
    phases.add(phases.size() - 1, "log-prepare-done-ascii-art");
    return phases;
}

@Override
public List<String> getPerformPhases() {
    final List<String> phases = new ArrayList<>(defaultStrategy.getPerformPhases());
    phases.add(phases.size() - 1, "log-perform-done-ascii-art");
    return phases;
}

Note that we insert the custom phases as second-last, not as last. The Maven Release Plugin assumes that a release:prepare invocation will end with the end-release phase having run. Inserting our custom phases as last would break this assumption, and it wouldn’t be possible to actually perform a release.

Creating the Phases

Now our Strategy points to phases that are not yet implemented. Time to write some more code. A phase in a Release Plugin goal needs to follow the ReleasePhase interface:

@Named("log-prepare-done-ascii-art")
@Singleton
public class AsciiArtPrepareDoneLoggingPhase extends AbstractReleasePhase implements ReleasePhase {
    @Inject
    AsciiArtGenerator asciiArtGenerator;

    @Override
    public ReleaseResult execute(final ReleaseDescriptor releaseDescriptor,
                                 final ReleaseEnvironment releaseEnvironment,
                                 final List<MavenProject> list) {
        final ReleaseResult result = new ReleaseResult();
        logInfo(result, asciiArtGenerator.generate("Prepared!"))
        result.setResultCode(SUCCESS);
        return result;
    }

    @Override
    public ReleaseResult simulate(final ReleaseDescriptor releaseDescriptor,
                                  final ReleaseEnvironment releaseEnvironment,
                                  final List<MavenProject> list) {
        final ReleaseResult result = new ReleaseResult();
        result.setResultCode(SUCCESS);
        return result;
    }
}

Both methods have the same argument list and the same return type. When you run mvn release:prepare -DdryRun, the simulate method gets invoked. The execute method gets invoked when you leave out -DdryRun. Both methods should return an instance of ReleaseResult. This is a container that holds the result of a release phase. Since generating ASCII art should not fail (it’s logging, after all) we can set the result code to SUCCESS.

For the other phase (logged after release:perform) we write a similar class. Of course, we can refactor both classes to re-use some of the code, but that’s not the main part of this blog. Right now, neither is generating the actual ASCII art. But there’s plenty of code samples that give a much better explanation of how that works.

Packaging and Distribution

But we cannot yet enjoy this ASCII art logging. We need to package and distribute the release strategy. Packaging is easy - mvn package does the trick for you.

Annotating classes with @Singleton is not enough to make them suitable for injection. Unlike Spring, Maven does not scan the classpath when it starts - it would take too much time. This means we must package metadata for injection with the components. Luckily, we don’t have to write that by hand. The Sisu Maven Plugin extracts that information and generates the necessary files.

For distribution, you have two choices:

  1. install the package in your local Maven repository (using mvn install).
  2. distribute it using a central artifact repository (using mvn deploy).

For your convenience, I have published this example to Bintray. You can add the following lines to your pom.xml:

<pluginRepositories>
    <pluginRepository>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
        <id>bintray-mthmulders-open-source</id>
        <name>bintray</name>
        <url>https://dl.bintray.com/mthmulders/open-source</url>
    </pluginRepository>
</pluginRepositories>

After that we can configure the Maven Release Plugin as follows:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-release-plugin</artifactId>
    <!-- You need at least version 3.0.0-M1! -->
    <version>3.0.0-M1</version>
    <dependencies>
        <dependency>
            <groupId>it.mulders.maven</groupId>
            <artifactId>ascii-art-logger</artifactId>
            <version>0.0.4</version>
        </dependency>
    </dependencies>
    <configuration>
        <!-- this value must be the hint of our Strategy implementation -->
        <releaseStrategyId>ascii-art-enhanced-strategy</releaseStrategyId>
    </configuration>
</plugin>

Now simulate a release by issuing mvn release:prepare -DdryRun. You shouldn’t see the rewarding ASCII art. After removing the -DdryRun, the command will run a bit longer, but in the end, you’ll see something like this:

ASCII art logging as part of the Maven Release Plugin invocation
ASCII art logging as part of the Maven Release Plugin invocation

Wrapping Up

Of course, this is a toy example, which will not be of much value in day-to-day work. But there’s a lot of other use cases that come within reach. A few suggestions:

  • Sending out a tweet once a release has been cut.
  • Informing a work-item tool (like JIRA or TFS) that the release has been made.
  • Auto-closing issues that were resolved in the current release.

I’m curious to hear your suggestions. What would you want to build? I’d to hear from you!

For convenience, I’ve shared the ASCII art logger on GitHub. An accompanying sample project is also available.