Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: by default determine version from Git tags #16

Merged
merged 1 commit into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading