Skip to content

Commit 349ee34

Browse files
authored
Add granular frequency control by groupId and artifactId (#2515)
1 parent aad9688 commit 349ee34

File tree

8 files changed

+504
-28
lines changed

8 files changed

+504
-28
lines changed

docs/repo-specific-configuration.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@ postUpdateHooks = [{
9393
groupId = "com.github.sbt",
9494
artifactId = "sbt-protobuf"
9595
}]
96+
97+
# You can override some config options for dependencies that matches the given pattern.
98+
# Currently, "pullRequests" can be overridden.
99+
# Each pattern must have `groupId`, and may have `artifactId` and `version`.
100+
# First-matched entry is used.
101+
# More-specific entry should be placed before less-specific entry.
102+
#
103+
# Default: empty `[]`
104+
dependencyOverrides = [
105+
{
106+
dependency = { groupId = "com.example", artifactId = "foo", version = "2." },
107+
pullRequests = { frequency = "1 day" },
108+
},
109+
{
110+
dependency = { groupId = "com.example", artifactId = "foo" },
111+
pullRequests = { frequency = "30 day" },
112+
},
113+
{
114+
dependency = { groupId = "com.example" },
115+
pullRequests = { frequency = "14 day" },
116+
}
117+
]
96118
```
97119

98120
The version information given in the patterns above can be in two formats:

modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import cats.{Id, Monad}
2121
import io.circe.Codec
2222
import io.circe.generic.semiauto.deriveCodec
2323
import org.http4s.Uri
24-
import org.scalasteward.core.data.{CrossDependency, Update, Version}
24+
import org.scalasteward.core.data.{CrossDependency, GroupId, Update, Version}
2525
import org.scalasteward.core.git
2626
import org.scalasteward.core.git.{Branch, Sha1}
2727
import org.scalasteward.core.nurture.PullRequestRepository.Entry
@@ -112,6 +112,17 @@ final class PullRequestRepository[F[_]](kvStore: KeyValueStore[F, Repo, Map[Uri,
112112

113113
def lastPullRequestCreatedAt(repo: Repo): F[Option[Timestamp]] =
114114
kvStore.get(repo).map(_.flatMap(_.values.map(_.entryCreatedAt).maxOption))
115+
116+
def lastPullRequestCreatedAtByArtifact(repo: Repo): F[Map[(GroupId, String), Timestamp]] =
117+
kvStore.get(repo).map {
118+
case None => Map.empty
119+
case Some(pullRequests) =>
120+
pullRequests.values
121+
.groupBy(entry => (entry.update.groupId, entry.update.mainArtifactId))
122+
.view
123+
.mapValues(_.map(_.entryCreatedAt).max)
124+
.toMap
125+
}
115126
}
116127

117128
object PullRequestRepository {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2018-2022 Scala Steward contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.scalasteward.core.repoconfig
18+
19+
import io.circe.Codec
20+
import io.circe.generic.extras.Configuration
21+
import io.circe.generic.semiauto.deriveCodec
22+
23+
final case class GroupRepoConfig(
24+
pullRequests: PullRequestsConfig = PullRequestsConfig(),
25+
dependency: UpdatePattern
26+
)
27+
28+
object GroupRepoConfig {
29+
implicit val groupPullConfigCodecConfig: Configuration =
30+
Configuration.default.withDefaults
31+
32+
implicit val groupPullConfigCodec: Codec[GroupRepoConfig] =
33+
deriveCodec
34+
35+
}

modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfig.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ final case class RepoConfig(
3131
updates: UpdatesConfig = UpdatesConfig(),
3232
postUpdateHooks: Option[List[PostUpdateHookConfig]] = None,
3333
updatePullRequests: Option[PullRequestUpdateStrategy] = None,
34-
buildRoots: Option[List[BuildRootConfig]] = None
34+
buildRoots: Option[List[BuildRootConfig]] = None,
35+
dependencyOverrides: List[GroupRepoConfig] = List.empty
3536
) {
3637
def buildRootsOrDefault: List[BuildRootConfig] =
3738
buildRoots
@@ -75,7 +76,8 @@ object RepoConfig {
7576
updates = x.updates |+| y.updates,
7677
postUpdateHooks = x.postUpdateHooks |+| y.postUpdateHooks,
7778
updatePullRequests = x.updatePullRequests.orElse(y.updatePullRequests),
78-
buildRoots = x.buildRoots |+| y.buildRoots
79+
buildRoots = x.buildRoots |+| y.buildRoots,
80+
dependencyOverrides = x.dependencyOverrides |+| y.dependencyOverrides
7981
)
8082
}
8183
)

modules/core/src/main/scala/org/scalasteward/core/update/PruningAlg.scala

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,16 @@
1717
package org.scalasteward.core.update
1818

1919
import cats.Monad
20-
import cats.data.OptionT
2120
import cats.implicits._
2221
import org.scalasteward.core.data._
2322
import org.scalasteward.core.nurture.PullRequestRepository
2423
import org.scalasteward.core.repocache.RepoCache
25-
import org.scalasteward.core.repoconfig.{PullRequestFrequency, RepoConfig}
24+
import org.scalasteward.core.repoconfig.{PullRequestFrequency, RepoConfig, UpdatePattern}
2625
import org.scalasteward.core.update.PruningAlg._
2726
import org.scalasteward.core.update.data.UpdateState
2827
import org.scalasteward.core.update.data.UpdateState._
2928
import org.scalasteward.core.util
30-
import org.scalasteward.core.util.{dateTime, DateTimeAlg, Nel}
29+
import org.scalasteward.core.util.{dateTime, DateTimeAlg, Nel, Timestamp}
3130
import org.scalasteward.core.vcs.data.Repo
3231
import org.typelevel.log4cats.Logger
3332
import scala.concurrent.duration._
@@ -129,37 +128,70 @@ final class PruningAlg[F[_]](implicit
129128
repoConfig: RepoConfig,
130129
updateStates: List[UpdateState]
131130
): F[Option[Nel[WithUpdate]]] =
132-
newPullRequestsAllowed(repo, repoConfig.pullRequests.frequencyOrDefault).flatMap { allowed =>
133-
Nel.fromList(updateStates.collect {
134-
case s: DependencyOutdated if allowed => s
135-
case s: PullRequestOutdated => s
136-
}) match {
131+
for {
132+
now <- dateTimeAlg.currentTimestamp
133+
repoLastPrCreatedAt <- pullRequestRepository.lastPullRequestCreatedAt(repo)
134+
lastPullRequestCreatedAtByArtifact <- pullRequestRepository
135+
.lastPullRequestCreatedAtByArtifact(repo)
136+
states <- updateStates.traverseFilter[F, WithUpdate] {
137+
case s: DependencyOutdated =>
138+
newPullRequestsAllowed(
139+
s,
140+
now,
141+
repoLastPrCreatedAt,
142+
artifactLastPrCreatedAt =
143+
lastPullRequestCreatedAtByArtifact.get(s.update.groupId -> s.update.mainArtifactId),
144+
repoConfig
145+
).map {
146+
case true => Some(s)
147+
case false => None
148+
}
149+
case s: PullRequestOutdated => Option[WithUpdate](s).pure[F]
150+
case _ => F.pure(None)
151+
}
152+
result <- Nel.fromList(states) match {
137153
case some @ Some(states) =>
138154
val lines = util.string.indentLines(states.map(UpdateState.show).sorted)
139155
logger.info(s"${repo.show} is outdated:\n" + lines).as(some)
140156
case None =>
141157
logger.info(s"${repo.show} is up-to-date").as(None)
142158
}
143-
}
159+
} yield result
144160

145-
private def newPullRequestsAllowed(repo: Repo, frequency: PullRequestFrequency): F[Boolean] =
146-
if (frequency === PullRequestFrequency.Asap) true.pure[F]
147-
else
148-
dateTimeAlg.currentTimestamp.flatMap { now =>
149-
val ignoring = "Ignoring outdated dependencies"
150-
if (!frequency.onSchedule(now))
151-
logger.info(s"$ignoring according to $frequency").as(false)
152-
else {
153-
val maybeWaitingTime = OptionT(pullRequestRepository.lastPullRequestCreatedAt(repo))
154-
.subflatMap(frequency.waitingTime(_, now))
155-
maybeWaitingTime.value.flatMap {
156-
case None => true.pure[F]
157-
case Some(waitingTime) =>
158-
val message = s"$ignoring for ${dateTime.showDuration(waitingTime)}"
159-
logger.info(message).as(false)
161+
private def newPullRequestsAllowed(
162+
dependencyOutdated: DependencyOutdated,
163+
now: Timestamp,
164+
repoLastPrCreatedAt: Option[Timestamp],
165+
artifactLastPrCreatedAt: Option[Timestamp],
166+
repoConfig: RepoConfig
167+
): F[Boolean] = {
168+
val (frequencyz: Option[PullRequestFrequency], lastPrCreatedAt: Option[Timestamp]) =
169+
repoConfig.dependencyOverrides
170+
.collectFirstSome { groupRepoConfig =>
171+
val matchResult = UpdatePattern
172+
.findMatch(List(groupRepoConfig.dependency), dependencyOutdated.update, include = true)
173+
if (matchResult.byArtifactId.nonEmpty && matchResult.filteredVersions.nonEmpty) {
174+
Some((groupRepoConfig.pullRequests.frequency, artifactLastPrCreatedAt))
175+
} else {
176+
None
160177
}
161178
}
179+
.getOrElse((repoConfig.pullRequests.frequency, repoLastPrCreatedAt))
180+
val frequency = frequencyz.getOrElse(PullRequestFrequency.Asap)
181+
182+
val dep = dependencyOutdated.crossDependency.head
183+
val ignoring = s"Ignoring outdated dependency ${dep.groupId}:${dep.artifactId.name}"
184+
if (!frequency.onSchedule(now))
185+
logger.info(s"$ignoring according to $frequency").as(false)
186+
else {
187+
lastPrCreatedAt.flatMap(frequency.waitingTime(_, now)) match {
188+
case None => true.pure[F]
189+
case Some(waitingTime) =>
190+
val message = s"$ignoring for ${dateTime.showDuration(waitingTime)}"
191+
logger.info(message).as(false)
162192
}
193+
}
194+
}
163195
}
164196

165197
object PruningAlg {

modules/core/src/test/scala/org/scalasteward/core/TestInstances.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,12 @@ object TestInstances {
108108
implicit val pullRequestFrequencyArbitrary: Arbitrary[PullRequestFrequency] =
109109
Arbitrary(Arbitrary.arbitrary[FiniteDuration].flatMap(fd => Gen.oneOf(Asap, Timespan(fd))))
110110

111+
implicit val groupRepoConfigArbitrary: Arbitrary[GroupRepoConfig] =
112+
Arbitrary(for {
113+
pullRequestsConfig <- Arbitrary.arbitrary[PullRequestsConfig]
114+
pattern <- Arbitrary.arbitrary[UpdatePattern]
115+
} yield GroupRepoConfig(pullRequestsConfig, pattern))
116+
111117
implicit val pullRequestsConfigArbitrary: Arbitrary[PullRequestsConfig] =
112118
Arbitrary(for {
113119
frequency <- Arbitrary.arbitrary[Option[PullRequestFrequency]]

modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import cats.syntax.all._
55
import eu.timepit.refined.types.numeric.NonNegInt
66
import munit.FunSuite
77
import org.scalasteward.core.TestSyntax._
8+
import org.scalasteward.core.data.GroupId
89
import org.scalasteward.core.mock.MockContext.context.repoConfigAlg
910
import org.scalasteward.core.mock.MockState.TraceEntry.Log
1011
import org.scalasteward.core.mock.{MockConfig, MockState}
@@ -39,6 +40,11 @@ class RepoConfigAlgTest extends FunSuite {
3940
|updates.limit = 4
4041
|updates.fileExtensions = [ ".txt" ]
4142
|pullRequests.frequency = "@weekly"
43+
|dependencyOverrides = [
44+
| { pullRequests.frequency = "@daily", dependency = { groupId = "eu.timepit" } },
45+
| { pullRequests.frequency = "@monthly", dependency = { groupId = "eu.timepit", artifactId = "refined.1" } },
46+
| { pullRequests.frequency = "@weekly", dependency = { groupId = "eu.timepit", artifactId = "refined.1", version = { prefix="1." } } },
47+
|]
4248
|commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
4349
|buildRoots = [ ".", "subfolder/subfolder" ]
4450
|""".stripMargin
@@ -74,7 +80,31 @@ class RepoConfigAlgTest extends FunSuite {
7480
commits = CommitsConfig(
7581
message = Some("Update ${artifactName} from ${currentVersion} to ${nextVersion}")
7682
),
77-
buildRoots = Some(List(BuildRootConfig.repoRoot, BuildRootConfig("subfolder/subfolder")))
83+
buildRoots = Some(List(BuildRootConfig.repoRoot, BuildRootConfig("subfolder/subfolder"))),
84+
dependencyOverrides = List(
85+
GroupRepoConfig(
86+
dependency = UpdatePattern(GroupId("eu.timepit"), None, None),
87+
pullRequests = PullRequestsConfig(
88+
frequency = Some(PullRequestFrequency.Timespan(1.day))
89+
)
90+
),
91+
GroupRepoConfig(
92+
dependency = UpdatePattern(GroupId("eu.timepit"), Some("refined.1"), None),
93+
pullRequests = PullRequestsConfig(
94+
frequency = Some(PullRequestFrequency.Timespan(30.days))
95+
)
96+
),
97+
GroupRepoConfig(
98+
dependency = UpdatePattern(
99+
GroupId("eu.timepit"),
100+
Some("refined.1"),
101+
Some(VersionPattern(prefix = Some("1.")))
102+
),
103+
pullRequests = PullRequestsConfig(
104+
frequency = Some(PullRequestFrequency.Timespan(7.days))
105+
)
106+
)
107+
)
78108
)
79109
assertEquals(config, Right(expected))
80110
}

0 commit comments

Comments
 (0)