Skip to content

Commit dce40bb

Browse files
authored
Merge pull request #3488 from scala-steward-org/retraction
2 parents 57c98ab + 65dfdc8 commit dce40bb

File tree

10 files changed

+229
-6
lines changed

10 files changed

+229
-6
lines changed

docs/repo-specific-configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,19 @@ updates.pin = [ { groupId = "com.example", artifactId="foo", version = "1.1." }
104104
# Defaults to empty `[]` which mean Scala Steward will not ignore dependencies.
105105
updates.ignore = [ { groupId = "org.acme", artifactId="foo", version = "1.0" } ]
106106

107+
# The dependencies which match the given pattern are retracted. Their existing pull-request will be closed.
108+
#
109+
# Each entry must have a `reason`, a `doc` URL and a list of dependency patterns.
110+
updates.retracted = [
111+
{
112+
reason = "Ignore version 3.6.0 as it is abandoned due to broken compatibility",
113+
doc = "https://contributors.scala-lang.org/t/broken-scala-3-6-0-release/6792",
114+
artifacts = [
115+
{ groupId = "org.scala-lang", artifactId = "scala3-compiler", version = { exact = "3.6.0" } }
116+
]
117+
}
118+
]
119+
107120
# The dependencies which match the given patterns are allowed to be updated to pre-release from stable.
108121
# This also implies, that it will be allowed for snapshot versions to be updated to snapshots of different series.
109122
#

modules/core/src/main/resources/default.scala-steward.conf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,23 @@ updates.ignore = [
278278
{ groupId = "org.tpolecat", artifactId="doobie-hikari", version="1.0.0-RC6" },
279279
{ groupId = "org.tpolecat", artifactId="doobie-postgres-circe", version="1.0.0-RC6" },
280280
]
281+
282+
updates.retracted = [
283+
{
284+
reason = "Ignore version 3.6.0 as it is abandoned due to broken compatibility",
285+
doc = "https://contributors.scala-lang.org/t/broken-scala-3-6-0-release/6792",
286+
artifacts = [
287+
{ groupId = "org.scala-lang", artifactId = "scala3-compiler", version = { exact = "3.6.0" } },
288+
{ groupId = "org.scala-lang", artifactId = "scala3-library", version = { exact = "3.6.0" } },
289+
{ groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = { exact = "3.6.0" } },
290+
{ groupId = "org.scala-lang", artifactId = "tasty-core", version = { exact = "3.6.0" } },
291+
{ groupId = "org.scala-lang", artifactId = "scala2-library-cc-tasty-experimental", version = { exact = "3.6.0" } },
292+
{ groupId = "org.scala-lang", artifactId = "scala2-library-tasty-experimental", version = { exact = "3.6.0" } },
293+
{ groupId = "org.scala-lang", artifactId = "scala3-language-server", version = { exact = "3.6.0" } },
294+
{ groupId = "org.scala-lang", artifactId = "scala3-presentation-compiler", version = { exact = "3.6.0" } },
295+
{ groupId = "org.scala-lang", artifactId = "scala3-staging", version = { exact = "3.6.0" } },
296+
{ groupId = "org.scala-lang", artifactId = "scala3-tasty-inspector", version = { exact = "3.6.0" } },
297+
{ groupId = "org.scala-lang", artifactId = "scaladoc", version = { exact = "3.6.0" } },
298+
]
299+
}
300+
]

modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,15 @@ final class StewardAlg[F[_]](config: Config)(implicit
5050
logger.infoTotalTime(label) {
5151
logger.attemptError.label(util.string.lineLeftRight(label), Some(label)) {
5252
F.guarantee(
53-
repoCacheAlg.checkCache(repo).flatMap { case (data, fork) =>
54-
pruningAlg.needsAttention(data).flatMap {
55-
_.traverse_(states => nurtureAlg.nurture(data, fork, states.map(_.update)))
56-
}
57-
},
53+
for {
54+
dataAndFork <- repoCacheAlg.checkCache(repo)
55+
(data, fork) = dataAndFork
56+
_ <- nurtureAlg.closeRetractedPullRequests(data)
57+
statesO <- pruningAlg.needsAttention(data)
58+
result <- statesO.traverse_(states =>
59+
nurtureAlg.nurture(data, fork, states.map(_.update))
60+
)
61+
} yield result,
5862
gitAlg.removeClone(repo)
5963
)
6064
}

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import org.scalasteward.core.forge.data.NewPullRequestData.{filterLabels, labels
2828
import org.scalasteward.core.forge.data._
2929
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeRepoAlg}
3030
import org.scalasteward.core.git.{Branch, Commit, GitAlg}
31-
import org.scalasteward.core.repoconfig.PullRequestUpdateStrategy
31+
import org.scalasteward.core.repoconfig.{PullRequestUpdateStrategy, RetractedArtifact}
3232
import org.scalasteward.core.util.logger.LoggerOps
3333
import org.scalasteward.core.util.{Nel, UrlChecker}
3434
import org.scalasteward.core.{git, util}
@@ -306,4 +306,30 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
306306
requestData <- preparePullRequest(data, edits)
307307
_ <- forgeApiAlg.updatePullRequest(number: PullRequestNumber, data.repo, requestData)
308308
} yield result
309+
310+
def closeRetractedPullRequests(data: RepoData): F[Unit] =
311+
pullRequestRepository
312+
.getRetractedPullRequests(data.repo, data.config.updates.retracted)
313+
.flatMap {
314+
_.traverse_ { case (oldPr, retractedArtifact) =>
315+
closeRetractedPullRequest(data, oldPr, retractedArtifact)
316+
}
317+
}
318+
319+
private def closeRetractedPullRequest(
320+
data: RepoData,
321+
oldPr: PullRequestData[Id],
322+
retractedArtifact: RetractedArtifact
323+
): F[Unit] =
324+
logger.attemptWarn.label_(
325+
s"Closing retracted PR ${oldPr.url.renderString} for ${oldPr.update.show} because of '${retractedArtifact.reason}'"
326+
) {
327+
for {
328+
_ <- pullRequestRepository.changeState(data.repo, oldPr.url, PullRequestState.Closed)
329+
comment = retractedArtifact.retractionMsg
330+
_ <- forgeApiAlg.commentPullRequest(data.repo, oldPr.number, comment)
331+
_ <- forgeApiAlg.closePullRequest(data.repo, oldPr.number)
332+
_ <- deleteRemoteBranch(data.repo, oldPr.updateBranch)
333+
} yield F.unit
334+
}
309335
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import org.scalasteward.core.git
2727
import org.scalasteward.core.git.{Branch, Sha1}
2828
import org.scalasteward.core.nurture.PullRequestRepository.Entry
2929
import org.scalasteward.core.persistence.KeyValueStore
30+
import org.scalasteward.core.repoconfig.RetractedArtifact
3031
import org.scalasteward.core.update.UpdateAlg
3132
import org.scalasteward.core.util.{DateTimeAlg, Timestamp}
3233

@@ -80,6 +81,29 @@ final class PullRequestRepository[F[_]](kvStore: KeyValueStore[F, Repo, Map[Uri,
8081
}.flatten.toList.sortBy(_.number.value)
8182
}
8283

84+
def getRetractedPullRequests(
85+
repo: Repo,
86+
allRetractedArtifacts: List[RetractedArtifact]
87+
): F[List[(PullRequestData[Id], RetractedArtifact)]] =
88+
kvStore.getOrElse(repo, Map.empty).map { pullRequets =>
89+
pullRequets.flatMap {
90+
case (
91+
url,
92+
Entry(baseSha1, u: Update.Single, PullRequestState.Open, _, number, updateBranch)
93+
) =>
94+
for {
95+
prNumber <- number
96+
retractedArtifact <- allRetractedArtifacts.find(_.isRetracted(u))
97+
} yield {
98+
val branch = updateBranch.getOrElse(git.branchFor(u, repo.branch))
99+
val data =
100+
PullRequestData[Id](url, baseSha1, u, PullRequestState.Open, prNumber, branch)
101+
(data, retractedArtifact)
102+
}
103+
case _ => Map.empty
104+
}.toList
105+
}
106+
83107
def findLatestPullRequest(
84108
repo: Repo,
85109
crossDependency: CrossDependency,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2018-2023 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.semiauto.deriveCodec
21+
import org.scalasteward.core.data.Update
22+
23+
final case class RetractedArtifact(
24+
reason: String,
25+
doc: String,
26+
artifacts: List[UpdatePattern] = List.empty
27+
) {
28+
def isRetracted(updateSingle: Update.Single): Boolean =
29+
updateSingle.forArtifactIds.exists { updateForArtifactId =>
30+
UpdatePattern
31+
.findMatch(artifacts, updateForArtifactId, include = true)
32+
.filteredVersions
33+
.nonEmpty
34+
}
35+
36+
def retractionMsg: String =
37+
s"""|PR retracted because of: ${reason}.
38+
|
39+
|Documentation: ${doc}
40+
|""".stripMargin
41+
}
42+
43+
object RetractedArtifact {
44+
implicit val retractedPatternCodec: Codec[RetractedArtifact] =
45+
deriveCodec
46+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ final case class UpdatesConfig(
4242
allow: List[UpdatePattern] = List.empty,
4343
allowPreReleases: List[UpdatePattern] = List.empty,
4444
ignore: List[UpdatePattern] = List.empty,
45+
retracted: List[RetractedArtifact] = List.empty,
4546
limit: Option[NonNegInt] = defaultLimit,
4647
fileExtensions: Option[List[String]] = None
4748
) {
@@ -124,6 +125,7 @@ object UpdatesConfig {
124125
allow = mergeAllow(x.allow, y.allow),
125126
allowPreReleases = mergeAllow(x.allowPreReleases, y.allowPreReleases),
126127
ignore = mergeIgnore(x.ignore, y.ignore),
128+
retracted = x.retracted ::: y.retracted,
127129
limit = x.limit.orElse(y.limit),
128130
fileExtensions = mergeFileExtensions(x.fileExtensions, y.fileExtensions)
129131
)

modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import org.scalasteward.core.mock.MockContext.context.pullRequestRepository
1414
import org.scalasteward.core.mock.MockState.TraceEntry
1515
import org.scalasteward.core.mock.MockState.TraceEntry.Cmd
1616
import org.scalasteward.core.mock.{MockEff, MockState}
17+
import org.scalasteward.core.repoconfig.{RetractedArtifact, UpdatePattern, VersionPattern}
1718
import org.scalasteward.core.util.Nel
1819

1920
import java.util.concurrent.atomic.AtomicInteger
@@ -122,6 +123,51 @@ class PullRequestRepositoryTest extends FunSuite {
122123
assertEquals(result, List.empty)
123124
}
124125

126+
test("getRetractedPullRequests with no retractions defined") {
127+
val (_, obtained) = beforeAndAfterPRCreation(portableScala) { repo =>
128+
pullRequestRepository.getRetractedPullRequests(repo, List.empty)
129+
}
130+
assertEquals(obtained, List.empty[(PullRequestData[Id], RetractedArtifact)])
131+
}
132+
133+
test("getRetractedPullRequests with retractions") {
134+
val retractedPortableScala = RetractedArtifact(
135+
"a reason",
136+
"doc URI",
137+
List(
138+
UpdatePattern(
139+
"org.portable-scala".g,
140+
Some("sbt-scalajs-crossproject"),
141+
Some(VersionPattern(exact = Some("1.0.0")))
142+
)
143+
)
144+
)
145+
val (_, obtained) = beforeAndAfterPRCreation(portableScala) { repo =>
146+
pullRequestRepository.getRetractedPullRequests(repo, List(retractedPortableScala))
147+
}
148+
assertEquals(obtained.size, 1)
149+
assertEquals(obtained.head._1.update, portableScala)
150+
assertEquals(obtained.head._2, retractedPortableScala)
151+
}
152+
153+
test("getRetractedPullRequests with retractions for different version") {
154+
val retractedPortableScala = RetractedArtifact(
155+
"a reason",
156+
"doc URI",
157+
List(
158+
UpdatePattern(
159+
"org.portable-scala".g,
160+
Some("sbt-scalajs-crossproject"),
161+
Some(VersionPattern(exact = Some("2.0.0")))
162+
)
163+
)
164+
)
165+
val (_, obtained) = beforeAndAfterPRCreation(portableScala) { repo =>
166+
pullRequestRepository.getRetractedPullRequests(repo, List(retractedPortableScala))
167+
}
168+
assertEquals(obtained, List.empty[(PullRequestData[Id], RetractedArtifact)])
169+
}
170+
125171
test("findLatestPullRequest ignores grouped updates") {
126172
val (_, result) = beforeAndAfterPRCreation(groupedUpdate(portableScala)) { repo =>
127173
pullRequestRepository.findLatestPullRequest(repo, portableScala.crossDependency, "1.0.0".v)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.scalasteward.core.repoconfig
2+
3+
import org.scalasteward.core.TestSyntax._
4+
5+
import munit.FunSuite
6+
7+
class RetractedArtifactTest extends FunSuite {
8+
private val retractedArtifact = RetractedArtifact(
9+
"a reason",
10+
"doc URI",
11+
List(
12+
UpdatePattern(
13+
"org.portable-scala".g,
14+
Some("sbt-scalajs-crossproject"),
15+
Some(VersionPattern(exact = Some("1.0.0")))
16+
)
17+
)
18+
)
19+
20+
test("isRetracted") {
21+
val update = ("org.portable-scala".g % "sbt-scalajs-crossproject".a % "0.9.2" %> "1.0.0").single
22+
assert(retractedArtifact.isRetracted(update))
23+
}
24+
25+
test("not isRetracted") {
26+
val update = ("org.portable-scala".g % "sbt-scalajs-crossproject".a % "0.9.2" %> "0.9.3").single
27+
assert(!retractedArtifact.isRetracted(update))
28+
}
29+
}

modules/docs/mdoc/repo-specific-configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ updates.pin = [ { groupId = "com.example", artifactId="foo", version = "1.1." }
110110
# Defaults to empty `[]` which mean Scala Steward will not ignore dependencies.
111111
updates.ignore = [ { groupId = "org.acme", artifactId="foo", version = "1.0" } ]
112112

113+
# The dependencies which match the given pattern are retracted. Their existing pull-request will be closed.
114+
#
115+
# Each entry must have a `reason`, a `doc` URL and a list of dependency patterns.
116+
updates.retracted = [
117+
{
118+
reason = "Ignore version 3.6.0 as it is abandoned due to broken compatibility",
119+
doc = "https://contributors.scala-lang.org/t/broken-scala-3-6-0-release/6792",
120+
artifacts = [
121+
{ groupId = "org.scala-lang", artifactId = "scala3-compiler", version = { exact = "3.6.0" } }
122+
]
123+
}
124+
]
125+
113126
# The dependencies which match the given patterns are allowed to be updated to pre-release from stable.
114127
# This also implies, that it will be allowed for snapshot versions to be updated to snapshots of different series.
115128
#

0 commit comments

Comments
 (0)