Skip to content

Commit

Permalink
feat: adding prerelease version increments (#397)
Browse files Browse the repository at this point in the history
**Background Summary**
This PR adds support for incrementing prerelease versions by default, if it ends in a number. Currently, if a prerelease version is incremented, the prerelease qualifier is simply dropped. E.g. `1.0.0-RC1` will be incremented to `1.0.0`. After this merge, `1.0.0-RC1` will be incremented to `1.0.0-RC2`, but prerelease versions without a version number will behave as before: `1.0.0-alpha` will be incremented to `1.0.0`.

**New/Updated Versioning Strategies**
`Next` (**updated - breaking change**):   Will now increment prerelease versions, unlike in the past. So `1.0-RC1` will become `1.0-RC2`. Previously `1.0-RC1` would become `1.1`.

`NextStable` (**new**): The same as `Next` except that it excludes any prerelease versions. So `1.0.0-RC1` becomes `1.0.0`
  • Loading branch information
Andrapyre authored Jan 22, 2024
1 parent c809a57 commit 3e9020f
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 88 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ target
project/target
.idea
.idea_modules
.bsp
38 changes: 18 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,31 @@ A cross release behaves analogous to using the `+` command:

In the section *Customizing the release process* we take a look at how to define a `ReleaseStep` to participate in a cross build.

### Convenient versioning
### Versioning Strategies

As of version 0.8, *sbt-release* comes with some strategies for computing the next snapshot version via the `releaseVersionBump` setting. These strategies are defined in `sbtrelease.Version.Bump`. By default, the `Next` strategy is used:
As of version 0.8, *sbt-release* comes with several strategies for computing the next snapshot version via the `releaseVersionBump` setting. These strategies are defined in `sbtrelease.Version.Bump`. By default, the `Next` strategy is used:

* `Major`: always bumps the *major* part of the version
* `Minor`: always bumps the *minor* part of the version
* `Bugfix`: always bumps the *bugfix* part of the version
* `Nano`: always bumps the *nano* part of the version
* `Next`: bumps the last version part (e.g. `0.17` -> `0.18`, `0.11.7` -> `0.11.8`, `3.22.3.4.91` -> `3.22.3.4.92`)
* `Next` (**default**): bumps the last version part, including the qualifier (e.g. `0.17` -> `0.18`, `0.11.7` -> `0.11.8`, `3.22.3.4.91` -> `3.22.3.4.92`, `1.0.0-RC1` -> `1.0.0-RC2`)
* `NextStable`: bumps exactly like `Next` except that any prerelease qualifier is excluded (e.g. `1.0.0-RC1` -> `1.0.0`)

Example:
Users can set their preferred versioning strategy in `build.sbt` as follows:
```sbt
releaseVersionBump := sbtrelease.Version.Bump.Major
```

### Default Versioning

The default settings make use of the helper class [`Version`](https://github.com/sbt/sbt-release/blob/master/src/main/scala/Version.scala) that ships with *sbt-release*.

`releaseVersion`: The current version in version.sbt, without the "-SNAPSHOT" ending. So, if `version.sbt` contains `1.0.0-SNAPSHOT`, the release version will be set to `1.0.0`.

releaseVersionBump := sbtrelease.Version.Bump.Major
`releaseNextVersion`: The "bumped" version according to the versioning strategy (explained above), including the `-SNAPSHOT` ending. So, if `releaseVersion` is `1.0.0`, `releaseNextVersion` will be `1.0.1-SNAPSHOT`.

### Custom versioning
### Custom Versioning

*sbt-release* comes with two settings for deriving the release version and the next development version from a given version.

Expand All @@ -132,20 +142,8 @@ These derived versions are used for the suggestions/defaults in the prompt and f
Let's take a look at the types:

```scala
val releaseVersion : SettingKey[String => String]
val releaseNextVersion : SettingKey[String => String]
```

The default settings make use of the helper class [`Version`](https://github.com/sbt/sbt-release/blob/master/src/main/scala/Version.scala) that ships with *sbt-release*.

```scala
// strip the qualifier off the input version, eg. 1.2.1-SNAPSHOT -> 1.2.1
releaseVersion := { ver => Version(ver).map(_.withoutQualifier.string).getOrElse(versionFormatError(ver)) }

// bump the version and append '-SNAPSHOT', eg. 1.2.1 -> 1.3.0-SNAPSHOT
releaseNextVersion := {
ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.string).getOrElse(versionFormatError(ver))
},
val releaseVersion : TaskKey[String => String]
val releaseNextVersion : TaskKey[String => String]
```

If you want to customize the versioning, keep the following in mind:
Expand Down
28 changes: 20 additions & 8 deletions src/main/scala/ReleasePlugin.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package sbtrelease

import java.io.Serializable

import sbt._
import Keys._
import sbt.complete.DefaultParsers._
import sbt.*
import Keys.*
import sbt.complete.DefaultParsers.*
import sbt.complete.Parser
import sbtrelease.Version.Bump

object ReleasePlugin extends AutoPlugin {

Expand Down Expand Up @@ -73,7 +73,7 @@ object ReleasePlugin extends AutoPlugin {
withStreams(extracted.structure, st) { str =>
val nv = nodeView(st, str, key :: Nil)
val (newS, result) = runTask(task, st, str, extracted.structure.index.triggers, config)(nv)
(newS, processResult(result, newS.log))
(newS, processResult2(result))
}._1
}

Expand Down Expand Up @@ -222,11 +222,23 @@ object ReleasePlugin extends AutoPlugin {
val snapshots = moduleIds.filter(m => m.isChanging || m.revision.endsWith("-SNAPSHOT"))
snapshots
},

releaseVersion := { ver => Version(ver).map(_.withoutQualifier.string).getOrElse(versionFormatError(ver)) },
releaseVersion := { rawVersion =>
Version(rawVersion).map { version =>
releaseVersionBump.value match {
case Bump.Next =>
if (version.isSnapshot) {
version.withoutSnapshot.unapply
} else {
expectedSnapshotVersionError(rawVersion)
}
case _ => version.withoutQualifier.unapply
}
}
.getOrElse(versionFormatError(rawVersion))
},
releaseVersionBump := Version.Bump.default,
releaseNextVersion := {
ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.string).getOrElse(versionFormatError(ver))
ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.unapply).getOrElse(versionFormatError(ver))
},
releaseUseGlobalVersion := true,
releaseCrossBuild := false,
Expand Down
130 changes: 108 additions & 22 deletions src/main/scala/Version.scala
Original file line number Diff line number Diff line change
@@ -1,24 +1,62 @@
package sbtrelease

import util.control.Exception._
import scala.util.matching.Regex
import util.control.Exception.*

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
/**
* Strategy to always bump the major version by default. Ex. 1.0.0 would be bumped to 2.0.0
*/
case object Major extends Bump { def bump: Version => Version = _.bumpMajor }
/**
* Strategy to always bump the minor version by default. Ex. 1.0.0 would be bumped to 1.1.0
*/
case object Minor extends Bump { def bump: Version => Version = _.bumpMinor }
/**
* Strategy to always bump the bugfix version by default. Ex. 1.0.0 would be bumped to 1.0.1
*/
case object Bugfix extends Bump { def bump: Version => Version = _.bumpBugfix }
/**
* Strategy to always bump the nano version by default. Ex. 1.0.0.0 would be bumped to 1.0.0.1
*/
case object Nano extends Bump { def bump: Version => Version = _.bumpNano }


/**
* Strategy to always increment to the next version from smallest to greatest, including prerelease versions
* Ex:
* Major: 1 becomes 2
* Minor: 1.0 becomes 1.1
* Bugfix: 1.0.0 becomes 1.0.1
* Nano: 1.0.0.0 becomes 1.0.0.1
* Qualifier with version number: 1.0-RC1 becomes 1.0-RC2
* Qualifier without version number: 1.0-alpha becomes 1.0
*/
case object Next extends Bump { def bump: Version => Version = _.bumpNext }

/**
* Strategy to always increment to the next version from smallest to greatest, excluding prerelease versions
* Ex:
* Major: 1 becomes 2
* Minor: 1.0 becomes 1.1
* Bugfix: 1.0.0 becomes 1.0.1
* Nano: 1.0.0.0 becomes 1.0.0.1
* Qualifier with version number: 1.0-RC1 becomes 1.0
* Qualifier without version number: 1.0-alpha becomes 1.0
*/
case object NextStable extends Bump { def bump: Version => Version = _.bumpNextStable }

val default: Bump = Next
}

val VersionR = """([0-9]+)((?:\.[0-9]+)+)?([\.\-0-9a-zA-Z]*)?""".r
val PreReleaseQualifierR = """[\.-](?i:rc|m|alpha|beta)[\.-]?[0-9]*""".r
val VersionR: Regex = """([0-9]+)((?:\.[0-9]+)+)?([\.\-0-9a-zA-Z]*)?""".r
val PreReleaseQualifierR: Regex = """[\.-](?i:rc|m|alpha|beta)[\.-]?[0-9]*""".r

def apply(s: String): Option[Version] = {
allCatch opt {
Expand All @@ -34,24 +72,52 @@ object Version {
}

case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String]) {
def bump = {
val maybeBumpedPrerelease = qualifier.collect {
case Version.PreReleaseQualifierR() => withoutQualifier

@deprecated("Use .bumpNext or .bumpNextStable instead")
def bump: Version = bumpNext

def bumpNext: Version = {
val bumpedPrereleaseVersionOpt = qualifier.collect {
case rawQualifier @ Version.PreReleaseQualifierR() =>
val qualifierEndsWithNumberRegex = """[0-9]*$""".r

val opt = for {
versionNumberQualifierStr <- qualifierEndsWithNumberRegex.findFirstIn(rawQualifier)
versionNumber <- Try(versionNumberQualifierStr.toInt)
.toRight(new Exception(s"Version number not parseable to a number. Version number received: $versionNumberQualifierStr"))
.toOption
newVersionNumber = versionNumber + 1
newQualifier = rawQualifier.replaceFirst(versionNumberQualifierStr, newVersionNumber.toString)
} yield Version(major, subversions, Some(newQualifier))

opt.getOrElse(this.withoutQualifier)
}
def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length-1)

bumpNextGeneric(bumpedPrereleaseVersionOpt)
}
private def bumpNextGeneric(bumpedPrereleaseVersionOpt: Option[Version]): Version = {
def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length - 1)

def bumpedMajor = copy(major = major + 1)

maybeBumpedPrerelease
bumpedPrereleaseVersionOpt
.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 bumpNextStable: Version = {
val bumpedPrereleaseVersionOpt = qualifier.collect {
case Version.PreReleaseQualifierR() => withoutQualifier
}
bumpNextGeneric(bumpedPrereleaseVersionOpt)
}

def bumpMajor: Version = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0))
def bumpMinor: Version = maybeBumpSubversion(0)
def bumpBugfix: Version = maybeBumpSubversion(1)
def bumpNano: Version = maybeBumpSubversion(2)

def maybeBumpSubversion(idx: Int) = bumpSubversionOpt(idx) getOrElse this
def maybeBumpSubversion(idx: Int): Version = bumpSubversionOpt(idx) getOrElse this

private def bumpSubversionOpt(idx: Int) = {
val bumped = subversions.drop(idx)
Expand All @@ -64,10 +130,30 @@ case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String])

def bump(bumpType: Version.Bump): Version = bumpType.bump(this)

def withoutQualifier = copy(qualifier = None)
def asSnapshot = copy(qualifier = Some("-SNAPSHOT"))
def withoutQualifier: Version = copy(qualifier = None)
def asSnapshot: Version = copy(qualifier = qualifier.map { qualifierStr =>
s"$qualifierStr-SNAPSHOT"
}.orElse(Some("-SNAPSHOT")))

def isSnapshot: Boolean = qualifier.exists { qualifierStr =>
val snapshotRegex = """(^.*)-SNAPSHOT$""".r
qualifierStr.matches(snapshotRegex.regex)
}

def withoutSnapshot: Version = copy(qualifier = qualifier.flatMap { qualifierStr =>
val snapshotRegex = """-SNAPSHOT""".r
val newQualifier = snapshotRegex.replaceFirstIn(qualifierStr, "")
if (newQualifier == qualifierStr) {
None
} else {
Some(newQualifier)
}
})

@deprecated("Use .unapply instead")
def string: String = unapply

def string = "" + major + mkString(subversions) + qualifier.getOrElse("")
def unapply: String = "" + major + mkString(subversions) + qualifier.getOrElse("")

private def mkString(parts: Seq[Int]) = parts.map("."+_).mkString
}
2 changes: 2 additions & 0 deletions src/main/scala/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ package object sbtrelease {
type Versions = (String, String)

def versionFormatError(version: String) = sys.error(s"Version [$version] format is not compatible with " + Version.VersionR.pattern.toString)

def expectedSnapshotVersionError(version: String) = sys.error(s"Expected snapshot version. Received: $version")
}
10 changes: 10 additions & 0 deletions src/sbt-test/sbt-release/with-defaults/build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sbt.complete.DefaultParsers._
import sbtrelease.ReleaseStateTransformations._

releaseVersionFile := file("version.sbt")
Expand All @@ -13,3 +14,12 @@ releaseProcess := Seq(
setNextVersion,
commitNextVersion
)

val checkContentsOfVersionSbt = inputKey[Unit]("Check that the contents of version.sbt is as expected")
val parser = Space ~> StringBasic

checkContentsOfVersionSbt := {
val expected = parser.parsed
val versionFile = ((baseDirectory).value) / "version.sbt"
assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}")
}
44 changes: 37 additions & 7 deletions src/sbt-test/sbt-release/with-defaults/test
Original file line number Diff line number Diff line change
@@ -1,11 +1,41 @@
$ exec git init .
# Test Suite Preparation
$ exec git init .
> update
$ exec git add .
$ exec git commit -m init
> reload

> update
# SCENARIO: When no release versions are specified in the release command
# TEST: Should fail to release if "with-defaults" is not specified
-> release

$ exec git add .
$ exec git commit -m init
# TEST: Should succeed if "with-defaults" is specified
> release with-defaults

> reload
# SCENARIO: When default bumping strategy is used
# Test Scenario Preparation
> 'release release-version 0.9.9 next-version 1.0.0-RC1-SNAPSHOT'
> reload
> checkContentsOfVersionSbt 1.0.0-RC1-SNAPSHOT

# TEST: Snapshot version should be correctly set
> release with-defaults
> checkContentsOfVersionSbt 1.0.0-RC2-SNAPSHOT

# TEST: Release version should be correctly set
$ exec git reset --hard HEAD~1
> reload
> checkContentsOfVersionSbt 1.0.0-RC1

# SCENARIO: When NextStable bumping strategy is used
# TEST: Snapshot version should be correctly set
$ exec git reset --hard HEAD~1
> set releaseVersionBump := sbtrelease.Version.Bump.NextStable
> release with-defaults
> checkContentsOfVersionSbt 1.0.1-SNAPSHOT

# TEST: Release version should be correctly set
$ exec git reset --hard HEAD~1
> reload
> checkContentsOfVersionSbt 1.0.0

-> release
> release with-defaults
Loading

0 comments on commit 3e9020f

Please sign in to comment.