Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce task versionPolicyAssessCompatibility #184

Merged
merged 4 commits into from
Nov 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 143 additions & 74 deletions README.md

Large diffs are not rendered by default.

9 changes: 2 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ inThisBuild(List(
url("https://github.com/alexarchambault")
)
),
versionPolicyIntention := Compatibility.BinaryAndSourceCompatible,
versionPolicyIntention := Compatibility.None,
libraryDependencySchemes += "com.typesafe" %% "mima-core" % "semver-spec"
))

Expand All @@ -29,18 +29,13 @@ 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",
"com.eed3si9n.verify" %% "verify" % "2.0.1" % Test,
),
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
),
)
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,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)
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 0 additions & 15 deletions sbt-version-policy/src/main/scala/sbtversionpolicy/Direction.scala

This file was deleted.

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

}
Original file line number Diff line number Diff line change
@@ -1,23 +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 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.")
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 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.")

Expand Down
Loading
Loading