Skip to content

Commit f90f842

Browse files
Merge pull request #2714 from alejandrohdezma/feature/grouping
Add support for grouping dependency updates
2 parents a80b77c + 095af21 commit f90f842

26 files changed

+1659
-346
lines changed

build.sbt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import scala.util.Properties
2+
import scala.reflect.io.Path
13
import com.typesafe.sbt.packager.docker._
24
import sbtcrossproject.{CrossProject, CrossType, Platform}
35
import sbtghactions.JavaSpec.Distribution.Adopt
@@ -196,6 +198,8 @@ lazy val core = myCrossProject("core")
196198
}.taskValue,
197199
run / fork := true,
198200
Test / fork := true,
201+
Test / testOptions +=
202+
Tests.Cleanup(() => Path(file(Properties.tmpDir) / "scala-steward").deleteRecursively()),
199203
Compile / unmanagedResourceDirectories ++= (`sbt-plugin`.jvm / Compile / unmanagedSourceDirectories).value
200204
)
201205

docs/repo-specific-configuration.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,43 @@ You can add `<YOUR_REPO>/.scala-steward.conf` to configure how Scala Steward upd
3232
#pullRequests.frequency = "0 0 ? * 3" # every thursday on midnight
3333
pullRequests.frequency = "7 days"
3434

35+
# pullRequests.grouping allows you to specify how Scala Steward should group
36+
# your updates in order to reduce the number of pull-requests.
37+
#
38+
# Updates will be placed in the first group with which they match, starting
39+
# from the first in the array. Those that do not match any group will follow
40+
# the default procedure (one PR per update).
41+
#
42+
# Each element in the array will have the following schema:
43+
#
44+
# - name (mandatory): the name of the group, will be used for things like naming the branch
45+
# - title (optional): if provided it will be used as the title for the PR
46+
# - filter (mandatory): a non-empty list containing the filters to use to know
47+
# if an update falls into this group.
48+
#
49+
# `filter` properties would have this format:
50+
#
51+
# {
52+
# version = "major" | "minor" | "patch" | "pre-release" | "build-metadata",
53+
# group = "{group}",
54+
# artifact = "{artifact}"
55+
# }
56+
#
57+
# For more information on the values for the `version` filter visit https://semver.org/
58+
#
59+
# Every field in a `filter` is optional but at least one must be provided.
60+
#
61+
# For grouping every update togeher a filter like {group = "*"} can be # provided.
62+
#
63+
# Default: []
64+
pullRequests.grouping = [
65+
{ name = "patches", "title" = "Patch updates", "filter" = [{"version" = "patch"}] },
66+
{ name = "minor_major", "title" = "Minor/major updates", "filter" = [{"version" = "minor"}, {"version" = "major"}] },
67+
{ name = "typelevel", "title" = "Typelevel updates", "filter" = [{"group" = "org.typelevel"}, {"group" = "org.http4s"}] },
68+
{ name = "my_libraries", "filter" = [{"artifact" = "my-library"}, {"artifact" = "my-other-library", "group" = "my-org"}] },
69+
{ name = "all", title = "Dependency updates", "filter" = [{"group" = "*"}] }
70+
]
71+
3572
# pullRequests.includeMatchedLabels allows to control which labels are added to PRs
3673
# via a regex check each label is checked against.
3774
# Defaults to no regex (all labels are added) which is equivalent to ".*".

modules/core/src/main/scala/org/scalasteward/core/data/SemVer.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.scalasteward.core.data
1818

1919
import cats.syntax.all._
20+
import io.circe.{Codec, Decoder, Encoder}
2021
import org.scalasteward.core.data.SemVer.Change._
2122

2223
import scala.annotation.tailrec
@@ -42,6 +43,22 @@ object SemVer {
4243
case object Patch extends Change("patch")
4344
case object PreRelease extends Change("pre-release")
4445
case object BuildMetadata extends Change("build-metadata")
46+
47+
final private val allowed = List(Major, Minor, Patch, PreRelease, BuildMetadata)
48+
.map(change => s"`${change.render}`")
49+
.mkString(", ")
50+
51+
implicit val ChangeCodec: Codec[Change] =
52+
Codec.from(Decoder[String].emap(from), Encoder[String].contramap(_.render))
53+
54+
def from(string: String): Either[String, Change] = string match {
55+
case Major.`render` => Right(Major)
56+
case Minor.`render` => Right(Minor)
57+
case Patch.`render` => Right(Patch)
58+
case PreRelease.`render` => Right(PreRelease)
59+
case BuildMetadata.`render` => Right(BuildMetadata)
60+
case other => Left(s"Invalid value for version change: $other. Allowed values are: $allowed")
61+
}
4562
}
4663

4764
def getChangeSpec(from: SemVer, to: SemVer): Option[Change] =

modules/core/src/main/scala/org/scalasteward/core/data/Update.scala

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,73 @@ package org.scalasteward.core.data
1919
import cats.Order
2020
import cats.implicits._
2121
import io.circe.Codec
22+
import io.circe.syntax._
2223
import io.circe.generic.semiauto._
23-
import org.scalasteward.core.data.Update.{Group, Single}
24+
import org.scalasteward.core.data.Update.Group
25+
import org.scalasteward.core.data.Update.Single
26+
import org.scalasteward.core.repoconfig.PullRequestGroup
2427
import org.scalasteward.core.util
2528
import org.scalasteward.core.util.Nel
2629
import org.scalasteward.core.util.string.MinLengthString
30+
import io.circe.Decoder
2731

28-
sealed trait Update extends Product with Serializable {
32+
sealed trait AnUpdate {
33+
34+
def on[A](update: Update => A, grouped: GroupedUpdate => A): A = this match {
35+
case g: GroupedUpdate => grouped(g)
36+
case u: Update => update(u)
37+
}
38+
39+
def show: String
40+
41+
}
42+
43+
object AnUpdate {
44+
45+
implicit val AnUpdateCodec: Codec[AnUpdate] = Codec.from(
46+
Decoder[GroupedUpdate].widen[AnUpdate].or(Decoder[Update].widen[AnUpdate]),
47+
_.on(_.asJson, _.asJson)
48+
)
49+
50+
}
51+
52+
final case class GroupedUpdate(name: String, title: Option[String], updates: List[Update])
53+
extends AnUpdate {
54+
55+
override def show: String = name
56+
57+
}
58+
59+
object GroupedUpdate {
60+
61+
/**
62+
* Processes the provided updates using the group configuration. Each update will only be present in the
63+
* first group it falls into.
64+
*
65+
* Updates that do not fall into any group will be returned back in the second return parameter.
66+
*/
67+
/**
68+
* Processes the provided updates using the group configuration. Each update will only be present in the
69+
* first group it falls into.
70+
*
71+
* Updates that do not fall into any group will be returned back in the second return parameter.
72+
*/
73+
def from(
74+
groups: List[PullRequestGroup],
75+
updates: List[Update.Single]
76+
): (List[GroupedUpdate], List[Update.Single]) =
77+
groups.foldLeft((List.empty[GroupedUpdate], updates)) { case ((grouped, notGrouped), group) =>
78+
notGrouped.partition(group.matches) match {
79+
case (Nil, rest) => (grouped, rest)
80+
case (matched, rest) => (grouped :+ GroupedUpdate(group.name, group.title, matched), rest)
81+
}
82+
}
83+
84+
implicit val GroupedUpdateCodec: Codec[GroupedUpdate] = deriveCodec
85+
86+
}
87+
88+
sealed trait Update extends Product with Serializable with AnUpdate {
2989
def crossDependencies: Nel[CrossDependency]
3090
def dependencies: Nel[Dependency]
3191
def groupId: GroupId
@@ -40,7 +100,7 @@ sealed trait Update extends Product with Serializable {
40100
final def nextVersion: Version =
41101
newerVersions.head
42102

43-
final def show: String = {
103+
final override def show: String = {
44104
val artifacts = this match {
45105
case s: Single => s.crossDependency.showArtifactNames
46106
case g: Group => g.crossDependencies.map(_.showArtifactNames).mkString_("{", ", ", "}")

modules/core/src/main/scala/org/scalasteward/core/data/UpdateData.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import org.scalasteward.core.vcs.data.Repo
2323
final case class UpdateData(
2424
repoData: RepoData,
2525
fork: Repo,
26-
update: Update,
26+
update: AnUpdate,
2727
baseBranch: Branch,
2828
baseSha1: Sha1,
2929
updateBranch: Branch

modules/core/src/main/scala/org/scalasteward/core/git/CommitMsg.scala

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package org.scalasteward.core.git
1818

1919
import cats.syntax.all._
20-
import org.scalasteward.core.data.Update
20+
import org.scalasteward.core.data.AnUpdate
2121
import org.scalasteward.core.update.show
2222
import org.scalasteward.core.util.Nel
2323

@@ -39,17 +39,25 @@ final case class CommitMsg(
3939
}
4040

4141
object CommitMsg {
42-
def replaceVariables(s: String)(update: Update, baseBranch: Option[Branch]): CommitMsg = {
43-
val artifactNameValue = show.oneLiner(update)
44-
val nextVersionValue = update.nextVersion.value
45-
val defaultValue = s"Update $artifactNameValue to $nextVersionValue" +
46-
baseBranch.fold("")(branch => s" in ${branch.name}")
47-
val title = s
48-
.replace("${default}", defaultValue)
49-
.replace("${artifactName}", artifactNameValue)
50-
.replace("${currentVersion}", update.currentVersion.value)
51-
.replace("${nextVersion}", nextVersionValue)
52-
.replace("${branchName}", baseBranch.map(_.name).orEmpty)
53-
CommitMsg(title = title)
54-
}
42+
def replaceVariables(s: String)(update: AnUpdate, baseBranch: Option[Branch]): CommitMsg =
43+
update.on(
44+
u => {
45+
val artifactNameValue = show.oneLiner(u)
46+
val nextVersionValue = u.nextVersion.value
47+
val defaultValue = s"Update $artifactNameValue to $nextVersionValue" +
48+
baseBranch.fold("")(branch => s" in ${branch.name}")
49+
val title = s
50+
.replace("${default}", defaultValue)
51+
.replace("${artifactName}", artifactNameValue)
52+
.replace("${currentVersion}", u.currentVersion.value)
53+
.replace("${nextVersion}", nextVersionValue)
54+
.replace("${branchName}", baseBranch.map(_.name).orEmpty)
55+
CommitMsg(title = title)
56+
},
57+
g =>
58+
CommitMsg(title =
59+
g.title.getOrElse(s"Update for group ${g.name}") +
60+
baseBranch.fold("")(branch => s" in ${branch.name}")
61+
)
62+
)
5563
}

modules/core/src/main/scala/org/scalasteward/core/git/package.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package org.scalasteward.core
1818

19-
import org.scalasteward.core.data.Update
19+
import org.scalasteward.core.data.{AnUpdate, GroupedUpdate, Update}
2020
import org.scalasteward.core.repoconfig.CommitsConfig
2121
import org.scalasteward.core.vcs.data.Repo
2222

@@ -27,9 +27,12 @@ package object git {
2727

2828
val updateBranchPrefix = "update"
2929

30-
def branchFor(update: Update, baseBranch: Option[Branch]): Branch = {
30+
def branchFor(update: AnUpdate, baseBranch: Option[Branch]): Branch = {
3131
val base = baseBranch.fold("")(branch => s"${branch.name}/")
32-
Branch(s"$updateBranchPrefix/$base${update.name}-${update.nextVersion}")
32+
update match {
33+
case g: GroupedUpdate => Branch(s"$updateBranchPrefix/$base${g.name}")
34+
case u: Update => Branch(s"$updateBranchPrefix/$base${u.name}-${u.nextVersion}")
35+
}
3336
}
3437

3538
def commitMsgFor(

0 commit comments

Comments
 (0)