Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retract previously opened Pull Request #3488

Merged
merged 2 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/repo-specific-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ updates.pin = [ { groupId = "com.example", artifactId="foo", version = "1.1." }
# Defaults to empty `[]` which mean Scala Steward will not ignore dependencies.
updates.ignore = [ { groupId = "org.acme", artifactId="foo", version = "1.0" } ]

# The dependencies which match the given pattern are retracted. Their existing pull-request will be closed.
#
# Each entry must have a `reason`, a `doc` URL and a list of dependency patterns.
updates.retracted = [
{
reason = "Ignore version 3.6.0 as it is abandoned due to broken compatibility",
doc = "https://contributors.scala-lang.org/t/broken-scala-3-6-0-release/6792",
artifacts = [
{ groupId = "org.scala-lang", artifactId = "scala3-compiler", version = { exact = "3.6.0" } }
]
}
]

# The dependencies which match the given patterns are allowed to be updated to pre-release from stable.
# This also implies, that it will be allowed for snapshot versions to be updated to snapshots of different series.
#
Expand Down
20 changes: 20 additions & 0 deletions modules/core/src/main/resources/default.scala-steward.conf
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,23 @@ updates.ignore = [
{ groupId = "org.tpolecat", artifactId="doobie-hikari", version="1.0.0-RC6" },
{ groupId = "org.tpolecat", artifactId="doobie-postgres-circe", version="1.0.0-RC6" },
]

updates.retracted = [
{
reason = "Ignore version 3.6.0 as it is abandoned due to broken compatibility",
doc = "https://contributors.scala-lang.org/t/broken-scala-3-6-0-release/6792",
artifacts = [
{ groupId = "org.scala-lang", artifactId = "scala3-compiler", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala3-library", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala3-library_sjs1", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "tasty-core", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala2-library-cc-tasty-experimental", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala2-library-tasty-experimental", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala3-language-server", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala3-presentation-compiler", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala3-staging", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scala3-tasty-inspector", version = { exact = "3.6.0" } },
{ groupId = "org.scala-lang", artifactId = "scaladoc", version = { exact = "3.6.0" } },
]
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ final class StewardAlg[F[_]](config: Config)(implicit
logger.infoTotalTime(label) {
logger.attemptError.label(util.string.lineLeftRight(label), Some(label)) {
F.guarantee(
repoCacheAlg.checkCache(repo).flatMap { case (data, fork) =>
pruningAlg.needsAttention(data).flatMap {
_.traverse_(states => nurtureAlg.nurture(data, fork, states.map(_.update)))
}
},
for {
dataAndFork <- repoCacheAlg.checkCache(repo)
(data, fork) = dataAndFork
_ <- nurtureAlg.closeRetractedPullRequests(data)
statesO <- pruningAlg.needsAttention(data)
result <- statesO.traverse_(states =>
nurtureAlg.nurture(data, fork, states.map(_.update))
)
} yield result,
gitAlg.removeClone(repo)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import org.scalasteward.core.forge.data.NewPullRequestData.{filterLabels, labels
import org.scalasteward.core.forge.data._
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeRepoAlg}
import org.scalasteward.core.git.{Branch, Commit, GitAlg}
import org.scalasteward.core.repoconfig.PullRequestUpdateStrategy
import org.scalasteward.core.repoconfig.{PullRequestUpdateStrategy, RetractedArtifact}
import org.scalasteward.core.util.logger.LoggerOps
import org.scalasteward.core.util.{Nel, UrlChecker}
import org.scalasteward.core.{git, util}
Expand Down Expand Up @@ -306,4 +306,30 @@ final class NurtureAlg[F[_]](config: ForgeCfg)(implicit
requestData <- preparePullRequest(data, edits)
_ <- forgeApiAlg.updatePullRequest(number: PullRequestNumber, data.repo, requestData)
} yield result

def closeRetractedPullRequests(data: RepoData): F[Unit] =
pullRequestRepository
.getRetractedPullRequests(data.repo, data.config.updates.retracted)
.flatMap {
_.traverse_ { case (oldPr, retractedArtifact) =>
closeRetractedPullRequest(data, oldPr, retractedArtifact)
}
}

private def closeRetractedPullRequest(
data: RepoData,
oldPr: PullRequestData[Id],
retractedArtifact: RetractedArtifact
): F[Unit] =
logger.attemptWarn.label_(
s"Closing retracted PR ${oldPr.url.renderString} for ${oldPr.update.show} because of '${retractedArtifact.reason}'"
) {
for {
_ <- pullRequestRepository.changeState(data.repo, oldPr.url, PullRequestState.Closed)
comment = retractedArtifact.retractionMsg
_ <- forgeApiAlg.commentPullRequest(data.repo, oldPr.number, comment)
_ <- forgeApiAlg.closePullRequest(data.repo, oldPr.number)
_ <- deleteRemoteBranch(data.repo, oldPr.updateBranch)
} yield F.unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.scalasteward.core.git
import org.scalasteward.core.git.{Branch, Sha1}
import org.scalasteward.core.nurture.PullRequestRepository.Entry
import org.scalasteward.core.persistence.KeyValueStore
import org.scalasteward.core.repoconfig.RetractedArtifact
import org.scalasteward.core.update.UpdateAlg
import org.scalasteward.core.util.{DateTimeAlg, Timestamp}

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

def getRetractedPullRequests(
repo: Repo,
allRetractedArtifacts: List[RetractedArtifact]
): F[List[(PullRequestData[Id], RetractedArtifact)]] =
kvStore.getOrElse(repo, Map.empty).map { pullRequets =>
pullRequets.flatMap {
case (
url,
Entry(baseSha1, u: Update.Single, PullRequestState.Open, _, number, updateBranch)
) =>
for {
prNumber <- number
retractedArtifact <- allRetractedArtifacts.find(_.isRetracted(u))
} yield {
val branch = updateBranch.getOrElse(git.branchFor(u, repo.branch))
val data =
PullRequestData[Id](url, baseSha1, u, PullRequestState.Open, prNumber, branch)
(data, retractedArtifact)
}
case _ => Map.empty
}.toList
}

def findLatestPullRequest(
repo: Repo,
crossDependency: CrossDependency,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2018-2023 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
import io.circe.generic.semiauto.deriveCodec
import org.scalasteward.core.data.Update

final case class RetractedArtifact(
reason: String,
doc: String,
artifacts: List[UpdatePattern] = List.empty
) {
def isRetracted(updateSingle: Update.Single): Boolean =
updateSingle.forArtifactIds.exists { updateForArtifactId =>
UpdatePattern
.findMatch(artifacts, updateForArtifactId, include = true)
.filteredVersions
.nonEmpty
}

def retractionMsg: String =
s"""|PR retracted because of: ${reason}.
|
|Documentation: ${doc}
|""".stripMargin
}

object RetractedArtifact {
implicit val retractedPatternCodec: Codec[RetractedArtifact] =
deriveCodec
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final case class UpdatesConfig(
allow: List[UpdatePattern] = List.empty,
allowPreReleases: List[UpdatePattern] = List.empty,
ignore: List[UpdatePattern] = List.empty,
retracted: List[RetractedArtifact] = List.empty,
limit: Option[NonNegInt] = defaultLimit,
fileExtensions: Option[List[String]] = None
) {
Expand Down Expand Up @@ -124,6 +125,7 @@ object UpdatesConfig {
allow = mergeAllow(x.allow, y.allow),
allowPreReleases = mergeAllow(x.allowPreReleases, y.allowPreReleases),
ignore = mergeIgnore(x.ignore, y.ignore),
retracted = x.retracted ::: y.retracted,
limit = x.limit.orElse(y.limit),
fileExtensions = mergeFileExtensions(x.fileExtensions, y.fileExtensions)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.scalasteward.core.mock.MockContext.context.pullRequestRepository
import org.scalasteward.core.mock.MockState.TraceEntry
import org.scalasteward.core.mock.MockState.TraceEntry.Cmd
import org.scalasteward.core.mock.{MockEff, MockState}
import org.scalasteward.core.repoconfig.{RetractedArtifact, UpdatePattern, VersionPattern}
import org.scalasteward.core.util.Nel

import java.util.concurrent.atomic.AtomicInteger
Expand Down Expand Up @@ -122,6 +123,51 @@ class PullRequestRepositoryTest extends FunSuite {
assertEquals(result, List.empty)
}

test("getRetractedPullRequests with no retractions defined") {
val (_, obtained) = beforeAndAfterPRCreation(portableScala) { repo =>
pullRequestRepository.getRetractedPullRequests(repo, List.empty)
}
assertEquals(obtained, List.empty[(PullRequestData[Id], RetractedArtifact)])
}

test("getRetractedPullRequests with retractions") {
val retractedPortableScala = RetractedArtifact(
"a reason",
"doc URI",
List(
UpdatePattern(
"org.portable-scala".g,
Some("sbt-scalajs-crossproject"),
Some(VersionPattern(exact = Some("1.0.0")))
)
)
)
val (_, obtained) = beforeAndAfterPRCreation(portableScala) { repo =>
pullRequestRepository.getRetractedPullRequests(repo, List(retractedPortableScala))
}
assertEquals(obtained.size, 1)
assertEquals(obtained.head._1.update, portableScala)
assertEquals(obtained.head._2, retractedPortableScala)
}

test("getRetractedPullRequests with retractions for different version") {
val retractedPortableScala = RetractedArtifact(
"a reason",
"doc URI",
List(
UpdatePattern(
"org.portable-scala".g,
Some("sbt-scalajs-crossproject"),
Some(VersionPattern(exact = Some("2.0.0")))
)
)
)
val (_, obtained) = beforeAndAfterPRCreation(portableScala) { repo =>
pullRequestRepository.getRetractedPullRequests(repo, List(retractedPortableScala))
}
assertEquals(obtained, List.empty[(PullRequestData[Id], RetractedArtifact)])
}

test("findLatestPullRequest ignores grouped updates") {
val (_, result) = beforeAndAfterPRCreation(groupedUpdate(portableScala)) { repo =>
pullRequestRepository.findLatestPullRequest(repo, portableScala.crossDependency, "1.0.0".v)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.scalasteward.core.repoconfig

import org.scalasteward.core.TestSyntax._

import munit.FunSuite

class RetractedArtifactTest extends FunSuite {
private val retractedArtifact = RetractedArtifact(
"a reason",
"doc URI",
List(
UpdatePattern(
"org.portable-scala".g,
Some("sbt-scalajs-crossproject"),
Some(VersionPattern(exact = Some("1.0.0")))
)
)
)

test("isRetracted") {
val update = ("org.portable-scala".g % "sbt-scalajs-crossproject".a % "0.9.2" %> "1.0.0").single
assert(retractedArtifact.isRetracted(update))
}

test("not isRetracted") {
val update = ("org.portable-scala".g % "sbt-scalajs-crossproject".a % "0.9.2" %> "0.9.3").single
assert(!retractedArtifact.isRetracted(update))
}
}
13 changes: 13 additions & 0 deletions modules/docs/mdoc/repo-specific-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ updates.pin = [ { groupId = "com.example", artifactId="foo", version = "1.1." }
# Defaults to empty `[]` which mean Scala Steward will not ignore dependencies.
updates.ignore = [ { groupId = "org.acme", artifactId="foo", version = "1.0" } ]

# The dependencies which match the given pattern are retracted. Their existing pull-request will be closed.
#
# Each entry must have a `reason`, a `doc` URL and a list of dependency patterns.
updates.retracted = [
{
reason = "Ignore version 3.6.0 as it is abandoned due to broken compatibility",
doc = "https://contributors.scala-lang.org/t/broken-scala-3-6-0-release/6792",
artifacts = [
{ groupId = "org.scala-lang", artifactId = "scala3-compiler", version = { exact = "3.6.0" } }
]
}
]

# The dependencies which match the given patterns are allowed to be updated to pre-release from stable.
# This also implies, that it will be allowed for snapshot versions to be updated to snapshots of different series.
#
Expand Down
Loading