Skip to content

Commit

Permalink
Retract previously opened Pull Request
Browse files Browse the repository at this point in the history
The `updates.retraced` section of the `scala-steward.conf` allows to declare updates that should be retracted. Each entry must have a `reason, a `doc` URL and a list of dependency patterns.

Example:
```
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" } }
    ]
  }
]
```
Retraction of Pull Request is only possible, if the workspace is persisted correctly. (https://github.com/scala-steward-org/scala-steward/blob/main/docs/faq.md#why-doesnt-self-hosted-scala-steward-close-obsolete-prs)

Fixes: #3445
  • Loading branch information
mzuehlke committed Nov 24, 2024
1 parent 916c6a1 commit a2c5435
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 6 deletions.
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,13 @@ 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)
states <- pruningAlg.needsAttention(data)
result <- states.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: Map[Uri, Entry] =>
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,30 @@
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"""|Retracted because of: ${reason}.
|
|Documentation: ${doc}
|""".stripMargin.trim
}

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,33 @@ 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("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))
}
}

0 comments on commit a2c5435

Please sign in to comment.