Thibaud Ledent

Home

Creating a Bitbucket pipeline to automate a Maven release

Published Mar 01, 2019

It’s quite paradoxical to think that my first GitHub blog post is about Bitbucket… But well, that’s the most interesting thing I have to write now and, as a dev, we’re curious and ready to try something new, aren’t we?

Context

To put things in context, I recently had to apply changes in an Open Source Java library (hosted to Bitbucket) that we use in a project at work. Even if the repo maintainer has merged two of my Pull Requests with the main changes, he did not release a new version of his library to Maven Central. The changes were therefore not available for our project.

To speed things up, I decided to fork the repo. In addition to being able to apply our fixes more quickly, we were also able to take control of the library’s release cycle.

Challenge

Having the control of the repository is one thing but I discovered that making a new Maven release of a library is a process in itself (and you have to store passwords, tokens and a GPG key somewhere…). I didn’t want to remember this process, nor to repeat it manually several times. So I tried to automate it.

Like many code hosting platforms, Bitbucket has its own integrated CI/CD feature called Bitbucket pipelines. For example, it is quite useful to create one pipeline to run and test your application.

You can maintain your pipelines in a YAML file called bitbucket-pipelines.yml. The challenge was to create a custom pipeline to release a new version of the library to Maven Central!

Steps

Create a Sonatype User account

  1. Go to Create your JIRA account and create an account for yourself.
  2. Create a New Project ticket (link to my Jira ticket)

Make sure your Group Id and other pom details are correct

Make sure you use the correct groupId in your pom.xml, example:

<groupId>org.bitbucket.thibaudledent.j8583</groupId>

Make sure your pom.xml (link here) includes a license, scm, distributionManagement, and developers section. Take note of the snapshotRepository and the repository. Example:

<url>https://bitbucket.org/thibaudledent/j8583</url>

<licenses>
    <license>
        <name>GNU Lesser General Public License, version 3</name>
        <url>http://www.gnu.org/licenses/lgpl-3.0.html</url>
        <distribution>repo</distribution>
    </license>
</licenses>

<scm>
    <connection>scm:git: https://bitbucket.org/thibaudledent/j8583.git</connection>
    <url>https://bitbucket.org/thibaudledent/j8583</url>
</scm>

<distributionManagement>
    <snapshotRepository>
        <id>ossrh</id>
        <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </snapshotRepository>
    <repository>
        <id>ossrh</id>
        <url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
    </repository>
</distributionManagement>

<developers>
    <developer>
        <name>Thibaud Ledent</name>
    </developer>
</developers>

Create a GPG certificate

Install gnupg2:

sudo apt-get install gnupg2

Then, you can generate a key:

gpg --gen-key

You should then see your key listed:

gpg2 --list-keys

And now distribute your public key:

gpg --keyserver pgpkeys.uk --send-keys DB85FB2159287141

The --keyserver parameter identifies the target key server address and --send-keys is the keyid of the key you want to distribute. You can get your keyid by listing the public keys.

Then, go to http://pgpkeys.uk/ and search 0xDB85FB2159287141. You should see your public key (example here).

If you have ‘No route to host’, see also this list of available servers (some of them might not work behind a company firewall).

Add the necessary plugins to the pom.xml file

I use the configuration skipRelease to enable/disable these plugins. The list of plugins to add is listed below (example here):

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-site-plugin</artifactId>
            <version>3.7.1</version>
            <configuration>
                <skip>${skipRelease}</skip>
                <locales>en,es</locales>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.0.1</version>
            <executions>
                <execution>
                    <id>attach-sources</id>
                    <goals>
                        <goal>jar-no-fork</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>3.0.1</version>
            <configuration>
                <skip>${skipRelease}</skip>
                <links>
                    <link>http://download.oracle.com/javase/8/docs/api/</link>
                    <link>http://slf4j.org/apidocs/</link>
                </links>
            </configuration>
            <executions>
                <execution>
                    <id>attach-javadocs</id>
                    <goals>
                        <goal>jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-gpg-plugin</artifactId>
            <version>1.6</version>
            <configuration>
                <skip>${skipRelease}</skip>
            </configuration>
            <executions>
                <execution>
                    <id>sign-artifacts</id>
                    <phase>verify</phase>
                    <goals>
                        <goal>sign</goal>
                    </goals>
                    <configuration>
                        <!-- Configuration to prevent the 'Signing Prompt' or the  -->
                        <!-- 'gpg: signing failed: No such file or directory' error -->
                        <!-- See https://myshittycode.com/2017/08/07/maven-gpg-plugin-prevent-signing-prompt-or-gpg-signing-failed-no-such-file-or-directory-error/ -->
                        <gpgArguments>
                            <arg>--pinentry-mode</arg>
                            <arg>loopback</arg>
                        </gpgArguments>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.sonatype.plugins</groupId>
            <artifactId>nexus-staging-maven-plugin</artifactId>
            <version>1.6.8</version>
            <extensions>true</extensions>
            <configuration>
                <serverId>ossrh</serverId>
                <nexusUrl>https://oss.sonatype.org/</nexusUrl>
                <!-- Set this to true and the release will automatically proceed and -->
                <!-- sync to Central Repository will follow -->
                <autoReleaseAfterClose>true</autoReleaseAfterClose>
            </configuration>
        </plugin>
    </plugins>
</build>

Create the Bitbucket pipeline

Now it’s time to edit the bitbucket-pipelines.yml file to automate the Maven release. After a few round trips to StackOverflow, I managed to automatically release the library.

Here is the code of the custom pipeline, it’s quite straightforward:

image: maven:3.6.0-jdk-8-slim

pipelines:
  custom:
    release-to-maven-central:
      - step:
          script:
            - apt-get update && apt-get install -y gpg --no-install-recommends
            - export GPG_TTY=$(tty) # to fix the 'gpg: signing failed: Inappropriate
            # ioctl for device', see https://github.com/keybase/keybase-issues/issues/2798#issue-205008630
            - echo $GPG_SECRET_KEYS | base64 --decode | gpg --batch --import # use 'batch'
            # otherwise gpg2 is asking for a passphrase.
            # See https://superuser.com/a/1135950
            - echo $GPG_OWNERTRUST | base64 --decode | gpg --import-ownertrust
            - mvn -V -B -s settings.xml deploy -DskipRelease=false
            # -V triggers an output of the Maven and Java versions at the beginning 
            #     of the build
            # -B batch mode makes Maven less verbose
            # -s causes the usage of the local settings with the required credentials

In addition, I created a settings.xml file:

<settings>
    <servers>
        <server>
            <id>ossrh</id>
            <username>${env.OSSRH_USER_TOKEN}</username>
            <password>${env.OSSRH_PWD_TOKEN}</password>
        </server>
    </servers>
    <profiles>
        <profile>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <gpg.keyname>${env.GPG_EXECUTABLE}</gpg.keyname>
                <gpg.passphrase>${env.GPG_PASSPHRASE}</gpg.passphrase>
            </properties>
        </profile>
    </profiles>
</settings>

For this pipeline to work, you need to set up repository variables (in Bitbucket, go to settings -> pipelines -> repository variables):

Get the local GPG_SECRET_KEYS:

gpg -a --export-secret-keys email_address_linked_to_your_gpg_key@mail.com | base64

Get the GPG_OWNERTRUST:

gpg --export-ownertrust | base64

To get OSSRH_USER_TOKEN, go to Sonatype user token.

I ended up with the following repository variables:

  • OSSRH_USER_TOKEN
  • OSSRH_PWD_TOKEN
  • GPG_PASSPHRASE
  • GPG_SECRET_KEYS
  • GPG_OWNERTRUST

Release

To release to Maven Central, go to branches and run the dedicated pipeline:

how_to_release_thumbnail.gif

(Click to enlarge)

After a few minutes, you will then see the artifacts in search.maven.org. You can use the release in a pom.xml:

<dependency>
  <groupId>org.bitbucket.thibaudledent.j8583</groupId>
  <artifactId>j8583</artifactId>
  <version>1.13.4</version>
</dependency>

Conclusion

The most time-consuming task is the setup of the Maven release (Jira ticket, GPG key…). The pipeline creation only takes a few minutes and, once completed, will save you a lot of time with each release!

References