The SBT Version Scheme Enforcer plugin is a plugin which automatically configures Migration Manager (MiMa) to verify the binary compatibility constraints of your library.
If you are using git, then all you need to do is add the plugin to your project/plugins.sbt
file. If you are not using git, then you will additionally have to set versionSchemeEnforcerPreviousVersion
with your previous version.
addSbtPlugin("io.isomarcte" % "sbt-version-scheme-enforcer-plugin" % "2.1.0.2")
And ensure you've set versionScheme
in your build.sbt
.
ThisBuild / versionScheme := Some("early-semver") // or Some("pvp") or Some("semver-spec")
That's it! Now all your MiMa settings will be automatically derived.
Because your intended binary compatibility guarantees is directly a function of the versionScheme, previous version, and next version, everything else can be derived.
The previous version of your project will be detected from the current commits most recent git tag.
You can then run either mimaReportBinaryIssues
as normal or versionSchemeEnforcerCheck
(differences explained below).
If you are using a multi-module build and you don't want to run this on your the root project, then add this to your root project only.
lazy val root = (project in file(".")).settings(/* settings */).disablePlugins(SbtVersionSchemeEnforcerPlugin)
The SBT Version Scheme enforcer plugin aims to allow library authors to configure mima for their library with minimal effort and is unopinionated about the version scheme you choose to use.
- SBT >= 1.4.0
- git on the PATH (for automatic previous version calculation)
The currently supported versioning schemes are,
This is the full set of SBT Tasks and Settings this plugin provides. Usually you won't need to bother with most of them.
You can view them in code here: https://github.com/isomarcte/sbt-version-scheme-enforcer/blob/main/plugin/src/main/scala/io/isomarcte/sbt/version/scheme/enforcer/plugin/Keys.scala
Any setting can be manually set at the project level and it will be left alone by the plugin.
Name | Type | Description |
---|---|---|
versionSchemeEnforcerPreviousVersion | Option[String] |
Previous version to compare against the current version for calculating binary compatibility. If this is not set manually and can not be derived from the Version Control System (VCS), then it will default to the value of versionSchemeEnforcerInitialVersion. |
versionSchemeEnforcerChangeType | Either[Throwable, VersionChangeType] |
The type of binary change. It is used to configured MiMa settings. Normally this is derived from versionSchemeEnforcerPreviousVersion and should not normally be set directly. If it results in an error and versionSchemeEnforcerCheck is run, that error is raised. |
versionSchemeEnforcerInitialVersion | Option[String] |
The initial version which should have the versionScheme enforced. If this is set then verions <= to this version will have Mima configured to not validate any binary compatibility constraints. This is particularly useful when you are adding a new module to an exsiting project. |
versionSchemeEnforcerPreviousVCSTagFilter | Tag => Boolean |
A filter used when determining the previous version from a VCS tag. The selected tag will be the most recent tag, reachable from the current commit, for which this filter returns true. A common use case for this is not considering pre-release in binary compatibility checks. For example, assuming your versionScheme is Semver or Early Semver, if you are releasing 1.1.0-M3, you may want to consider binary compatibility compared to the last 1.0.x release, and permit arbitrary binary changes between various milestone releases. By default, comparing two versions which have the same numeric base version will imply that no visible changes have been made to the binary API, e.g. comparing 1.1.0-M2 to 1.1.0-M3 will yield a binary change type of Patch (assuming Semver or Early Semver). This setting operates directly on the Tag data type which gives full access to the Tag metadata. If you only want to inspect the String representation of a Tag, you can use versionSchemeEnforcerPreviousVCSTagStringFilter. At most one of these settings may be defined. Defining both will result in an error. If none are set then no filtering will be done on tags, which is equivalent to a filter with the definition Function.const(true) |
versionSchemeEnforcerPreviousVCSTagStringFilter | String => Boolean |
A filter used when determining the previous version from a VCS tag. The selected tag will be the most recent tag, reachable from the current commit, for which this filter returns true. A common use case for this is not considering pre-release in binary compatibility checks. For example, assuming your versionScheme is Semver or Early Semver, if you are releasing 1.1.0-M3, you may want to consider binary compatibility compared to the last 1.0.x release, and permit arbitrary binary changes between various milestone releases. By default, comparing two versions which have the same numeric base version will imply that no visible changes have been made to the binary API, e.g. comparing 1.1.0-M2 to 1.1.0-M3 will yield a binary change type of Patch (assuming Semver or Early Semver). This setting operates only on the String representation of a Tag. If you need to inspect the full metadata of a VCS tag, then you can use versionSchemeEnforcerPreviousVCSTagFilter. At most one of these settings may be defined. Defining both will result in an error. If none are set then no filtering will be done on tags, which is equivalent to a filter with the definition Function.const(true) |
versionSchemeEnforcerTagDomain | TagDomain |
The domain of VCS tags to consider when looking for previous releases to use in the binary compatibility check. For example, this can be TagDomain.All to consider all tags on the repository, or TagDomain.Reachable to only consider tags which are reachable (ancestors) of the current commit. The later case can be useful when you have multiple branches which should not be considered directly related for the purposes of binary compatibility. TagDomain.All is the default as of 2.1.1.0. The behavior prior to 2.1.1.0 was equivalent to TagDomain.Reachable. |
Name | Type | Description |
---|---|---|
versionSchemeEnforcerIntialVersion | Option[String] |
DO NOT USE THIS. Use versionSchemeEnforcerInitialVersion instead. This key has a spelling error in its name (Intial -> Initial). If this is set and versionSchemeEnforcerInitialVersion is not set, this will be used as versionSchemeEnforcerInitialVersion. If versionSchemeEnforcerInitialVersion is set this will be ignored. |
versionSchemeEnforcerPreviousTagFilter | String => Boolean |
Please use versionSchemeEnforcerPreviousVCSTagStringFilter or versionSchemeEnforcerPreviousVCSTagFilter instead of this. Setting this value is equivalent to setting versionSchemeEnforcerPreviousVCSTagStringFilter. Only one may be set. If both are set then an error will occur. |
Name | Type | Description |
---|---|---|
versionSchemeEnforcerCheck | Unit |
Verifies that the sbt-version-scheme-enforcer settings are valid and runs MiMa with the derived settings. |
versionSchemeEnforcerCheck
and mimaReportBinaryIssues
are very similar, and in fact most of what versionSchemeEnforcerCheck
does is just run mimaReportBinaryIssues
. The only significant difference is that versionSchemeEnforcerCheck
will raise an error if any of the settings required from validating the version scheme are missing or invalid. For example, if you don't set versionScheme
or set it to an invalid value versionSchemeEnforcerCheck
will fail. On the other hand, if any of the settings required for verifying the version scheme are invalid/missing none of the mima settings are modified by this plugin.
This means that you can still run mimaReportBinaryIssues
and this plugin will stay out of your way.
As of version 2.0.0.0 of this plugin when determining the previous version from a VCS tag, e.g. a git tag, you can provide a filter to remove certain tags from consideration. This is done by setting the versionSchemeEnforcerPreviousTagFilter
key. The tag selected will be the most recent tag, reachable from the current commit, for which the function defined by versionSchemeEnforcerPreviousTagFilter
returns true
.
A common use case for this is filtering out milestone releases. Consider if we have a project (using PVP) and we are attempting to release version 1.1.0.0-M2
. By default, the previous version selected from the VCS tag will be 1.1.0.0-M1
, which will yield a numeric version of 1.1.0.0
. This would result in a version change type of Patch
, which would mean that the publicly exported symbols (methods/functions/class/etc.) must have not changed at all. This is probably not what we want. We likely want to compare 1.1.0.0-M2
with the most recent full release, in our example let's say that is 1.0.2.1
. Comparing 1.0.2.1
and 1.1.0.0-M1
under PVP would mean that this is a binary breaking release and that any symbols may have been removed or added between the two version, e.g. it is a Major
change type. We can achieve this behavior by setting versionSchemeEnforcerPreviousTagFilter
like so,
ThisBuild / versionSchemeEnforcerPreviousTagFilter := {(value: String) =>
if(value.matches(""".*-M\d+$""")) {
false
} else {
true
}
}
This will automatically filter out any tags which end in a -M\d+
, e.g. -M2
. As this is a common use case a prebuilt filter is available in TagFilters
.
// You need to fully qualify the import to avoid conflicts with sbt.io
import _root_.io.isomarcte.sbt.version.scheme.enforcer.plugin.TagFilters
ThisBuild / versionSchemeEnforcerPreviousTagFilter := TagFilters.noMilestoneFilter
versionSchemeEnforcerTagDomain
describes which VCS tags to consider when attempting to automatically calculate the previous version. The default value is TagDomain.All
, the possible values are described here.
Name | Description |
---|---|
All | Consider all tags on the repository, even if they are not ancestors of the current commit. |
Reachable | Only consider tags which are reachable (ancestors) of the current commit. |
Unreachable | Only consider tags which are unreachable (not ancestors) of the current commit. I don't know why you'd use this. |
Contains | Only consider tags which contain this commit. I don't know why you'd use this. |
NoContains | Only consider tags which do not contain this commit. This is similar to All, but will never include any tags which are present on this commit. In typical usage of this plugin it is unlikely this circumstance will occur. |
There are a couple other plugins out there which provide similar features compared to this plugin. In particular SBT Version Policy and SBT Mima Version Check. These plugins are both great. sbt-version-policy also provides some additional features which this plugin does not, namely checking the versioning information of dependencies.
However, the primary reason that I decided to write this plugin is that both sbt-version-policy and sbt-mima-version-check only support configuring Mima for Early SemVer, and in the case of sbt-version-policy they indicated that at this time they didn't intend to support any other versioning scheme (pvp or semver). There are some use cases in versioning which Early SemVer is fundamentally unable to express, namely supporting multiple long lived versions of the same project.
For example, let's say that you have a library which depends on Cats Effect at version 2.x.x. Now you want to update your library to use the new upcoming cats-effect 3 release, but you still want to maintain an old version for your users who haven't yet updated for Cats Effect 3. In a SemVer or Early SemVer world you update your library from version 1.2.3
to 2.0.0
. At this point you can keep release 1.x.x
branches as long as you never break binary compatibility. If this fits your use case, then you are all set, however if you do end up needing to make a binary incompatible release on the 1.x.x
branch (perhaps becomes some other library you depend on forced a binary incompatible update), then you are in an difficult situation. What version should your new 1.x.x
release have? By definition, it can't be a 1.x.x
release, nor can it be 2.x.x
because that is already in use for your Cats Effect 3 branch, and obviously 3.0.0
would be valid but extremely confusing for your users.
However, if you are using pvp then the situation is different. In PVP the first two version numbers both describe a binary breaking change.
For example,
0.0.0.1
->0.1.0.0
is a binary incompatible change.0.1.0.0
->0.2.0.0
is a binary incompatible change.0.2.0.0
->1.0.0.0
is a binary incompatible change.0.0.0.1
->1.0.0.0
is a binary incompatible change.0.1.0.1
->1.0.0.0
is a binary incompatible change.
Taking our Cats Effect 3 example above. If we were maintaining two versions of our library, one Cats Effect 2 and one Cats Effect 3, then we would have versions 0.1.0.0
and 1.0.0.0
respectively. If we needed for some reason to make a binary breaking change to our Cats Effect 2 branch, then we can release at version 0.2.0.0
. This version indicates that we have broken binary compatibility with 0.1.0.0
but importantly also indicates that 0.2.0.0
-> 1.0.0.0
is also binary breaking.
To be very clear, maintaining multiple long lived branches like that is difficult and requires significantly more work from the library authors. If you don't have that use case and are sure you never will, then semver or Early SemVer are both perfectly good versioning schemes, with the obvious advantage that they are more widely understood (especially in the JVM community). However, if you do have that use case (and I personally do), then PVP allows you to handle it. As a final motivating example, a project which does have this use case is actually the Scala compiler/standard library itself. Scala 2.11.x
is binary incompatible with 2.12.x
is binary incompatible with 2.13.x
is binary incompatible 3.x.x
. While perhaps not intentionally, this is a good example of pvp versioning and its utility.