Skip to content

Commit

Permalink
feat!: by default determine version from Git tags
Browse files Browse the repository at this point in the history
The plugin now is not configurable any more.
However, there is a plugin variant that can be used as before, where the
configuration can be adapted and also a version file used if desired.
  • Loading branch information
stempler committed Apr 9, 2024
1 parent 6a23a9d commit 4bc9a13
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 150 deletions.
75 changes: 62 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,75 @@
gradle-semantic-release-version
===============================

Gradle plugin that manages the project version based on:
Gradle plugin that manages the project version based on information from the Git repository the project resides in, primarily the Git tags.

- A version file containing the last release version
- Information from the git repository the project resides in
Optionally instead of determining the version from the Git tags, a version file containing the last release version can be used.

The plugin works based on the following assumptions:

1. When a release is created
- The release version is written to the version file
- The task `setReleaseVersion` can be used for that
- A commit is created the includes the change to the version file and other release related changes
- A tag is created that marks the commit as the release
2. If the git repository is clean and HEAD points to a tag, the project version is the release version
3. If the git repository is not clean or HEAD does not point to a tag, the project version is a SNAPSHOT version that increases the minor version compared to the last release version
4. If no release version is configured or the version file is missing, the project version is `1.0.0-SNAPSHOT`
1. When a release is created a respective Git tag is created that marks a commit as the release
2. Versions use semantic versioning (`<major>.<minor>.<patch>`) and tags follow this pattern or have a `v` as prefix
3. If the git repository is clean and HEAD points to such a tag, the project version is the release version
4. If the git repository is not clean or HEAD does not point to such a tag, the project version is a SNAPSHOT version that increases the minor version compared to the last release version
5. If no release version can be determined, the project version is `1.0.0-SNAPSHOT`

For a tag to be recognized it needs to match the configured release version, optionally with the prefix `v`, for example `1.0.0` or `v1.0.0`.
Plugin variants
---------------

There are two variants of the plugin:

1. `semantic-release-version` is the variant that uses default settings and is not configurable
2. `semantic-release-version-custom` is the variant that is configurable where the behavior can be adapted

The `semantic-release-version` plugin does not use a version file and assumes tags that use the semantic version for a release, optionally including a prefix `v`.

Easiest way to use it is using the version published in the [Gradle Plugin Portal](https://plugins.gradle.org/):

```groovy
plugins {
id 'to.wetransform.semantic-release-version' version '<version>'
}
```

The `semantic-release-version-custom` plugin allows adapting the plugin configuration and changing settings related to which tags are recognized as version tags and also allows to determine the last release version from a file that is part of the repository.

```groovy
plugins {
id 'to.wetransform.semantic-release-version-custom' version '<version>'
}
```

When using this plugin variant you need to be aware that the version is only set after evaluation of the Gradle configuration.
That means any logic using the project version also must be executed after evaluation, for example:

```groovy
afterEvaluate {
// access `version`
}
```

Use release version for dirty repository
----------------------------------------

If the environment variable `RELEASE` is set to `true`, the release version is used, even if the repository is not clean.
This is intended for use cases where the release version was set, but the repository is expected to be dirty, e.g. due to other CI tasks.
This is intended for use cases where a release tag was created, but the repository is expected to be dirty, e.g. due to other CI tasks.

A cleaner method is avoid the repository being dirty, e.g. by adding additional files that are created during the release process to `.gitignore` if possible.

Configuration
-------------

Please note that configuring the plugin is only possible when using the plugin variant `semantic-release-version-custom`.

### Using a version file

If you do not want to rely on determining the last release version from Git, you can instead use a version file.

In that case when a release is created it is expected that:

- The release version is written to the version file
- The task `setReleaseVersion` can be used for that
- A commit is created the includes the change to the version file and other release related changes
- A tag is created that marks the commit as the release

By default the version file is assumed to be the file `version.txt` in the root project.
10 changes: 9 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ gradlePlugin {
id = 'to.wetransform.semantic-release-version'
implementationClass = 'to.wetransform.gradle.version.VersionPlugin'
displayName = 'semantic-release-version'
description = 'Gradle plugin that determines the current version from a file with the last release version and information from Git. Intended to be used with semantic-release.'
description = 'Gradle plugin that determines the current release or SNAPSHOT version from Git based on existing tags. Intended to be used with semantic-release.'
tags.set(['semver', 'release', 'version', 'semantic-release', 'git'])
}

configurableVersionPlugin {
id = 'to.wetransform.semantic-release-version-custom'
implementationClass = 'to.wetransform.gradle.version.ConfigurableVersionPlugin'
displayName = 'semantic-release-version-custom'
description = 'Gradle plugin that determines the current version based on information from Git and optionally a version file. Compared to the semantic-release-version plugin the behavior can be customized.'
tags.set(['semver', 'release', 'version', 'semantic-release'])
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void canRunTask() throws IOException {
BuildResult result = GradleRunner.create()
.forwardOutput()
.withPluginClasspath()
.withArguments("greet")
.withArguments("showVersion")
.withProjectDir(projectDir)
.build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package to.wetransform.gradle.version;

import java.util.function.BiPredicate
import org.gradle.api.Plugin;
import org.gradle.api.Project;

abstract class AbstractVersionPlugin implements Plugin<Project> {

private static def SEM_VER_REGEX = /^(\d+)\.(\d+)\.(\d+)$/

private static def SEM_VER_EXTRACT_REGEX = /((\d+)\.(\d+)\.(\d+))$/

public static def DEFAULT_SNAPSHOT = '1.0.0-SNAPSHOT'

private final boolean allowConfiguration

protected AbstractVersionPlugin(boolean allowConfiguration) {
this.allowConfiguration = allowConfiguration
}

void apply(Project project) {
//XXX not sure how to easiest use the service - instead the repo is opened manually
// project.apply(plugin: 'org.ajoberstar.grgit.service')

// register extension
VersionExtension extension
if (allowConfiguration) {
extension = project.extensions.create('versionConfig', VersionExtension, project)
}
else {
extension = new VersionExtension(project)
}

// define tasks
project.task('showVersion') {
group 'Version'
description 'Print current project version'

doLast {
println "Version: ${project.version}"
}
}

if (allowConfiguration || extension.useVersionFile) {
project.task('setReleaseVersion') {
group 'Version'
description 'Set a new release version (write to version file), provide version to set as Gradle property `newVersion`'

doLast {
def versionFile = project.versionConfig.versionFile
versionFile.text = project.properties['newVersion']
}
}
}

project.task('verifyReleaseVersion') {
group 'Version'
description 'Check if a release version is configured, otherwise (if the version is a -SNAPSHOT version) fail'

doLast {
def version = project.version

assert version
assert !version.endsWith('-SNAPSHOT')
assert version =~ SEM_VER_REGEX
}
}

// set version
if (allowConfiguration) {
project.afterEvaluate {
// apply after evaluate to allow configuring settings
applyVersion(it, extension)
}
}
else {
// directly apply with defaults
applyVersion(project, extension)
}
}

void applyVersion(Project project, VersionExtension extension) {
if (project.version != Project.DEFAULT_VERSION) {
throw new IllegalStateException("Version may not be configured if version plugin is applied")
} else {
project.version = determineVersion(
project, extension.useVersionFile, extension.tagGlobPatterns, extension.versionFile, extension.gitDir, extension.verifyTag
)
}
}

String determineVersion(Project project, boolean useVersionFile, Iterable<String> tagMatchPatterns, File versionFile, File gitDir, BiPredicate<String, String> verifyTag) {
def releaseVersion = null
def grgit = null

if (useVersionFile) {
// read version from file
if (versionFile.exists()) {
releaseVersion = versionFile.text.trim()

// verify version
if (releaseVersion) {
def match = releaseVersion ==~ SEM_VER_REGEX
if (!match) {
throw new IllegalStateException("Provided version for last release is not a valid semantic version: $releaseVersion")
}
}
}

if (!releaseVersion) {
// assume initial snapshot
project.logger.info("Version file does not exist or contains no version, assuming ${DEFAULT_SNAPSHOT}")
return DEFAULT_SNAPSHOT
}
} else {
// use information from git to determine last version
try {
grgit = org.ajoberstar.grgit.Grgit.open(dir: gitDir)
} catch (Exception e) {
project.logger.warn("Could not open Git repository in $gitDir", e)
}

if (!grgit) {
project.logger.info("No Git repository found, assuming ${DEFAULT_SNAPSHOT} as version")
return DEFAULT_SNAPSHOT
}

def describe = grgit.describe(abbrev: 0, tags: true, match: tagMatchPatterns as List)
if (!describe) {
// nothing found
project.logger.info("No tag found for determining version, assuming ${DEFAULT_SNAPSHOT}")
return DEFAULT_SNAPSHOT
}
else {
def matcher = describe =~ SEM_VER_EXTRACT_REGEX
if (matcher) {
releaseVersion = matcher[0][1]
}
else {
throw new IllegalStateException("Cannot extract release version from tag: $describe")
}
}
}

def dirty = false
def tagOnCurrentCommit = false
if (grgit == null) {
try {
grgit = org.ajoberstar.grgit.Grgit.open(dir: gitDir)
} catch (Exception e) {
project.logger.warn("Could not open Git repository in $gitDir", e)
grgit = null
}
}
if (grgit) {
dirty = !grgit.status().isClean()
def currentCommit = grgit.head().id
tagOnCurrentCommit = grgit.tag.list().findAll { tag ->
tag.commit.id == currentCommit && verifyTag.test(tag.name, releaseVersion)
}
}

if ('true'.equalsIgnoreCase(System.getenv('RELEASE'))) {
// force release version if repo is dirty (e.g. during release in CI)
// but still verify tag
if (tagOnCurrentCommit) {
return releaseVersion
}
else {
throw new IllegalStateException("There is no matching tag for the configured release version $releaseVersion")
}
}

if (tagOnCurrentCommit && !dirty) {
project.logger.info("Current commit is tagged and repository clean, using release version specified in file: $releaseVersion")
releaseVersion
}
else {
// build snapshot version with next minor version
def matcher = releaseVersion =~ SEM_VER_REGEX
if (matcher) {
project.logger.info("Current commit is not tagged or repository is dirty, using snapshot version based on last release")

def major = matcher[0][1] as int
def minor = matcher[0][2] as int

"${major}.${minor+1}.0-SNAPSHOT"
}
else {
throw new IllegalStateException("Provided version not a semantic version")
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package to.wetransform.gradle.version

/**
* Version plugin that allows customization through VersionExtension.
*/
class ConfigurableVersionPlugin extends AbstractVersionPlugin {
ConfigurableVersionPlugin() {
super(true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ class VersionExtension {
this.gitDir = project.rootProject.projectDir
}

/**
* States if a version file is used to determine the (release) version.
* If disabled will solely rely on informaton on Git tags.
*/
boolean useVersionFile = false

/**
* Valid glob patterns for selecting tags when determininig version from Git.
* See also https://git-scm.com/docs/git-describe#Documentation/git-describe.txt---matchltpatterngt
*/
Iterable<String> tagGlobPatterns = ["*.*.*"]

/**
* Location of the file that holds the last release version, if it exists.
*/
Expand Down
Loading

0 comments on commit 4bc9a13

Please sign in to comment.