Skip to content

Commit

Permalink
Introduce task versionPolicyAssessCompatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
julienrf committed Nov 28, 2023
1 parent fde33bc commit ac7a294
Show file tree
Hide file tree
Showing 15 changed files with 441 additions and 157 deletions.
214 changes: 140 additions & 74 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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)

}
Original file line number Diff line number Diff line change
@@ -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.")
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % sys.props("plugin.version"))
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit ac7a294

Please sign in to comment.