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: adding prerelease version increments #397

Merged
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
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking we are fine with merging .gitignore changes, but regardless, I'll say: you'll probably want to add .bsp to your global gitignore, so then you won't need to submit this change to every repository that you contribute to.

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