From 9e7f7b326a5523de43c28f54dab124a9c10e793b Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Fri, 28 Nov 2025 10:04:48 +0000 Subject: [PATCH 01/10] Print filenames in JsonKeyValueStore Helpful for testing --- .../scalasteward/core/persistence/JsonKeyValueStore.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala b/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala index 8bc5733c44..ac42833cc0 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/persistence/JsonKeyValueStore.scala @@ -54,8 +54,11 @@ final class JsonKeyValueStore[F[_], K, V](storeRoot: File, name: String)(implici } } - private def jsonFile(key: K): File = - storeRoot / keyEncoder(key) / s"$name.json" + private def jsonFile(key: K): File = { + val file = storeRoot / keyEncoder(key) / s"$name.json" + println(s"file is ${file}") + file + } } object JsonKeyValueStore { From 4f4637ed8279249b792c60c784d1217e4adc23ca Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Fri, 28 Nov 2025 10:41:25 +0000 Subject: [PATCH 02/10] Add deserialisation test for VersionsCache Value --- ...versions-cache-value-without-first-seen.json | 5 +++++ .../core/coursier/VersionsCacheTest.scala | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 modules/core/src/test/resources/versions-cache-value-without-first-seen.json create mode 100644 modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala diff --git a/modules/core/src/test/resources/versions-cache-value-without-first-seen.json b/modules/core/src/test/resources/versions-cache-value-without-first-seen.json new file mode 100644 index 0000000000..c5b2f305b4 --- /dev/null +++ b/modules/core/src/test/resources/versions-cache-value-without-first-seen.json @@ -0,0 +1,5 @@ +{ + "updatedAt": 1000, + "versions": ["1.0.0", "1.0.1"], + "maybeError": null +} diff --git a/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala b/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala new file mode 100644 index 0000000000..b9f49719d6 --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala @@ -0,0 +1,17 @@ +package org.scalasteward.core.coursier + +import io.circe.parser +import munit.FunSuite +import org.scalasteward.core.coursier.VersionsCache.Value +import org.scalasteward.core.data.Version +import org.scalasteward.core.util.Timestamp + +import scala.io.Source + +class VersionsCacheTest extends FunSuite { + test("version cache deserialisation without first seen") { + val input = Source.fromResource("versions-cache-value-without-first-seen.json").mkString + val expected = Value(Timestamp(1000), List(Version("1.0.0"), Version("1.0.1")), None) + assertEquals(parser.decode[Value](input), Right(expected)) + } +} From c342785a349b22d9696b8df926888516d94ca69f Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Fri, 28 Nov 2025 10:41:43 +0000 Subject: [PATCH 03/10] Add json fixture for new Value type --- .../resources/versions-cache-value-with-first-seen.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 modules/core/src/test/resources/versions-cache-value-with-first-seen.json diff --git a/modules/core/src/test/resources/versions-cache-value-with-first-seen.json b/modules/core/src/test/resources/versions-cache-value-with-first-seen.json new file mode 100644 index 0000000000..93c0d72224 --- /dev/null +++ b/modules/core/src/test/resources/versions-cache-value-with-first-seen.json @@ -0,0 +1,8 @@ +{ + "updatedAt": 10002, + "versions": [ + { "version": "1.0.0", "firstSeen": 10000 }, + { "version": "1.0.1", "firstSeen": 10001 } + ], + "maybeError": null +} From ead3d1ae4f286b9ab7914dd6181e47fc0fc3f89f Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Wed, 26 Nov 2025 14:52:15 +0000 Subject: [PATCH 04/10] Add VersionWithFirstSeen --- .../core/coursier/VersionsCache.scala | 22 ++++++++++++++++--- .../org/scalasteward/core/data/Version.scala | 1 + 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala index 59d7fe90f8..8391d17fd0 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala @@ -18,13 +18,15 @@ package org.scalasteward.core.coursier import cats.implicits.* import cats.{MonadThrow, Parallel} -import io.circe.generic.semiauto.deriveCodec +import io.circe.generic.semiauto.{deriveCodec, deriveDecoder, deriveEncoder} import io.circe.{Codec, KeyEncoder} import org.scalasteward.core.coursier.VersionsCache.{Key, Value} import org.scalasteward.core.data.{Dependency, Resolver, Scope, Version} import org.scalasteward.core.persistence.KeyValueStore import org.scalasteward.core.util.{DateTimeAlg, Timestamp} import scala.concurrent.duration.FiniteDuration +import io.circe.Encoder +import io.circe.Decoder final class VersionsCache[F[_]]( cacheTtl: FiniteDuration, @@ -53,7 +55,7 @@ final class VersionsCache[F[_]]( case maybeValue => coursierAlg.getVersions(dependency, resolver).attempt.flatMap { case Right(versions) => - store.put(key, Value(now, versions, None)).as(versions) + store.put(key, Value(now, versions.map(addCurrentTime), None)).as(versions) case Left(throwable) => val versions = maybeValue.map(_.versions).getOrElse(List.empty) store.put(key, Value(now, versions, Some(throwable.toString))).as(versions) @@ -79,7 +81,7 @@ object VersionsCache { final case class Value( updatedAt: Timestamp, - versions: List[Version], + versions: List[(Version, Instant)], maybeError: Option[String] ) { def maxAgeFactor: Long = @@ -90,4 +92,18 @@ object VersionsCache { implicit val valueCodec: Codec[Value] = deriveCodec } + + final case class VersionWithFirstSeen( + version: Version, + firstSeen: Option[Timestamp] + ) + + object VersionWithFirstSeen { + implicit val valueEncoder: Encoder[VersionWithFirstSeen] = deriveEncoder + implicit val valueDecoder: Decoder[VersionWithFirstSeen] = + deriveDecoder[VersionWithFirstSeen] + .or( + Decoder[Version].map(v => VersionWithFirstSeen(v, None)) + ) + } } diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala index 87ee74ec78..6395c8e6ad 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala @@ -21,6 +21,7 @@ import cats.implicits.* import cats.parse.{Numbers, Parser, Rfc5234} import io.circe.{Decoder, Encoder} import org.scalasteward.core.data.Version.startsWithDate +import java.time.Instant final case class Version(value: String) { override def toString: String = value From 13b0ff9c16ecd2e2d3dc371f4ebe07bb10355a7d Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Fri, 28 Nov 2025 11:00:38 +0000 Subject: [PATCH 05/10] WIP: continue VersionWithFirstSeen --- .../org/scalasteward/core/coursier/VersionsCache.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala index 8391d17fd0..9107af4a11 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala @@ -51,7 +51,7 @@ final class VersionsCache[F[_]]( val key = Key(dependency, resolver) store.get(key).flatMap { case Some(value) if value.updatedAt.until(now) <= (maxAge * value.maxAgeFactor) => - F.pure(value.versions) + F.pure(value.versions.map(_.version)) case maybeValue => coursierAlg.getVersions(dependency, resolver).attempt.flatMap { case Right(versions) => @@ -62,6 +62,8 @@ final class VersionsCache[F[_]]( } } } + + private def addCurrentTime(version: Version): VersionWithFirstSeen = {} } object VersionsCache { @@ -81,7 +83,7 @@ object VersionsCache { final case class Value( updatedAt: Timestamp, - versions: List[(Version, Instant)], + versions: List[VersionWithFirstSeen], maybeError: Option[String] ) { def maxAgeFactor: Long = From f7dc7c0ccf2d3c254c87399969c5c6962c3ad58c Mon Sep 17 00:00:00 2001 From: Roberto Tyley Date: Fri, 28 Nov 2025 11:46:56 +0000 Subject: [PATCH 06/10] Add merging of new and old first-seen version data --- .../core/coursier/VersionsCache.scala | 38 +++++++++++-------- .../org/scalasteward/core/data/Version.scala | 1 - .../core/coursier/VersionsCacheTest.scala | 11 +++++- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala index 9107af4a11..38d5459fd3 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala @@ -19,14 +19,13 @@ package org.scalasteward.core.coursier import cats.implicits.* import cats.{MonadThrow, Parallel} import io.circe.generic.semiauto.{deriveCodec, deriveDecoder, deriveEncoder} -import io.circe.{Codec, KeyEncoder} -import org.scalasteward.core.coursier.VersionsCache.{Key, Value} +import io.circe.{Codec, Decoder, Encoder, KeyEncoder} +import org.scalasteward.core.coursier.VersionsCache.{Key, Value, VersionWithFirstSeen} import org.scalasteward.core.data.{Dependency, Resolver, Scope, Version} import org.scalasteward.core.persistence.KeyValueStore import org.scalasteward.core.util.{DateTimeAlg, Timestamp} + import scala.concurrent.duration.FiniteDuration -import io.circe.Encoder -import io.circe.Decoder final class VersionsCache[F[_]]( cacheTtl: FiniteDuration, @@ -40,30 +39,39 @@ final class VersionsCache[F[_]]( def getVersions(dependency: Scope.Dependency, maxAge: Option[FiniteDuration]): F[List[Version]] = dependency.resolvers .parFlatTraverse(getVersionsImpl(dependency.value, _, maxAge.getOrElse(cacheTtl))) - .map(_.distinct.sorted) + .map(_.map(_.version).distinct.sorted) // TODO - remove `.map(_.version)` private def getVersionsImpl( dependency: Dependency, resolver: Resolver, maxAge: FiniteDuration - ): F[List[Version]] = + ): F[List[VersionWithFirstSeen]] = dateTimeAlg.currentTimestamp.flatMap { now => val key = Key(dependency, resolver) store.get(key).flatMap { case Some(value) if value.updatedAt.until(now) <= (maxAge * value.maxAgeFactor) => - F.pure(value.versions.map(_.version)) + F.pure(value.versions) case maybeValue => coursierAlg.getVersions(dependency, resolver).attempt.flatMap { case Right(versions) => - store.put(key, Value(now, versions.map(addCurrentTime), None)).as(versions) + val existingFirstSeenByVersion: Map[Version, Timestamp] = + maybeValue + .map(_.versions.flatMap(vwfs => vwfs.firstSeen.map(vwfs.version -> _)).toMap) + .getOrElse(Map.empty) + val updatedVersionsWithFirstSeen = + versions.map(v => + VersionWithFirstSeen(v, Some(existingFirstSeenByVersion.getOrElse(v, now))) + ) + + store + .put(key, Value(now, updatedVersionsWithFirstSeen, None)) + .as(updatedVersionsWithFirstSeen) case Left(throwable) => val versions = maybeValue.map(_.versions).getOrElse(List.empty) store.put(key, Value(now, versions, Some(throwable.toString))).as(versions) } } } - - private def addCurrentTime(version: Version): VersionWithFirstSeen = {} } object VersionsCache { @@ -102,10 +110,10 @@ object VersionsCache { object VersionWithFirstSeen { implicit val valueEncoder: Encoder[VersionWithFirstSeen] = deriveEncoder - implicit val valueDecoder: Decoder[VersionWithFirstSeen] = - deriveDecoder[VersionWithFirstSeen] - .or( - Decoder[Version].map(v => VersionWithFirstSeen(v, None)) - ) + implicit val valueDecoder: Decoder[VersionWithFirstSeen] = { + val legacyVersionStringDecoder = Decoder[Version].map(VersionWithFirstSeen(_, None)) + + deriveDecoder[VersionWithFirstSeen].or(legacyVersionStringDecoder) + } } } diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala index 6395c8e6ad..87ee74ec78 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala @@ -21,7 +21,6 @@ import cats.implicits.* import cats.parse.{Numbers, Parser, Rfc5234} import io.circe.{Decoder, Encoder} import org.scalasteward.core.data.Version.startsWithDate -import java.time.Instant final case class Version(value: String) { override def toString: String = value diff --git a/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala b/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala index b9f49719d6..01e4bea6db 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala @@ -2,7 +2,7 @@ package org.scalasteward.core.coursier import io.circe.parser import munit.FunSuite -import org.scalasteward.core.coursier.VersionsCache.Value +import org.scalasteward.core.coursier.VersionsCache.{Value, VersionWithFirstSeen} import org.scalasteward.core.data.Version import org.scalasteward.core.util.Timestamp @@ -11,7 +11,14 @@ import scala.io.Source class VersionsCacheTest extends FunSuite { test("version cache deserialisation without first seen") { val input = Source.fromResource("versions-cache-value-without-first-seen.json").mkString - val expected = Value(Timestamp(1000), List(Version("1.0.0"), Version("1.0.1")), None) + val expected = Value( + Timestamp(1000), + List( + VersionWithFirstSeen(Version("1.0.0"), None), + VersionWithFirstSeen(Version("1.0.1"), None) + ), + None + ) assertEquals(parser.decode[Value](input), Right(expected)) } } From 9d16cf47fc514b9316d0735f65f97542af643aec Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Fri, 28 Nov 2025 15:54:22 +0000 Subject: [PATCH 07/10] Add deserialisation test for new format --- .../core/coursier/VersionsCacheTest.scala | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala b/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala index 01e4bea6db..faff6207ec 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/coursier/VersionsCacheTest.scala @@ -21,4 +21,17 @@ class VersionsCacheTest extends FunSuite { ) assertEquals(parser.decode[Value](input), Right(expected)) } + + test("version cache deserialisation with first seen") { + val input = Source.fromResource("versions-cache-value-with-first-seen.json").mkString + val expected = Value( + Timestamp(10002), + List( + VersionWithFirstSeen(Version("1.0.0"), Some(Timestamp(10000))), + VersionWithFirstSeen(Version("1.0.1"), Some(Timestamp(10001))) + ), + None + ) + assertEquals(parser.decode[Value](input), Right(expected)) + } } From 0d83ea8046e90e5956955ede5a4807f1f6198e99 Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Fri, 28 Nov 2025 16:57:04 +0000 Subject: [PATCH 08/10] Begin propagating firstSeen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It looks like we probably want to update UpdatesConfig.scala to add a new method `isTooNew: Update.ForArtifactId => FilterResult` which rules out proposed updates that use too new a version (according to the user’s config). To do that, we reckon we’ll need Update.ForArtifactId to list versions with their firstSeen information, i.e. update the contained type from Version to VersionWithFirstSeen. This commit begins making that change (and so will not compile). --- .../scalasteward/core/coursier/VersionsCache.scala | 7 +++++-- .../scala/org/scalasteward/core/data/Update.scala | 3 ++- .../scalasteward/core/repoconfig/UpdatePattern.scala | 11 ++++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala index 38d5459fd3..a6c9d53225 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/coursier/VersionsCache.scala @@ -36,10 +36,13 @@ final class VersionsCache[F[_]]( parallel: Parallel[F], F: MonadThrow[F] ) { - def getVersions(dependency: Scope.Dependency, maxAge: Option[FiniteDuration]): F[List[Version]] = + def getVersions( + dependency: Scope.Dependency, + maxAge: Option[FiniteDuration] + ): F[List[VersionWithFirstSeen]] = dependency.resolvers .parFlatTraverse(getVersionsImpl(dependency.value, _, maxAge.getOrElse(cacheTtl))) - .map(_.map(_.version).distinct.sorted) // TODO - remove `.map(_.version)` + .map(_.distinct.sortBy(_.version)) private def getVersionsImpl( dependency: Dependency, diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala index d8aabe72ba..0d1782e6c5 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala @@ -22,6 +22,7 @@ import io.circe.{Decoder, Encoder} import org.scalasteward.core.repoconfig.PullRequestGroup import org.scalasteward.core.util import org.scalasteward.core.util.Nel +import org.scalasteward.core.coursier.VersionsCache.VersionWithFirstSeen sealed trait Update { @@ -86,7 +87,7 @@ object Update { final case class ForArtifactId( crossDependency: CrossDependency, - newerVersions: Nel[Version], + newerVersions: Nel[VersionWithFirstSeen], newerGroupId: Option[GroupId] = None, newerArtifactId: Option[String] = None ) extends Single { diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatePattern.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatePattern.scala index 149d52d8f2..492c1e391c 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatePattern.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatePattern.scala @@ -20,6 +20,7 @@ import cats.syntax.all.* import io.circe.Codec import io.circe.generic.semiauto.* import org.scalasteward.core.data.{GroupId, Update, Version} +import org.scalasteward.core.coursier.VersionsCache.VersionWithFirstSeen final case class UpdatePattern( groupId: GroupId, @@ -32,18 +33,22 @@ final case class UpdatePattern( object UpdatePattern { final case class MatchResult( byArtifactId: List[UpdatePattern], - filteredVersions: List[Version] + filteredVersions: List[VersionWithFirstSeen] ) def findMatch( patterns: List[UpdatePattern], update: Update.ForArtifactId, - include: Boolean + include: Boolean, + versionPredicate: VersionWithFirstSeen => Boolean = _ => true ): MatchResult = { val byGroupId = patterns.filter(_.groupId === update.groupId) val byArtifactId = byGroupId.filter(_.artifactId.forall(_ === update.artifactId.name)) val filteredVersions = update.newerVersions.filter(newVersion => - byArtifactId.exists(_.version.forall(_.matches(newVersion.value))) === include + (byArtifactId.exists(_.version.forall(_.matches(newVersion.value))) && versionPredicate( + newVersion + )) + === include ) MatchResult(byArtifactId, filteredVersions) } From a1dffded2799b13ce6db739fb624572e83fb374b Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Wed, 3 Dec 2025 15:52:24 +0000 Subject: [PATCH 09/10] Replace withNewerVersions with supersedes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit removes withNewerVersions and replaces the one usage of it with a new function supersedes, which checks whether an update matches another while having a newer nextVersion. withNewerVersions was a little awkward to update to work with us having changed some Versions to VersionWithFirstSeens, because it accepts a list of Versions (rather than just producing one). It was originally added in response to [this PR comment](https://github.com/scala-steward-org/scala-steward/pull/1667#discussion_r506648775), but we reckon replacing it with a slightly different check of the group and artifact IDs makes for clearer, more explicit code. We think (though it’s somewhat hard to tell) that this doesn’t remove any existing functionality: part of the argument for adding withNewerVersions in the first place was to support Update.Group, but it appears to only be called for Update.Singles. --- .../main/scala/org/scalasteward/core/data/Update.scala | 9 +++------ .../core/nurture/PullRequestRepository.scala | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala index 0d1782e6c5..94ed47e3d9 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala @@ -77,12 +77,9 @@ object Update { s"$groupId:$artifacts : $versions" } - def withNewerVersions(versions: Nel[Version]): Update.Single = this match { - case s @ ForArtifactId(_, _, _, _) => - s.copy(newerVersions = versions) - case ForGroupId(forArtifactIds) => - ForGroupId(forArtifactIds.map(_.copy(newerVersions = versions))) - } + def supersedes(that: Update.Single): Boolean = + this.groupAndMainArtifactId == that.groupAndMainArtifactId + && this.nextVersion > that.nextVersion } final case class ForArtifactId( diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala index 672770e546..8fd44d4438 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala @@ -71,9 +71,7 @@ final class PullRequestRepository[F[_]](kvStore: KeyValueStore[F, Repo, Map[Uri, kvStore.getOrElse(repo, Map.empty).map { _.collect { case (url, Entry(baseSha1, u: Update.Single, state, _, number, updateBranch)) - if state === PullRequestState.Open && - u.withNewerVersions(update.newerVersions) === update && - u.nextVersion < update.nextVersion => + if state === PullRequestState.Open && update.supersedes(u) => for { number <- number branch = updateBranch.getOrElse(git.branchFor(u, repo.branch)) From e7c5545b3320fd6d791c409382da1e3e2c624148 Mon Sep 17 00:00:00 2001 From: Emily Bourke Date: Thu, 4 Dec 2025 16:46:33 +0000 Subject: [PATCH 10/10] Add CooldownConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To facilitate using dependency cooldowns, this commit adds a class CooldownConfig to represent a user’s desired cooldown settings: a list of ages (specified as finite durations, using the same string syntax as supported for pullRequests.frequency), each with a list of UpdatePatterns for the age to apply to. Co-authored-by: Roberto Tyley --- .../core/repoconfig/CooldownConfig.scala | 37 +++++++++++++++++++ .../core/repoconfig/UpdatesConfig.scala | 3 +- 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 modules/core/src/main/scala/org/scalasteward/core/repoconfig/CooldownConfig.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/CooldownConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/CooldownConfig.scala new file mode 100644 index 0000000000..89380db312 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/CooldownConfig.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2018-2025 Scala Steward contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.scalasteward.core.repoconfig + +import io.circe.{Codec, Decoder, Encoder} +import io.circe.generic.semiauto.deriveCodec +import scala.concurrent.duration.FiniteDuration +import cats.syntax.either.* + +import org.scalasteward.core.util.dateTime.parseFiniteDuration + +final case class CooldownConfig( + age: FiniteDuration, + artifacts: List[UpdatePattern] = List.empty +) + +object CooldownConfig { + implicit val codec: Codec[CooldownConfig] = deriveCodec + implicit val finiteDurationDecoder: Decoder[FiniteDuration] = + Decoder[String].emap(s => parseFiniteDuration(s).leftMap(_.toString)) + implicit val finiteDurationEncoder: Encoder[FiniteDuration] = + Encoder[String].contramap(_.toString) +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala index d521373b61..75b424a201 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala @@ -40,7 +40,8 @@ final case class UpdatesConfig( private val ignore: Option[List[UpdatePattern]] = None, private val retracted: Option[List[RetractedArtifact]] = None, limit: Option[NonNegInt] = UpdatesConfig.defaultLimit, - private val fileExtensions: Option[List[String]] = None + private val fileExtensions: Option[List[String]] = None, + private val cooldown: Option[List[CooldownConfig]] = None ) { private[repoconfig] def pinOrDefault: List[UpdatePattern] = pin.getOrElse(Nil)