diff --git a/README.md b/README.md index b535b28..9b7ca06 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. @@ -242,27 +227,20 @@ for an example of sbt project using both sbt-version-policy and sbt-release. 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.fromAssessesCompatibilityWithLatestRelease.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 } ~~~ 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..795fe58 --- /dev/null +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/withsbtrelease/ReleaseVersion.scala @@ -0,0 +1,85 @@ +package sbtversionpolicy.withsbtrelease + +import sbtversionpolicy.Compatibility +import sbtversionpolicy.SbtVersionPolicyPlugin.aggregatedAssessedCompatibilityWithLatestRelease +import sbtversionpolicy.SbtVersionPolicyPlugin.autoImport.versionPolicyAssessCompatibility +import sbt.* + +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. + */ + def fromCompatibility(compatibility: Compatibility): 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 + (maybeBump match { + case Some(bump) => versionWithoutQualifier.bump(bump) + case None => versionWithoutQualifier + }).string + } + } + + /** + * 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 + * }}} + */ + val fromAssessedCompatibilityWithLatestRelease: 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) + } + + /** + * 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 + * ) + * }}} + */ + val fromAggregatedAssessedCompatibilityWithLatestRelease: 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) + } + +} 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/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