How to build a cross-product plug-in supporting both Bitbucket Server and Stash

If you have a Stash plug-in in the Atlassian Marketplace, you certainly know that it will not work anymore with Bitbucket Server if you don’t change it accordingly to the Bitbucket Server upgrade guide. There are basically two strategies two upgrade your plug-in: either you create separate branches for Stash and Bitbucket Server and build two different plug-in JARs from them, or you try to support both Bitbucket Server and Stash within the same plug-in.

While the first option is definitely easier because all you need to do is to follow the upgrade guide and perform the API renames (mostly just s/com.atlassian.stash/com.atlassian.bitbucket/g), the second one has the advantage that you don’t need to support multiple branch lines for your target products. And it is a viable option when you create a new plug-in with the requirement to support both products.

Because we were interested in building a cross-product plug-in anyway, we accepted the challenge and built a sample plug-in supporting both Bitbucket Server and Stash in one plug-in JAR which we will discuss in this blog post. As we do all of our plug-in development in Scala, we will also show what changes you have to make in order to use Scala in both Stash and Bitbucket Server. We hope the following instructions will help you if you either have to migrate your Stash plug-in or if you are generally interested in cross-product plug-in development. You will find the code for the sample project on Github.

The task

The task we want to accomplish with our sample plug-in is to provide a button on the pull request page which resolves all open tasks in the pull request. The button should only be shown if there are any open tasks in the current pull request. Here’s the plug-in in action:


A screenshot of the example plug-in we built to resolve open taks in a pull request.

Maven module setup

If you want to create a plug-in that runs on both Stash and Bitbucket Server, you should create 4 different Maven modules:

  • An API module which provides the interfaces used from your plug-in to abstract the API changes in Bitbucket Server and Stash
  • A Bitbucket Server module implementing the API and using the Bitbucket Server specific classes from com.atlassian.bitbucket
  • A Stash module implementing the API and using the Stash specific classes from com.atlassian.stash
  • A plug-in module which uses the other three modules and also contains the atlassian-plugin.xml, the I18N stuff, CSS/JS/Soy templates etc.

In Maven, we can achieve this layout by using a parent POM file with packaging level “pom” and by declaring the four modules:

Maven modules necessary for a cross-product plug-in

Java version issues

While Stash requires that your plug-in is compiled to Java 7 bytecode as the underlying plug-in framework doesn’t support plug-ins compiled to Java 8, Bitbucket Server requires Java 8. This means that we have to compile the code in our Bitbucket module with Java 8, while we have to instruct Maven to use Java 7 for the Stash module:

Maven compiler plug-in configuration to use Java 1.8

Using Scala in your plug-in

One nice thing about Atlassian products like Stash and Bitbucket Server is that they bundle the Scala library. We therefore don’t have to ship the Scala library within our plug-in which would blow up our JAR file (of course you could use tools like Proguard to somewhat reduce this).

To support both Stash and Bitbucket Server, we now have a problem when using Scala: Stash bundles Scala 2.10 which does not support Java 8, while Bitbucket Server provides both Scala 2.10 and Scala 2.11. Additionally, Scala 2.10 and 2.11 are binary incompatible. To circumvent all these problems, we decided to ship the Scala 2.11 standard library within the plug-in JAR and use that in the whole project:

Shipping the Scala library for 2.11 with the plug-in

Extending both Bitbucket Server and Stash in one atlassian-plugin.xml

Because we are only allowed to have one atlassian-plugin.xml in our plug-in JAR file, we have to differentiate between the two host applications Stash and Bitbucket Server by specifying the application for which each XML element in the plug-in descriptor should be used for. This is important as we often have to use different XML elements (e.g., for Stash which is now called for Bitbucket Server) or different XML attributes within the same XML element (e.g., when using contexts like stash.page.pullRequest.view which is now called bitbucket.page.pullRequest.view).

or our plug-in, we want to provide a button on the top right of the pull request to resolve all open tasks of the pull request. We also only want to show this button if there are any open tasks in the pull request. We can achieve this with a conditional web-item:

Web item to resolve open tasks in a pull request in Stash

To be able to add an event handler to this button, we need to use JavaScript and therefore we have to include a JS file with a resource to the page containing the button:

Stash resource for loading JavaScript files to resolve all open tasks in a pull request

Please note that we again use application= "stash" to tell the plug-in framework to only use this resource in case the host is Stash. To complete this example, we also show the equivalent XML elements for Bitbucket Server:

Bitbucket Server resource for resolving all open tasks in a pull request

Finally, we have to configure the components we use from the host product and also the component imports we want to have injected into our component by Spring. Please note that we again have to differentiate between Stash and Bitbucket Server. The components use the interfaces from our API module (i.e., OpenTaskCounter):

Components used for resolving open tasks in a pull request

Implementing the API interfaces for Bitbucket Server and Stash

As explained before, we only want to show the button to resolve open tasks in the pull request if there are any open pull requests. As the implementations to achieve this are very specific to the underlying product API, we create an interface to count open tasks in our API module:

Scala trait to count open tasks in a pull request

As the two pull request classes com.atlassian.bitbucket.pull.PullRequest and com.atlassian.stash.pull.PullRequest do not share a common interface we can use for this purpose, we cannot implement this once for both products, but instead have to provide product-specific implementations. Here's the one for Bitbucket Server:

Open task counter implementation in Scala for Bitbucket Server

Dependency injection with API interface and its product-specific component implementations

To give you an example of how we can make use of the API interface and their product-specific implementations as components, we show you the condition for only showing the button if there are open tasks:

Bitbucket Server condition in Scala to only display a button if open tasks exist

Please note that we use the interface OpenTasksCounter of the API module as a dependency here. At run-time, Spring will inject the right implementation depending if we are in Stash or Bitbucket Server thanks to the application definition we did in the plug-in descriptor.

Sharing common JS code in your plug-in module

In our plug-in module, we put all JS/CSS/Soy code. We will now show how common JS code for Stash and Bitbucket Server can be shared. Let's start with the code we use to wire the button in the pull request with an event handler:

JavaScript module for Bitbucket Server to resolve open tasks

As you can see, we use the Stash specific AMD module stash/api/util/state to get the current pull request, the current repo and Stash project the user is looking at. To reuse the common code to resolve all open tasks in the pull request, we provide our own AMD module which can then be accessed by both the Stash and Bitbucket server specific JS file:

JavaScript AMD module to resolve open tasks in Bitbucket Server

As you can see, with the dynamic typing nature of JS code, we can reuse almost all the code by using the state object for both Bitbucket Server and Stash.

Is it worth the effort?

As you can see now, there is quite a lot of effort to build a cross-product plug-in. Extracting common API interfaces and providing product-specific implementations provides a nice separation of code, but can take a significant amount of time for existing plug-ins. So while this might not be an approach you choose for your existing plug-ins, it is definitely the way to go if you start with a new plug-in and want to support both Stash and Bitbucket server. This will allow you to put your common code in your plug-in module and clearly separate the product-specific implementations (all that make usage of the Stash and Bitbucket Server API) from it - which will result in a very clean plug-in architecture.

Credits

Thanks to Bryan Turner from Atlassian which gave us some important tips in building a cross-product plug-in.

Comments

comments powered by Disqus