Skip to content

Commit 3e9020f

Browse files
authored
feat: adding prerelease version increments (#397)
**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`
1 parent c809a57 commit 3e9020f

File tree

8 files changed

+290
-88
lines changed

8 files changed

+290
-88
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ target
22
project/target
33
.idea
44
.idea_modules
5+
.bsp

README.md

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,31 @@ A cross release behaves analogous to using the `+` command:
109109

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

112-
### Convenient versioning
112+
### Versioning Strategies
113113

114-
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:
114+
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:
115115

116116
* `Major`: always bumps the *major* part of the version
117117
* `Minor`: always bumps the *minor* part of the version
118118
* `Bugfix`: always bumps the *bugfix* part of the version
119119
* `Nano`: always bumps the *nano* part of the version
120-
* `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`)
120+
* `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`)
121+
* `NextStable`: bumps exactly like `Next` except that any prerelease qualifier is excluded (e.g. `1.0.0-RC1` -> `1.0.0`)
121122

122-
Example:
123+
Users can set their preferred versioning strategy in `build.sbt` as follows:
124+
```sbt
125+
releaseVersionBump := sbtrelease.Version.Bump.Major
126+
```
127+
128+
### Default Versioning
129+
130+
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*.
131+
132+
`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`.
123133

124-
releaseVersionBump := sbtrelease.Version.Bump.Major
134+
`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`.
125135

126-
### Custom versioning
136+
### Custom Versioning
127137

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

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

134144
```scala
135-
val releaseVersion : SettingKey[String => String]
136-
val releaseNextVersion : SettingKey[String => String]
137-
```
138-
139-
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*.
140-
141-
```scala
142-
// strip the qualifier off the input version, eg. 1.2.1-SNAPSHOT -> 1.2.1
143-
releaseVersion := { ver => Version(ver).map(_.withoutQualifier.string).getOrElse(versionFormatError(ver)) }
144-
145-
// bump the version and append '-SNAPSHOT', eg. 1.2.1 -> 1.3.0-SNAPSHOT
146-
releaseNextVersion := {
147-
ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.string).getOrElse(versionFormatError(ver))
148-
},
145+
val releaseVersion : TaskKey[String => String]
146+
val releaseNextVersion : TaskKey[String => String]
149147
```
150148

151149
If you want to customize the versioning, keep the following in mind:

src/main/scala/ReleasePlugin.scala

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package sbtrelease
22

33
import java.io.Serializable
4-
5-
import sbt._
6-
import Keys._
7-
import sbt.complete.DefaultParsers._
4+
import sbt.*
5+
import Keys.*
6+
import sbt.complete.DefaultParsers.*
87
import sbt.complete.Parser
8+
import sbtrelease.Version.Bump
99

1010
object ReleasePlugin extends AutoPlugin {
1111

@@ -73,7 +73,7 @@ object ReleasePlugin extends AutoPlugin {
7373
withStreams(extracted.structure, st) { str =>
7474
val nv = nodeView(st, str, key :: Nil)
7575
val (newS, result) = runTask(task, st, str, extracted.structure.index.triggers, config)(nv)
76-
(newS, processResult(result, newS.log))
76+
(newS, processResult2(result))
7777
}._1
7878
}
7979

@@ -222,11 +222,23 @@ object ReleasePlugin extends AutoPlugin {
222222
val snapshots = moduleIds.filter(m => m.isChanging || m.revision.endsWith("-SNAPSHOT"))
223223
snapshots
224224
},
225-
226-
releaseVersion := { ver => Version(ver).map(_.withoutQualifier.string).getOrElse(versionFormatError(ver)) },
225+
releaseVersion := { rawVersion =>
226+
Version(rawVersion).map { version =>
227+
releaseVersionBump.value match {
228+
case Bump.Next =>
229+
if (version.isSnapshot) {
230+
version.withoutSnapshot.unapply
231+
} else {
232+
expectedSnapshotVersionError(rawVersion)
233+
}
234+
case _ => version.withoutQualifier.unapply
235+
}
236+
}
237+
.getOrElse(versionFormatError(rawVersion))
238+
},
227239
releaseVersionBump := Version.Bump.default,
228240
releaseNextVersion := {
229-
ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.string).getOrElse(versionFormatError(ver))
241+
ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.unapply).getOrElse(versionFormatError(ver))
230242
},
231243
releaseUseGlobalVersion := true,
232244
releaseCrossBuild := false,

src/main/scala/Version.scala

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,62 @@
11
package sbtrelease
22

3-
import util.control.Exception._
3+
import scala.util.matching.Regex
4+
import util.control.Exception.*
45

56
object Version {
67
sealed trait Bump {
78
def bump: Version => Version
89
}
910

1011
object Bump {
11-
case object Major extends Bump { def bump = _.bumpMajor }
12-
case object Minor extends Bump { def bump = _.bumpMinor }
13-
case object Bugfix extends Bump { def bump = _.bumpBugfix }
14-
case object Nano extends Bump { def bump = _.bumpNano }
15-
case object Next extends Bump { def bump = _.bump }
1612

17-
val default = Next
13+
/**
14+
* Strategy to always bump the major version by default. Ex. 1.0.0 would be bumped to 2.0.0
15+
*/
16+
case object Major extends Bump { def bump: Version => Version = _.bumpMajor }
17+
/**
18+
* Strategy to always bump the minor version by default. Ex. 1.0.0 would be bumped to 1.1.0
19+
*/
20+
case object Minor extends Bump { def bump: Version => Version = _.bumpMinor }
21+
/**
22+
* Strategy to always bump the bugfix version by default. Ex. 1.0.0 would be bumped to 1.0.1
23+
*/
24+
case object Bugfix extends Bump { def bump: Version => Version = _.bumpBugfix }
25+
/**
26+
* Strategy to always bump the nano version by default. Ex. 1.0.0.0 would be bumped to 1.0.0.1
27+
*/
28+
case object Nano extends Bump { def bump: Version => Version = _.bumpNano }
29+
30+
31+
/**
32+
* Strategy to always increment to the next version from smallest to greatest, including prerelease versions
33+
* Ex:
34+
* Major: 1 becomes 2
35+
* Minor: 1.0 becomes 1.1
36+
* Bugfix: 1.0.0 becomes 1.0.1
37+
* Nano: 1.0.0.0 becomes 1.0.0.1
38+
* Qualifier with version number: 1.0-RC1 becomes 1.0-RC2
39+
* Qualifier without version number: 1.0-alpha becomes 1.0
40+
*/
41+
case object Next extends Bump { def bump: Version => Version = _.bumpNext }
42+
43+
/**
44+
* Strategy to always increment to the next version from smallest to greatest, excluding prerelease versions
45+
* Ex:
46+
* Major: 1 becomes 2
47+
* Minor: 1.0 becomes 1.1
48+
* Bugfix: 1.0.0 becomes 1.0.1
49+
* Nano: 1.0.0.0 becomes 1.0.0.1
50+
* Qualifier with version number: 1.0-RC1 becomes 1.0
51+
* Qualifier without version number: 1.0-alpha becomes 1.0
52+
*/
53+
case object NextStable extends Bump { def bump: Version => Version = _.bumpNextStable }
54+
55+
val default: Bump = Next
1856
}
1957

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

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

3674
case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String]) {
37-
def bump = {
38-
val maybeBumpedPrerelease = qualifier.collect {
39-
case Version.PreReleaseQualifierR() => withoutQualifier
75+
76+
@deprecated("Use .bumpNext or .bumpNextStable instead")
77+
def bump: Version = bumpNext
78+
79+
def bumpNext: Version = {
80+
val bumpedPrereleaseVersionOpt = qualifier.collect {
81+
case rawQualifier @ Version.PreReleaseQualifierR() =>
82+
val qualifierEndsWithNumberRegex = """[0-9]*$""".r
83+
84+
val opt = for {
85+
versionNumberQualifierStr <- qualifierEndsWithNumberRegex.findFirstIn(rawQualifier)
86+
versionNumber <- Try(versionNumberQualifierStr.toInt)
87+
.toRight(new Exception(s"Version number not parseable to a number. Version number received: $versionNumberQualifierStr"))
88+
.toOption
89+
newVersionNumber = versionNumber + 1
90+
newQualifier = rawQualifier.replaceFirst(versionNumberQualifierStr, newVersionNumber.toString)
91+
} yield Version(major, subversions, Some(newQualifier))
92+
93+
opt.getOrElse(this.withoutQualifier)
4094
}
41-
def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length-1)
95+
96+
bumpNextGeneric(bumpedPrereleaseVersionOpt)
97+
}
98+
private def bumpNextGeneric(bumpedPrereleaseVersionOpt: Option[Version]): Version = {
99+
def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length - 1)
100+
42101
def bumpedMajor = copy(major = major + 1)
43102

44-
maybeBumpedPrerelease
103+
bumpedPrereleaseVersionOpt
45104
.orElse(maybeBumpedLastSubversion)
46105
.getOrElse(bumpedMajor)
47106
}
48107

49-
def bumpMajor = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0))
50-
def bumpMinor = maybeBumpSubversion(0)
51-
def bumpBugfix = maybeBumpSubversion(1)
52-
def bumpNano = maybeBumpSubversion(2)
108+
def bumpNextStable: Version = {
109+
val bumpedPrereleaseVersionOpt = qualifier.collect {
110+
case Version.PreReleaseQualifierR() => withoutQualifier
111+
}
112+
bumpNextGeneric(bumpedPrereleaseVersionOpt)
113+
}
114+
115+
def bumpMajor: Version = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0))
116+
def bumpMinor: Version = maybeBumpSubversion(0)
117+
def bumpBugfix: Version = maybeBumpSubversion(1)
118+
def bumpNano: Version = maybeBumpSubversion(2)
53119

54-
def maybeBumpSubversion(idx: Int) = bumpSubversionOpt(idx) getOrElse this
120+
def maybeBumpSubversion(idx: Int): Version = bumpSubversionOpt(idx) getOrElse this
55121

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

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

67-
def withoutQualifier = copy(qualifier = None)
68-
def asSnapshot = copy(qualifier = Some("-SNAPSHOT"))
133+
def withoutQualifier: Version = copy(qualifier = None)
134+
def asSnapshot: Version = copy(qualifier = qualifier.map { qualifierStr =>
135+
s"$qualifierStr-SNAPSHOT"
136+
}.orElse(Some("-SNAPSHOT")))
137+
138+
def isSnapshot: Boolean = qualifier.exists { qualifierStr =>
139+
val snapshotRegex = """(^.*)-SNAPSHOT$""".r
140+
qualifierStr.matches(snapshotRegex.regex)
141+
}
142+
143+
def withoutSnapshot: Version = copy(qualifier = qualifier.flatMap { qualifierStr =>
144+
val snapshotRegex = """-SNAPSHOT""".r
145+
val newQualifier = snapshotRegex.replaceFirstIn(qualifierStr, "")
146+
if (newQualifier == qualifierStr) {
147+
None
148+
} else {
149+
Some(newQualifier)
150+
}
151+
})
152+
153+
@deprecated("Use .unapply instead")
154+
def string: String = unapply
69155

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

72158
private def mkString(parts: Seq[Int]) = parts.map("."+_).mkString
73159
}

src/main/scala/package.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ package object sbtrelease {
22
type Versions = (String, String)
33

44
def versionFormatError(version: String) = sys.error(s"Version [$version] format is not compatible with " + Version.VersionR.pattern.toString)
5+
6+
def expectedSnapshotVersionError(version: String) = sys.error(s"Expected snapshot version. Received: $version")
57
}

src/sbt-test/sbt-release/with-defaults/build.sbt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sbt.complete.DefaultParsers._
12
import sbtrelease.ReleaseStateTransformations._
23

34
releaseVersionFile := file("version.sbt")
@@ -13,3 +14,12 @@ releaseProcess := Seq(
1314
setNextVersion,
1415
commitNextVersion
1516
)
17+
18+
val checkContentsOfVersionSbt = inputKey[Unit]("Check that the contents of version.sbt is as expected")
19+
val parser = Space ~> StringBasic
20+
21+
checkContentsOfVersionSbt := {
22+
val expected = parser.parsed
23+
val versionFile = ((baseDirectory).value) / "version.sbt"
24+
assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}")
25+
}

src/sbt-test/sbt-release/with-defaults/test

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,41 @@
1-
$ exec git init .
1+
# Test Suite Preparation
2+
$ exec git init .
3+
> update
4+
$ exec git add .
5+
$ exec git commit -m init
6+
> reload
27

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

5-
$ exec git add .
6-
$ exec git commit -m init
12+
# TEST: Should succeed if "with-defaults" is specified
13+
> release with-defaults
714

8-
> reload
15+
# SCENARIO: When default bumping strategy is used
16+
# Test Scenario Preparation
17+
> 'release release-version 0.9.9 next-version 1.0.0-RC1-SNAPSHOT'
18+
> reload
19+
> checkContentsOfVersionSbt 1.0.0-RC1-SNAPSHOT
20+
21+
# TEST: Snapshot version should be correctly set
22+
> release with-defaults
23+
> checkContentsOfVersionSbt 1.0.0-RC2-SNAPSHOT
24+
25+
# TEST: Release version should be correctly set
26+
$ exec git reset --hard HEAD~1
27+
> reload
28+
> checkContentsOfVersionSbt 1.0.0-RC1
29+
30+
# SCENARIO: When NextStable bumping strategy is used
31+
# TEST: Snapshot version should be correctly set
32+
$ exec git reset --hard HEAD~1
33+
> set releaseVersionBump := sbtrelease.Version.Bump.NextStable
34+
> release with-defaults
35+
> checkContentsOfVersionSbt 1.0.1-SNAPSHOT
36+
37+
# TEST: Release version should be correctly set
38+
$ exec git reset --hard HEAD~1
39+
> reload
40+
> checkContentsOfVersionSbt 1.0.0
941

10-
-> release
11-
> release with-defaults

0 commit comments

Comments
 (0)