JVM Advent

The JVM Programming Advent Calendar

Automating your release process

Your project has reached that state when it’s a good time to create a release. Everyone has an opinion on how releases should be made. Some developers rely on a set of scripts (at times arcane and outdated) to automate as much as they can; others have a todo list with a number of steps (some automated, some manual) that must be followed to get a release through the door; and some people have no idea where to start as they are underwhelmed by too many choices.

Death by a Thousand Options

Would you like to release artifacts to Maven Central? Would you like a GitHub tag and release page with a discussion topic to go along with it? What if you’re not into GitHub but GitLab, or Gitea? If the project produces runnable binaries (Java desktop app, CLI app, or similar) would you like to publish them via platform packagers such as Homebrew, Snapcraft, Chocolatey, and/or other options? What if you also need to publish said binaries to JFrog Artifactory or an Amazon S3 bucket? More over, once the release is done, would you like to tweet about it? Slack it? E-mail an announcement?

All these things are possible as many of the mentioned tools and services either provide integration APIs or require a set of files that may be created by hand or following a set of templates. But that’s a lot to take in if you want to do it by hand. This is where JReleaser, an upstart tool in the Java tooling space, comes into play.

What is it?

JReleaser provides answers to all previously mentioned concerns, and then some more. The tool is inspired by a popular project found in the Golang space, GoReleaser, also in JBang (which we saw on the 1st day of the Java Advent Calendar 2021) whose build setup served as a starting point for the tool. Nowadays JBang uses JReleaser to create and post its releases and binaries to a wide variety of platform packagers.

There are several options to configure JReleaser in your existing (or new) release process. You may use it as a CLI tool that may be installed in various ways. Or perhaps you’d prefer to configure it as a Maven plugin, or a Gradle plugin, or yes even a set of Ant tasks. Finally, you can also run it as a Docker image.

OK, Show me!

Let’s start simple. Let’s say you have a typical Maven based project that produces JARs that should be published to Maven Central. You keep using Maven to publish said publication (a combination of maven-sources-plugin, maven-javadoc-plugin, maven-gpg-plugin, maven-release-plugin, and nexus-staging-maven-plugin). By the way did you check all pom files comply with the guidelines for publishing to Maven Central before you upload them to a staging repository? If you have not done so yet then be sure to review your poms, don’t let the release fail at a later stage, catch these errors earlier with PomChecker.

Configuring JReleaser to your project now is as easy as adding the following snippet inside the <plugins> section of your pom:

<plugin>
    <groupId>org.jreleaser</groupId>
    <artifactId>jreleaser-maven-plugin</artifactId>
    <version>0.9.1</version>
</plugin>

Assuming that the current branch is named main and that your project is hosted at GitHub, you’ll need a JRELEASER_GITHUB_TOKEN environment variable whose value is a personal access token. Follow the official GitHub documentation to create such token. All that is left is to create a release by invoking:

$ mvn jreleaser:release

And after a few seconds you may browse to your project’s GitHub page and see the new release on the right side bar. If your project follows a convention for commit messages then perhaps you may use it to further customize changelog generation. JReleaser provides configuration out-of-the-box to support Conventional Commits and Gitmoji. Here’s for example a JBang release that follows Conventional Commits

While ModiTect’s oss-quickstart archetypes project follows Gitmoji.

What if there are additional binaries?

This is where things get more interesting as JReleaser gives you the option to package your binaries in such a way that they can be published to several distribution channels via platform packagers. For example, let’s say your project produces a binary distribution packaged as a Zip file. This type of distribution follows a particular file structure that both Maven’s appassembler and assembly plugins may create. This Zip file may be repackaged as Homebrew formula, a popular choice among people that use OSX as their operating system of choice. It could also be packaged as a Chocolatey or Scoop package, options preferred by Windows users. Might as well publish it as an SDK supported by Sdkman which targets all major platforms. Assuming the name of your project is app, the release version is 1.0.0, and the distribution Zip file is found at target/distribution/app/app-1.0.0.zip then the following configuration will enable publication to Homebrew, Scoop, and Sdkman altogether:

<plugin>
  <groupId>org.jreleaser</groupId>
  <artifactId>jreleaser-maven-plugin</artifactId>
  <version>0.9.1</version>
  <configuration>
    <jreleaser>
      <distributions>
        <app>
          <brew>
            <active>ALWAYS</active>
          </brew>
          <scoop>
            <active>ALWAYS</active>
          </scoop>
          <sdkman>
            <active>ALWAYS</active>
          </sdkman>
          <artifacts>
            <artifact>
              <path>${project.build.directory}/distributions/app/app-1.0.0.zip</path>
            </artifact>
          </artifacts>
        </app>
      </distributions>
    </jreleaser>
  </configuration>
</plugin>

Both Homebrew and Scoop packagers are driven by files created from templates. Sdkman requires additional credentials to publish packages. These credentials may be defined as environment variables or inside a configuration file that should not be committed to source control; this configuration file is usually stored in your home directory. Once all credentials, tokens, and secrets are configured you may have a look at the effective model before a release is posted, by invoking mvn jreleaser:config, here’s the output related to the distribution we just configured:

distributions:
    app:
        enabled: true
        active: always
        type: JAVA_BINARY
        executable: app
        executableExtension: bat
        artifacts:
            artifact 0:
                path: target/distributions/app/app-1.0.0.zip
                extraProperties:
                    artifactIndividualChecksum: true
        java:
            enabled: true
            version: 8
            groupId: com.acme
            artifactId: app
            mainClass: com.acme.Main
            multiProject: false
        brew:
            enabled: true
            active: always
            continueOnError: false
            templateDirectory: src/jreleaser/distributions/app/brew
            commitAuthor:
                name: jreleaserbot
                email: jreleaser@kordamp.org
            formulaName: app
            multiPlatform: false
            tap:
                enabled: true
                active: release
                owner: aalmiray
                name: homebrew-tap
                branch: HEAD
                username: aalmiray
                token: ************
                commitMessage: {{distributionName}} {{tagName}}
            cask:
                enabled: false
        scoop:
            enabled: true
            active: always
            continueOnError: false
            templateDirectory: src/jreleaser/distributions/app/scoop
            commitAuthor:
                name: jreleaserbot
                email: jreleaser@kordamp.org
            checkverUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/releases/latest
            autoupdateUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/releases/download/{{tagName}}/{{artifactFileName}}
            bucket:
                enabled: true
                active: release
                owner: aalmiray
                name: scoop-aalmiray
                branch: HEAD
                username: aalmiray
                token: ************
                commitMessage: {{distributionName}} {{tagName}}
        sdkman:
            enabled: true
            active: always
            continueOnError: false
            candidate: app
            command: major
            releaseNotesUrl: https://{{repoHost}}/{{repoOwner}}/{{repoName}}/releases/tag/{{tagName}}
            connectTimeout: 20
            readTimeout: 60
            consumerKey: ************
            consumerToken: ************

As you can appreciate, there’s more information that JReleaser has about your project, all coming from sensible defaults such as the name of the GitHub repositories where the Homebew formula and the scoop manifest will be committed, the commit author’s details and commit message, as well as other features. Note that some values use a special format for defining placeholders, these placeholders are Mustache templates that may be used to further customize and parameterize the release model. If you’re happy with the configuration you can take the next step and give the release a try but if you’re still hesitant to go live then don’t worry you can execute the release in dryrun mode, such as mvn jreleaser:full-release -Djreleaser.dryrun=true:

[INFO]  JReleaser 0.9.1
[INFO]  Configuring with jreleaser.yml
[INFO]    - basedir set to /Users/aalmiray/tmp/app
[INFO]  Reading configuration
[INFO]  Loading variables from /Users/aalmiray/.jreleaser/config.toml
[INFO]  Validating configuration
[INFO]  Project version set to 1.0.0
[INFO]  Release is not snapshot
[INFO]  Timestamp is 2021-12-07T18:37:58.207+01:00
[INFO]  HEAD is at 0357971
[INFO]  Platform is osx-x86_64
[INFO]  dryrun set to true
[INFO]  Generating changelog: out/jreleaser/release/CHANGELOG.md
[INFO]  Calculating checksums
[INFO]    [checksum] target/distributions/app/app-1.0.0.zip.sha256
[INFO]  Signing files
[INFO]  Signing is not enabled. Skipping
[INFO]  Uploading is not enabled. Skipping
[INFO]  Releasing to https://github.com/aalmiray/app
[INFO]   - uploading app-1.0.0.zip
[INFO]   - uploading app-1.0.0.zip.sha256
[INFO]   - uploading checksums_sha256.txt
[INFO]  Preparing distributions
[INFO]    - Preparing app distribution
[INFO]      [brew] preparing app distribution
[INFO]      [scoop] preparing app distribution
[INFO]      [sdkman] preparing app distribution
[INFO]  Packaging distributions
[INFO]    - Packaging app distribution
[INFO]      [brew] packaging app distribution
[INFO]      [scoop] packaging app distribution
[INFO]      [sdkman] packaging app distribution
[INFO]  Publishing distributions
[INFO]    - Publishing app distribution
[INFO]      [brew] publishing app distribution
[INFO]      [brew] setting up repository aalmiray/homebrew-tap
[INFO]      [scoop] publishing app distribution
[INFO]      [scoop] setting up repository aalmiray/scoop-aalmiray
[INFO]      [sdkman] publishing app distribution
[INFO]      [sdkman] publishing major release of 'app' candidate
[INFO]  Announcing release
[INFO]    [sdkman] announcing major release of 'app' candidate
[INFO]  Writing output properties to out/jreleaser/output.properties
[INFO]  JReleaser succeeded after 1.280 s

We can appreciate in this run (now with the full-release command) the following things:

  • A changelog was generated.
  • Checksums for all input files.
  • No GPG signing occurred because we did not configure it.
  • A release and all assets uploaded to the project’s GitHub repository.
  • Homebrew and Scoop packagers uploaded to their corresponding GitHub repositories.
  • A major release posted via Sdkman.

All files required by the release can be found inside target/jreleaser. Feel free to inspect them and make any adjustments as required. When done you may invoke the full-release command once again but this time without the dryrun flag to make it an official release!

What’s Next?

It’s up to you actually. The tool is flexible enough so tat its capabilities may be used as a whole or in smaller parts, depending on which aspects of your release process you’d like to automate. There are several integrations that you may configure with JReleaser to close the gap between your binaries and their users. One more thing, did I mention you can run the tool in multiple CI/CD services? Give it a try, you won’t be disappointed.

Conclusion

JReleaser started with one goal in mind: make it easier for Java binaries to be consumed by users. This is still its primary goal however it has grown since then to accommodate more features that make it a snap to post releases, create platform packagers and installers, announce a release in many ways.

Author: Andres Almiray

Andres is a Java/Groovy developer and a Java Champion with more than 2 decades of experience in software design and development. He has been involved in web and desktop application development since the early days of Java. Andres is a true believer in open source and has participated on popular projects like Groovy, Griffon, and DbUnit, as well as starting his own projects. Founding member of the Griffon framework and Hackergarten community event.

Next Post

Previous Post

Leave a Reply

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

© 2022 JVM Advent | Powered by Jetbrains LogoJetBrains & steinhauer.software Logosteinhauer.software

Theme by Anders Norén

%d bloggers like this: