From fde33bcc089427d3299e37b57c70e44542329f53 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Mon, 27 Nov 2023 16:12:50 +0100 Subject: [PATCH 1/4] Make sbt-ci-release tests pass on JDK 17 --- .../sbt-version-policy/example-sbt-ci-release/build.sbt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-ci-release/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-ci-release/build.sbt index 9d51931..4662dc4 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-ci-release/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-ci-release/build.sbt @@ -28,8 +28,12 @@ Global / onLoad := { val configureCiRelease = { (s: State) => val env = System.getenv() val field = env.getClass.getDeclaredField("m") - field.setAccessible(true) - val writeableEnv = field.get(env).asInstanceOf[java.util.Map[String, String]] + val unsafe = { + val f = classOf[sun.misc.Unsafe].getDeclaredField("theUnsafe") + f.setAccessible(true) + f.get(null).asInstanceOf[sun.misc.Unsafe] + } + val writeableEnv = unsafe.getObject(env, unsafe.objectFieldOffset(field)).asInstanceOf[java.util.Map[String, String]] writeableEnv.put("CI_RELEASE", "+publishLocal") // Publish locally for our tests only, in practice you will publish to Sonatype writeableEnv.put("CI_SONATYPE_RELEASE", "") writeableEnv.put("PGP_PASSPHRASE", "") From ac7a294490b81ed8ad862344a9f2c757047f51e7 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Tue, 28 Nov 2023 14:08:41 +0100 Subject: [PATCH 2/4] Introduce task versionPolicyAssessCompatibility --- README.md | 214 ++++++++++++------ .../typesafe/tools/mima/MimaInternals.scala | 15 ++ .../SbtVersionPolicyKeys.scala | 10 +- .../SbtVersionPolicySettings.scala | 133 +++++++---- .../internal/MimaIssues.scala | 53 ++--- .../assess-compatibility-level/build.sbt | 80 +++++++ .../project/plugins.sbt | 1 + .../assess-compatibility-level/test | 24 ++ .../main/scala/librarytest/LibraryTest.scala | 10 + .../main/scala/librarytest/LibraryTest.scala | 9 + .../main/scala/librarytest/LibraryTest.scala | 9 + .../main/scala/librarytest/LibraryTest.scala | 11 + .../main/scala/librarytest/LibraryTest.scala | 11 + .../main/scala/librarytest/LibraryTest.scala | 9 + .../main/scala/librarytest/LibraryTest.scala | 9 + 15 files changed, 441 insertions(+), 157 deletions(-) create mode 100644 sbt-version-policy/src/main/scala/com/typesafe/tools/mima/MimaInternals.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/project/plugins.sbt create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-0/src/main/scala/librarytest/LibraryTest.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-1/src/main/scala/librarytest/LibraryTest.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-2/src/main/scala/librarytest/LibraryTest.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-1-0/src/main/scala/librarytest/LibraryTest.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-2-0/src/main/scala/librarytest/LibraryTest.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2-0-0/src/main/scala/librarytest/LibraryTest.scala create mode 100644 sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3-0-0/src/main/scala/librarytest/LibraryTest.scala diff --git a/README.md b/README.md index 9e8a5c3..58d54ce 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ sbt-version-policy helps library maintainers to follow the [recommended versioni This plugin: - configures [MiMa] to check for binary or source incompatibilities, -- ensures that none of your dependencies are bumped or removed in an incompatible way. +- ensures that none of your dependencies are bumped or removed in an incompatible way, +- reports incompatibilities with previous releases. ## Install @@ -21,100 +22,135 @@ The latest version is ![Scaladex](https://index.scala-lang.org/scalacenter/sbt-v sbt-version-policy depends on [MiMa], so that you don't need to explicitly depend on it. -## Configure +## Use + +The plugin supports multiple types of workflow. It can validate that pull requests don’t break the binary compatibility or source compatibility, it can assess the compatibility level of a project compared to a previous release, and it can be used in combination with release plugins such as `sbt-ci-release` or `sbt-release`. + +### How to check that a project does not violate the desired compatibility level? + +The main use case in `sbt-version-policy` is to check that incoming pull requests don’t break the intended level of compatibility. For instance, a contribution targeting a branch that accepts only bug fixes should not introduce binary incompatibilities nor source incompatibilities. + +To achieve this, you need to set the intended level of compatibility of the project with the setting `versionPolicyIntention`, to set the next release version with the setting `version`, and to run the task `versionPolicyCheck` in your continuous integration system. -The plugin introduces a new key, `versionPolicyIntention`, that you need -to set to the level of compatibility that your next release is intended -to provide. It can take the following three values: +#### 1. Set `versionPolicyIntention` + +The setting `versionPolicyIntention` can take the following three values: - ~~~ scala // Your next release will provide no compatibility guarantees with the - // previous one. + // previous one (ie, it will be a major release). ThisBuild / versionPolicyIntention := Compatibility.None ~~~ - ~~~ scala // Your next release will be binary compatible with the previous one, - // but it may not be source compatible. + // but it may not be source compatible (ie, it will be a minor release). ThisBuild / versionPolicyIntention := Compatibility.BinaryCompatible ~~~ - ~~~ scala // Your next release will be both binary compatible and source compatible - // with the previous one. + // with the previous one (ie, it will be a patch release). ThisBuild / versionPolicyIntention := Compatibility.BinaryAndSourceCompatible ~~~ -The plugin uses [MiMa] to check for incompatibilities with the previous -release. The previous release version is automatically computed from -the current value of the `version` key in your build. This means that -you have to set this key to the _next_ version you want to release: +#### 2. Run `versionPolicyCheck` + +The task `versionPolicyCheck` will report any incompatibilities beyond the intended compatibility level. You typically want to run this task in your CI pipeline to fail it when the changes in a pull request violate the intended compatibility level. +~~~ shell +sbt versionPolicyCheck ~~~ + +The task `versionPolicyCheck` checks that the dependencies of the module did not change in an incompatible way (for instance, if the intended compatibility level is `BinaryCompatible`, you cannot bump a dependency of your module to a new major version, otherwise the classpath would end up not being binary compatible), and that the code changes in the module itself do not violate the intended compatibility level (ie, it checks that the type signatures of existing public methods stay unchanged if the compatibility level is `BinaryCompatible`). [More details](#how-does-versionpolicycheck-work). + +The plugin uses [MiMa] to check for incompatibilities with the previous release. To achieve this, it has to know what was the previous release version. By default, the previous release version is automatically computed from the current value of the `version` key in your build (more details [here](#automatic-previous-version-calculation)). This means that you have to set this key to the _next_ version you want to release: + +~~~ scala // Next version will be 1.1.0 ThisBuild / version := "1.1.0" ~~~ -In case you use a plugin like [sbt-dynver], which automatically sets -the `version` based on the Git status, [read below](#how-to-integrate-with-sbt-dynver). +In practice, the way the version is defined in your build depends on your release process. For instance, if you use a plugin like [sbt-dynver] or [sbt-ci-release], which automatically set +the `version` based on the Git status, [read below](#how-to-integrate-with-sbt-dynver). If you use [sbt-release], read the [corresponding section](#how-to-integrate-with-sbt-release). -## Use +Alternatively, you can define your own logic to compute the previous version (e.g. to not require the `version` to be set) by redefining the setting `versionPolicyPreviousVersions`. -### Check that pull requests don’t break the intended compatibility level +Note that `versionPolicyCheck` fails if it finds incompatibilities that violate the intended compatibility level. If you want to find such incompatibilities without failing, use the task `versionPolicyFindIssues`. -In your CI server, run the task `versionPolicyCheck` on pull requests. +### How to check that the release version is valid with respect to the compatibility guarantees it provides? -~~~ -$ sbt versionPolicyCheck -~~~ +Some release processes require you to manually set the release version. This is the case for all the release processes triggered by pushing a Git tag, such as [sbt-ci-release]. -This task checks that the PR does not break the compatibility guarantees -claimed by your `versionPolicyIntention`. For instance, if your intention -is to have `BinaryAndSourceCompatible` changes, the task -`versionPolicyCheck` will fail if the PR breaks binary compatibility -or source compatibility. +In such a case, your release process should check that the version you set is valid with respect to the compatibility guarantees of the release (as defined by `versionPolicyIntention`). For instance, a release that breaks the binary compatibility should bump the major version number. -### Check that release version numbers are valid with respect to the compatibility guarantees they provide - -Before you cut a release, run the task `versionCheck`. - -~~~ -$ sbt versionCheck -~~~ +You can check that by running the task `versionCheck` in your release process: -Note: make sure that the `version` is set to the new release version -number before you run `versionCheck`. +1. set the `version` to the new release version (e.g., `"1.2.3"`), +2. make sure `versionPolicyIntention` is set to the intended compatibility level of the release, +3. run `sbt versionCheck` before publishing your module artifacts. -This task checks that the release version number is consistent with the +The task `versionCheck` checks that the release version number is consistent with the intended compatibility level as per `versionPolicyIntention`. For instance, if your intention is to publish a release that breaks binary compatibility, the task `versionCheck` will fail if you didn’t bump the major version number. -## How to integrate with `sbt-ci-release`? +See below how to integrate [with sbt-ci-release](#how-to-integrate-with-sbt-ci-release) or [with sbt-release](#how-to-integrate-with-sbt-release) for instructions specific to these release processes. -sbt-version-policy itself uses sbt-version-policy and [sbt-ci-release](https://github.com/olafurpg/sbt-ci-release). -You can have a look at our [Github workflow](./.github/workflows/ci.yml) as an example of integration. +### How to assess the compatibility level of a project? -The key step is to run the task `versionCheck` before running the command `ci-release` (assuming the task -`versionPolicyCheck` has run already, in another step of the CI pipeline): +In case you don’t want to force a compatibility level but are interested in knowing the current level of compatibility of the project compared to its previous version, you can use the task `versionPolicyAssessCompatibility`: -~~~ yaml -steps - - name: Release - run: sbt versionCheck ci-release -~~~ +1. do not assign a value to `versionPolicyIntention`, +2. set the `version` to the next release version, +3. use the task `versionPolicyAssessCompatibility` to compute the compatibility level. + +The reason why you need to set the `version` to the next release version before running `versionPolicyAssessCompatiblity` is because we use it to compute the previous release version, against which assess the compatibility level. Alternatively, you can manually define the previous release version by redefining the setting `versionPolicyPreviousVersions`. + +## Integrate with your release process + +Some release processes require you to define the release version beforehand (e.g., [sbt-ci-release]), and some of them compute the release version as part of the process (e.g., [sbt-release]). That difference impacts the integration with sbt-version-policy. + +### How to integrate with `sbt-ci-release`? + +[sbt-ci-release] uses Git tags to compute the project version. You can integrate sbt-version-policy into a project that uses [sbt-ci-release] as follows: + +- check that incoming pull requests do not violate the intended compatibility level ([detailed documentation](#how-to-check-that-a-project-does-not-violate-the-desired-compatibility-level)) + 1. if your project contains multiple sub-projects, set `versionPolicyIgnoredInternalDependencyVersions` as explained in the [sbt-dynver integration](#supporting-multi-projects-builds): + ~~~ scala + versionPolicyIgnoredInternalDependencyVersions := Some("^\\d+\\.\\d+\\.\\d+\\+\\d+".r) + ~~~ + 2. set the intended compatibility level of the next release with the setting `versionPolicyIntention` + 3. run `sbt versionPolicyCheck` in your CI pipeline: + ~~~ yaml + steps + - name: Check compatibility + run: sbt versionPolicyCheck + ~~~ +- check that a new release version is valid with respect to its compatibility guarantees ([detailed documentation](#how-to-check-that-the-release-version-is-valid-with-respect-to-the-compatibility-guarantees-it-provides)) + 1. run `sbt versionCheck` in your CI pipeline before running `ci-release`: + ~~~ yaml + steps + - name: Release + run: sbt versionCheck ci-release + ~~~ + +Since [sbt-ci-release] uses [sbt-dynver] under the hood, please +read over the [next section](#how-to-integrate-with-sbt-dynver). + +#### Examples + +sbt-version-policy itself uses sbt-version-policy and [sbt-ci-release]. +You can have a look at our [Github workflow](./.github/workflows/ci.yml) as an example of integration. You can also have a look at the test [example-sbt-ci-release](./sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-ci-release) for a minimalistic sbt project using both sbt-version-policy and sbt-ci-release. -Last, since `sbt-ci-release` uses `sbt-dynver` under the hood, please -read over the next section. - -## How to integrate with `sbt-dynver`? +### How to integrate with `sbt-dynver`? `sbt-dynver` generates version numbers looking like `1.2.3+4-abcd1234` when the Git history contains commits, or changes, after the last tag. -### Supporting multi-projects builds +#### Supporting multi-projects builds The version numbers generated by sbt-dynver are usually not a problem, except when checking for dependency issues between projects of the current build (e.g., if a project `a` depends on another project `b` @@ -130,7 +166,7 @@ projects when their version number matches some regular expression: ThisBuild / versionPolicyIgnoredInternalDependencyVersions := Some("^\\d+\\.\\d+\\.\\d+\\+\\d+".r) ~~~ -### Unsupported custom `dynverSeparator` +#### Unsupported custom `dynverSeparator` When sbt-version-policy computes the previous version of the release, it only supports `"+"` as a `dynverSeparator`. @@ -145,29 +181,71 @@ is to keep the default `dynverSeparator` value (`"+"`), and to tweak the Docker / version := version.value.replace('+', '-') ~~~ -### Example +#### Example You can have a look at the test [example-sbt-dynver](./sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-dynver) for a minimalistic sbt project using both sbt-version-policy and sbt-dynver. -## How to integrate with `sbt-release`? +### How to integrate with `sbt-release`? -[sbt-release](https://github.com/sbt/sbt-release) is able to run sophisticated release pipelines +[sbt-release] is able to run sophisticated release pipelines including running the tests, setting the release version, publishing the artifacts, and pushing a Git tag named after the release version. +There are two ways to use sbt-version-policy along with sbt-release: + - define the intended compatibility level of the next release, and check that the changes applied to the project do not violate it, + - or, let the project evolve freely and, at the time of the release, compute the release version according to the level of incompatibilities introduced in the project. + +#### Constrained compatibility level + +In this mode, you can use sbt-version-policy to check that incoming pull requests do not violate the intended compatibility level, and to compute the next release version according to the compatibility level. + +- check that incoming pull requests do not violate the intended compatibility level ([detailed documentation](#how-to-check-that-a-project-does-not-violate-the-desired-compatibility-level)) + 1. set the intended compatibility level of the next release with the setting `versionPolicyIntention` + 2. run `sbt versionPolicyCheck` in your CI pipeline: + ~~~ yaml + steps + - name: Check compatibility + run: sbt versionPolicyCheck + ~~~ +- 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 + } + } + ~~~ + 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. + +##### Example + You can have a look at the test [example-sbt-release](./sbt-version-policy/src/sbt-test/sbt-version-policy/example-sbt-release) for an example of sbt project using both sbt-version-policy and sbt-release. -This example project customizes sbt-release to: +#### Unconstrained compatibility level -1. Compute the release version based on its compatibility guarantees (as per `versionPolicyIntention`). - - We achieve this by setting `releaseVersion` like the following: +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` ~~~ scala releaseVersion := { - val maybeBump = versionPolicyIntention.value match { + val maybeBump = versionPolicyAssessCompatibility.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 @@ -185,20 +263,6 @@ This example project customizes sbt-release to: } ~~~ -2. Run `versionCheck` after setting the release version, by adding the following - release step: - - ~~~ scala - releaseStepCommand("versionCheck") - ~~~ - -3. Reset `versionPolicyIntention` to `Compatibility.BinaryAndSourceCompatible` after - every release. - - We achieve this 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. - ## How does `versionPolicyCheck` work? The `versionPolicyCheck` task: @@ -321,4 +385,6 @@ versionPolicyModuleVersionExtractor := { [recommended versioning scheme]: https://docs.scala-lang.org/overviews/core/binary-compatibility-for-library-authors.html#recommended-versioning-scheme [MiMa]: https://github.com/lightbend/mima -[sbt-dynver]: https://github.com/dwijnand/sbt-dynver +[sbt-dynver]: https://github.com/sbt/sbt-dynver +[sbt-release]: https://github.com/sbt/sbt-release +[sbt-ci-release]: https://github.com/sbt/sbt-ci-release diff --git a/sbt-version-policy/src/main/scala/com/typesafe/tools/mima/MimaInternals.scala b/sbt-version-policy/src/main/scala/com/typesafe/tools/mima/MimaInternals.scala new file mode 100644 index 0000000..544ba94 --- /dev/null +++ b/sbt-version-policy/src/main/scala/com/typesafe/tools/mima/MimaInternals.scala @@ -0,0 +1,15 @@ +package com.typesafe.tools.mima + +import com.typesafe.tools.mima.core.{Problem, ProblemFilter, ProblemReporting} + +// Access the internals of Mima and use them internally. NOT INTENDED for users. +// See https://github.com/lightbend/mima/pull/793 +object MimaInternals { + def isProblemReported( + version: String, + filters: Seq[ProblemFilter], + versionedFilters: Map[String, Seq[ProblemFilter]] + )(problem: Problem): Boolean = + ProblemReporting.isReported(version, filters, versionedFilters)(problem) + +} diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala index 797cb17..f177f85 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala @@ -1,19 +1,25 @@ package sbtversionpolicy +import com.typesafe.tools.mima.core.Problem import coursier.version.VersionCompatibility -import sbt._ +import sbt.* import sbt.librarymanagement.DependencyBuilders.OrganizationArtifactName +import sbtversionpolicy.internal.MimaIssues import scala.util.matching.Regex trait SbtVersionPolicyKeys { final val versionPolicyIntention = settingKey[Compatibility]("Compatibility intentions for the next release.") - final val versionPolicyPreviousArtifacts = taskKey[Seq[ModuleID]]("") + final val versionPolicyPreviousArtifacts = taskKey[Seq[ModuleID]]("Previous released artifacts used to test compatibility.") final val versionPolicyReportDependencyIssues = taskKey[Unit]("Check for removed or updated dependencies in an incompatible way.") final val versionPolicyCheck = taskKey[Unit]("Runs both versionPolicyReportDependencyIssues and versionPolicyMimaCheck") final val versionPolicyMimaCheck = taskKey[Unit]("Runs Mima to check backward or forward compatibility depending on the intended change defined via versionPolicyIntention.") + @deprecated("Use versionPolicyMimaCheck instead", "2.2.0") final val versionPolicyForwardCompatibilityCheck = taskKey[Unit]("Report forward binary compatible issues from Mima.") final val versionPolicyFindDependencyIssues = taskKey[Seq[(ModuleID, DependencyCheckReport)]]("Compatibility issues in the library dependencies.") + final def versionPolicyFindMimaIssues = TaskKey[Seq[(ModuleID, Seq[(MimaIssues.ProblemType, Problem)])]]("versionPolicyFindMimaIssues", "Binary or source compatibility issues over the previously released artifacts.") + final def versionPolicyFindIssues = TaskKey[Seq[(ModuleID, (DependencyCheckReport, Seq[(MimaIssues.ProblemType, Problem)]))]]("versionPolicyFindIssues", "Find both dependency issues and Mima issues.") + final def versionPolicyAssessCompatibility = TaskKey[Seq[(ModuleID, Compatibility)]]("versionPolicyAssessCompatibility", "Assess the compatibility level of the project compared to its previous releases.") final val versionCheck = taskKey[Unit]("Checks that the version is consistent with the intended compatibility level defined via versionPolicyIntention") final val versionPolicyIgnored = settingKey[Seq[OrganizationArtifactName]]("Exclude these dependencies from versionPolicyReportDependencyIssues.") diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala index 1a01b15..4958346 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala @@ -1,13 +1,14 @@ package sbtversionpolicy -import com.typesafe.tools.mima.plugin.{MimaPlugin, SbtMima} +import com.typesafe.tools.mima.core.Problem +import com.typesafe.tools.mima.plugin.MimaPlugin import coursier.version.{ModuleMatchers, Version, VersionCompatibility} -import sbt._ -import sbt.Keys._ +import sbt.* +import sbt.Keys.* import sbt.librarymanagement.CrossVersion import lmcoursier.CoursierDependencyResolution import sbtversionpolicy.internal.{DependencyCheck, DependencySchemes, MimaIssues} -import sbtversionpolicy.SbtVersionPolicyMima.autoImport._ +import sbtversionpolicy.SbtVersionPolicyMima.autoImport.* import scala.util.Try @@ -120,7 +121,7 @@ object SbtVersionPolicySettings { val compatibilityIntention = versionPolicyIntention.?.value - .getOrElse(throw new MessageOnlyException("Please set the key versionPolicyIntention to declare the compatibility you want to check")) + .getOrElse(Compatibility.BinaryAndSourceCompatible) // If not defined, report all the possible incompatibilities val depRes = versionPolicyDependencyResolution.value val scalaModuleInf = versionPolicyScalaModuleInfo.value val updateConfig = versionPolicyUpdateConfiguration.value @@ -245,22 +246,8 @@ object SbtVersionPolicySettings { val ignored2 = versionPolicyReportDependencyIssues.value }).value, versionPolicyForwardCompatibilityCheck := { - import MimaPlugin.autoImport._ - val it = MimaIssues.forwardBinaryIssuesIterator.value - it.foreach { - case (moduleId, problems) => - SbtMima.reportModuleErrors( - moduleId, - problems._1, - problems._2, - true, - mimaBinaryIssueFilters.value, - mimaBackwardIssueFilters.value, - mimaForwardIssueFilters.value, - Keys.streams.value.log, - name.value, - ) - } + versionPolicyMimaCheck.value + Keys.streams.value.log.warn("The task versionPolicyForwardCompatibilityCheck is deprecated. Please use versionPolicyMimaCheck instead.") }, versionPolicyVersionCompatResult := { val ver = version.value @@ -272,45 +259,97 @@ object SbtVersionPolicySettings { } else Compatibility.None }, + versionPolicyFindMimaIssues := Def.taskDyn[Seq[(ModuleID, Seq[(MimaIssues.ProblemType, Problem)])]] { + val compatibility = + versionPolicyIntention.?.value.getOrElse(Compatibility.BinaryAndSourceCompatible) + compatibility match { + case Compatibility.None => + Def.task { Nil } + case Compatibility.BinaryCompatible | Compatibility.BinaryAndSourceCompatible => + Def.task { + MimaIssues.binaryIssuesIterator.value.map { case (previousModule, (binaryIncompatibilities, sourceIncompatibilities)) => + val incompatibilities = + if (compatibility == Compatibility.BinaryCompatible) binaryIncompatibilities.map(MimaIssues.BinaryIncompatibility -> _) + else binaryIncompatibilities.map(MimaIssues.BinaryIncompatibility -> _) ++ sourceIncompatibilities.map(MimaIssues.SourceIncompatibility -> _) + previousModule -> incompatibilities + }.toSeq + } + } + }.value, versionPolicyMimaCheck := Def.taskDyn { - import Compatibility._ + import Compatibility.* val compatibility = versionPolicyIntention.?.value .getOrElse(throw new MessageOnlyException("Please set the key versionPolicyIntention to declare the compatibility you want to check")) val log = streams.value.log val currentModule = projectID.value - val formattedPreviousVersions = formatVersions(versionPolicyPreviousVersions.value) - - val reportBackwardBinaryCompatibilityIssues: Def.Initialize[Task[Unit]] = - MimaPlugin.autoImport.mimaReportBinaryIssues.result.map(_.toEither.left.foreach { error => - log.error(s"Module ${nameAndRevision(currentModule)} is not binary compatible with ${formattedPreviousVersions}. You have to relax your compatibility intention by changing the value of versionPolicyIntention.") - throw new MessageOnlyException(error.directCause.map(_.toString).getOrElse("mimaReportBinaryIssues failed")) - }) - - val reportForwardBinaryCompatibilityIssues: Def.Initialize[Task[Unit]] = - versionPolicyForwardCompatibilityCheck.result.map(_.toEither.left.foreach { error => - log.error(s"Module ${nameAndRevision(currentModule)} is not source compatible with ${formattedPreviousVersions}. You have to relax your compatibility intention by changing the value of versionPolicyIntention.") - throw new MessageOnlyException(error.directCause.map(_.toString).getOrElse("versionPolicyForwardCompatibilityCheck failed")) - }) + val formattedModule = nameAndRevision(currentModule) compatibility match { - case BinaryCompatible => - reportBackwardBinaryCompatibilityIssues.map { _ => - log.info(s"Module ${nameAndRevision(currentModule)} is binary compatible with ${formattedPreviousVersions}") - } - case BinaryAndSourceCompatible => + case BinaryCompatible | BinaryAndSourceCompatible => Def.task { - val ignored1 = reportForwardBinaryCompatibilityIssues.value - val ignored2 = reportBackwardBinaryCompatibilityIssues.value - }.map { _ => - log.info(s"Module ${nameAndRevision(currentModule)} is binary and source compatible with ${formattedPreviousVersions}") + val issues = versionPolicyFindMimaIssues.value + val formattedCompatibility = if (compatibility == BinaryCompatible) "binary" else "binary and source" + var hadErrors = false + for ((previousModule, problems) <- issues) { + val formattedPreviousModule = nameAndRevision(previousModule) + if (problems.isEmpty) { + log.info(s"Module ${formattedModule} is ${formattedCompatibility} compatible with ${formattedPreviousModule}") + } else { + val formattedProblems = + problems.map { case (problemType, problem) => + val affected = problemType match { case MimaIssues.BinaryIncompatibility => "current" case MimaIssues.SourceIncompatibility => "previous" } + val howToFilter = problem.howToFilter.fold("")(hint => s"\n filter with: ${hint}") + s" * ${problem.description(affected)}${howToFilter}" + }.mkString("\n") + log.error( + s"""Module ${formattedModule} is not ${formattedCompatibility} compatible with ${formattedPreviousModule}. + |You have to relax our compatibility intention by changing the value of versionPolicyIntention, or to fix the incompatibilities. + |We found the following incompatibilities: + |${formattedProblems}""".stripMargin) + hadErrors = true + } + } + if (hadErrors) { + throw new MessageOnlyException("versionPolicyMimaCheck failed") + } } case None => Def.task { - // skip mima if no compatibility is intented - log.info(s"Not checking compatibility of module ${nameAndRevision(currentModule)} because versionPolicyIntention is set to 'Compatibility.None'") + // skip Mima if no compatibility is intended + log.info(s"Not checking compatibility of module ${formattedModule} because versionPolicyIntention is set to 'Compatibility.None'") + } + } + }.value, + versionPolicyFindIssues := { + val dependencyIssues = versionPolicyFindDependencyIssues.value + val mimaIssues = versionPolicyFindMimaIssues.value + assert(dependencyIssues.size == mimaIssues.size) + for ((previousModule, dependencyReport) <- dependencyIssues) yield { + mimaIssues.find { case (id, _) => previousModule.revision == id.revision } match { + case Some((_, mimaIssues)) => previousModule -> (dependencyReport, mimaIssues) + case None => throw new MessageOnlyException(s"Illegal state: dependency issues and Mima issues were not searched against the same previous versions.") } } - }.value + }, + versionPolicyAssessCompatibility := { + // Results will be flawed if the `versionPolicyIntention` is set to `BinaryCompatible` or `None` + // because `versionPolicyFindIssues` only reports the issues that violate the intended compatibility level + if (versionPolicyIntention.?.value.exists(_ != Compatibility.BinaryAndSourceCompatible)) { + throw new MessageOnlyException("versionPolicyIntention should not be set when you run versionPolicyAssessCompatibility.") + } + val issues = versionPolicyFindIssues.value + issues.map { case (previousRelease, (dependencyIssues, mimaIssues)) => + val compatibility = + if (dependencyIssues.validated(Direction.both) && mimaIssues.isEmpty) { + Compatibility.BinaryAndSourceCompatible + } else if (dependencyIssues.validated(Direction.backward) && mimaIssues.collectFirst { case (MimaIssues.BinaryIncompatibility, _) => true }.isEmpty) { + Compatibility.BinaryCompatible + } else { + Compatibility.None + } + previousRelease -> compatibility + } + } ) def skipSettings = Seq( diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala index e16f4ef..c7218b1 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala @@ -1,44 +1,29 @@ package sbtversionpolicy.internal -import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._ -import sbt.Def -import sbt.Keys._ -import com.typesafe.tools.mima.plugin.SbtMima +import com.typesafe.tools.mima.MimaInternals +import com.typesafe.tools.mima.core.Problem +import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport.* +import com.typesafe.tools.mima.plugin.MimaPlugin.binaryIssuesFinder +import sbt.{Def, Task} -object MimaIssues { +private[sbtversionpolicy] object MimaIssues { - import com.typesafe.tools.mima.core.util.log.Logging - import sbt.Logger + sealed trait ProblemType + case object BinaryIncompatibility extends ProblemType + case object SourceIncompatibility extends ProblemType - // adapted from https://github.com/lightbend/mima/blob/fde02955c4908a6423b12edf044799a868b51706/sbtplugin/src/main/scala/com/typesafe/tools/mima/plugin/MimaPlugin.scala#L82-L99 - def forwardBinaryIssuesIterator = Def.task { - val log = streams.value.log + val binaryIssuesIterator: Def.Initialize[Task[Iterator[(sbt.ModuleID, (List[Problem], List[Problem]))]]] = Def.task { + val binaryIssueFilters = mimaBackwardIssueFilters.value + val sourceIssueFilters = mimaForwardIssueFilters.value + val issueFilters = mimaBinaryIssueFilters.value val previousClassfiles = mimaPreviousClassfiles.value - val currentClassfiles = mimaCurrentClassfiles.value - val excludeAnnotations = mimaExcludeAnnotations.value - val cp = (mimaFindBinaryIssues / fullClasspath).value - val scalaVersionValue = scalaVersion.value - if (previousClassfiles.isEmpty) - log.info(s"${name.value}: mimaPreviousArtifacts is empty, not analyzing binary compatibility.") - - previousClassfiles - .iterator - .map { - case (moduleId, prevClassfiles) => - moduleId -> SbtMima.runMima( - prevClassfiles, - currentClassfiles, - cp, - "forward", - scalaVersionValue, - log, - excludeAnnotations.toList - ) - } - .filter { - case (_, (problems, problems0)) => - problems.nonEmpty || problems0.nonEmpty + binaryIssuesFinder.value.runMima(previousClassfiles, "both") + .map { case (previousModule, (binaryIssues, sourceIssues)) => + val moduleRevision = previousModule.revision + val filteredBinaryIssues = binaryIssues.filter(MimaInternals.isProblemReported(moduleRevision, issueFilters, binaryIssueFilters)) + val filteredSourceIssues = sourceIssues.filter(MimaInternals.isProblemReported(moduleRevision, issueFilters, sourceIssueFilters)) + previousModule -> (filteredBinaryIssues, filteredSourceIssues) } } diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt new file mode 100644 index 0000000..242f09b --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt @@ -0,0 +1,80 @@ +ThisBuild / organization := "com.example" +ThisBuild / scalaVersion := "2.13.2" +ThisBuild / versionScheme := Some("semver-spec") + +val checkTasks = Seq( + TaskKey[Unit]("checkAssessedCompatibilityIsBinaryAndSourceCompatible") := { + val (_, compatibility) = versionPolicyAssessCompatibility.value.head + assert(compatibility == Compatibility.BinaryAndSourceCompatible, s"Unexpected assessed compatibility: ${compatibility}") + }, + TaskKey[Unit]("checkAssessedCompatibilityIsBinaryCompatible") := { + val (_, compatibility) = versionPolicyAssessCompatibility.value.head + assert(compatibility == Compatibility.BinaryCompatible, s"Unexpected assessed compatibility: ${compatibility}") + }, + TaskKey[Unit]("checkAssessedCompatibilityIsNone") := { + val (_, compatibility) = versionPolicyAssessCompatibility.value.head + assert(compatibility == Compatibility.None, s"Unexpected assessed compatibility: ${compatibility}") + } +) + +val `v1-0-0` = + project.settings( + name := "assess-compatibility-test", + version := "1.0.0", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.0", + checkTasks, + ) + +// binary and source compatible change in the code +val `v1-0-1` = + project.settings( + name := "assess-compatibility-test", + version := "1.0.1", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.0", + checkTasks, + ) + +// No changes in the code, patch bump of library dependency +val `v1-0-2` = + project.settings( + name := "assess-compatibility-test", + version := "1.0.2", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.1", + checkTasks, + ) + +// Source incompatible change in the code +val `v1-1-0` = + project.settings( + name := "assess-compatibility-test", + version := "1.1.0", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.1", + checkTasks, + ) + +// No changes in the code, minor bump of library dependency +val `v1-2-0` = + project.settings( + name := "assess-compatibility-test", + version := "1.2.0", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0", + checkTasks, + ) + +// Binary incompatible change in the code +val `v2-0-0` = + project.settings( + name := "assess-compatibility-test", + version := "2.0.0", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0", + checkTasks, + ) + +// No changes in the code, breaking change in the dependencies +val `v3-0-0` = + project.settings( + name := "assess-compatibility-test", + version := "3.0.0", + // no library dependency anymore + checkTasks, + ) diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/project/plugins.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/project/plugins.sbt new file mode 100644 index 0000000..2843375 --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % sys.props("plugin.version")) diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test new file mode 100644 index 0000000..163e1ff --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test @@ -0,0 +1,24 @@ +> v1-0-0/publishLocal +> reload + +> v1-0-1/checkAssessedCompatibilityIsBinaryAndSourceCompatible +> v1-0-1/publishLocal +> reload + +> v1-0-2/checkAssessedCompatibilityIsBinaryAndSourceCompatible +> v1-0-2/publishLocal +> reload + +> v1-1-0/checkAssessedCompatibilityIsBinaryCompatible +> v1-1-0/publishLocal +> reload + +> v1-2-0/checkAssessedCompatibilityIsBinaryCompatible +> v1-2-0/publishLocal +> reload + +> v2-0-0/checkAssessedCompatibilityIsNone +> v2-0-0/publishLocal +> reload + +> v3-0-0/checkAssessedCompatibilityIsNone diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-0/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..adcfc5a --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-0/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,10 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + println("bar") + x + } + +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-1/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-1/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..6d14b8b --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-1/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,9 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + x + } + +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-2/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-2/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..6d14b8b --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-2/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,9 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + x + } + +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-1-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-1-0/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..0f9d75d --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-1-0/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,11 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + x + } + + def quux: Int = 0 + +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-2-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-2-0/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..0f9d75d --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-2-0/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,11 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + x + } + + def quux: Int = 0 + +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2-0-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2-0-0/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..6d14b8b --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2-0-0/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,9 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + x + } + +} diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3-0-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3-0-0/src/main/scala/librarytest/LibraryTest.scala new file mode 100644 index 0000000..6d14b8b --- /dev/null +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3-0-0/src/main/scala/librarytest/LibraryTest.scala @@ -0,0 +1,9 @@ +package librarytest + +trait Foo { + + def bar(x: Int): Int = { + x + } + +} From 088577c3359708243cbba659a136da39a9726511 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Tue, 28 Nov 2023 19:14:07 +0100 Subject: [PATCH 3/4] Fix implementation of versionPolicyFindDependencyIssues Co-authored-by: Roberto Tyley --- README.md | 5 +- build.sbt | 9 +- .../DependencyCheckReport.scala | 74 +++++------- .../scala/sbtversionpolicy/Direction.scala | 15 --- .../IncompatibilityType.scala | 13 +++ .../SbtVersionPolicyKeys.scala | 10 +- .../SbtVersionPolicySettings.scala | 108 +++++++++--------- .../internal/DependencyCheck.scala | 44 +------ .../internal/MimaIssues.scala | 4 - .../assess-compatibility-level/build.sbt | 30 ++--- .../assess-compatibility-level/test | 32 ++++-- .../main/scala/librarytest/LibraryTest.scala | 0 .../main/scala/librarytest/LibraryTest.scala | 0 .../main/scala/librarytest/LibraryTest.scala | 0 .../main/scala/librarytest/LibraryTest.scala | 0 .../main/scala/librarytest/LibraryTest.scala | 0 .../main/scala/librarytest/LibraryTest.scala | 0 .../main/scala/librarytest/LibraryTest.scala | 0 .../sbt-version-policy/defaults/build.sbt | 3 +- .../sbt-version-policy/simple/build.sbt | 3 +- 20 files changed, 143 insertions(+), 207 deletions(-) delete mode 100644 sbt-version-policy/src/main/scala/sbtversionpolicy/Direction.scala create mode 100644 sbt-version-policy/src/main/scala/sbtversionpolicy/IncompatibilityType.scala rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v1-0-0 => v1_0_0}/src/main/scala/librarytest/LibraryTest.scala (100%) rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v1-0-1 => v1_0_1}/src/main/scala/librarytest/LibraryTest.scala (100%) rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v1-0-2 => v1_0_2}/src/main/scala/librarytest/LibraryTest.scala (100%) rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v1-1-0 => v1_1_0}/src/main/scala/librarytest/LibraryTest.scala (100%) rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v1-2-0 => v1_2_0}/src/main/scala/librarytest/LibraryTest.scala (100%) rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v2-0-0 => v2_0_0}/src/main/scala/librarytest/LibraryTest.scala (100%) rename sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/{v3-0-0 => v3_0_0}/src/main/scala/librarytest/LibraryTest.scala (100%) diff --git a/README.md b/README.md index 58d54ce..b535b28 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,10 @@ In this mode, you can use sbt-version-policy to assess the incompatibilities int 2. define `releaseVersion` from the compatibility level returned by `versionPolicyAssessCompatibility` ~~~ scala releaseVersion := { - val maybeBump = versionPolicyAssessCompatibility.value match { + 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 diff --git a/build.sbt b/build.sbt index c0f4fd6..4a3ec66 100644 --- a/build.sbt +++ b/build.sbt @@ -12,7 +12,7 @@ inThisBuild(List( url("https://github.com/alexarchambault") ) ), - versionPolicyIntention := Compatibility.BinaryAndSourceCompatible, + versionPolicyIntention := Compatibility.None, libraryDependencySchemes += "com.typesafe" %% "mima-core" % "semver-spec" )) @@ -29,10 +29,6 @@ lazy val `sbt-version-policy` = project scriptedLaunchOpts += "-Dplugin.version=" + version.value, scriptedBufferLog := false, addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3"), - libraryDependencies ++= Seq( - "io.github.alexarchambault" %% "data-class" % "0.2.6" % Provided, - compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full) - ), libraryDependencies ++= Seq( "io.get-coursier" % "interface" % "1.0.18", "io.get-coursier" %% "versions" % "0.3.1", @@ -40,7 +36,6 @@ lazy val `sbt-version-policy` = project ), testFrameworks += new TestFramework("verify.runner.Framework"), mimaBinaryIssueFilters ++= Seq( - // this class is `private` and it's only used from `extractSemVerNumbers` method, which is private - ProblemFilters.exclude[MissingClassProblem]("sbtversionpolicy.DependencyCheckReport$SemVerVersion*") + // Add Mima filters here ), ) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala index dd1993e..23497cc 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/DependencyCheckReport.scala @@ -1,42 +1,36 @@ package sbtversionpolicy import coursier.version.{ModuleMatchers, Version, VersionCompatibility} -import dataclass.data -import lmcoursier.definitions.{ModuleMatchers => _, _} +import lmcoursier.definitions.{ModuleMatchers => *, *} -@data class DependencyCheckReport( - backwardStatuses: Map[(String, String), DependencyCheckReport.ModuleStatus], - forwardStatuses: Map[(String, String), DependencyCheckReport.ModuleStatus] +case class DependencyCheckReport( + compatibilityReports: Map[IncompatibilityType, Map[(String, String), DependencyCheckReport.ModuleStatus]] ) { - def validated(direction: Direction): Boolean = - (!direction.backward || backwardStatuses.forall(_._2.validated)) && - (!direction.forward || forwardStatuses.forall(_._2.validated)) - def errors(direction: Direction, ignored: Set[(String, String)] = Set.empty): (Seq[String], Seq[String]) = { + def validated(incompatibilityType: IncompatibilityType): Boolean = + compatibilityReports(incompatibilityType).forall(_._2.validated) - val backwardElems = - if (direction.backward) backwardStatuses else Map() - val forwardElems = - if (direction.forward) forwardStatuses else Map() + def errors(incompatibilityType: IncompatibilityType, ignored: Set[(String, String)] = Set.empty): (Seq[String], Seq[String]) = { - val baseErrors = (backwardElems.iterator.map((_, true)) ++ forwardElems.iterator.map((_, false))) - .filter(!_._1._2.validated) + val relevantErrors = compatibilityReports(incompatibilityType) + + val baseErrors = relevantErrors + .filter(!_._2.validated) .toVector - .sortBy(_._1._1) + .sortBy(_._1) - def message(org: String, name: String, backward: Boolean, status: DependencyCheckReport.ModuleStatus): String = { - val direction = if (backward) "backward" else "forward" + def message(org: String, name: String, status: DependencyCheckReport.ModuleStatus): String = { s"$org:$name: ${status.message}" } val actualErrors = baseErrors.collect { - case ((orgName @ (org, name), status), backward) if !ignored(orgName) => - message(org, name, backward, status) + case (orgName @ (org, name), status) if !ignored(orgName) => + message(org, name, status) } val warnings = baseErrors.collect { - case ((orgName @ (org, name), status), backward) if ignored(orgName) => - message(org, name, backward, status) + case (orgName @ (org, name), status) if ignored(orgName) => + message(org, name, status) } (warnings, actualErrors) @@ -48,49 +42,35 @@ object DependencyCheckReport { sealed abstract class ModuleStatus(val validated: Boolean) extends Product with Serializable { def message: String } - @data class SameVersion(version: String) extends ModuleStatus(true) { + case class SameVersion(version: String) extends ModuleStatus(true) { def message = s"found same version $version" } - @data class CompatibleVersion(version: String, previousVersion: String, reconciliation: VersionCompatibility) extends ModuleStatus(true) { + case class CompatibleVersion(version: String, previousVersion: String, reconciliation: VersionCompatibility) extends ModuleStatus(true) { def message = s"compatible version change from $previousVersion to $version (compatibility: ${reconciliation.name})" } - @data class IncompatibleVersion(version: String, previousVersion: String, reconciliation: VersionCompatibility) extends ModuleStatus(false) { + case class IncompatibleVersion(version: String, previousVersion: String, reconciliation: VersionCompatibility) extends ModuleStatus(false) { def message = s"incompatible version change from $previousVersion to $version (compatibility: ${reconciliation.name})" } - @data class Missing(version: String) extends ModuleStatus(false) { + case class Missing(version: String) extends ModuleStatus(false) { def message = "missing dependency" } private case class SemVerVersion(major: Int, minor: Int, patch: Int, suffix: Seq[Version.Item]) - @deprecated("This method is internal.", "1.1.0") - def apply( - currentModules: Map[(String, String), String], - previousModules: Map[(String, String), String], - reconciliations: Seq[(ModuleMatchers, VersionCompatibility)], - defaultReconciliation: VersionCompatibility - ): DependencyCheckReport = - apply( - Compatibility.BinaryCompatible, - currentModules, - previousModules, - reconciliations, - defaultReconciliation - ) - private[sbtversionpolicy] def apply( - compatibilityIntention: Compatibility, currentModules: Map[(String, String), String], previousModules: Map[(String, String), String], reconciliations: Seq[(ModuleMatchers, VersionCompatibility)], defaultReconciliation: VersionCompatibility ): DependencyCheckReport = { - // FIXME These two lines compute the same result. What is the reason for having two directions? - val backward = moduleStatuses(compatibilityIntention, currentModules, previousModules, reconciliations, defaultReconciliation) - val forward = moduleStatuses(compatibilityIntention, currentModules, previousModules, reconciliations, defaultReconciliation) + def report(compatibility: Compatibility) = + moduleStatuses(compatibility, currentModules, previousModules, reconciliations, defaultReconciliation) - DependencyCheckReport(backward, forward) + DependencyCheckReport(Map( + IncompatibilityType.BinaryIncompatibility -> report(Compatibility.BinaryCompatible), + IncompatibilityType.SourceIncompatibility -> report(Compatibility.BinaryAndSourceCompatible) + )) } @deprecated("This method is internal.", "1.1.0") @@ -182,7 +162,7 @@ object DependencyCheckReport { private def extractSemVerNumbers(versionString: String): Option[SemVerVersion] = { val version = Version(versionString) version.items match { - case Vector(major: Version.Number, minor: Version.Number, patch: Version.Number, suffix @ _*) => + case Vector(major: Version.Number, minor: Version.Number, patch: Version.Number, suffix*) => Some(SemVerVersion(major.value, minor.value, patch.value, suffix)) case _ => None // Not a semantic version number (e.g., 1.0-RC1) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/Direction.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/Direction.scala deleted file mode 100644 index d75bcf7..0000000 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/Direction.scala +++ /dev/null @@ -1,15 +0,0 @@ -package sbtversionpolicy - -import dataclass.data - -@data class Direction( - backward: Boolean, - forward: Boolean -) - -object Direction { - def none: Direction = Direction(false, false) - def backward: Direction = Direction(true, false) - def forward: Direction = Direction(false, true) - def both: Direction = Direction(true, true) -} diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/IncompatibilityType.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/IncompatibilityType.scala new file mode 100644 index 0000000..5360071 --- /dev/null +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/IncompatibilityType.scala @@ -0,0 +1,13 @@ +package sbtversionpolicy + +/** Incompatibilities can be binary incompatibilities or + * source incompatibilities + */ +sealed trait IncompatibilityType + +object IncompatibilityType { + + case object BinaryIncompatibility extends IncompatibilityType + case object SourceIncompatibility extends IncompatibilityType + +} diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala index f177f85..490b5bb 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicyKeys.scala @@ -4,7 +4,6 @@ import com.typesafe.tools.mima.core.Problem import coursier.version.VersionCompatibility import sbt.* import sbt.librarymanagement.DependencyBuilders.OrganizationArtifactName -import sbtversionpolicy.internal.MimaIssues import scala.util.matching.Regex @@ -14,16 +13,13 @@ trait SbtVersionPolicyKeys { final val versionPolicyReportDependencyIssues = taskKey[Unit]("Check for removed or updated dependencies in an incompatible way.") final val versionPolicyCheck = taskKey[Unit]("Runs both versionPolicyReportDependencyIssues and versionPolicyMimaCheck") final val versionPolicyMimaCheck = taskKey[Unit]("Runs Mima to check backward or forward compatibility depending on the intended change defined via versionPolicyIntention.") - @deprecated("Use versionPolicyMimaCheck instead", "2.2.0") - final val versionPolicyForwardCompatibilityCheck = taskKey[Unit]("Report forward binary compatible issues from Mima.") final val versionPolicyFindDependencyIssues = taskKey[Seq[(ModuleID, DependencyCheckReport)]]("Compatibility issues in the library dependencies.") - final def versionPolicyFindMimaIssues = TaskKey[Seq[(ModuleID, Seq[(MimaIssues.ProblemType, Problem)])]]("versionPolicyFindMimaIssues", "Binary or source compatibility issues over the previously released artifacts.") - final def versionPolicyFindIssues = TaskKey[Seq[(ModuleID, (DependencyCheckReport, Seq[(MimaIssues.ProblemType, Problem)]))]]("versionPolicyFindIssues", "Find both dependency issues and Mima issues.") - final def versionPolicyAssessCompatibility = TaskKey[Seq[(ModuleID, Compatibility)]]("versionPolicyAssessCompatibility", "Assess the compatibility level of the project compared to its previous releases.") + final val versionPolicyFindMimaIssues = taskKey[Seq[(ModuleID, Seq[(IncompatibilityType, Problem)])]]("Binary or source compatibility issues over the previously released artifacts.") + final val versionPolicyFindIssues = taskKey[Seq[(ModuleID, (DependencyCheckReport, Seq[(IncompatibilityType, Problem)]))]]("Find both dependency issues and Mima issues.") + final val versionPolicyAssessCompatibility = taskKey[Seq[(ModuleID, Compatibility)]]("Assess the compatibility level of the project compared to its previous releases.") final val versionCheck = taskKey[Unit]("Checks that the version is consistent with the intended compatibility level defined via versionPolicyIntention") final val versionPolicyIgnored = settingKey[Seq[OrganizationArtifactName]]("Exclude these dependencies from versionPolicyReportDependencyIssues.") - final val versionPolicyCheckDirection = settingKey[Direction]("Direction to check the version compatibility. Default: Direction.backward.") // Note: defined as a def because adding a val to a trait is not binary compatible final def versionPolicyIgnoredInternalDependencyVersions = SettingKey[Option[Regex]]("versionPolicyIgnoredInternalDependencyVersions", "Exclude dependencies to projects of the current build whose version matches this regular expression.") diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala index 4958346..8e11f9c 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala @@ -45,7 +45,6 @@ object SbtVersionPolicySettings { ) def reconciliationGlobalSettings = Def.settings( - versionPolicyCheckDirection := Direction.backward, versionPolicyIgnoreSbtDefaultReconciliations := true, versionPolicyUseCsrConfigReconciliations := true, versionPolicyDefaultDependencySchemes := defaultSchemes, @@ -151,7 +150,6 @@ object SbtVersionPolicySettings { previousModuleIds.map { previousModuleId => val report0 = DependencyCheck.report( - compatibilityIntention, excludedModules, currentDependencies, previousModuleId, @@ -175,7 +173,6 @@ object SbtVersionPolicySettings { val log = streams.value.log val sv = scalaVersion.value val sbv = scalaBinaryVersion.value - val direction = versionPolicyCheckDirection.value val reports = versionPolicyFindDependencyIssues.value val intention = versionPolicyIntention.?.value @@ -183,31 +180,36 @@ object SbtVersionPolicySettings { val currentModule = projectID.value val formattedPreviousVersions = formatVersions(versionPolicyPreviousVersions.value) - val ignored = versionPolicyIgnored.value - .map { orgName => - val mod = orgName % "foo" - val name = CrossVersion(mod.crossVersion, sv, sbv).fold(mod.name)(_(mod.name)) - (mod.organization, name) - } - .toSet - - var anyError = false - for ((previousModule, report) <- reports) { - val (warnings, errors) = report.errors(direction, ignored) - if (errors.nonEmpty) { - anyError = true - log.error(s"Incompatibilities with dependencies of ${nameAndRevision(previousModule)}") - for (error <- errors) - log.error(" " + error) + if (intention == Compatibility.None) { + log.info(s"Not checking dependencies compatibility of module ${nameAndRevision(currentModule)} because versionPolicyIntention is set to 'Compatibility.None'") + } else { + + val ignored = versionPolicyIgnored.value + .map { orgName => + val mod = orgName % "foo" + val name = CrossVersion(mod.crossVersion, sv, sbv).fold(mod.name)(_(mod.name)) + (mod.organization, name) + } + .toSet + + val incompatibilityType = + if (intention == Compatibility.BinaryCompatible) IncompatibilityType.BinaryIncompatibility + else IncompatibilityType.SourceIncompatibility + + var anyError = false + for ((previousModule, report) <- reports) { + val (warnings, errors) = report.errors(incompatibilityType, ignored) + if (errors.nonEmpty) { + anyError = true + log.error(s"Incompatibilities with dependencies of ${nameAndRevision(previousModule)}") + for (error <- errors) + log.error(" " + error) + } } - } - if (anyError) - throw new MessageOnlyException(s"Dependencies of module ${nameAndRevision(currentModule)} break the intended compatibility guarantees 'Compatibility.${intention}' (see messages above). You have to relax your compatibility intention by changing the value of versionPolicyIntention.") - else { - if (intention == Compatibility.None) { - log.info(s"Not checking dependencies compatibility of module ${nameAndRevision(currentModule)} because versionPolicyIntention is set to 'Compatibility.None'") - } else { + if (anyError) + throw new MessageOnlyException(s"Dependencies of module ${nameAndRevision(currentModule)} break the intended compatibility guarantees 'Compatibility.${intention}' (see messages above). You have to relax your compatibility intention by changing the value of versionPolicyIntention.") + else { log.info(s"Module ${nameAndRevision(currentModule)} has no dependency issues with ${formattedPreviousVersions} (versionPolicyIntention is set to 'Compatibility.${intention}')") } } @@ -245,21 +247,8 @@ object SbtVersionPolicySettings { val ignored1 = versionPolicyMimaCheck.value val ignored2 = versionPolicyReportDependencyIssues.value }).value, - versionPolicyForwardCompatibilityCheck := { - versionPolicyMimaCheck.value - Keys.streams.value.log.warn("The task versionPolicyForwardCompatibilityCheck is deprecated. Please use versionPolicyMimaCheck instead.") - }, - versionPolicyVersionCompatResult := { - val ver = version.value - val prevs = versionPolicyPreviousVersions.value - if (prevs.nonEmpty) { - val maxPrev = prevs.map(Version(_)).max.repr - val compat = versionPolicyVersionCompatibility.value - Compatibility(maxPrev, ver, compat) - } - else Compatibility.None - }, - versionPolicyFindMimaIssues := Def.taskDyn[Seq[(ModuleID, Seq[(MimaIssues.ProblemType, Problem)])]] { + // For every previous module, returns a list of problems paired with the type of incompatibility + versionPolicyFindMimaIssues := Def.taskDyn[Seq[(ModuleID, Seq[(IncompatibilityType, Problem)])]] { val compatibility = versionPolicyIntention.?.value.getOrElse(Compatibility.BinaryAndSourceCompatible) compatibility match { @@ -268,9 +257,11 @@ object SbtVersionPolicySettings { case Compatibility.BinaryCompatible | Compatibility.BinaryAndSourceCompatible => Def.task { MimaIssues.binaryIssuesIterator.value.map { case (previousModule, (binaryIncompatibilities, sourceIncompatibilities)) => + def annotatedBinaryIncompatibilities = binaryIncompatibilities.map(IncompatibilityType.BinaryIncompatibility -> _) + def annotatedSourceIncompatibilities = sourceIncompatibilities.map(IncompatibilityType.SourceIncompatibility -> _) val incompatibilities = - if (compatibility == Compatibility.BinaryCompatible) binaryIncompatibilities.map(MimaIssues.BinaryIncompatibility -> _) - else binaryIncompatibilities.map(MimaIssues.BinaryIncompatibility -> _) ++ sourceIncompatibilities.map(MimaIssues.SourceIncompatibility -> _) + if (compatibility == Compatibility.BinaryCompatible) annotatedBinaryIncompatibilities + else annotatedBinaryIncompatibilities ++ annotatedSourceIncompatibilities previousModule -> incompatibilities }.toSeq } @@ -297,8 +288,11 @@ object SbtVersionPolicySettings { log.info(s"Module ${formattedModule} is ${formattedCompatibility} compatible with ${formattedPreviousModule}") } else { val formattedProblems = - problems.map { case (problemType, problem) => - val affected = problemType match { case MimaIssues.BinaryIncompatibility => "current" case MimaIssues.SourceIncompatibility => "previous" } + problems.map { case (incompatibilityType, problem) => + val affected = incompatibilityType match { + case IncompatibilityType.BinaryIncompatibility => "current" + case IncompatibilityType.SourceIncompatibility => "previous" + } val howToFilter = problem.howToFilter.fold("")(hint => s"\n filter with: ${hint}") s" * ${problem.description(affected)}${howToFilter}" }.mkString("\n") @@ -323,12 +317,16 @@ object SbtVersionPolicySettings { versionPolicyFindIssues := { val dependencyIssues = versionPolicyFindDependencyIssues.value val mimaIssues = versionPolicyFindMimaIssues.value - assert(dependencyIssues.size == mimaIssues.size) + assert( + dependencyIssues.map(_._1.revision).toSet == mimaIssues.map(_._1.revision).toSet, + "Dependency issues and Mima issues must be checked against the same previous releases" + ) for ((previousModule, dependencyReport) <- dependencyIssues) yield { - mimaIssues.find { case (id, _) => previousModule.revision == id.revision } match { - case Some((_, mimaIssues)) => previousModule -> (dependencyReport, mimaIssues) - case None => throw new MessageOnlyException(s"Illegal state: dependency issues and Mima issues were not searched against the same previous versions.") - } + val (_, problems) = + mimaIssues + .find { case (id, _) => previousModule.revision == id.revision } + .get // See assertion above + previousModule -> (dependencyReport, problems) } }, versionPolicyAssessCompatibility := { @@ -340,9 +338,15 @@ object SbtVersionPolicySettings { val issues = versionPolicyFindIssues.value issues.map { case (previousRelease, (dependencyIssues, mimaIssues)) => val compatibility = - if (dependencyIssues.validated(Direction.both) && mimaIssues.isEmpty) { + if ( + dependencyIssues.validated(IncompatibilityType.SourceIncompatibility) && + mimaIssues.isEmpty + ) { Compatibility.BinaryAndSourceCompatible - } else if (dependencyIssues.validated(Direction.backward) && mimaIssues.collectFirst { case (MimaIssues.BinaryIncompatibility, _) => true }.isEmpty) { + } else if ( + dependencyIssues.validated(IncompatibilityType.BinaryIncompatibility) && + !mimaIssues.exists(_._1 == IncompatibilityType.BinaryIncompatibility) + ) { Compatibility.BinaryCompatible } else { Compatibility.None diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/DependencyCheck.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/DependencyCheck.scala index 7fcf5d5..3a0ae67 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/DependencyCheck.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/DependencyCheck.scala @@ -5,19 +5,10 @@ import sbt.Compile import sbt.librarymanagement.{ConfigurationReport, CrossVersion, ModuleID} import sbt.util.Logger import sbt.librarymanagement.{DependencyResolution, ScalaModuleInfo, UnresolvedWarningConfiguration, UpdateConfiguration} -import sbtversionpolicy.{Compatibility, DependencyCheckReport} +import sbtversionpolicy.DependencyCheckReport object DependencyCheck { - @deprecated("This method is internal to sbt-version-policy", "1.2.0") - def modulesOf( - report: ConfigurationReport, - scalaVersion: String, - scalaBinaryVersion: String, - log: Logger - ): Map[(String, String), String] = - modulesOf(report, Set.empty, scalaVersion, scalaBinaryVersion, PartialFunction.empty, log) - private[sbtversionpolicy] def modulesOf( report: ConfigurationReport, excludedModules: Set[(String, String)], @@ -46,39 +37,7 @@ object DependencyCheck { (orgName, versions.head) } - @deprecated("This method is internal to sbt-version-policy", "1.1.0") - def report( - currentModules: Map[(String, String), String], - previousModuleId: ModuleID, - reconciliations: Seq[(ModuleMatchers, VersionCompatibility)], - defaultReconciliation: VersionCompatibility, - sv: String, - sbv: String, - depRes: DependencyResolution, - scalaModuleInf: Option[ScalaModuleInfo], - updateConfig: UpdateConfiguration, - warningConfig: UnresolvedWarningConfiguration, - log: Logger - ): DependencyCheckReport = - report( - Compatibility.BinaryCompatible, - Set.empty, - currentModules, - previousModuleId, - reconciliations, - defaultReconciliation, - sv, - sbv, - depRes, - scalaModuleInf, - updateConfig, - warningConfig, - PartialFunction.empty, - log - ) - private[sbtversionpolicy] def report( - compatibilityIntention: Compatibility, excludedModules: Set[(String, String)], currentDependencies: Map[(String, String), String], previousModuleId: ModuleID, @@ -117,7 +76,6 @@ object DependencyCheck { } DependencyCheckReport( - compatibilityIntention, currentDependencies, previousDependencies, reconciliations, diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala index c7218b1..7532ebd 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/internal/MimaIssues.scala @@ -8,10 +8,6 @@ import sbt.{Def, Task} private[sbtversionpolicy] object MimaIssues { - sealed trait ProblemType - case object BinaryIncompatibility extends ProblemType - case object SourceIncompatibility extends ProblemType - val binaryIssuesIterator: Def.Initialize[Task[Iterator[(sbt.ModuleID, (List[Problem], List[Problem]))]]] = Def.task { val binaryIssueFilters = mimaBackwardIssueFilters.value val sourceIssueFilters = mimaForwardIssueFilters.value diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt index 242f09b..bf66fa0 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/build.sbt @@ -17,7 +17,7 @@ val checkTasks = Seq( } ) -val `v1-0-0` = +val `v1_0_0` = project.settings( name := "assess-compatibility-test", version := "1.0.0", @@ -26,55 +26,55 @@ val `v1-0-0` = ) // binary and source compatible change in the code -val `v1-0-1` = +val `v1_0_1` = project.settings( name := "assess-compatibility-test", - version := "1.0.1", + version := "1.0.0+n", // we don’t set the version yet, it will be set by the scripted test libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.0", checkTasks, ) // No changes in the code, patch bump of library dependency -val `v1-0-2` = +val `v1_0_2` = project.settings( name := "assess-compatibility-test", - version := "1.0.2", + version := "1.0.1+n", libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.1", checkTasks, ) // Source incompatible change in the code -val `v1-1-0` = +val `v1_1_0` = project.settings( name := "assess-compatibility-test", - version := "1.1.0", + version := "1.0.2+n", libraryDependencies += "org.typelevel" %% "cats-core" % "2.6.1", checkTasks, ) // No changes in the code, minor bump of library dependency -val `v1-2-0` = +val `v1_2_0` = project.settings( name := "assess-compatibility-test", - version := "1.2.0", - libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0", + version := "1.1.0+n", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.7.0", checkTasks, ) // Binary incompatible change in the code -val `v2-0-0` = +val `v2_0_0` = project.settings( name := "assess-compatibility-test", - version := "2.0.0", - libraryDependencies += "org.typelevel" %% "cats-core" % "2.9.0", + version := "1.2.0+n", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.7.0", checkTasks, ) // No changes in the code, breaking change in the dependencies -val `v3-0-0` = +val `v3_0_0` = project.settings( name := "assess-compatibility-test", - version := "3.0.0", + version := "2.0.0+n", // no library dependency anymore checkTasks, ) diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test index 163e1ff..b068bdf 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/test @@ -1,24 +1,32 @@ -> v1-0-0/publishLocal +# Publish the first version +> v1_0_0/publishLocal > reload -> v1-0-1/checkAssessedCompatibilityIsBinaryAndSourceCompatible -> v1-0-1/publishLocal +# Assess the compatibility level of the next iteration +> v1_0_1/checkAssessedCompatibilityIsBinaryAndSourceCompatible +# Now that we know we are binary and source compatible, we can set its version and publish it +> set v1_0_1/version := "1.0.1" +> v1_0_1/publishLocal > reload -> v1-0-2/checkAssessedCompatibilityIsBinaryAndSourceCompatible -> v1-0-2/publishLocal +> v1_0_2/checkAssessedCompatibilityIsBinaryAndSourceCompatible +> set v1_0_2/version := "1.0.2" +> v1_0_2/publishLocal > reload -> v1-1-0/checkAssessedCompatibilityIsBinaryCompatible -> v1-1-0/publishLocal +> v1_1_0/checkAssessedCompatibilityIsBinaryCompatible +> set v1_1_0/version := "1.1.0" +> v1_1_0/publishLocal > reload -> v1-2-0/checkAssessedCompatibilityIsBinaryCompatible -> v1-2-0/publishLocal +> v1_2_0/checkAssessedCompatibilityIsBinaryCompatible +> set v1_2_0/version := "1.2.0" +> v1_2_0/publishLocal > reload -> v2-0-0/checkAssessedCompatibilityIsNone -> v2-0-0/publishLocal +> v2_0_0/checkAssessedCompatibilityIsNone +> set v2_0_0/version := "2.0.0" +> v2_0_0/publishLocal > reload -> v3-0-0/checkAssessedCompatibilityIsNone +> v3_0_0/checkAssessedCompatibilityIsNone diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_0_0/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-0/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_0_0/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-1/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_0_1/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-1/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_0_1/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-2/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_0_2/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-0-2/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_0_2/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-1-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_1_0/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-1-0/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_1_0/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-2-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_2_0/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1-2-0/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v1_2_0/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2-0-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2_0_0/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2-0-0/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v2_0_0/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3-0-0/src/main/scala/librarytest/LibraryTest.scala b/sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3_0_0/src/main/scala/librarytest/LibraryTest.scala similarity index 100% rename from sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3-0-0/src/main/scala/librarytest/LibraryTest.scala rename to sbt-version-policy/src/sbt-test/sbt-version-policy/assess-compatibility-level/v3_0_0/src/main/scala/librarytest/LibraryTest.scala diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/defaults/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/defaults/build.sbt index 6383753..86b5b3f 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/defaults/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/defaults/build.sbt @@ -52,9 +52,8 @@ lazy val check = taskKey[Unit]("") lazy val checkFails = Def.settings( check := { - val direction = versionPolicyCheckDirection.value val reports = versionPolicyFindDependencyIssues.value - val failed = reports.exists(!_._2.validated(direction)) + val failed = reports.exists(!_._2.validated(sbtversionpolicy.IncompatibilityType.BinaryIncompatibility)) assert(failed, s"Expected a failed report in $reports") } ) diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/simple/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/simple/build.sbt index 4fc3ed7..bb3e429 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/simple/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/simple/build.sbt @@ -130,9 +130,8 @@ lazy val shared = Def.settings( lazy val checkFails = Def.settings( check := { check.value - val direction = versionPolicyCheckDirection.value val reports = versionPolicyFindDependencyIssues.value - val failed = reports.exists(!_._2.validated(direction)) + val failed = reports.exists(!_._2.validated(sbtversionpolicy.IncompatibilityType.BinaryIncompatibility)) assert(failed, s"Expected a failed report in $reports") } ) From 5c651a54bd5bd20d1e4f0e00f49a5dd92007da20 Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Wed, 29 Nov 2023 09:24:44 +0100 Subject: [PATCH 4/4] Bypass versionPolicyFindIssues and versionPolicyAssessCompatibility if skip is true This is a bit dirty because those tasks are expected to return a result. Skipping a task that returns a result means that we still have to return a result somehow (unless we fail the task). Here, we return an empty result. --- .../SbtVersionPolicySettings.scala | 51 ++++++++++++------- .../sbt-version-policy/skip-publish/build.sbt | 7 +++ .../sbt-version-policy/skip-publish/test | 4 ++ 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala index 8e11f9c..dc1d9f5 100644 --- a/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala +++ b/sbt-version-policy/src/main/scala/sbtversionpolicy/SbtVersionPolicySettings.scala @@ -314,22 +314,35 @@ object SbtVersionPolicySettings { } } }.value, - versionPolicyFindIssues := { - val dependencyIssues = versionPolicyFindDependencyIssues.value - val mimaIssues = versionPolicyFindMimaIssues.value - assert( - dependencyIssues.map(_._1.revision).toSet == mimaIssues.map(_._1.revision).toSet, - "Dependency issues and Mima issues must be checked against the same previous releases" - ) - for ((previousModule, dependencyReport) <- dependencyIssues) yield { - val (_, problems) = - mimaIssues - .find { case (id, _) => previousModule.revision == id.revision } - .get // See assertion above - previousModule -> (dependencyReport, problems) - } - }, - versionPolicyAssessCompatibility := { + versionPolicyFindIssues := Def.ifS((versionPolicyFindIssues / skip).toTask)(Def.task { + streams.value.log.debug("Not finding incompatibilities with previous releases because 'versionPolicyFindIssues / skip' is 'true'") + Seq.empty[(ModuleID, (DependencyCheckReport, Seq[(IncompatibilityType, Problem)]))] + })( + Def.ifS[Seq[(ModuleID, (DependencyCheckReport, Seq[(IncompatibilityType, Problem)]))]](Def.task { + versionPolicyPreviousVersions.value.isEmpty + })(Def.task { + throw new MessageOnlyException("Unable to find compatibility issues because versionPolicyPreviousVersions is empty.") + })(Def.task { + versionPolicyPreviousVersions.value + val dependencyIssues = versionPolicyFindDependencyIssues.value + val mimaIssues = versionPolicyFindMimaIssues.value + assert( + dependencyIssues.map(_._1.revision).toSet == mimaIssues.map(_._1.revision).toSet, + "Dependency issues and Mima issues must be checked against the same previous releases" + ) + for ((previousModule, dependencyReport) <- dependencyIssues) yield { + val (_, problems) = + mimaIssues + .find { case (id, _) => previousModule.revision == id.revision } + .get // See assertion above + previousModule -> (dependencyReport, problems) + } + }) + ).value, + versionPolicyAssessCompatibility := Def.ifS((versionPolicyAssessCompatibility / skip).toTask)(Def.task { + streams.value.log.debug("Not assessing the compatibility with previous releases because 'versionPolicyAssessCompatibility / skip' is 'true'") + Seq.empty[(ModuleID, Compatibility)] + })(Def.task { // Results will be flawed if the `versionPolicyIntention` is set to `BinaryCompatible` or `None` // because `versionPolicyFindIssues` only reports the issues that violate the intended compatibility level if (versionPolicyIntention.?.value.exists(_ != Compatibility.BinaryAndSourceCompatible)) { @@ -353,12 +366,14 @@ object SbtVersionPolicySettings { } previousRelease -> compatibility } - } + }).value ) def skipSettings = Seq( versionCheck / skip := (publish / skip).value, - versionPolicyCheck / skip := (publish / skip).value + versionPolicyCheck / skip := (publish / skip).value, + versionPolicyFindIssues / skip := (publish / skip).value, + versionPolicyAssessCompatibility / skip := (publish / skip).value, ) def schemesGlobalSettings = Seq( diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/build.sbt b/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/build.sbt index 85df998..910700b 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/build.sbt +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/build.sbt @@ -14,3 +14,10 @@ val v101 = version := "1.0.1", versionPolicyIntention := Compatibility.BinaryCompatible ) + +val root = + project.in(file(".")) + .settings( + name := "library-test-skip-root", + ) + .aggregate(v101) diff --git a/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/test b/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/test index b1447dd..547c8e7 100644 --- a/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/test +++ b/sbt-version-policy/src/sbt-test/sbt-version-policy/skip-publish/test @@ -8,3 +8,7 @@ > set v101/publish/skip := true > v101/versionPolicyCheck > v101/versionCheck + +-> root/versionPolicyAssessCompatibility +> set root/publish/skip := true +> root/versionPolicyAssessCompatibility