Skip to content

Latest commit

 

History

History

semantic-versioning

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 

io.github.serpro69.semantic-versioning:$version

Gradle Plugin Portal

Intro

Gradle settings plugin for semantic versioning releases.

This Gradle plugin provides support for semantic versioning of gradle builds. It is easy to use and extremely configurable. The plugin allows you to bump the major, minor, patch or pre-release version based on the latest version (identified from a git tag). It's main advantage (and main motivation for creating this project) over other similar semantic-versioning projects is that it explicitly avoids to write versions to build files and only uses git tags, thus eliminating the "Release new version" noise from the git logs.

The version can be bumped by using version-component-specific project properties, or alternatively based on the contents of a commit message. If no manual bumping is done via commit message or project property, the plugin will increment the version-component with the lowest precedence; this is usually the patch version, but can be the pre-release version if the latest version is a pre-release one. The plugin does its best to ensure that you do not accidentally violate semver rules while generating your versions; in cases where this might happen the plugin forces you to be explicit about violating these rules.

This is a settings plugin and is applied to settings.gradle(.kts). Therefore, version calculation is performed right at the start of the build, before any projects are configured. This means that the project version is immediately available (almost as if it was set explicitly - which it effectively is), and will never change during the build (barring some other, external task that attempts to modify the version during the build). While the build is running, tagging or changing the project properties will not influence the version that was calculated at the start of the build.

Note: The gradle documentation specifies that the version property is an Object instance. So to be absolutely safe, and especially if you might change versioning-plugins later, you should use the toString() method on project.version. However, this plugin does set the value of project.version to a String instance and hence you can treat it as such. While the version property is a string, it does expose some additional properties. These are snapshot, major, minor, patch and preRelease. snapshot is a boolean and can be used for release vs. snapshot project-configuration, instead of having to do an endsWith() check. major, minor, patch and preRelease bear the single version components for further usage in the build process. major, minor and patch are of type int and are always set, preRelease is a String and can be null if the current version is not a pre-release version.

Usage

Requirements

  • gradle 7.5+

Installation

The latest version of this plugin can be found on semantic-versioning gradle plugin page.

NB! While gradle makes a new release of a plugin available instantly, maven central takes some time to sync, and hence a new version of the plugin might not always work right away and report missing dependencies on the other submodules in this project.

Using the plugin is quite simple:

In settings.gradle.kts

plugins {
    id("io.github.serpro69.semantic-versioning") version "$ver"
}

Additionally, you may want to add semantic-versioning.json configuration file in the corresponding project-directory of each project in the build that should be handled by this plugin. The file must be in the same directory as settings.gradle file. This file allows you to set options to configure the plugin's behavior (see Json Configuration).

In most cases you don't want to version your subprojects separately from the main (root) project, and instead want to keep their versions in sync. For this you can simply set the version of each subproject to the version of the root project in the root project's build.gradle.kts:

subprojects {
    version = rootProject.version
}

The plugin will still evaluate each project in a gradle build and will set the versions for each of the projects found at build configuration time.

This is usually enough to start using the plugin. Assuming that you already have tags that are (or contain) semantic versions, the plugin will search for all nearest ancestor-tags, select the latest1 of them as the base version, and increment the component with the least precedence. The nearest ancestor-tags are those tags with a path between them and the HEAD commit, without any intervening tags. This is the default behavior of the plugin.

If you need the TagTask class in your Gradle build script, for example, for a construct like tasks.withType(TagTask) { it.dependsOn publish }, or when you want to define additional tag tasks, you can add the plugin's classes to the build script classpath by simply doing plugins { id 'io.github.serpro69.semantic-versioning' version '$version' apply false }.

1 Latest based on ordering-rules defined in the semantic-version specification, not latest by date.

Release Workflow

Incrementing new version can be done in one of the following ways, in ascending precedence order:

  • default increment
  • commit-based increment
  • gradle property-based increment
  • manually setting the version via -Pversion

Releasing from commits

A release based on the commit messages makes use of git.message configuration (see Configuration section for more details), and looks up release keywords in square brackets (i.e [minor]) in commit messages between the current git HEAD (inclusive) and the latest version (exclusive).

For example, if the latest version is 1.0.0, and one of the commits since then contains [minor] string, the next version will be 1.1.0

Version precedence follows semver rules ([major] -> [minor] -> [patch] -> [pre release]), and if several commits contain a release keyword, then the highest precedence keyword will be used. For example, if there were 3 commits between latest version and current HEAD, and one of those commits contains [minor] keyword and another contains [major] keyword, the next version will be bumped with the major increment.

By default, releasing with uncommitted changes is not allowed and will fail the :tag task. This can be overridden by setting git.repo.cleanRule configuration property to none. The default rule will only consider tracked changes, and will allow releases with untracked files in the repository. To override this and only allow releases of "clean" repository, set the cleanRule property to all (See Configuration section for more details.)

Skipping a release commit

There could be situations where a "release keyword" was added to a commit unintentionally, or a release with a given past commit is otherwise unwanted anymore. In this case the plugin supports "skipping" the past commits from the next release calculation via "[skip]" keyword in the commit message (Can be configured via git.message.skip configuration property.)

If a commit with the "skip" keyword is found between HEAD and the latest version when calculating the next release, only commits until the "skip commit" will be used to determine the next version.

Note

This only applies when releasing the next version from a commit message. Using -Pincrement gradle property will override this behavior as it takes precedence over commit messages.

Releasing via gradle properties

Instead of having the plugin look up keywords in commits, one can trigger the release with -Prelease property and set the next increment via -Pincrement instead.

See Gradle Properties for more details on available increment values and properties usage information.

Note

As mentioned above, releasing via gradle property (i.e. adding -Prelease to the gradle command) takes precedence over commit-based releases via keywords.

Configuration

Json Configuration

The full config file looks like this:

{
  "git": {
    "repo": {
      "directory": ".",
      "remoteName": "origin",
      "cleanRule": "tracked"
    },
    "tag": {
      "prefix": "v",
      "separator": "",
      "useBranches": "false"
    },
    "message": {
      "major": "[major]",
      "minor": "[minor]",
      "patch": "[patch]",
      "preRelease": "[pre release]",
      "skip": "[skip]",
      "ignoreCase": "true"
    }
  },
  "version": {
    "initialVersion": "0.1.0",
    "placeholderVersion": "0.0.0",
    "defaultIncrement": "minor",
    "preReleaseId": "rc",
    "initialPreRelease": "1",
    "snapshotSuffix": "SNAPSHOT"
  },
  "monorepo": {
    "sources": ".",
    "modules": [
      {
        "name": ":foo",
        "sources": "src/main",
        "tag": {
          "prefix": "foo-v",
          "separator": "",
          "useBranches": "false"
        }
      },
      {
        "name": ":bar",
        "sources": "."
      }
    ]
  }
}

Refer to Configuration class for kdocs on each available property.

  • TODO: add kdocs and link them instead of referring to the file

semantic-versioning Extension

The plugin provides a settings-extension called semantic-versioning, which, if used, takes precedence over the json-based configuration for any declared properties.

In the settings.gradle.kts, the extension can be configured as follows:

import io.github.serpro69.semverkt.gradle.plugin.SemverPluginExtension
import io.github.serpro69.semverkt.release.configuration.ModuleConfig
import io.github.serpro69.semverkt.release.configuration.TagPrefix
import kotlin.io.path.Path

settings.extensions.configure<SemverPluginExtension>("semantic-versioning") {
    git {
        message {
            major = "<major>"
            minor = "<minor>"
            patch = "<patch>"
            ignoreCase = true
        }
    }
    version {
        defaultIncrement = Increment.MINOR
        preReleaseId = "rc"
    }
    monorepo {
        sources = Path("src")
        module(":foo") {
            sources = "src/main"
            tag  {
                prefix = TagPrefix("foo-v")
                separator = ""
                useBranches = false
            }
        }
        module(":bar") {}
        modules.add(ModuleConfig(":baz"))
    }
}

Gradle Properties

The plugin makes use of the following properties:

name type description
release boolean creates a new release via gradle properties
preRelease boolean creates a new pre-release version from the current release
promoteRelease boolean promotes current pre-release to a release version
increment string sets increment for the next version

Note

Setting increment via gradle property (when using -Prelease property) takes precedence over commit-based keyword increments that are be configured via json and plugin-extension configurations.

Important

Using "secondary properties" (preRelease, promoteRelease, increment) requires setting release property, otherwise the properties will have no effect.

Accepted values for the increment property are (case-insensitive):

  • major
  • minor
  • patch
  • pre_release

Monorepo Support

This plugin supports the following types of projects:

  • non-monorepo - a project that does not configure monorepo modules; it uses the same tag prefix for all (if any) modules and is effectively tagged with a single git tag
  • single-tag monorepo - a project with monorepo configuration containing one or more modules; all modules use the same (default) tag prefix and are effectively tagged with a single git tag
  • multi-tag monorepo - a project with monorepo configuration containing one or more modules; some (or all) modules declare their own tag.prefix, and hence have their own git tags

Note

The main difference between the first two is that in "single-tag monorepo" projects, the project version is applied to each (configured) submodule individually, based on discovered changes in the configured sources, and some modules may be "kept back" on the previous version if no sources changes are discovered for that given module.

Single-Tag Monorepo

The plugin supports individual versioning of submodules (subprojects) of monorepo projects. To configure a monorepo project, add the following configuration (also supported via json configuration):

import io.github.serpro69.semverkt.gradle.plugin.SemverPluginExtension
import kotlin.io.path.Path

settings.extensions.configure<SemverPluginExtension>("semantic-versioning") {
  monorepo {
    // path to track changes for the monorepo submodules that are not configured in this block
    // in this case it will be used for :baz project
    sources = Path(".")
    module(":foo") {}
    module(":bar") {
        // customize given module sources to track changes
        sources = Path("src/main")
    }
    module(":foo:bar")
  }
}

include("foo")
include("bar")
include("baz")

Note

The module path should be a fully-qualified gradle project path. So for ./bar module in the root of a gradle mono-repo, this would be :bar, and for ./foo/bar module in a gradle mono-repo, this would be :foo:bar.

By default, the entire submodule directory is used to lookup changes. This can be customized via sources config property for a given submodule. In the above example, for bar module only changes to bar/src/main would be considered when making a new release. If no changes are detected between current git HEAD and last version in the repo, then the version property will not be applied to the submodule.

Root project and any submodule that is not included in the monorepo configuration are always versioned, regardless of detected changes. In the above example, baz submodule would always have a new version applied (if applicable according to Release Workflow rules.)

By versioning submodules separately one can avoid publishing modules that do not contain any changes between current HEAD and last version, for example by configuring maven publication task as such:

tasks.withType<PublishToMavenRepository>().configureEach {
  val predicate = provider { version.toString() != "0.0.0" }
  onlyIf("new release") { predicate.get() }
}

(Read more about conditional publishing in official gradle docs.)

We use version 0.0.0 above as a "placeholder version" (exact value can be configured by modifying the version.placeholderVersion config property via plugin extension or json configuration), which is set in gradle.properties file in project's root directory:

# gradle.properties
version=0.0.0

Any module that does not have changes will not get the new version applied to it, and hence will stay on version 0.0.0 throughout the build process runtime (barring some external modifications to the version property), and hence this can be used in conditional checks to skip certain tasks for a given module.

Note

Using the aforementioned "version placeholder" concept is mostly useful in the context of single-tag monorepo project, because we can't determine the latest version of a given module from a single git tag. With the multi-tag monorepo project, each configured submodule will have a real version assigned to it based on its own git tag.

This comes with some downsides which are good to be aware of when considering to version each submodule separately:

  • the whole project is still versioned in git via tags and according to semver rules, however (configured) submodules are versioned individually
    • this could lead to confusions because git tag v0.7.0 could potentially mean foo:0.7.0 and at the same time bar:0.6.0
    • there will be "version jumps" for individual submodules, e.g. last version of bar was 0.6.0 and next is 0.8.0

It can still be useful though, especially when each submodule has its own publishable artifacts. In such cases, more often than not one might not want to publish next version of an artifact that is exactly the same as the previous version.

Multi-Tag Monorepo

Since v0.10.0, the plugin also supports multi-tagging - each individual submodule can have a separate tag; it is also possible to mix and match, where one or more submodules follow the "root tag", and others have individual tags.

This can be useful to avoid some limitations of the single-tag monorepos, e.g. "version jumps".

Multi-Tag support is enabled when one or more modules declares a custom tag prefix via configuration, e.g. with settings extension:

settings.extensions.configure<SemverPluginExtension>("semantic-versioning") {
  monorepo {
    // path to track changes for the monorepo submodules that are not configured in this block
    // in this case it will be used for :baz project
    sources = Path(".")
    module(":foo") {}
    module(":bar") {
      // customize given module sources to track changes
      sources = Path("src/main")
      // modify tag configuration for the module
      tag {
        prefix = TagPrefix("bar-v")
      }
    }
    module(":foo:bar") {}
  }
}

Monorepo Versioning Workflow

For example monorepo versioning workflow diagrams refer to single-tag_monorepo_workflow.png and multi-tag_monorepo_workflow.png

Note

These diagrams were made with obsidian. The original canvas file can be found in docs/assets/monorepo_workflow.canvas

Important

In monorepo multi-tag projects, unlike single-tag monorepo and non-monorepo projects, each (configured) module will have a version applied to it. For modules that don't have any changes between the latest version and the next release, the "latest version" will be set.

Single-tag monorepo project type does not support this as it would be impossible to determine "latest version" of a given module from a single git tag.

Release-candidate versions in multi-tag monorepo projects

When a new submodule with a custom tag prefix is added to a multi-tag monorepo project that is currently in "pre-release state", the new submodule would use the last rc version identifier of the root project when creating the tag for self.

Consider the following scenario:

  • We have a multi-tag monorepo project with foo and bar modules that are versioned separately, and have versions 2.0.0-rc.1 and 2.0.0-rc.2 respectively
  • Current root project version is 2.0.0-rc.4
  • We add a new baz module that also has a custom tag prefix
  • Assuming that all modules had some changes between last version and HEAD
    • IF we create a new version with pre_release increment, the tags created would be as follows:
      • foo would be foo-v2.0.0-rc.2
      • bar would be bar-v2.0.0-rc.3
      • root would be v2.0.0-rc.5
      • baz would be baz-v2.0.0-rc.5 Here baz did not have a "previous version", hence will use the major and (optional) rc identifiers from root
    • IF we create a new version using promoteRelease property, all the modules would be promoted from RC version to release version and would be tagged with 2.0.0 version with their own prefixes
      • baz will also be released with version 2.0.0. It did not have a "previous version", hence it will "inherit" major and (optional) rc identifiers from root. And because new root version does not have any RC identifiers, it's not applied to baz either.
    • IF we create a new version using minor increment, the tags created would be as follows:
      • foo would be foo-v2.1.0
      • bar would be bar-v2.1.0
      • root would be v2.1.0
      • baz would be baz-v2.0.0 Again, since baz did not have a "previous version", it will "inherit" major and (optional) rc identifiers from root. Since new root version does not have any RC identifiers, it's not applied to baz either. NB! baz, being a newly added module, would always set minor and patch version identifier to 0 for the "initial version"

Note

There is currently no support for handling "pre-release versions" with "release versions" at the same time, because bumping the next version requires a different set of inputs. I.e. to bump a next rc version one would use -Prelease -Pincrement=pre_release; to create a pre-release version we would need -Prelease -PpreRelease (with an optional increment of major, minor, or patch) parameters.

Development

TODO

Testing

To run all tests execute ./gradlew clean test functionalTest, which will run both unit and functional tests.

Testing from IDE

To run functional tests in an IDE, a gradle runner has to be used because gradle needs to generate plugin-under-test-medatada.properties file.

If running tests with gradle runner is not possible (I, for one, couldn't yet figure out how to do that with kotest tests in Intellij, even though I have set "Run tests using: Gradle" in Intellij's Build Tools -> Gradle settings), one could first generate the metadata with gradle by running pluginUnderTestMedatata task, and then execute tests in the IDE without cleaning the build directory.

Manual Testing

To publish plugin and dependencies locally, run ./gradlew publishToMavenLocal publishAllPublicationsToLocalPluginRepoRepository (or use the make local), which will publish dependencies to local maven directory (e.g. ~/.m2/repository), and the plugin to ./build/local-plugin-repo.

Once that's done, one can set up gradle to fetch the plugin from local sources by updating settings.gradle.kts:

pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        mavenLocal() // needed to fetch dependencies of the plugin which were published locally
        mavenLocal {
            url = uri("/path/to/semver.kt/semantic-versioning/build/local-plugin-repo")
        }
    }
}

plugins {
    id("io.github.serpro69.semantic-versioning") version "0.0.0-dev"
}

rootProject.name = "test"