A plugin to take care of versioning your code, so you don't have to.
gver provides a domain specific language (DSL) that lets you describe in simple terms how to derive your version numbers from your git history. This includes expressions to test the names of affected files and to check the type of referenced issues, allowing you to skip a version update when no code or build file was altered or to derive the type of change (feature vs. bugfix) from the implemented/fixed ticket.
In order to use the plugin, it needs to be configured. Currently, there is no default configuration.
The following example considers commits to be breaking changes when the commit message contains either the
hashtags #major
or #break
. If the commit message
contains neither, the commit message is searched for the pattern #\d
and, in case one is found, a matching issue is
looked up in the Github repository
"dmfs/jems". If an issue is found, the change type is determined by whether the issue has a bug
tag or not. If no
Github issue was found, the change type is
considered to be a minor change when the commit message contains either the pattern #feature\b
or
(?i)\b(implement(s|ed)?|close[sd]?) #\d+\b
, and a patch when it contains (?i)\b(fix(e[sd])?|resolve[sd]?) #\d+\b
.
gver {
issueTracker GitHub {
repo = "dmfs/jems" // the name of the GitHub repo that contains the issues for this project
if (project.hasProperty("GITHUB_API_TOKEN")) {
accessToken = GITHUB_API_TOKEN // should be stored in your global or local gradle properties
}
}
changes {
are none when {
// trivial changes don't get a new version number
commitMessage contains("(?i)#trivial\\b")
}
are major when {
commitMessage contains("(?i)#major\\b")
}
are major when {
commitMessage contains("(?i)#break\\b")
}
are minor when {
commitMessage contains(~/(?i)\b(implement(s|ed)?|close[sd]?) #(?<issue>\d+)\b/) {
where("issue") { isIssue { not(labeled "bug") } }
}
}
are patch when {
commitMessage contains(~/(?i)\b(fix(e[sd])?|resolve[sd]?) #(?<issue>\d+)\b/) {
where("issue") { isIssue { labeled "bug" } }
}
}
are patch otherwise // every other change always results in a new patch version
}
preReleases {
// pre-release on the main branch are versioned like a.b.c-beta.x
on ~/main/ use { "beta" }
// pre-release on the feature branches are versioned like a.b.c-alpha-<feature-name>.x
on ~/feature\/(?<name>.*)/ use { "alpha-${group('name')}" }
// every other pre-release is versioned like a.b.c-SNAPSHOT.x
on ~/.*/ use { "SHAPSHOT" }
}
suffixes {
append ".${new Date().format("yyyyMMdd'T'HHmmss")}-SNAPSHOT" when {
branch not(matches(~/main/))
}
}
tag {
// releases can only be tagged on the main branch, on other branches the `gitTagRelease` task will fail
with release when {
branch matches(~/main|release.*/)
}
// pre-releases can only be tagged on feature and bugfix branches
with preRelease when {
branch matches(~/^(feature|bugfix).*/)
}
// other branches can't be tagged
with nothing otherwise
}
}
The following sections describe the DSL to specify your versioning scheme. Note that the DSL is not stable yet and may change with every new version.
When practising semantic versioning, the most important step is to understand the kind of change (major, minor, bugfix). gver provides a DSL to describe how to derive the kind of change based on commit message or referenced issues.
The top-level element is changes
which takes a closure describing when a change is considered a major, minor or bugfix
change.
The list of conditions is evaluated top to bottom until the first one matches.
gver {
changes {
are major when {
// condition
}
are minor when {
// another condition
}
// more change types
}
}
A change type can appear multiple times in the list if it's to be applied under multiple conditions.
In addition to major
, minor
, patch
, gver also knows a none
type to identify trivial changes that should not
result in a new version,
e.g. typo fixes in documentation files.
The invalid
change type can be used to validate commits. As soon as the project version is determined, this will
throw an exception if the condition is fulfilled and the build will fail.
Example:
gver {
changes {
are invalid when {
// enforce messages compliant to conventionalcommits.org
commitTitle not(matches(~/^[a-z]+(\([^)]+\))?!?: .*/))
}
}
}
Note that all conditions inside a change type closure must match in order to apply the change type. If you need to
express a logical or
, describe the same change type with the other condition underneath the first one or use anyOf
described below.
The otherwise
case should be last in the list as it catches all cases that didn't match any of the other conditions.
The default is to
increase any pre-release version or create a new minor pre-release if no pre-release version is present yet.
Note, the notation
gver {
changes {
...
otherwise patch
}
}
has been deprecated in favour of
gver {
changes {
...
are patch otherwise
}
}
There are a couple of existing conventions for semantic commit messages.
At present, gver supports applying Conventional Commits using the
follow
expression like so
gver {
changes {
follow conventionalCommits
}
}
You can still add rules with higher or lower priority, for instance:
gver {
changes {
are major when { commitTitle contains(~/💥/) }
are minor when { commitTitle contains(~/🎀/) }
are patch when { commitTitle contains(~/🩹/) }
follow conventionalCommits
are none otherwise
}
}
There are two flavours strictConventionalCommits
and conventionalCommits
. The latter falls through and applies
the next or default change rules if a commit doesn't conform to conventional commits, whereas the former will fail and
break the build when a commit doesn't conform to the convention.
At present, the type of change can be determined from the current branch name or the commit history up to the last tagged version.
commitMessage
commitMessage
tests the entire commit message. You'd typically use it with contains
or matches
to test it with a
regular expression.
gver {
changes {
are major when {
commitMessage contains(~/#breaking\b/)
}
}
}
This identifies major changes by the presence of the #breaking
hashtag in the commit message.
Another common pattern is to consider a change a bugfix when it contains one of "fixes", "fixed", "resolves" or "
resolved" followed by a #
and
a numeric issue identifier.
gver {
changes {
are patch when {
commitMessage contains("(?i)\\b(fix(e[sd])?|resolve[sd]?) #\\d+\\b")
}
}
}
commitTitle
This works almost like commitMessage
but only takes the first line of a commit message into account.
branch
This can be used to match the name of the current Git head.
The following configuration considers all changes on the main branch as minor changes, whereas changes on release
branches are considered to be
patches.
gver {
changes {
are minor when {
branch matches(~/main/)
}
are patch when {
branch matches(~/release\/.*/)
}
}
}
This condition allows you to determine a change type based on the files that have been affected by a commit. It takes
a Predicate
of a Set<String>
like anyThat
, noneThat
or only
.
Example:
gver {
changes {
are none when {
affects only(matches(~/.*\.(md|adoc)/)) // only documentation updated, don't generate new version
}
...
}
}
In some environments, e.g. Jenkins builds, the plugin may not be able to
determine branch names from the Git repo.
Instead, you may have to grab the current branch name from
an environment variable like BRANCH_NAME
. Typically, you probably
still want to combine that with the branch
condition to support
local builds.
Example:
gver {
changes {
are minor when {
branch matches(~/feature\/.*/) // for local builds
}
are minor when {
envVariable "BRANCH_NAME", matches(~/feature\/.*/) // for Jenkins builds
}
are patch when {
branch matches(~/bugfix\/.*/)
}
are patch when {
envVariable "BRANCH_NAME", matches(~/bugfix\/.*/)
}
...
}
}
A change type matches when all conditions in the Closure after when
match. An "any of"
behavior is achieved by adding more change types underneath each other, with the first one
matching being applied.
Sometimes this can become a bit lengthy and more difficult to understand. If all conditions
have the same priority, you can put them together into one change type and combine them with
anyOf
.
gver {
changes {
are minor when {
anyOf {
branch matches(~/feature\/.*/) // for local builds
envVariable "BRANCH_NAME", matches(~/feature\/.*/) // for Jenkins builds
}
}
are patch when {
anyOf {
branch matches(~/bugfix\/.*/)
envVariable "BRANCH_NAME", matches(~/bugfix\/.*/)
}
}
...
}
}
gver can apply different pre-release versions, based on the current Git head's name, e.g.
gver {
...
preReleases {
on ~/main/ use { "beta" }
on ~/feature\/(?<name>.*)/ use { "alpha-${group('name')}" }
on ~/.*/ use { "SHAPSHOT" }
}
}
In the closure passed to use
you can use any groups declared in your regular expression. The resulting pre-release
version will automatically
be sanitized to comply with SemVer syntax.
When your pre-release doesn't end with a numeric segment, the next pre-relase will automatically append .1
and
continue counting from that.
If the pre-release already ends with a numeric segment, it will be incremented by 1 with every subsequent pre-release.
gver can append a suffix to pre-release versions. The suffix is always appended verbatim. This is primarily useful to
create
SNAPSHOT
releases.
Suffixes are specified with append "<suffix>" when {}
where the closure contains one of the conditions also used in
the changes
DSL.
Examples
gver {
...
suffixes {
append ".${new Date().format("yyyy-MM-dd")}-SNAPSHOT" when {
branch not(matches(~/main/))
}
}
...
}
Appends a suffix like 2022-12-13-SNAPSHOT
to every pre-release that's not on a main branch.
You can set a suffix unconditionally by using otherwise
instead of when
gver {
...
suffixes {
append ".${new Date().format("yyyy-MM-dd")}-SNAPSHOT" otherwise
}
...
}
Unconditional suffixes always match, hence any append
clause following an unconditional suffix will never be applied.
If no suffixes are specified, the suffix ".<timestamp>-SNAPSHOT"
is added to every pre-release where <timestamp>
is
either the date and time
(in UTC and RFC 5545 notation) of the HEAD commit in case the working tree is clean or the current date and time if the
working tree is dirty.
In order to apply the default suffix conditionally you can use the special suffix DEFAULT
like
gver {
...
suffixes {
append DEFAULT when { branch not(matches(~/main/)) }
}
...
}
To disable any suffix use
gver {
...
suffixes {
append "" otherwise
}
...
}
gver can tag your current Git head with the current pre-release or the next release version.
By default, tags can only be created on main
or master
branches.
Tagging can be configured with conditions like above. The following tag types are currently supported
release
preRelease
none
With none causing any tag task to fail when the condition matches.
Example:
gver {
...
tag {
with release when {
branch matches(~/^release\/.*/)
}
with preRelease when {
branch matches(~/^(feature|bugfix)\/.*/)
}
with none otherwise
}
}
Note: the releaseBranchPattern
directive is deprecated and will be removed.
The following tasks are available to tag the current Git head:
gitTagRelease
Adds a release version tag to the current Git head, if allowed by the tag
configuration. Fails with an Exception
otherwise.
gitTagPreRelease
Adds a pre-release version tag to the current Git head, if allowed by the tag
configuration. Fails with an Exception
otherwise.
gitTag
Adds a version tag to the current Git head. The release type is determined by the tag
configuration. The first
entry that matches the current head to be precise.
In contrast to gitTagRelease
and gitTagPreRelease
this does not throw an Exception when the configuration does not
permit creation of a tag.
⚠️ This behavior has changed since 0.35.0, when this always created a pre-release version tag. To create a pre-release
version tag use gitTagPreRelease
now.
gver can determine the type of change by checking the issues referred to in the commit message. At present, it supports two issue trackers, GitHub and Gitea.
If your tickets are tracked at GitHub, you can determine the type of change from the labels of an issue. First you configure gver to check issues on GitHub:
gver {
issueTracker GitHub {
repo = "dmfs/gver" // account/repo
if (project.hasProperty("GITHUB_API_TOKEN")) {
// put the api token into your global gradle properties, never under version control
accessToken = GITHUB_API_TOKEN
}
}
...
}
For public repos you can omit the API token. Note, however, that GitHub has a quota for unauthenticated API requests. When you don't provide an API token, the resulting version may be incorrect. Make sure you always provide a valid token in deployment environments.
Now you can specify change types testing the issues.
gver {
...
are minor when {
commitMessage contains(~/#(?<issue>\d+)\b/) {
where("issue") { isIssue { labeled "enhancement" } }
}
}
are patch when {
commitMessage contains(~/#(?<issue>\d+)\b/) {
where("issue") { isIssue { not(labeled "enhancement") } }
}
}
...
}
This considers a change to be minor when it contains a GitHub issue reference to an issue with the label enhancement
and to be a patch when
it contains a GitHub issue reference to an issue without the label enhancement
.
The Gitea DSL is much like the GitHub DSL, you just need to provide the Gitea host name
gver {
issueTracker Gitea {
host = "gitea.example.com"
repo = "dmfs/gver" // account/repo
if (project.hasProperty("GITEA_API_TOKEN")) {
// put the api token into your global gradle properties, never under version control
accessToken = GITEA_API_TOKEN
}
}
...
}
At present a dirty working tree always results in a pre-release version. Depending on the last tag, either the pre-release or the minor version is incremented. This prevents accidental release builds after a file has been changed.
Note, the Maven Plugin is currently in experimental state and may be subject to change.
You can use some of the features with Maven too. There is a plugin that takes the same configuration and provides a goal to apply the version to the project and create a tag.
Apply the gver-maven
plugin and configure it like this:
<build>
<plugins>
<plugin>
<groupId>org.dmfs</groupId>
<artifactId>gver-maven</artifactId>
<version>0.37.0</version>
<configuration>
<config>L: {
changes {
are none when {
affects only(matches(~/.*\.md/))
}
are major when {
commitMessage contains(~/(?i)#(major|break(ing)?)\b/)
}
are minor when {
commitMessage^ contains(~/(?i)#minor\b/)
}
otherwise patch
}}
</config>
</configuration>
</plugin>
</plugins>
</build>
Note, the L:
is not a typo, it must be present before the closure.
The configuration itself works as described for the Gradle Plugin above, although some options may not be usable with
Maven.
gver:tag-and-set-version
To update the project to the current version (as determined using the tag configuration) run
mvn gver:tag-and-set-version
Important: (unless the project version equals the determined version) this modifies the pom files, resulting in a dirty working tree. This means, after executing this goal, the next version will be e different every time.
Copyright 2024 dmfs GmbH
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an " AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.