diff --git a/.scalafmt.conf b/.scalafmt.conf index 5227e15ff..4541f0952 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.7.14 +version = 3.7.17 runner.dialect = scala213source3 align.preset = none diff --git a/build.sbt b/build.sbt index 5da657280..de4719267 100644 --- a/build.sbt +++ b/build.sbt @@ -2,22 +2,23 @@ import FreeGen2.* val catsVersion = "2.10.0" -val catsEffectVersion = "3.5.1" +val catsEffectVersion = "3.5.2" val circeVersion = "0.14.6" -val fs2Version = "3.9.2" +val fs2Version = "3.9.3" val h2Version = "2.2.224" -val hikariVersion = "5.0.1" +val hikariVersion = "5.1.0" val magnoliaVersion = "1.1.3" val munitVersion = "1.0.0-M10" -val mysqlVersion = "8.1.0" +val mysqlVersion = "8.2.0" +val openTelemetryVersion = "1.32.0" val postgisVersion = "2021.1.0" val postgresVersion = "42.6.0" val scalatestVersion = "3.2.17" val shapelessVersion = "2.3.10" val slf4jVersion = "2.0.9" val weaverVersion = "0.8.3" -val zioInteropCats = "23.0.0.8" -val zioVersion = "2.0.17" +val zioInteropCats = "23.1.0.0" +val zioVersion = "2.0.19" val Scala213 = "2.13.12" val Scala3 = "3.3.1" @@ -130,6 +131,8 @@ lazy val noPublishSettings = Seq( mimaPreviousArtifacts := Set.empty, ) +lazy val runningInIntelliJ = System.getProperty("idea.managed", "false").toBoolean + def filterScalacConsoleOpts(options: Seq[String]) = { options.filterNot { opt => opt == "-Xfatal-warnings" || opt.startsWith("-Xlint") || opt.startsWith("-W") @@ -142,16 +145,28 @@ def module(name: String) = Project(name, file(s"modules/$name")) .settings( mimaPreviousArtifacts := previousStableVersion.value.map(organization.value %% moduleName.value % _).toSet ) + .settings( + if (runningInIntelliJ) Seq( + Test / unmanagedSourceDirectories += baseDirectory.value / "src" / "it" / "scala", + ) else Seq.empty + ) def moduleIT(name: String) = Project(s"$name-it", file(s"modules/$name-it")) .settings(moduleName := s"foobie-$name-it") .settings(commonSettings) .settings( publish / skip := true, - Compile / javaSource := baseDirectory.value / ".." / name / "src" / "main-it" / "java", - Compile / scalaSource := baseDirectory.value / ".." / name / "src" / "main-it" / "scala", - Test / javaSource := baseDirectory.value / ".." / name / "src" / "it" / "java", - Test / scalaSource := baseDirectory.value / ".." / name / "src" / "it" / "scala", + Test / fork := true, + Test / javaOptions += "-Xmx1000m", + ) + .settings( + // intellij complains about shared content roots, so it gets the source appended in `module` + if (runningInIntelliJ) Seq.empty else Seq( + Compile / javaSource := baseDirectory.value / ".." / name / "src" / "main-it" / "java", + Compile / scalaSource := baseDirectory.value / ".." / name / "src" / "main-it" / "scala", + Test / javaSource := baseDirectory.value / ".." / name / "src" / "it" / "java", + Test / scalaSource := baseDirectory.value / ".." / name / "src" / "it" / "scala", + ) ) .disablePlugins(MimaPlugin) @@ -390,6 +405,7 @@ lazy val zio = module("zio") "com.mysql" % "mysql-connector-j" % mysqlVersion % Optional, "org.postgresql" % "postgresql" % postgresVersion % Optional, "net.postgis" % "postgis-jdbc" % postgisVersion % Optional, + "io.opentelemetry" % "opentelemetry-api" % openTelemetryVersion % Optional, "dev.zio" %% "zio-test" % zioVersion % Test, "dev.zio" %% "zio-test-sbt" % zioVersion % Test, @@ -403,6 +419,7 @@ lazy val `zio-it` = moduleIT("zio") libraryDependencies ++= Seq( "dev.zio" %% "zio-test" % zioVersion % Test, "dev.zio" %% "zio-test-sbt" % zioVersion % Test, + "io.opentelemetry" % "opentelemetry-api" % openTelemetryVersion % Test, ), ) .dependsOn(zio, postgres) diff --git a/docker-compose.yml b/docker-compose.yml index 1bcb0e694..f7e7e5153 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: shm_size: 128m command: [ "postgres", - #"-c", "log_statement=all" + #"-c", "log_statement=all", "-c", "max_connections=400", "-c", "shared_buffers=250MB", # 25% of RAM "-c", "effective_cache_size=700MB", # 70% of RAM diff --git a/modules/zio/src/it/scala/zio/internal/metrics/MetricRegistryExposed.scala b/modules/zio/src/it/scala/zio/internal/metrics/MetricRegistryExposed.scala deleted file mode 100644 index 942e131fc..000000000 --- a/modules/zio/src/it/scala/zio/internal/metrics/MetricRegistryExposed.scala +++ /dev/null @@ -1,10 +0,0 @@ -package zio.internal.metrics - -import zio.Unsafe - -object MetricRegistryExposed { - - val snapshot = { - Unsafe.unsafe(implicit u => metricRegistry.snapshot()) - } -} diff --git a/modules/zio/src/it/scala/zoobie/postgres/PostgreSQLIntegrationSpec.scala b/modules/zio/src/it/scala/zoobie/postgres/PostgreSQLIntegrationSpec.scala index 6374b3fcb..1f4113256 100644 --- a/modules/zio/src/it/scala/zoobie/postgres/PostgreSQLIntegrationSpec.scala +++ b/modules/zio/src/it/scala/zoobie/postgres/PostgreSQLIntegrationSpec.scala @@ -77,7 +77,7 @@ object PostgreSQLIntegrationSpec extends ZIOSpecDefault { p <- pool(connectionConfig, config) transactor = Transactor.fromPoolTransactional(p) results <- run(transactor) - metrics = zio.internal.metrics.MetricRegistryExposed.snapshot + metrics <- ZIO.metrics.map(_.metrics) } yield { val metricPairs = metrics.map { p => val tags = p.metricKey.tags.toList.sortBy(_.key) diff --git a/modules/zio/src/it/scala/zoobie/sqlcommenter/SQLCommenterIntegrationSpec.scala b/modules/zio/src/it/scala/zoobie/sqlcommenter/SQLCommenterIntegrationSpec.scala new file mode 100644 index 000000000..a34c1ddae --- /dev/null +++ b/modules/zio/src/it/scala/zoobie/sqlcommenter/SQLCommenterIntegrationSpec.scala @@ -0,0 +1,78 @@ +package zoobie.sqlcommenter + +import doobie.syntax.string.* +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.common.Attributes +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.StatusCode +import io.opentelemetry.api.trace.TraceFlags +import io.opentelemetry.api.trace.TraceState +import zio.Chunk +import zio.ZIO +import zio.durationInt +import zio.test.TestAspect +import zio.test.ZIOSpecDefault +import zio.test.assertCompletes +import zoobie.ConnectionPoolConfig +import zoobie.Transactor +import zoobie.postgres.PostgreSQLConnectionConfig +import zoobie.postgres.pool + +import java.util.concurrent.TimeUnit + +object SQLCommenterIntegrationSpec extends ZIOSpecDefault { + + override val spec = test("SQLCommenterIntegrationSpec") { + val spanContext = new SpanContext { + override val getTraceId = "3b120af54ca6f7efacddf3e538dd4988" + override val getSpanId = "7cdf802020b41208" + override val getTraceFlags = TraceFlags.getSampled + override val getTraceState = TraceState.builder().put("key", "value").build() + override val isRemote = false + } + val span = new Span { + override def setAttribute[T](key: AttributeKey[T], value: T) = ??? + override def addEvent(name: String, attributes: Attributes) = ??? + override def addEvent(name: String, attributes: Attributes, timestamp: Long, unit: TimeUnit) = ??? + override def setStatus(statusCode: StatusCode, description: String) = ??? + override def recordException(exception: Throwable, additionalAttributes: Attributes) = ??? + override def updateName(name: String) = ??? + override def end(): Unit = ??? + override def end(timestamp: Long, unit: TimeUnit): Unit = ??? + override def isRecording = ??? + override def getSpanContext = spanContext + } + + for { + p <- pool(connectionConfig, config) + interpreter = TraceInterpreter.create(Transactor.kleisliInterpreter, ZIO.succeed(Some(span))) + transactor = Transactor(p.get, interpreter.ConnectionInterpreter, Transactor.strategies.transactional) + _ <- transactor.run(fr"SELECT 1".query[Int].unique) + } yield { + assertCompletes + } + } + + override val aspects = super.aspects ++ Chunk( + TestAspect.timed, + TestAspect.timeout(90.seconds), + TestAspect.withLiveClock, + ) + + private lazy val connectionConfig = PostgreSQLConnectionConfig( + host = "localhost", + database = "world", + username = "postgres", + password = "password", + applicationName = "doobie", + ) + + private lazy val config = ConnectionPoolConfig( + name = "zoobie-postgres-it", + size = 5, + queueSize = 1_000, + maxConnectionLifetime = 30.seconds, + validationTimeout = 2.seconds, + ) +} diff --git a/modules/zio/src/main/scala/zoobie/Transactor.scala b/modules/zio/src/main/scala/zoobie/Transactor.scala index 172c542e2..82377cb0a 100644 --- a/modules/zio/src/main/scala/zoobie/Transactor.scala +++ b/modules/zio/src/main/scala/zoobie/Transactor.scala @@ -87,7 +87,9 @@ object Transactor { private val sync: Sync[Task] = zio.interop.catz.asyncInstance[Any] - val interpreter: Interpreter[Task] = KleisliInterpreter(sync).ConnectionInterpreter + val kleisliInterpreter: KleisliInterpreter[Task] = KleisliInterpreter(sync) + + val interpreter: Interpreter[Task] = kleisliInterpreter.ConnectionInterpreter object strategies { val noop: Strategy = Strategy.void diff --git a/modules/zio/src/main/scala/zoobie/sqlcommenter/SQLCommenter.scala b/modules/zio/src/main/scala/zoobie/sqlcommenter/SQLCommenter.scala new file mode 100644 index 000000000..91f3bbdf1 --- /dev/null +++ b/modules/zio/src/main/scala/zoobie/sqlcommenter/SQLCommenter.scala @@ -0,0 +1,87 @@ +package zoobie.sqlcommenter + +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import scala.collection.immutable.SortedMap +import scala.jdk.CollectionConverters.* + +// https://google.github.io/sqlcommenter/spec/ +final case class SQLCommenter( + controller: Option[String], + action: Option[String], + framework: Option[String], + trace: Option[SQLCommenter.Trace], +) { + import SQLCommenter.serializeKeyValues + + def format: String = { + val traceState = trace.flatMap(_.state).map { state => + state + .filter { case (k, _) => k.nonEmpty } + .map { case (k, v) => s"$k=$v" } + .mkString(",") + } + val m = SortedMap( + "controller" -> controller, + "action" -> action, + "framework" -> framework, + "traceparent" -> trace.map(_.parent), + "tracestate" -> traceState, + ).collect { case (k, Some(v)) => (k, v) } + serializeKeyValues(m) + } + +} +object SQLCommenter { + + final case class Trace( + traceId: String, + spanId: String, + options: Byte, + state: Option[Map[String, String]], + ) { + def parent = String.format("00-%s-%s-%02X", traceId, spanId, options) + } + object Trace { + + def fromOpenTelemetryContext(spanContext: io.opentelemetry.api.trace.SpanContext) = { + Option(spanContext).filter(_.isValid).map { ctx => + val traceId = ctx.getTraceId + val spanId = ctx.getSpanId + val options = ctx.getTraceFlags + + val state = Option(ctx.getTraceState).filter(!_.isEmpty).map { state => + state.asMap().asScala.toMap + } + + Trace(traceId = traceId, spanId = spanId, options.asByte, state) + } + } + } + + private[sqlcommenter] val serializeKey = + urlEncode andThen escapeMetaCharacters + private[sqlcommenter] val serializeValue = + urlEncode andThen escapeMetaCharacters andThen sqlEscape + + private[sqlcommenter] def serializeKeyValue(k: String, v: String) = s"${serializeKey(k)}=${serializeValue(v)}" + + private[sqlcommenter] def serializeKeyValues(m: Map[String, String]) = { + if (m.isEmpty) "" + else m.toList.sorted.map(serializeKeyValue.tupled).mkString(",") + } + + @SuppressWarnings(Array("org.wartremover.warts.Null")) + private def urlEncode(s: String) = { + URLEncoder.encode(s, StandardCharsets.UTF_8) + .replaceAll("%27", "'") + .replaceAll("\\+", "%20") + } + private def escapeMetaCharacters(s: String) = s.replaceAll("'", "\\\\'") + private def sqlEscape(s: String) = s"'$s'" + + def affix(state: SQLCommenter, sql: String): String = { + val commentStr = state.format + if (commentStr.isEmpty) sql else sql.concat(s"\n/*${commentStr}*/") + } +} diff --git a/modules/zio/src/main/scala/zoobie/sqlcommenter/TraceInterpreter.scala b/modules/zio/src/main/scala/zoobie/sqlcommenter/TraceInterpreter.scala new file mode 100644 index 000000000..9992a6209 --- /dev/null +++ b/modules/zio/src/main/scala/zoobie/sqlcommenter/TraceInterpreter.scala @@ -0,0 +1,118 @@ +package zoobie.sqlcommenter + +import cats.data.Kleisli +import cats.effect.kernel.Sync +import doobie.free.KleisliInterpreter +import zio.Task +import zio.ZIO + +object TraceInterpreter { + + def create( + i: KleisliInterpreter[Task], + currentSpan: ZIO[Any, Nothing, Option[io.opentelemetry.api.trace.Span]], + ): KleisliInterpreter[Task] = { + + implicit val syncM: Sync[Task] = i.syncM + + def addTraceInfo[A, B](sql: String, run: String => Kleisli[Task, A, B]): Kleisli[Task, A, B] = { + val a: Task[String] = currentSpan.map { + case None => sql + case Some(span) => + val ctx = SQLCommenter.Trace.fromOpenTelemetryContext(span.getSpanContext) + val state = SQLCommenter(controller = None, action = None, framework = None, ctx) + SQLCommenter.affix(state, sql) + } + Kleisli.liftK(a).flatMap(run(_)) + } + + val TraceConnectionInterpreter = new i.ConnectionInterpreter { + override def prepareCall(a: String) = addTraceInfo(a, super.prepareCall(_)) + override def prepareCall(a: String, b: Int, c: Int) = addTraceInfo(a, super.prepareCall(_, b, c)) + override def prepareCall(a: String, b: Int, c: Int, d: Int) = addTraceInfo(a, super.prepareCall(_, b, c, d)) + + override def prepareStatement(a: String) = addTraceInfo(a, super.prepareStatement(_)) + override def prepareStatement(a: String, b: Array[Int]) = addTraceInfo(a, super.prepareStatement(_, b)) + override def prepareStatement(a: String, b: Array[String]) = addTraceInfo(a, super.prepareStatement(_, b)) + override def prepareStatement(a: String, b: Int) = addTraceInfo(a, super.prepareStatement(_, b)) + override def prepareStatement(a: String, b: Int, c: Int) = addTraceInfo(a, super.prepareStatement(_, b, c)) + override def prepareStatement(a: String, b: Int, c: Int, d: Int) = + addTraceInfo(a, super.prepareStatement(_, b, c, d)) + } + + val TraceStatementInterpreter = new i.StatementInterpreter { + override def execute(a: String) = addTraceInfo(a, super.execute(_)) + override def execute(a: String, b: Array[Int]) = addTraceInfo(a, super.execute(_, b)) + override def execute(a: String, b: Array[String]) = addTraceInfo(a, super.execute(_, b)) + override def execute(a: String, b: Int) = addTraceInfo(a, super.execute(_, b)) + + override def executeLargeUpdate(a: String) = addTraceInfo(a, super.executeLargeUpdate(_)) + override def executeLargeUpdate(a: String, b: Array[Int]) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + override def executeLargeUpdate(a: String, b: Array[String]) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + override def executeLargeUpdate(a: String, b: Int) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + + override def executeQuery(a: String) = addTraceInfo(a, super.executeQuery(_)) + + override def executeUpdate(a: String) = addTraceInfo(a, super.executeUpdate(_)) + override def executeUpdate(a: String, b: Array[Int]) = addTraceInfo(a, super.executeUpdate(_, b)) + override def executeUpdate(a: String, b: Array[String]) = addTraceInfo(a, super.executeUpdate(_, b)) + override def executeUpdate(a: String, b: Int) = addTraceInfo(a, super.executeUpdate(_, b)) + } + + val TracePreparedStatementInterpreter = new i.PreparedStatementInterpreter { + override def execute(a: String) = addTraceInfo(a, super.execute(_)) + override def execute(a: String, b: Array[Int]) = addTraceInfo(a, super.execute(_, b)) + override def execute(a: String, b: Array[String]) = addTraceInfo(a, super.execute(_, b)) + override def execute(a: String, b: Int) = addTraceInfo(a, super.execute(_, b)) + + override def executeLargeUpdate(a: String) = addTraceInfo(a, super.executeLargeUpdate(_)) + override def executeLargeUpdate(a: String, b: Array[Int]) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + override def executeLargeUpdate(a: String, b: Array[String]) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + override def executeLargeUpdate(a: String, b: Int) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + + override def executeQuery(a: String) = addTraceInfo(a, super.executeQuery(_)) + + override def executeUpdate(a: String) = addTraceInfo(a, super.executeUpdate(_)) + override def executeUpdate(a: String, b: Array[Int]) = addTraceInfo(a, super.executeUpdate(_, b)) + override def executeUpdate(a: String, b: Array[String]) = addTraceInfo(a, super.executeUpdate(_, b)) + override def executeUpdate(a: String, b: Int) = addTraceInfo(a, super.executeUpdate(_, b)) + } + + val TraceCallableStatementInterpreter = new i.CallableStatementInterpreter { + override def execute(a: String) = addTraceInfo(a, super.execute(_)) + override def execute(a: String, b: Array[Int]) = addTraceInfo(a, super.execute(_, b)) + override def execute(a: String, b: Array[String]) = addTraceInfo(a, super.execute(_, b)) + override def execute(a: String, b: Int) = addTraceInfo(a, super.execute(_, b)) + + override def executeLargeUpdate(a: String) = addTraceInfo(a, super.executeLargeUpdate(_)) + override def executeLargeUpdate(a: String, b: Array[Int]) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + override def executeLargeUpdate(a: String, b: Array[String]) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + override def executeLargeUpdate(a: String, b: Int) = addTraceInfo(a, super.executeLargeUpdate(_, b)) + + override def executeQuery(a: String) = addTraceInfo(a, super.executeQuery(_)) + + override def executeUpdate(a: String) = addTraceInfo(a, super.executeUpdate(_)) + override def executeUpdate(a: String, b: Array[Int]) = addTraceInfo(a, super.executeUpdate(_, b)) + override def executeUpdate(a: String, b: Array[String]) = addTraceInfo(a, super.executeUpdate(_, b)) + override def executeUpdate(a: String, b: Int) = addTraceInfo(a, super.executeUpdate(_, b)) + } + + new KleisliInterpreter[Task] { + override lazy val NClobInterpreter = i.NClobInterpreter + override lazy val BlobInterpreter = i.BlobInterpreter + override lazy val ClobInterpreter = i.ClobInterpreter + override lazy val DatabaseMetaDataInterpreter = i.DatabaseMetaDataInterpreter + override lazy val DriverInterpreter = i.DriverInterpreter + override lazy val RefInterpreter = i.RefInterpreter + override lazy val SQLDataInterpreter = i.SQLDataInterpreter + override lazy val SQLInputInterpreter = i.SQLInputInterpreter + override lazy val SQLOutputInterpreter = i.SQLOutputInterpreter + override lazy val ConnectionInterpreter = TraceConnectionInterpreter + override lazy val StatementInterpreter = TraceStatementInterpreter + override lazy val PreparedStatementInterpreter = TracePreparedStatementInterpreter + override lazy val CallableStatementInterpreter = TraceCallableStatementInterpreter + override lazy val ResultSetInterpreter = i.ResultSetInterpreter + } + } + +} diff --git a/modules/zio/src/test/scala/zoobie/sqlcommenter/SQLCommenterSpec.scala b/modules/zio/src/test/scala/zoobie/sqlcommenter/SQLCommenterSpec.scala new file mode 100644 index 000000000..13030a6a0 --- /dev/null +++ b/modules/zio/src/test/scala/zoobie/sqlcommenter/SQLCommenterSpec.scala @@ -0,0 +1,76 @@ +package zoobie.sqlcommenter + +import zio.test.ZIOSpecDefault +import zio.test.assertTrue + +object SQLCommenterSpec extends ZIOSpecDefault { + import SQLCommenter.* + + override val spec = suite("SQLCommenter")( + suite("serialization")( + test("key") { + assertTrue(serializeKey("1234") == "1234") && + assertTrue(serializeKey("route parameter") == "route%20parameter") && + assertTrue(serializeKey("FOO 'BAR") == "FOO%20\\'BAR") + }, + test("value") { + assertTrue(serializeValue("1234") == "'1234'") && + assertTrue(serializeValue("/param first") == "'%2Fparam%20first'") && + assertTrue(serializeValue("FOO 'BAR") == "'FOO%20\\'BAR'") && + assertTrue(serializeValue("DROP TABLE FOO") == "'DROP%20TABLE%20FOO'") + }, + test("key value") { + assertTrue(serializeKeyValue("route", "/polls 1000") == "route='%2Fpolls%201000'") + }, + test("key values") { + val in = Map( + "route" -> "/param*d", + "controller" -> "index", + "traceparent" -> "00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-01", + "tracestate" -> "congo=t61rcWkgMzE,rojo=00f067aa0ba902b7", + ) + assertTrue(serializeKeyValues(in) == "controller='index',route='%2Fparam*d',traceparent='00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-01',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'") + }, + suite("affix")( + test("empty") { + val state = SQLCommenter( + controller = None, + action = None, + framework = None, + trace = None, + ) + assertTrue(affix(state, "SELECT * FROM foo") == "SELECT * FROM foo") + }, + test("not empty") { + val state = SQLCommenter( + controller = None, + action = Some("/param*d"), + framework = None, + trace = None, + ) + assertTrue(affix(state, "SELECT * FROM foo") == "SELECT * FROM foo\n/*action='%2Fparam*d'*/") + }, + test("trace") { + val state = SQLCommenter( + controller = None, + action = None, + framework = None, + trace = Some(SQLCommenter.Trace( + traceId = "5bd66ef5095369c7b0d1f8f4bd33716a", + spanId = "c532cb4098ac3dd2", + options = 0, + state = Some(Map("congo" -> "t61rcWkgMzE", "rojo" -> "00f067aa0ba902b7")), + )), + ) + assertTrue( + affix( + state, + "SELECT * FROM foo", + ) == """SELECT * FROM foo + |/*traceparent='00-5bd66ef5095369c7b0d1f8f4bd33716a-c532cb4098ac3dd2-00',tracestate='congo%3Dt61rcWkgMzE%2Crojo%3D00f067aa0ba902b7'*/""".stripMargin, + ) + }, + ), + ), + ) +} diff --git a/project/build.properties b/project/build.properties index 27430827b..e8a1e246e 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.6 +sbt.version=1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2afc47394..8b3c9b91e 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -5,7 +5,7 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.5") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.3") +addSbtPlugin("org.wartremover" % "sbt-wartremover" % "3.1.5") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.3")