Skip to content

Version creation

axion-release-plugin comes with rich set of features for version parsing and decorating. Extracting version information from repository can be split in six phases:

  • reading - read tags from repository
  • parsing - extracting version from tag (serializing version to tag)
  • incrementing - when not on tag, version patch (least significant) number is incremented
  • decorating - adding additional transformations to create final version
  • appending snapshot - when not on tag, -SNAPSHOT suffix is appended (can be customized)
  • sanitization - making sure there are no unwanted characters in version

Reading

Version is kept in repository in form of a tag:

# git tag
v1.0.0 v1.0.1 v1.1.0

Only tags which match the predefined prefix are taken into account when calculating current version. Prefix can be set using scmVersion.tag.prefix property:

scmVersion {
    tag {
        prefix.set("my-prefix")
    }
}

Default prefix is v.

In case a tag does not match the main prefix, fallback prefixes will be examined. You can use fallback prefixes to migrate from one prefix to another. For example, if you use prefix my-service- but want to migrate to prefix v, you can configure it like that:

scmVersion {
    tag {
        prefix.set("v")
        fallbackPrefixes.set(listOf("my-service-"))
    }
}

Axion will use fallback prefixes to calculate latest version, but the next release tag will be created using the main prefix.

There is also an option to set prefix per-branch (i.e. to use different version prefix on legacy- branches):

scmVersion {
    tag {
        prefix.set("default-prefix")
        branchPrefix.putAll( [
            "legacy.*" : "legacy-prefix"
        ])
    }
}

Git implementation of a repository interface can operate in two modes when searching for tag to read the version from.

First tag encountered

This is the default mode

In default mode, search for tag starts from current commit and goes up the commit tree until first tag with matching prefix is encountered. This tag is returned as current position in repository (along with current branch) and commit number.

Tree walking algorithm might lead to various misunderstandings. Take this tree as an example:

   [T1]
    |
 |      |
[_]   [T2]
 |      |
[C]    [_]

Let T* be a tagged commit and C current commit. After releasing version with tag T1, we have been working on two separate branches. Then, branch on the right has been released and marked with T2 tag. When traversing this tree from commit C upwards, first discovered tag will be T1 and so reported version will come from parsing T1 tag, even though tag T2 has higher version number. It all comes down to what is the history of current commit, not what has happened in repository in general.

Tag with the highest version

Second mode is searching for highest version visible in the git tree's history. This means that all commits from HEAD till first commit will be analysed.

In order to activate this feature:

scmVersion {
    useHighestVersion.set(true)
}

With a tree similar to this:

Tag: 1.0.0
Tag: 1.5.0
Tag: 1.2.0

This changes behavior from:

# ./gradlew currentVersion
1.2.0

to:

# ./gradlew currentVersion
1.5.0

You can also activate this option using command line:

# ./gradlew currentVersion -Prelease.useHighestVersion
1.5.0

Parsing

Having current tag name, we can deserialize it to extract raw version. Actually, this has to be a two-way process, since on reading version we deserialize and serialize on creating new tag to mark new release.

Deserialization

Default deserialization function is a simple closure that strips tag name off prefix and separator between prefix and version. It ignores separator when prefix is an empty string:

deserialize(prefix: 'v', separator: '', tag: 'v1.0.0') == 1.0.0
deserialize(prefix: '', separator: '-', tag: '1.0.0') == 1.0.0

You can implement own deserializer by setting closure that would accept deserialization config object and position in SCM:

scmVersion {
    tag {
        deserialize({config, position, tagName -> ...})
    }
}

config object is instance of TagNameSerializationRules class. Useful properties are:

  • prefix: tag prefix
  • separator: separator between prefix and version

position object contains:

  • branch - the name of the current branch

Last but not least, tagName contains prepared tag name that should be used to extract version. position.latestTag might point to next version tag with additional suffix.

Serialization

Default serializer prepends prefix and separator to version number. Separator is ignored if prefix is an empty string:

serialize(prefix: 'v', separator: '', version: '1.0.0') == v1.0.0
serialize(prefix: '', separator: '-', version: '1.0.0') == 1.0.0

You can implement own serializer by setting closure that would accept serialization config object and version:

scmVersion {
    tag {
        serialize({config, version -> ...})
    }
}

config object has been described above.

Initial version

When starting work on new project there are no tags available and so there is no way to deserialize version. By default 0.1.0 version is returned, but you can override that behavior by specifying own closure that will construct initial version:

scmVersion {
    tag {
        initialVersion({config, position -> ...})
    }
}

Input objects have same structure as deserialization closure inputs.

Incrementing

Incrementing phase does increment the version in accordance with version incrementer. By default, version patch (least significant) number is incremented. There are other predefined rules:

  • incrementPatch - increment patch number
  • incrementMinor - increment minor (middle) number
  • incrementMajor - increment major number
  • incrementMinorIfNotOnRelease - increment patch number if on release branch. Increment minor otherwise
  • incrementPrerelease - increment pre-release suffix if possible (-rc1 to -rc2). Add initialPreReleaseIfNotOnPrerelease to increment patch with pre-release version. Increment patch otherwise

You can set one of predefined rules via scmVersion.versionIncrementer method:

scmVersion {
    versionIncrementer('incrementPatch')
}

Or via release.versionIncrementer command line argument, which overrides any other incrementer settings:

./gradlew release -Prelease.versionIncrementer=incrementMajor

If rule accepts parameters, they can be passed via configuration map:

scmVersion {
    versionIncrementer('someIncrementer', [:])
}

Alternatively you can specify a custom rule by setting a closure that would accept a context object and return a Version object:

scmVersion {
    versionIncrementer({ context -> ... })
}

The context object passed to closure contains the following:

  • currentVersion - current Version object that should be used to calculate next version (Version API)
  • position - widely used position object, for more see ScmPosition

You can also specify different incrementers per branch. They can be either closure, name of predefined incrementer or name and list of arguments in case predefined incrementer requires configuration:

scmVersion {
    branchVersionIncrementer.putAll( [
        'feature/.*' : 'incrementMinor',
        'bugfix/.*' : { c -> c.currentVersion.incrementPatchVersion() },
        'legacy/.*' : [ 'incrementMinorIfNotOnRelease', [releaseBranchPattern: 'legacy/release.*'] ]
    ])
}

If none matches current branch, incrementer set in versionIncrementer field is used.

incrementMinorIfNotOnRelease

This rule uses additional parameter releaseBranchPattern (by default it's set to v/.+):

scmVersion {
    versionIncrementer('incrementMinorIfNotOnRelease', [releaseBranchPattern: 'v.*'])
}

incrementPrerelease

This rule uses additional parameter initialPreReleaseIfNotOnPrerelease (by default it's empty):

scmVersion {
    versionIncrementer('incrementPrerelease', [initialPreReleaseIfNotOnPrerelease: 'rc1'])
}

releaseOnlyOnReleaseBranches

You can set this flag to true to prevent releasing on branches that are not marked as release branches. By default, this flag is set to false.

scmVersion {
    releaseOnlyOnReleaseBranches = true
}

This flag can also be set via command line:

./gradlew release -Prelease.releaseOnlyOnReleaseBranches

And works well in combination with releaseBranchNames option

scmVersion {
    releaseOnlyOnReleaseBranches = true
    releaseBranchNames = ['main', 'master']
}

or as command line

./gradlew release -Prelease.releaseOnlyOnReleaseBranches -Prelease.releaseBranchNames=main,release

Decorating

Decorating phase happens only when version is read (and deserialized). During this phase, the version will be decorated with a branch name (default behavior). axion-release-plugin supports adding predefined named version creators (so don't be afraid to post pull request if you have something useful!). Decoration phase is conducted by version creators, you can configure it via scmVersion.versionCreator method:

scmVersion {
    versionCreator('versionWithCommitHash')
}

Or via release.versionCreator command line argument, which overrides any other versionCreator settings:

./gradlew release -Prelease.versionCreator=simple

You can also set decorators per branches that match specific regular expression:

scmVersion {
    branchVersionCreator.putAll( [
        'feature/.*': { version, position -> ...},
        'bugfix/.*': 'simple'
    ])
}

Per-branch version creators must be closures, there is no support for predefined creators. First match wins, but the order depends on collection type used (default for [:] is LinkedHashMap).

versionWithBranch [default]

This is the default version creator since version 1.18.0 of the plugin.

scmVersion {
    versionCreator('versionWithBranch')
}

This version creator appends branch name to version unless you are on main/master or detached HEAD:

decorate(version: '0.1.0', branch: 'master') == 0.1.0
decorate(version: '0.1.0', branch: 'my-special-branch') == 0.1.0-my-special-branch

If your Gradle build is executed within workflow on GitHub Actions and that workflow is triggered by either pull_request or pull_request_target event, GitHub will not check out the source branch of the pull request but a custom reference refs/pull/<pr_number>/merge (known as "merge branch"). Consequently, the repository will be in detached-HEAD state.

In that case, the branch name will be taken from GITHUB_HEAD_REF environment variable provided by GitHub.

simple

This version creator is no operation one:

decorate(version: '0.1.0') == 0.1.0

It might be useful when you want some branches to do nothing:

scmVersion {
    branchVersionCreator.putAll([
        'feature/.*': { version, position -> ...},
        'release/.*': 'simple'
    ])
}

versionWithCommitHash

scmVersion {
    versionCreator('versionWithCommitHash')
}

This version creator appends short SHA-1 hash to version unless you are on main/master or detached HEAD:

decorate(version: '0.1.0', branch: 'main') == 0.1.0
decorate(version: '0.1.0', branch: 'some-other-branch', revision: 'c1439767113643abda121896ee3fa42b500f16d0') == 0.1.0-c143976

Custom version creator

Custom version creators can be implemented by creating closure:

{version, position -> ...}
  • version - string version resolved by previous steps
  • position - ScmPosition object

Snapshot

By default, when not on tag, -SNAPSHOT suffix is appended

It can be customized by

scmVersion {
   snapshotCreator({ version, position -> ...})
}

Snapshot creator can be implemented by creating closure:

{version, position -> ...}
  • version - string version resolved by previous steps
  • position - ScmPosition object

Sanitization

After decorating versions, there might be some characters left in version that are not i.e. filename friendly. That's why last phase of version creation is sanitizing version string. By all characters that do not match [A-Za-z0-9._-] group are replaced with -. For example:

sanitize('0.1.0-feature/myfeatureBranch-SNAPSHOT') == '0.1.0-feature-my-feature-branch-SNAPSHOT'

You can switch off version sanitization via scmVersion.sanitizeVersion property:

scmVersion {
    sanitizeVersion.set(false)
}