From 6e3357345e1a946d039d4a497b5edaa11b4d50ed Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Thu, 30 Nov 2023 09:43:19 +0100 Subject: [PATCH 1/2] Simplify integration with sbt-release --- README.md | 58 +++++------ .../withsbtrelease/ReleaseVersion.scala | 99 +++++++++++++++++++ .../withsbtrelease/Version.scala | 77 +++++++++++++++ .../.gitignore | 3 + .../build.sbt | 54 ++++++++++ .../file-to-add.template | 3 + .../scala/org/organization/module/Dummy.scala | 3 + .../project/plugins.sbt | 2 + .../example-sbt-release-unconstrained/test | 30 ++++++ .../version.sbt | 1 + .../example-sbt-release/build.sbt | 21 +--- 11 files changed, 296 insertions(+), 55 deletions(-) create mode 100644 sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala create mode 100644 sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/Version.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/.gitignore create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/file-to-add.template create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/module/src/main/scala/org/organization/module/Dummy.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/project/plugins.sbt create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt diff --git a/README.md b/README.md index b535b28..9cd7df7 100644 --- a/README.md +++ b/README.md @@ -211,23 +211,8 @@ In this mode, you can use sbt-version-policy to check that incoming pull request - compute the next release version according to its compatibility guarantees 1. set the key `releaseVersion` as follows: ~~~ scala - releaseVersion := { - val maybeBump = versionPolicyIntention.value match { - case Compatibility.None => Some(Version.Bump.Major) - case Compatibility.BinaryCompatible => Some(Version.Bump.Minor) - case Compatibility.BinaryAndSourceCompatible => None // No need to bump the patch version, because it has already been bumped when sbt-release set the next release version - } - { (currentVersion: String) => - val versionWithoutQualifier = - Version(currentVersion) - .getOrElse(versionFormatError(currentVersion)) - .withoutQualifier - (maybeBump match { - case Some(bump) => versionWithoutQualifier.bump(bump) - case None => versionWithoutQualifier - }).string - } - } + import sbtversionpolicy.withsbtrelease.ReleaseVersion + releaseVersion := ReleaseVersion.fromCompatibility(versionPolicyIntention.value) ~~~ 2. Reset `versionPolicyIntention` to `Compatibility.BinaryAndSourceCompatible` after every release. This can be achieved by managing the setting `versionPolicyIntention` in a separate file (like [sbt-release] manages the setting `version` in a separate file, by default), and by adding a step that overwrites the content of that file and commits it. @@ -237,35 +222,36 @@ In this mode, you can use sbt-version-policy to check that incoming pull request You can have a look at the test [example-sbt-release](./sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release) for an example of sbt project using both sbt-version-policy and sbt-release. +In that example, we also automatically reset the intended compatibility level to `BinaryAndSourceCompatible` as the last step of the release process. + #### Unconstrained compatibility level In this mode, you can use sbt-version-policy to assess the incompatibilities introduced in the project since the last release and compute the new release version accordingly (ie, to bump the major version number if you introduced binary incompatibilities): 1. make sure `versionPolicyIntention` is not set -2. define `releaseVersion` from the compatibility level returned by `versionPolicyAssessCompatibility` +2. define `releaseVersion` from the compatibility level returned by `versionPolicyAssessCompatibility`: + ~~~ scala + import sbtversionpolicy.withsbtrelease.ReleaseVersion + + releaseVersion := { + ReleaseVersion.fromAssessedCompatibilityWithLatestRelease().value + } + ~~~ + Alternatively, if your project contains multiple modules, you want to use the aggregated assessed compatibility level: ~~~ scala + import sbtversionpolicy.withsbtrelease.ReleaseVersion + releaseVersion := { - val compatibilityWithPreviousReleases = versionPolicyAssessCompatibility.value - val compatibilityWithLastRelease = compatibilityWithPreviousReleases.head - val (_, compatibility) = compatibilityWithLastRelease - val maybeBump = compatibility match { - case Compatibility.None => Some(Version.Bump.Major) - case Compatibility.BinaryCompatible => Some(Version.Bump.Minor) - case Compatibility.BinaryAndSourceCompatible => None // No need to bump the patch version, because it has already been bumped when sbt-release set the next release version - } - { (currentVersion: String) => - val versionWithoutQualifier = - Version(currentVersion) - .getOrElse(versionFormatError(currentVersion)) - .withoutQualifier - (maybeBump match { - case Some(bump) => versionWithoutQualifier.bump(bump) - case None => versionWithoutQualifier - }).string - } + ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value } ~~~ +Note that this mode can be enabled only _after_ the first release of the project has already been published. + +##### Example + +We demonstrate the “unconstrained” mode in [this example](./sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained). + ## How does `versionPolicyCheck` work? The `versionPolicyCheck` task: diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala new file mode 100644 index 0000000..8181e2e --- /dev/null +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala @@ -0,0 +1,99 @@ +package sbtversionpolicy.withsbtrelease + +import sbtversionpolicy.Compatibility +import sbtversionpolicy.SbtVersionPolicyPlugin.aggregatedAssessedCompatibilityWithLatestRelease +import sbtversionpolicy.SbtVersionPolicyPlugin.autoImport.versionPolicyAssessCompatibility +import sbt.* + +/** Convenient methods to integrate with the plugin sbt-release */ +object ReleaseVersion { + + /** + * @return a [release version function](https://github.com/sbt/sbt-release?tab=readme-ov-file#custom-versioning) + * that bumps the patch, minor, or major version number depending on the provided + * compatibility level. + * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). Empty by default. + */ + def fromCompatibility(compatibility: Compatibility, qualifier: String = ""): String => String = { + val maybeBump = + compatibility match { + case Compatibility.None => Some(Version.Bump.Major) + case Compatibility.BinaryCompatible => Some(Version.Bump.Minor) + case Compatibility.BinaryAndSourceCompatible => None // No need to bump the patch version, because it has already been bumped when sbt-release set the next release version + } + { (currentVersion: String) => + val versionWithoutQualifier = + Version(currentVersion) + .getOrElse(Version.formatError(currentVersion)) + .withoutQualifier + val bumpedVersion = + (maybeBump match { + case Some(bump) => versionWithoutQualifier.bump(bump) + case None => versionWithoutQualifier + }).string + s"${bumpedVersion}${qualifier}" + } + } + + /** + * Task returning a [release version function](https://github.com/sbt/sbt-release?tab=readme-ov-file#custom-versioning) + * based on the assessed compatibility level of the project. + * + * Use it in your `.sbt` build definition as follows: + * + * {{{ + * import sbtversionpolicy.withsbtrelease.ReleaseVersion + * + * releaseVersion := ReleaseVersion.fromAssessedCompatibilityWithLatestRelease.value + * }}} + * + * sbt-release uses the `releaseVersion` function to set the version before publishing a release (at step + * `setReleaseVersion`). It reads the current `version` (usually defined in a file `version.sbt`, and looking + * like `"1.2.3-SNAPSHOT"`), and applies the function to it. + * + * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). Empty by default. + */ + def fromAssessedCompatibilityWithLatestRelease(qualifier: String = ""): Def.Initialize[Task[String => String]] = + Def.task { + val compatibilityResults = versionPolicyAssessCompatibility.value + val log = Keys.streams.value.log + val compatibilityWithLatestRelease = + compatibilityResults.headOption + .getOrElse(throw new MessageOnlyException("Unable to assess the compatibility level of this project. Is 'versionPolicyPreviousVersions' defined?")) + val (_, compatibility) = compatibilityWithLatestRelease + log.debug(s"Compatibility level is ${compatibility}") + fromCompatibility(compatibility, qualifier) + } + + /** + * Task returning a [release version function](https://github.com/sbt/sbt-release?tab=readme-ov-file#custom-versioning) + * based on the assessed compatibility level of the project (ie, the highest level of compatibility + * satisfied by all the sub-projects aggregated by this project). + * + * Use it in the root project of your `.sbt` build definition as follows: + * + * {{{ + * import sbtversionpolicy.withsbtrelease.ReleaseVersion + * + * val `my-project` = + * project + * .in(file(".")) + * .aggregate(mySubproject1, mySubproject2) + * .settings( + * releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease.value + * ) + * }}} + * + * sbt-release uses the `releaseVersion` function to set the version before publishing a release (at step + * `setReleaseVersion`). It reads the current `version` (usually defined in a file `version.sbt`, and looking + * like `"1.2.3-SNAPSHOT"`), and applies the function to it. + */ + def fromAggregatedAssessedCompatibilityWithLatestRelease(qualifier: String = ""): Def.Initialize[Task[String => String]] = + Def.task { + val log = Keys.streams.value.log + val compatibility = aggregatedAssessedCompatibilityWithLatestRelease.value + log.debug(s"Aggregated compatibility level is ${compatibility}") + fromCompatibility(compatibility, qualifier) + } + +} diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/Version.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/Version.scala new file mode 100644 index 0000000..dc9f3b6 --- /dev/null +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/Version.scala @@ -0,0 +1,77 @@ +package sbtversionpolicy.withsbtrelease + +// All the code below has been copied from https://github.com/sbt/sbt-release/blob/master/src/main/scala/Version.scala + +import util.control.Exception._ + +private[withsbtrelease] object Version { + sealed trait Bump { + def bump: Version => Version + } + + object Bump { + case object Major extends Bump { def bump = _.bumpMajor } + case object Minor extends Bump { def bump = _.bumpMinor } + case object Bugfix extends Bump { def bump = _.bumpBugfix } + case object Nano extends Bump { def bump = _.bumpNano } + case object Next extends Bump { def bump = _.bump } + + val default = Next + } + + val VersionR = """([0-9]+)((?:\.[0-9]+)+)?([\.\-0-9a-zA-Z]*)?""".r + val PreReleaseQualifierR = """[\.-](?i:rc|m|alpha|beta)[\.-]?[0-9]*""".r + + def apply(s: String): Option[Version] = { + allCatch opt { + val VersionR(maj, subs, qual) = s + // parse the subversions (if any) to a Seq[Int] + val subSeq: Seq[Int] = Option(subs) map { str => + // split on . and remove empty strings + str.split('.').filterNot(_.trim.isEmpty).map(_.toInt).toSeq + } getOrElse Nil + Version(maj.toInt, subSeq, Option(qual).filterNot(_.isEmpty)) + } + } + + def formatError(version: String) = sys.error(s"Version [$version] format is not compatible with " + Version.VersionR.pattern.toString) +} + +private[withsbtrelease] case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String]) { + def bump = { + val maybeBumpedPrerelease = qualifier.collect { + case Version.PreReleaseQualifierR() => withoutQualifier + } + def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length-1) + def bumpedMajor = copy(major = major + 1) + + maybeBumpedPrerelease + .orElse(maybeBumpedLastSubversion) + .getOrElse(bumpedMajor) + } + + def bumpMajor = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0)) + def bumpMinor = maybeBumpSubversion(0) + def bumpBugfix = maybeBumpSubversion(1) + def bumpNano = maybeBumpSubversion(2) + + def maybeBumpSubversion(idx: Int) = bumpSubversionOpt(idx) getOrElse this + + private def bumpSubversionOpt(idx: Int) = { + val bumped = subversions.drop(idx) + val reset = bumped.drop(1).length + bumped.headOption map { head => + val patch = (head+1) +: Seq.fill(reset)(0) + copy(subversions = subversions.patch(idx, patch, patch.length)) + } + } + + def bump(bumpType: Version.Bump): Version = bumpType.bump(this) + + def withoutQualifier = copy(qualifier = None) + def asSnapshot = copy(qualifier = Some("-SNAPSHOT")) + + def string = "" + major + mkString(subversions) + qualifier.getOrElse("") + + private def mkString(parts: Seq[Int]) = parts.map("."+_).mkString +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/.gitignore b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/.gitignore new file mode 100644 index 0000000..513796c --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/.gitignore @@ -0,0 +1,3 @@ +target/ +.bsp/ +global/ diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt new file mode 100644 index 0000000..249b396 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt @@ -0,0 +1,54 @@ +import sbtversionpolicy.withsbtrelease.ReleaseVersion +import sbtrelease._ +import ReleaseTransformations._ + +inThisBuild(List( + organization := "org.organization", + homepage := Some(url("https://github.com/organization/project")), + licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")), + developers := List( + Developer( + "julienrf", + "Julien Richard-Foy", + "julien@organization.org", + url("https://github.com/julienrf") + ) + ), + scalaVersion := "3.0.1" +)) + +val module = + project + .settings( + name := "sbt-release-unconstrained-test" + ) + +val root = project.in(file(".")) + .aggregate(module) + .settings( + // Custom release process for testing purpose only: the artifacts are locally published, + // and we don’t push to the remote repository. + releaseProcess := Seq[ReleaseStep]( + checkSnapshotDependencies, + inquireVersions, + runClean, + runTest, + setReleaseVersion, + commitReleaseVersion, + tagRelease, + releaseStepTask(module / publishLocal), // Publish locally for our tests only, in practice you will publish to Sonatype + setNextVersion, + commitNextVersion, + // pushChanges // Disable pushing the changes to the remote repository for our tests only + ) + ) + +TaskKey[Unit]("checkTag_1_0_0") := { + import scala.sys.process._ + assert("git describe --tags".lineStream.exists(_.contains("v1.0.0"))) +} + +TaskKey[Unit]("checkTag_1_1_0") := { + import scala.sys.process._ + assert("git describe --tags".lineStream.exists(_.contains("v1.1.0"))) +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/file-to-add.template b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/file-to-add.template new file mode 100644 index 0000000..550a3a5 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/file-to-add.template @@ -0,0 +1,3 @@ +package org.organization.module + +class NewAPI diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/module/src/main/scala/org/organization/module/Dummy.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/module/src/main/scala/org/organization/module/Dummy.scala new file mode 100644 index 0000000..2b07381 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/module/src/main/scala/org/organization/module/Dummy.scala @@ -0,0 +1,3 @@ +package org.organization.module + +class Dummy diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/project/plugins.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/project/plugins.sbt new file mode 100644 index 0000000..3552a89 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % sys.props("plugin.version")) +addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test new file mode 100644 index 0000000..07b9299 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test @@ -0,0 +1,30 @@ +# Setup repository +$ exec git init +$ exec git config user.email "email" +$ exec git config user.name "name" + +$ exec git add . +$ exec git commit -m "Initial commit" +> reload + +# First release +> release with-defaults release-version 1.0.0 +> checkTag_1_0_0 +> reload + +# New contributions +$ copy-file file-to-add.template module/src/main/scala/org/organization/module/NewAPI.scala +$ exec git add module/src/main/scala/org/organization/module/NewAPI.scala +$ exec git commit -m "Some hard work" + +# Configure releaseVersion to bump the patch, minor, or major version number according +# to the assessed compatibility level. +# In practice, you would assign the releaseVersion in your build.sbt. +# Here, we show how to optionally add a qualifier to the release version (e.g. `"-RC1"`) +# via an environment variable RELEASE_VERSION_QUALIFIER +> set releaseVersion := sbtversionpolicy.withsbtrelease.ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease(qualifier = sys.env.getOrElse("RELEASE_VERSION_QUALIFIER", "")).value + +# Second release +> release with-defaults +# Check that sbt-version-policy bumped the minor version +> checkTag_1_1_0 diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt new file mode 100644 index 0000000..a3a28d6 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt @@ -0,0 +1 @@ +ThisBuild / version := "0.0.1-SNAPSHOT" diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release/build.sbt index 57b5d50..e10b81b 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release/build.sbt @@ -1,3 +1,4 @@ +import sbtversionpolicy.withsbtrelease.ReleaseVersion import sbtrelease._ import ReleaseTransformations._ @@ -24,7 +25,7 @@ val root = project.in(file(".")) publish / skip := true, // Configure releaseVersion to bump the patch, minor, or major version number according // to the compatibility intention set by versionPolicyIntention. - releaseVersion := setReleaseVersionFunction(versionPolicyIntention.value), + releaseVersion := ReleaseVersion.fromCompatibility(versionPolicyIntention.value), // Custom release process: run `versionCheck` after we have set the release version, and // reset compatibility intention to `Compatibility.BinaryAndSourceCompatible` after the release. // There are some other modifications for testing: the artifacts are locally published, @@ -46,24 +47,6 @@ val root = project.in(file(".")) ) ) -def setReleaseVersionFunction(compatibilityIntention: Compatibility): String => String = { - val maybeBump = compatibilityIntention match { - case Compatibility.None => Some(Version.Bump.Major) - case Compatibility.BinaryCompatible => Some(Version.Bump.Minor) - case Compatibility.BinaryAndSourceCompatible => None // No need to bump the patch version, because it has already been bumped when sbt-release set the next release version - } - { (currentVersion: String) => - val versionWithoutQualifier = - Version(currentVersion) - .getOrElse(versionFormatError(currentVersion)) - .withoutQualifier - (maybeBump match { - case Some(bump) => versionWithoutQualifier.bump(bump) - case None => versionWithoutQualifier - }).string - } -} - lazy val setAndCommitNextCompatibilityIntention = taskKey[Unit]("Set versionPolicyIntention to Compatibility.BinaryAndSourceCompatible, and commit the change") ThisBuild / setAndCommitNextCompatibilityIntention := { val log = streams.value.log From 985ace1acd526a54ca0b2f63fd493e4cc4e16dcb Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Thu, 30 Nov 2023 19:05:07 +0100 Subject: [PATCH 2/2] A couple of improvements - Fallback to the default releaseVersion behavior for the first release - Read the version qualifier from an environment variable --- README.md | 10 ++- .../withsbtrelease/ReleaseVersion.scala | 64 +++++++++++++------ .../build.sbt | 2 + .../example-sbt-release-unconstrained/test | 9 +-- .../version.sbt | 2 +- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9cd7df7..5694d20 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,9 @@ In this mode, you can use sbt-version-policy to check that incoming pull request import sbtversionpolicy.withsbtrelease.ReleaseVersion releaseVersion := ReleaseVersion.fromCompatibility(versionPolicyIntention.value) ~~~ + The `releaseVersion` function bumps the release version according to the compatibility guarantees defined + by `versionPolicyIntention`. Optionally, you can also define a _qualifier_ to append to the release version + by setting the environment variable `VERSION_POLICY_RELEASE_QUALIFIER` (e.g., `VERSION_POLICY_RELEASE_QUALIFIER=RC1`). 2. Reset `versionPolicyIntention` to `Compatibility.BinaryAndSourceCompatible` after every release. This can be achieved by managing the setting `versionPolicyIntention` in a separate file (like [sbt-release] manages the setting `version` in a separate file, by default), and by adding a step that overwrites the content of that file and commits it. @@ -245,8 +248,13 @@ In this mode, you can use sbt-version-policy to assess the incompatibilities int ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value } ~~~ + In both cases, the `releaseVersion` function sets the release version according to the compatibility level + with the latest release. Optionally, you can also define a _qualifier_ to append to the release version + by setting the environment variable `VERSION_POLICY_RELEASE_QUALIFIER` (e.g., `VERSION_POLICY_RELEASE_QUALIFIER="-RC1"`). -Note that this mode can be enabled only _after_ the first release of the project has already been published. +Note that for the first release you have to set the release version yourself via the file `version.sbt` (e.g., set +`1.0.0-SNAPSHOT` or `0.1.0-SNAPSHOT`). This is because `sbt-version-policy` needs a previous release to exist to be +able to assess the compatibility level of the current state of the project with that release. ##### Example diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala index 8181e2e..6ac01b6 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala @@ -4,17 +4,21 @@ import sbtversionpolicy.Compatibility import sbtversionpolicy.SbtVersionPolicyPlugin.aggregatedAssessedCompatibilityWithLatestRelease import sbtversionpolicy.SbtVersionPolicyPlugin.autoImport.versionPolicyAssessCompatibility import sbt.* +import sbtversionpolicy.SbtVersionPolicyMima.autoImport.versionPolicyPreviousVersions /** Convenient methods to integrate with the plugin sbt-release */ object ReleaseVersion { + private val qualifierVariableName = "VERSION_POLICY_RELEASE_QUALIFIER" + /** * @return a [release version function](https://github.com/sbt/sbt-release?tab=readme-ov-file#custom-versioning) * that bumps the patch, minor, or major version number depending on the provided * compatibility level. - * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). Empty by default. + * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). By default, it tries to read + * it from the environment variable VERSION_POLICY_RELEASE_QUALIFIER. */ - def fromCompatibility(compatibility: Compatibility, qualifier: String = ""): String => String = { + def fromCompatibility(compatibility: Compatibility, qualifier: String = sys.env.getOrElse(qualifierVariableName, "")): String => String = { val maybeBump = compatibility match { case Compatibility.None => Some(Version.Bump.Major) @@ -31,10 +35,29 @@ object ReleaseVersion { case Some(bump) => versionWithoutQualifier.bump(bump) case None => versionWithoutQualifier }).string - s"${bumpedVersion}${qualifier}" + bumpedVersion + qualifier } } + private def fromAssessedCompatibility(qualifier: String)(assessCompatibility: Def.Initialize[Task[Compatibility]]): Def.Initialize[Task[String => String]] = + Def.ifS(Def.task { + versionPolicyPreviousVersions.value.isEmpty + })(Def.task { + // If there are no previous versions to assess the compatibility with, + // fallback to the default release version function, which drops the qualifier + // from the version set in the file `version.sbt` + // (e.g., "1.0.0-SNAPSHOT" => "1.0.0") + (version: String) => + Version(version) + .map(_.withoutQualifier.string + qualifier) + .getOrElse(Version.formatError(version)) + })(Def.task { + val log = Keys.streams.value.log + val compatibility = assessCompatibility.value + log.debug(s"Compatibility level is ${compatibility}") + fromCompatibility(compatibility, qualifier) + }) + /** * Task returning a [release version function](https://github.com/sbt/sbt-release?tab=readme-ov-file#custom-versioning) * based on the assessed compatibility level of the project. @@ -44,26 +67,27 @@ object ReleaseVersion { * {{{ * import sbtversionpolicy.withsbtrelease.ReleaseVersion * - * releaseVersion := ReleaseVersion.fromAssessedCompatibilityWithLatestRelease.value + * releaseVersion := ReleaseVersion.fromAssessedCompatibilityWithLatestRelease().value * }}} * * sbt-release uses the `releaseVersion` function to set the version before publishing a release (at step * `setReleaseVersion`). It reads the current `version` (usually defined in a file `version.sbt`, and looking * like `"1.2.3-SNAPSHOT"`), and applies the function to it. * - * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). Empty by default. + * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). By default, it tries to read + * it from the environment variable VERSION_POLICY_RELEASE_QUALIFIER. */ - def fromAssessedCompatibilityWithLatestRelease(qualifier: String = ""): Def.Initialize[Task[String => String]] = - Def.task { + def fromAssessedCompatibilityWithLatestRelease( + qualifier: String = sys.env.getOrElse(qualifierVariableName, "") + ): Def.Initialize[Task[String => String]] = + fromAssessedCompatibility(qualifier)(Def.task { val compatibilityResults = versionPolicyAssessCompatibility.value - val log = Keys.streams.value.log val compatibilityWithLatestRelease = compatibilityResults.headOption - .getOrElse(throw new MessageOnlyException("Unable to assess the compatibility level of this project. Is 'versionPolicyPreviousVersions' defined?")) + .getOrElse(throw new MessageOnlyException("Unable to assess the compatibility level of this project.")) val (_, compatibility) = compatibilityWithLatestRelease - log.debug(s"Compatibility level is ${compatibility}") - fromCompatibility(compatibility, qualifier) - } + compatibility + }) /** * Task returning a [release version function](https://github.com/sbt/sbt-release?tab=readme-ov-file#custom-versioning) @@ -80,20 +104,20 @@ object ReleaseVersion { * .in(file(".")) * .aggregate(mySubproject1, mySubproject2) * .settings( - * releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease.value + * releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value * ) * }}} * * sbt-release uses the `releaseVersion` function to set the version before publishing a release (at step * `setReleaseVersion`). It reads the current `version` (usually defined in a file `version.sbt`, and looking * like `"1.2.3-SNAPSHOT"`), and applies the function to it. + * + * @param qualifier Optional qualifier to append to the version (e.g. `"-RC1"`). By default, it tries to read + * it from the environment variable VERSION_POLICY_RELEASE_QUALIFIER. */ - def fromAggregatedAssessedCompatibilityWithLatestRelease(qualifier: String = ""): Def.Initialize[Task[String => String]] = - Def.task { - val log = Keys.streams.value.log - val compatibility = aggregatedAssessedCompatibilityWithLatestRelease.value - log.debug(s"Aggregated compatibility level is ${compatibility}") - fromCompatibility(compatibility, qualifier) - } + def fromAggregatedAssessedCompatibilityWithLatestRelease( + qualifier: String = sys.env.getOrElse(qualifierVariableName, "") + ): Def.Initialize[Task[String => String]] = + fromAssessedCompatibility(qualifier)(aggregatedAssessedCompatibilityWithLatestRelease) } diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt index 249b396..8479228 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/build.sbt @@ -26,6 +26,8 @@ val module = val root = project.in(file(".")) .aggregate(module) .settings( + // Tell sbt-release to set the release version based on the level of compatibility with the previous release + releaseVersion := ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease().value, // Custom release process for testing purpose only: the artifacts are locally published, // and we don’t push to the remote repository. releaseProcess := Seq[ReleaseStep]( diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test index 07b9299..f787c39 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/test @@ -8,7 +8,7 @@ $ exec git commit -m "Initial commit" > reload # First release -> release with-defaults release-version 1.0.0 +> release with-defaults > checkTag_1_0_0 > reload @@ -17,13 +17,6 @@ $ copy-file file-to-add.template module/src/main/scala/org/organization/module/N $ exec git add module/src/main/scala/org/organization/module/NewAPI.scala $ exec git commit -m "Some hard work" -# Configure releaseVersion to bump the patch, minor, or major version number according -# to the assessed compatibility level. -# In practice, you would assign the releaseVersion in your build.sbt. -# Here, we show how to optionally add a qualifier to the release version (e.g. `"-RC1"`) -# via an environment variable RELEASE_VERSION_QUALIFIER -> set releaseVersion := sbtversionpolicy.withsbtrelease.ReleaseVersion.fromAggregatedAssessedCompatibilityWithLatestRelease(qualifier = sys.env.getOrElse("RELEASE_VERSION_QUALIFIER", "")).value - # Second release > release with-defaults # Check that sbt-version-policy bumped the minor version diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt index a3a28d6..a5f65fc 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release-unconstrained/version.sbt @@ -1 +1 @@ -ThisBuild / version := "0.0.1-SNAPSHOT" +ThisBuild / version := "1.0.0-SNAPSHOT"