Revisiting Publication to Maven Central with Apache Maven

Some weeks ago I posted an entry on Publishing to Maven Central with Apache Maven which shows the configuration I put in place on a handful of Open Source projects I maintain. The trigger that starts a release workflow is a commit message with "[release]" as a prefix. I was quite happy with the results, after all I've got the whole release pipeline working on automatic, however two things were nagging me. Firstly that a release workflow would always require a commit. What if I just wanted to publish a release on the go? Add up pushing a release from any branch? Secondly is that while the maven release plugin takes care of setting the release version, tagging, building & publishing, then bumping all POM versions again and committing all changes I just couldn't shake the feeling that something might break during the release and I'll end up with a broken tag. Mind that the release plugin cannot fully rollback a broken release just yet. Also, passing additional arguments to the release build is a bit tricky.

Thus I turned out to the best possible source of inspiration and answers (</sarcasm>): I twitted my frustration with the release plugin.

And what do you know, I've got into a conversation with fellow Java Champion Manfred Riem (@mnriem). He shared his setup (manorrock/parrot) which skips the use of the release plugin and configures a manual trigger for pushing a release. We went back a forth with a few questions and ended up with a release workflow that fits my needs much better. Thank you Manfred!

What follows are the changes I made. As before, some of these steps may be suitable for your needs, other may be customized or skipped depending on what you want.

First things first, I've got rid of maven-release plugin, instead I'll use the nexus-staging plugin that bounds to the deploy phase. This means I no longer have automatic versioning & tagging, but I can do that in the workflow itself. Also, moved the nexus-staging plugin to a profile, there's no need to have it configured on every run, particularly as it's run as an extension which adds a few milliseconds on every invocation. Lesson learned, if you don't need a plugin for your main execution then put it in a profile and activate when required; don't waste time executing goals that do not provide value for that particular build invocation. The POM contains the following profile definition

<profile>
    <id>remote-deploy</id>
    <activation>
        <property>
            <name>release</name>
        </property>
    </activation>
    <build>
        <defaultGoal>deploy</defaultGoal>
        <plugins>
            <plugin>
                <groupId>org.sonatype.plugins</groupId>
                <artifactId>nexus-staging-maven-plugin</artifactId>
                <extensions>true</extensions>
                <configuration>
                    <serverId>central</serverId>
                    <nexusUrl>${nexus.url}</nexusUrl>
                    <autoReleaseAfterClose>true</autoReleaseAfterClose>
                </configuration>
            </plugin>
        </plugins>
    </build>
</profile>

This profile can be activated by setting the release property (-Drelease) or specifying the profile's name (-Premote-deploy). Where's the plugin version you say? Where it should be, inside the <pluginManagement> block of course. I believe those are all the required changes to the POM. Next is to figure out the Github workflow itself. Manfred configured a workflow dispatch (trigger) that takes care of updating the version and tagging. Once a tag is created another workflow (release) is started, which takes case of the actual release. Manfred chose to create the tag on a temporary branch which means the tag and the update POMs will be gone after the release. Personally I wanted to keep the changes, also I wanted to make sure the project builds before the tag is made. Thus my trigger workflow ended up looking like this

name: Trigger

on:
  workflow_dispatch:
    inputs:
      branch:
        description: "Branch to release from"
        required: true
        default: "main"
      version:
        description: "Release version"
        required: true

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.4

      - name: Setup Java
        uses: actions/setup-java@v1.4.3
        with:
          java-version: 1.8

      - name: Cache Maven
        uses: actions/cache@v2.1.4
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2

      - name: Build
        run: ./mvnw --no-transfer-progress -B --file pom.xml verify

  tag:
    name: Tag
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Checkout sources
        uses: actions/checkout@v2.3.4
        with:
          # required for triggering release workflow on tagging
          token: ${{ secrets.GIT_ACCESS_TOKEN }}

      - name: Setup Java
        uses: actions/setup-java@v1.4.3
        with:
          java-version: 1.8

      - name: Cache Maven
        uses: actions/cache@v2.1.4
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2

      - name: Create tag
        run: |
          git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
          BRANCH=${{ github.event.inputs.branch }}
          VERSION=${{ github.event.inputs.version }}
          echo "Releasing $VERSION from $BRANCH branch"
          git checkout $BRANCH
          ./mvnw -B versions:set versions:commit -DnewVersion=$VERSION
          git config --global user.email "kordamp-release-bot@kordamp.org"
          git config --global user.name "kordamp-release-bot"
          git commit -a -m "Releasing version $VERSION"
          git tag v$VERSION
          git push origin $BRANCH
          git push origin v$VERSION

There are 3 sections in this workflow:

  1. Defines the conditions that trigger this workflow (it's a workflow dispatch) and the arguments required to execute said workflow. The arguments are the branch to release from and the release version.
  2. Defines a build job. Note that the build instructions are mvn verify. If you're wondering why that's the case and not the often commonly seen mvn clean install then I'd suggest you to have a look at this post.
  3. Defines the tag job, which depends on build. This means the tag job will be skipped if the build fails. There's no file sharing between jobs enabled by default thus we have to checkout the repository again but take special note that this time an additional access token is required. This is to make sure that committed changes will trigger other workflows. This is the secret sauce! The token is a personal access token that you can create and store as a secret on the repository. In this section we can appreciate that the maven-versions plugin is used to update the version of all POMs, as well as creating the tag and pushing the changes on the same branch as the input.

The final step is to define the release workflow as follows:

name: Release

on:
  push:
    tags:
      - v*

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.4

      - name: Cache Maven
        uses: actions/cache@v2.1.4
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-m2

      - name: Setup Java
        uses: actions/setup-java@v1.4.3
        with:
          java-version: 1.8
          server-id: central
          server-username: MAVEN_USERNAME
          server-password: MAVEN_CENTRAL_TOKEN
          gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
          gpg-passphrase: MAVEN_GPG_PASSPHRASE

      - name: Release
        env:
          MAVEN_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
          MAVEN_CENTRAL_TOKEN: ${{ secrets.SONATYPE_PASSWORD }}
          MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
        run: |
          export GPG_TTY=$(tty)
          ./mvnw --no-transfer-progress -B --file pom.xml \
            -Drepository.url=https://${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git \
            -Dmaven.site.skip=true -Drelease=true deploy

The 2 changes from the previous version are:

  1. The trigger is when a new tag starting with v is added to the repository. This is what the trigger workflow does but also gives me the option to manually push a tag and kickstart the release process. Neat!
  2. The maven command now sets the release property which enables all profiles required for a release, such as publication (sources & javadoc), gpg (signatures), and remote-deploy (self explanatory).

I feel that this workflow setup gives me more fine grained control. The only thing I miss is the automatic version bump that the maven-release plugin performs at the end, however this can easily be solved by manually invoking the maven-versions plugin when needed.

After fumbling around with the configuration a bit more I ended up creating a parent POM project for all my Maven projects. This parent POM defines all the common behavior I want to share on all other projects, and as such it defines the profiles previously mentioned. The project is located at https://github.com/kordamp/kordamp-maven-parent/ and it also defines a manual trigger as explained in this post.

Keep on coding!

Image by jplenio from Pixabay

Liked it? Take a second to support aalmiray on Patreon!

Trackbacks/Pingbacks

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ˆ Back To Top