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.
- gradle 7.5+
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.
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
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.)
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.
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.
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
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"))
}
}
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
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 owntag.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.
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 meanfoo:0.7.0
and at the same timebar:0.6.0
- there will be "version jumps" for individual submodules, e.g. last version of
bar
was0.6.0
and next is0.8.0
- this could lead to confusions because git tag
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.
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") {}
}
}
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.
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
andbar
modules that are versioned separately, and have versions2.0.0-rc.1
and2.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 befoo-v2.0.0-rc.2
bar
would bebar-v2.0.0-rc.3
root
would bev2.0.0-rc.5
baz
would bebaz-v2.0.0-rc.5
Here baz did not have a "previous version", hence will use themajor
and (optional)rc
identifiers fromroot
- 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 with2.0.0
version with their own prefixesbaz
will also be released with version2.0.0
. It did not have a "previous version", hence it will "inherit"major
and (optional)rc
identifiers fromroot
. And because newroot
version does not have any RC identifiers, it's not applied tobaz
either.
- IF we create a new version using
minor
increment, the tags created would be as follows:foo
would befoo-v2.1.0
bar
would bebar-v2.1.0
root
would bev2.1.0
baz
would bebaz-v2.0.0
Again, since baz did not have a "previous version", it will "inherit"major
and (optional)rc
identifiers fromroot
. Since newroot
version does not have any RC identifiers, it's not applied tobaz
either. NB!baz
, being a newly added module, would always setminor
andpatch
version identifier to0
for the "initial version"
- IF we create a new version with
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.
TODO
To run all tests execute ./gradlew clean test functionalTest
, which will run both unit and functional tests.
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.
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"