Set projects versions based on git tags and following semantic versioning.
Inspired on Reckon but centered on supporting multi-project
versions and combine normal stages with snapshot
stage.
In order to support configuration cache or project isolation the plugin must be applied to each
project or using the settings plugin. Avoid using allprojects
or subprojects
.
// build.gradle.kts
plugins {
id("com.javiersc.semver") version "$version"
}
It is possible to apply the plugin to all projects if the plugin is applied in the settings.gradle
or settings.gradle.kts
file:
// settings.gradle.kts
plugins {
id("com.javiersc.semver") version "$version"
}
Check documented examples or test examples to understand easily how it works.
There are three project properties which the plugin uses to detect automatically the current version
based on the last tag in the current branch: semver.stage
, semver.scope
and semver.tagPrefix
.
They can be set via CLI, for example:
./gradlew "-Psemver.stage=final" "-Psemver.scope=major" "-Psemver.tagPrefix=v"
semver.stage
indicates the stage to be changed, for example,alpha
semver.scope
indicates the scope to be changed, for example,patch
semver.tagPrefix
is used to know which version is going to be changed based on the tag prefix, for examplev
. If the projects have different tag prefix, it is necessary to disambiguate which version is going to be bumped.semver.logOnlyOnRootProject
is used to log the version in the CLI only on the root project, by default it isfalse
. It can be useful to avoid the version log in multi-project builds. It can be added to the rootgradle.properties
file to avoid passing it constantly via CLI.
Default values:
default value | Optional | |
---|---|---|
stage | auto |
Yes* |
scope | auto |
Yes* |
tagPrefix | auto |
Yes* |
logOnlyOnRootProject | false |
Yes* |
Depends on the use case*
semver {
isEnabled.set(true)
tagPrefix.set("")
commitsMaxCount.set(-1)
gitDir.set(rootDir.resolve(".git"))
}
- Default values:
default value | |
---|---|
isEnabled | true |
tagPrefix | , empty string |
commitsMaxCount | -1 |
gitDir | rootDir.resolve(".git") |
mapVersion function | calculated version |
tagPrefix
is used to asociate a project version with a tag prefix, and it allows having different
versions in multi-project builds.
An example can be setting the extension prefix to v
in a specific project A and the last tags in
the last commit are: v1.0.0
and w3.0.1
. The project A version is v1.0.0
. If a project B sets
the prefix to w
, the project B version is w3.0.1
.
In order to improve the performance on large repositories with a lot of commits, it is possible to
limit the number of commits to be checked via commitsMaxCount
. By default, it is -1
which means
that all commits are checked.
semver
contains commits: Provider<List<Commit>>
to get the commits in the current branch and the
associated tags to each one which can be useful in some use cases.
"-Psemver.tagPrefix=v"
semver.tagPrefix=v
If it is necessary to bump the project version, for example to v3.0.2
from v3.0.1
, but at same
time there are more project with different prefixes, the plugin needs to know which tag prefix is
going to be bumped, so semver.tagPrefix
property is the solution to that problem.
To get it working:
./gradlew "-Psemver.scope=patch" "-Psemver.tagPrefix=v"
It is possible to set the project tag prefix via Gradle property if some third-party plugin requires the version in configuration phase. This property is:
semver.project.tagPrefix=v
As it can be useful to change the number of commits to be checked, it is possible to set it via properties too:
semver.commitsMaxCount=100
Or in the CLI:
./gradlew "-Psemver.commitsMaxCount=100"
The semver
extension has a mapVersion
function which allows to map the version easily:
// if last tag is v3.0.1, and the Kotlin version is 1.9.0,
// the version will be `v3.0.1+1.9.0`
semver {
tagPrefix.set("v")
mapVersion { gradleVersion: GradleVersion ->
val kotlinVersion: String = getKotlinPluginVersion()
"${gradleVersion.copy(metadata = kotlinVersion)}"
}
}
semver {
mapVersion { "1.0.0" }
}
If all projects are not using a tag prefix, or in other words, the tag prefix is empty, both the property in the extension and the project property via CLI or
gradle.properties
file are irrelevant.
If all projects share a tag prefix, it is easier to set it in the root project
gradle.properties
file instead of passing it constantly via CLI.
The whole format can be:
<major>.<minor>.<patch>[-<stage>.<num>][.<commits number>+<hash>][+<metadata>]
- Format:
<major>.<minor>.<patch>
- Example:
1.0.0
- Format:
<major>.<minor>.<patch>[-<stage>.<num>]
- Example:
1.0.0-alpha.1
-
Format:
- Clean repository:
<major>.<minor>.<patch>[-<stage>.<num>][.<commits number>+<hash>]
- Dirty repository:
<major>.<minor>.<patch>[-<stage>.<num>][.<commits number>+<DIRTY>]
- Clean repository:
-
Examples:
1.0.0.4+26f0484
1.0.0.4+DIRTY
It is used the
DIRTY
suffix instead of a timestamp in order to avoid issues with any Gradle cache.
- Format:
<major>.<minor>.<patch>-SNAPSHOT
- Example:
1.0.0-SNAPSHOT
To change between stages, use the Gradle project property -Psemver.stage=<stage>
There are reserved stages that can be used to create certain versions:
final
: It creates a version without a suffix stage, for example,1.0.1
.auto
: It calculates automatically the next stage based on the previous stage.snapshot
: It generate the next snapshot version, for example,1.0.1-SNAPSHOT
.
# gradle.properties
semver.tagPrefix=v
# Last tag = v1.0.0-alpha.1
./gradlew "-Psemver.stage=alpha" # v1.0.0-alpha.2
./gradlew "-Psemver.stage=beta" # v1.0.0-beta.1
./gradlew "-Psemver.stage=rc" # v1.0.0-rc.1
./gradlew "-Psemver.stage=snapshot" # v1.0.1-SNAPSHOT (uses the next patch version)
./gradlew "-Psemver.stage=final" # v1.0.0
./gradlew "-Psemver.stage=auto" # v1.0.0-alpha.2
# Last tag = v1.0.0
./gradlew "-Psemver.stage=alpha" # v1.0.1-alpha.1
./gradlew "-Psemver.stage=beta" # v1.0.1-beta.1
./gradlew "-Psemver.stage=rc" # v1.0.1-rc.1
./gradlew "-Psemver.stage=snapshot" # v1.0.1-SNAPSHOT (still uses the same patch version)
./gradlew "-Psemver.stage=final" # v1.0.1
./gradlew "-Psemver.stage=auto" # v1.0.1
The stage order is based on the Gradle official rules, some samples are:
- If both are non-numeric, the parts are compared alphabetically, in a case-sensitive manner:
1.0.0-ALPHA.1
<1.0.0-BETA.1
<1.0.0-alpha.1
<1.0.0-beta.1
. dev
is considered lower than any non-numeric part:1.0.0-dev.1
<1.0.0-ALPHA.1
<1.0.0-alpha.1
<1.0.0-rc.1
.- The strings
rc
,snapshot
,final
,ga
,release
andsp
are considered higher than any other string part (sorted in this order):1.0.0-zeta.1
<1.0.0-rc.1
<1.0.0-snapshot
<1.0.0-ga.1
<1.0.0-release.1
<1.0.0-sp.1
<1.0.0
. - These particular values are NOT case-sensitive, as opposed to regular string parts and do not
depend on the separator used around them:
1.0.0-RC.1
==1.0.0-rc.1
.
Gradle's docs can be found here
To change between scopes, use the Gradle property -Psemver.scope=<scope>
The scope has to be one of major
, minor
, patch
or auto
.
# gradle.properties
semver.tagPrefix=v
# Last tag = v1.0.0-alpha.1
./gradlew "-Psemver.scope=major" # v2.0.0
./gradlew "-Psemver.scope=minor" # v1.1.0
./gradlew "-Psemver.scope=patch" # v1.0.1
./gradlew "-Psemver.scope=auto" # v1.0.0-alpha.2 (uses the next num version)
# Last tag = v1.0.0
./gradlew "-Psemver.scope=major" # v2.0.0
./gradlew "-Psemver.scope=minor" # v1.1.0
./gradlew "-Psemver.scope=patch" # v1.0.1
./gradlew "-Psemver.scope=auto" # v1.0.1 (uses the next patch version)
# gradle.properties
semver.tagPrefix=v
# Last tag = v1.0.0-alpha.1
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=major" # v2.0.0-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=major" # v2.0.0-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=major" # v2.0.0-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=major" # v2.0.0
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=major" # v2.0.0-SNAPSHOT
./gradlew "-Psemver.stage=auto" "-Psemver.scope=auto" # v1.0.0-alpha.2
# Last tag = v1.0.0-alpha.1
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=minor" # v1.1.0-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=minor" # v1.1.0-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=minor" # v1.1.0-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=minor" # v1.1.0
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=minor" # v1.1.0-SNAPSHOT
# Last tag = v1.0.0-alpha.1
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=patch" # v1.0.1-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=patch" # v1.0.1-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=patch" # v1.0.1-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=patch" # v1.0.1
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=patch" # v1.0.1-SNAPSHOT
# Last tag = v1.0.0-alpha.1
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=auto" # v1.0.0-alpha.2
./gradlew "-Psemver.stage=beta" "-Psemver.scope=auto" # v1.0.1-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=auto" # v1.0.1-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=auto" # v1.0.1
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=auto" # v1.0.1-SNAPSHOT
# Last tag = v1.0.0
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=major" # v2.0.0-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=major" # v2.0.0-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=major" # v2.0.0-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=major" # v2.0.0
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=major" # v2.0.0-SNAPSHOT
./gradlew "-Psemver.stage=auto" "-Psemver.scope=auto" # v1.0.1
# Last tag = v1.0.0
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=minor" # v1.1.0-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=minor" # v1.1.0-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=minor" # v1.1.0-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=minor" # v1.1.0
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=minor" # v1.1.0-SNAPSHOT
# Last tag = v1.0.0
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=patch" # v1.0.1-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=patch" # v1.0.1-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=patch" # v1.0.1-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=patch" # v1.0.1
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=patch" # v1.0.1-SNAPSHOT
# Last tag = v1.0.0
./gradlew "-Psemver.stage=alpha" "-Psemver.scope=auto" # v1.0.1-alpha.1
./gradlew "-Psemver.stage=beta" "-Psemver.scope=auto" # v1.0.1-beta.1
./gradlew "-Psemver.stage=rc" "-Psemver.scope=auto" # v1.0.1-rc.1
./gradlew "-Psemver.stage=final" "-Psemver.scope=auto" # v1.0.1
./gradlew "-Psemver.stage=snapshot" "-Psemver.scope=auto" # v1.0.1-SNAPSHOT
There are three tasks:
printSemver
: Prints the tag in CLI and create a file inbuild/semver/version.txt
which has two lines; the version without the tag and the version including the tag.createSemverTag
. Creates a git tag.pushSemverTag
. Creates and pushes a git tag to the remote.
You can combine them with any semver
project properties to ensure the correct tag version is
printed, created or pushed.
pushSemverTag
can use a specific remote if the Gradle property semver.remote
is set. If it is
not set, origin
is used if it exists, if not, the first remote by name is used. If there is no
remote, the task fails.
Samples:
./gradlew createSemverTag
./gradlew createSemverTag "-Psemver.stage=alpha"
./gradlew pushSemverTag
./gradlew pushSemverTag "-Psemver.stage=alpha"
By default, if the repository status is not clean, the version shows the suffix DIRTY
but that can
be avoided by setting the Gradle property semver.checkClean
.
For example, if the last tag is 1.0.0
, there are 23 commits between that tag and the last commit
and the repo is not clean:
./gradlew "-Psemver.stage=final" "-Psemver.scope=patch"
semver: 1.0.0.23+DIRTY
./gradlew "-Psemver.stage=final" "-Psemver.scope=patch" "-Psemver.checkClean=false"
semver: 1.0.1
./gradlew "-Psemver.checkClean=false"
semver: 1.0.0.23+1a2cd5b2 # 1a2cd5b2 is the last commit hash
Due to calling mapVersion
in Groovy, it is possible to get the error:
Could not serialize value of type $Proxy96
Being 96
a random number.
The workaround is:
class SemverWorkaround {
static mapper = new VersionMapper() {
@Override
String map(GradleVersion gradleVersion) {
"2.0.0"
}
}
}
semver {
mapVersion SemverWorkaround.mapper
}
It works on settings and project script files.
Copyright 2024 Javier Segovia Córdoba
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
https://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.