-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: by default determine version from Git tags
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
Showing
9 changed files
with
325 additions
and
150 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
194 changes: 194 additions & 0 deletions
194
src/main/groovy/to/wetransform/gradle/version/AbstractVersionPlugin.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
src/main/groovy/to/wetransform/gradle/version/ConfigurableVersionPlugin.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.