Repository: scala-steward-org/scala-steward
Pull request #1744 (2020-11-13 15:16:31)

Extract PR number from URL only if absent in PullRequestData
This uses `PullRequestData.number` for closing obsolete PRs if it is
present and falls back to `vcs.extractPullRequestNumberFrom` otherwise.
It also uses `PullRequestNumber` in `VCSApiAlg` and its implementations
and moves `extractPRIdFromUrls` from `VCSExtraAlg` to the `vcs` pacakge
since it doesn't have any dependencies.

Follow-up to #1741.


Patch:
diff --git a/README.md b/README.md
index 6904a792..db2f6ad7 100644
--- a/README.md
+++ b/README.md
@@ -146,7 +146,6 @@ Consider creating PR to add your company to the list and join the community.
 * [Chartboost](https://www.chartboost.com/)
 * [Colisweb](https://www.colisweb.com/)
 * [commercetools](https://docs.commercetools.com/)
-* [Dataswift.io](https://dataswift.io/)
 * [Enliven Systems](https://enliven.systems)
 * [Evolution Gaming](https://www.evolutiongaming.com/)
 * [Firstbird](https://firstbird.com)
diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
index 4eb9e85a..5cc36ef8 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
@@ -33,7 +33,7 @@ import org.scalasteward.core.repoconfig.{PullRequestUpdateStrategy, RepoConfigAl
 import org.scalasteward.core.scalafix.MigrationAlg
 import org.scalasteward.core.scalafmt.ScalafmtAlg
 import org.scalasteward.core.util.{BracketThrow, HttpExistenceClient}
-import org.scalasteward.core.vcs.data.{NewPullRequestData, PullRequestState, Repo, RepoOut}
+import org.scalasteward.core.vcs.data._
 import org.scalasteward.core.vcs.{VCSApiAlg, VCSExtraAlg, VCSRepoAlg}
 import org.scalasteward.core.{git, util, vcs}
 import scala.util.control.NonFatal
@@ -122,34 +122,21 @@ final class NurtureAlg[F[_]](config: Config)(implicit
     } yield result
 
   def closeObsoletePullRequests(data: UpdateData): F[Unit] = {
-
-    def closePR(repo: Repo, id: Int): F[Unit] =
-      vcsApiAlg.closePullRequest(repo, id).as(())
-
-    def close(id: Int, repo: Repo, updateData: Update, url: Uri): F[Unit] =
+    def close(number: PullRequestNumber, repo: Repo, update: Update, url: Uri): F[Unit] =
       for {
-        _ <- closePR(repo, id)
-        _ <- logger.info(
-          s"Closed a PR @ ${url.renderString} for ${updateData.show}"
-        )
+        _ <- vcsApiAlg.closePullRequest(repo, number)
+        _ <- logger.info(s"Closed a PR @ ${url.renderString} for ${update.show}")
         _ <- pullRequestRepository.changeState(repo, url, PullRequestState.Closed)
       } yield ()
 
     for {
       _ <- logger.info(s"Looking to close obsolete PRs for ${data.update.name}")
       prsToClose <- pullRequestRepository.getObsoleteOpenPullRequests(data.repo, data.update)
-      dataPerId = prsToClose.flatMap { case (url, data) =>
-        vcsExtraAlg
-          .extractPRIdFromUrls(config.vcsType, url)
-          .tupleRight((data, url))
-          .toList
-      }
-      _ <- dataPerId.traverse { case (id, (pullRequestData, url)) =>
-        close(id, data.repo, pullRequestData, url).handleErrorWith { case NonFatal(ex) =>
-          logger.warn(ex)(s"Failed to close obsolete PR #$id for ${data.updateBranch.name}")
+      _ <- prsToClose.traverse { case (number, url, update) =>
+        close(number, data.repo, update, url).handleErrorWith { case NonFatal(ex) =>
+          logger.warn(ex)(s"Failed to close obsolete PR #$number for ${data.updateBranch.name}")
         }
       }
-
     } yield ()
   }
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala
index 2f386757..a750ba32 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestRepository.scala
@@ -25,6 +25,7 @@ import org.scalasteward.core.persistence.KeyValueStore
 import org.scalasteward.core.update.UpdateAlg
 import org.scalasteward.core.util.{DateTimeAlg, Timestamp}
 import org.scalasteward.core.vcs.data.{PullRequestNumber, PullRequestState, Repo}
+import org.scalasteward.core.vcs
 
 final class PullRequestRepository[F[_]](
     kvStore: KeyValueStore[F, Repo, Map[Uri, PullRequestData]]
@@ -69,16 +70,19 @@ final class PullRequestRepository[F[_]](
 
   def getObsoleteOpenPullRequests(
       repo: Repo,
-      updateData: Update
-  ): F[List[(Uri, Update)]] =
+      update: Update
+  ): F[List[(PullRequestNumber, Uri, Update)]] =
     kvStore.get(repo).map {
-      _.getOrElse(Map.empty).collect {
-        case (url, data)
-            if data.update.withNewerVersions(updateData.newerVersions) === updateData &&
-              Version(data.update.nextVersion) < Version(updateData.nextVersion) &&
-              data.state === PullRequestState.Open =>
-          url -> data.update
-      }.toList
+      _.getOrElse(Map.empty)
+        .collect {
+          case (url, data)
+              if data.update.withNewerVersions(update.newerVersions) === update &&
+                Version(data.update.nextVersion) < Version(update.nextVersion) &&
+                data.state === PullRequestState.Open =>
+            data.number.orElse(vcs.extractPullRequestNumberFrom(url)).map((_, url, data.update))
+        }
+        .flatten
+        .toList
     }
 
   def findPullRequest(
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala
index 7dd78fb9..628eaf47 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala
@@ -26,7 +26,7 @@ trait VCSApiAlg[F[_]] {
 
   def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut]
 
-  def closePullRequest(repo: Repo, id: Int): F[PullRequestOut]
+  def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut]
 
   def getBranch(repo: Repo, branch: Branch): F[BranchOut]
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSExtraAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSExtraAlg.scala
index bc99634c..b617f287 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSExtraAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSExtraAlg.scala
@@ -19,14 +19,13 @@ package org.scalasteward.core.vcs
 import cats.Monad
 import cats.syntax.all._
 import org.http4s.Uri
-import org.scalasteward.core.application.{Config, SupportedVCS}
+import org.scalasteward.core.application.Config
 import org.scalasteward.core.data.{ReleaseRelatedUrl, Update}
 import org.scalasteward.core.util.HttpExistenceClient
 import org.scalasteward.core.vcs
 
 trait VCSExtraAlg[F[_]] {
   def getReleaseRelatedUrls(repoUrl: Uri, update: Update): F[List[ReleaseRelatedUrl]]
-  def extractPRIdFromUrls(vsc: SupportedVCS, uri: Uri): Option[Int]
 }
 
 object VCSExtraAlg {
@@ -39,24 +38,5 @@ object VCSExtraAlg {
         vcs
           .possibleReleaseRelatedUrls(config.vcsType, config.vcsApiHost, repoUrl, update)
           .filterA(releaseRelatedUrl => existenceClient.exists(releaseRelatedUrl.url))
-
-      override def extractPRIdFromUrls(vcs: SupportedVCS, uri: Uri): Option[Int] = {
-        def extractIntAfter(value: String, matchPart: String): Option[Int] = {
-          val regex = raw".*/$matchPart/(\d+)".r
-          value match {
-            case regex(id) => scala.util.Try(id.toInt).toOption
-            case _         => None
-          }
-        }
-
-        vcs match {
-          case SupportedVCS.GitHub =>
-            extractIntAfter(uri.path, "pull")
-          case SupportedVCS.Bitbucket | SupportedVCS.BitbucketServer =>
-            extractIntAfter(uri.path, "pullrequests")
-          case SupportedVCS.GitLab =>
-            extractIntAfter(uri.path, "merge_requests")
-        }
-      }
     }
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala
index ec992fa8..0d5acfd1 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlg.scala
@@ -87,9 +87,9 @@ class BitbucketApiAlg[F[_]](
       .get[Page[PullRequestOut]](url.listPullRequests(repo, head), modify(repo))
       .map(_.values)
 
-  override def closePullRequest(repo: Repo, id: Int): F[PullRequestOut] =
+  override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client.putWithBody[PullRequestOut, UpdateState](
-      url.pullRequest(repo, id),
+      url.pullRequest(repo, number),
       UpdateState(PullRequestState.Closed),
       modify(repo)
     )
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala
index 206d0b65..2b397794 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/Url.scala
@@ -18,7 +18,7 @@ package org.scalasteward.core.vcs.bitbucket
 
 import org.http4s.Uri
 import org.scalasteward.core.git.Branch
-import org.scalasteward.core.vcs.data.Repo
+import org.scalasteward.core.vcs.data.{PullRequestNumber, Repo}
 
 private[bitbucket] class Url(apiHost: Uri) {
   def forks(rep: Repo): Uri =
@@ -30,8 +30,8 @@ private[bitbucket] class Url(apiHost: Uri) {
   def pullRequests(rep: Repo): Uri =
     repo(rep) / "pullrequests"
 
-  def pullRequest(rep: Repo, id: Int): Uri =
-    repo(rep) / "pullrequests" / id.toString
+  def pullRequest(rep: Repo, number: PullRequestNumber): Uri =
+    repo(rep) / "pullrequests" / number.toString
 
   def branch(rep: Repo, branch: Branch): Uri =
     repo(rep) / "refs" / "branches" / branch.name
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala
index 4318fdfe..8be8dccc 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlg.scala
@@ -93,9 +93,9 @@ class BitbucketServerApiAlg[F[_]](
 
   def ni(name: String): Nothing = throw new NotImplementedError(name)
 
-  override def closePullRequest(repo: Repo, id: Int): F[PullRequestOut] =
+  override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client.putWithBody[PullRequestOut, UpdateState](
-      url.pullRequest(repo, id),
+      url.pullRequest(repo, number),
       UpdateState(PullRequestState.Closed),
       modify(repo)
     )
@@ -110,7 +110,8 @@ final class StashUrls(base: Uri) {
 
   def pullRequests(r: Repo): Uri = repo(r) / "pull-requests"
 
-  def pullRequest(r: Repo, id: Int): Uri = repo(r) / "pull-requests" / id.toString
+  def pullRequest(r: Repo, number: PullRequestNumber): Uri =
+    repo(r) / "pull-requests" / number.toString
 
   def reviewers(repo: Repo): Uri =
     reviewerApi / "projects" / repo.owner / "repos" / repo.repo / "conditions"
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestNumber.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestNumber.scala
index 2636b066..8f44a239 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestNumber.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestNumber.scala
@@ -19,7 +19,9 @@ package org.scalasteward.core.vcs.data
 import io.circe.Codec
 import io.circe.generic.extras.semiauto.deriveUnwrappedCodec
 
-final case class PullRequestNumber(value: Int)
+final case class PullRequestNumber(value: Int) {
+  override def toString: String = value.toString
+}
 
 object PullRequestNumber {
   implicit val pullRequestNumberCodec: Codec[PullRequestNumber] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/github/GitHubApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/github/GitHubApiAlg.scala
index 41975170..c54cf3cb 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/github/GitHubApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/github/GitHubApiAlg.scala
@@ -51,9 +51,9 @@ final class GitHubApiAlg[F[_]](
     client.get(url.listPullRequests(repo, head, base), modify(repo))
 
   /** https://developer.github.com/v3/pulls/#update-a-pull-request */
-  override def closePullRequest(repo: Repo, id: Int): F[PullRequestOut] =
+  override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client.patchWithBody[PullRequestOut, UpdateState](
-      url.pull(repo, id),
+      url.pull(repo, number),
       UpdateState(PullRequestState.Closed),
       modify(repo)
     )
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/github/Url.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/github/Url.scala
index 9a5181e7..a3264889 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/github/Url.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/github/Url.scala
@@ -18,7 +18,7 @@ package org.scalasteward.core.vcs.github
 
 import org.http4s.Uri
 import org.scalasteward.core.git.Branch
-import org.scalasteward.core.vcs.data.Repo
+import org.scalasteward.core.vcs.data.{PullRequestNumber, Repo}
 
 class Url(apiHost: Uri) {
   def branches(repo: Repo, branch: Branch): Uri =
@@ -36,8 +36,8 @@ class Url(apiHost: Uri) {
   def pulls(repo: Repo): Uri =
     repos(repo) / "pulls"
 
-  def pull(repo: Repo, id: Int): Uri =
-    repos(repo) / "pulls" / id.toString
+  def pull(repo: Repo, number: PullRequestNumber): Uri =
+    repos(repo) / "pulls" / number.toString
 
   def repos(repo: Repo): Uri =
     apiHost / "repos" / repo.owner / repo.repo
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala
index 8825ea48..d399c50b 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlg.scala
@@ -47,10 +47,10 @@ final private[gitlab] case class MergeRequestOut(
     webUrl: Uri,
     state: PullRequestState,
     title: String,
-    iid: Int,
+    iid: PullRequestNumber,
     mergeStatus: String
 ) {
-  val pullRequestOut: PullRequestOut = PullRequestOut(webUrl, state, PullRequestNumber(iid), title)
+  val pullRequestOut: PullRequestOut = PullRequestOut(webUrl, state, iid, title)
 }
 
 final private[gitlab] case class CommitId(id: Sha1) {
@@ -95,7 +95,7 @@ private[gitlab] object GitLabJsonCodec {
       webUrl <- c.downField("web_url").as[Uri]
       state <- c.downField("state").as[PullRequestState]
       title <- c.downField("title").as[String]
-      iid <- c.downField("iid").as[Int]
+      iid <- c.downField("iid").as[PullRequestNumber]
       mergeStatus <- c.downField("merge_status").as[String]
     } yield MergeRequestOut(webUrl, state, title, iid, mergeStatus)
   }
@@ -159,12 +159,15 @@ class GitLabApiAlg[F[_]](
       )
     } yield res
 
-    def waitForMergeRequestStatus(internalId: Int, retries: Int = 10): F[MergeRequestOut] =
+    def waitForMergeRequestStatus(
+        number: PullRequestNumber,
+        retries: Int = 10
+    ): F[MergeRequestOut] =
       client
-        .get[MergeRequestOut](url.existingMergeRequest(repo, internalId), modify(repo))
+        .get[MergeRequestOut](url.existingMergeRequest(repo, number), modify(repo))
         .flatMap {
           case mr if (mr.mergeStatus =!= GitLabMergeStatus.Checking) => F.pure(mr)
-          case _ if (retries > 0)                                    => waitForMergeRequestStatus(internalId, retries - 1)
+          case _ if (retries > 0)                                    => waitForMergeRequestStatus(number, retries - 1)
           case other                                                 => F.pure(other)
         }
 
@@ -199,10 +202,10 @@ class GitLabApiAlg[F[_]](
     updatedMergeRequest.map(_.pullRequestOut)
   }
 
-  def closePullRequest(repo: Repo, id: Int): F[PullRequestOut] =
+  def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client
       .putWithBody[MergeRequestOut, UpdateState](
-        url.existingMergeRequest(repo, id),
+        url.existingMergeRequest(repo, number),
         UpdateState(PullRequestState.Closed),
         modify(repo)
       )
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/Url.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/Url.scala
index a613ace2..d56c6de7 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/Url.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/gitlab/Url.scala
@@ -18,7 +18,7 @@ package org.scalasteward.core.vcs.gitlab
 
 import org.http4s.Uri
 import org.scalasteward.core.git.Branch
-import org.scalasteward.core.vcs.data.Repo
+import org.scalasteward.core.vcs.data.{PullRequestNumber, Repo}
 
 class Url(apiHost: Uri) {
   def encodedProjectId(repo: Repo): String = s"${repo.owner}%2F${repo.repo}"
@@ -35,11 +35,11 @@ class Url(apiHost: Uri) {
   def mergeRequest(repo: Repo): Uri =
     repos(repo) / "merge_requests"
 
-  def existingMergeRequest(repo: Repo, internalId: Int): Uri =
-    mergeRequest(repo) / internalId.toString()
+  def existingMergeRequest(repo: Repo, number: PullRequestNumber): Uri =
+    mergeRequest(repo) / number.toString
 
-  def mergeWhenPiplineSucceeds(repo: Repo, internalId: Int) =
-    (existingMergeRequest(repo, internalId) / "merge")
+  def mergeWhenPiplineSucceeds(repo: Repo, number: PullRequestNumber): Uri =
+    (existingMergeRequest(repo, number) / "merge")
       .withQueryParam("merge_when_pipeline_succeeds", "true")
 
   def listMergeRequests(repo: Repo, source: String, target: String): Uri =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/package.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/package.scala
index 458cad60..ac8adacc 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/package.scala
@@ -22,9 +22,16 @@ import org.scalasteward.core.application.SupportedVCS
 import org.scalasteward.core.application.SupportedVCS.{Bitbucket, BitbucketServer, GitHub, GitLab}
 import org.scalasteward.core.data.ReleaseRelatedUrl.VersionDiff
 import org.scalasteward.core.data.{ReleaseRelatedUrl, Update}
-import org.scalasteward.core.vcs.data.Repo
+import org.scalasteward.core.vcs.data.{PullRequestNumber, Repo}
 
 package object vcs {
+  def extractPullRequestNumberFrom(uri: Uri): Option[PullRequestNumber] = {
+    val regex = raw".*/(pull|pullrequests|merge_requests)/(\d+)".r
+    uri.path match {
+      case regex(_, id) => scala.util.Try(PullRequestNumber(id.toInt)).toOption
+      case _            => None
+    }
+  }
 
   /** Determines the `head` (GitHub) / `source_branch` (GitLab, Bitbucket) parameter for searching
     * for already existing pull requests.
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 3cec7ed3..dd8e5285 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -20,7 +20,7 @@ object Dependencies {
   val coursierCore = "io.get-coursier" %% "coursier" % "2.0.6"
   val coursierCatsInterop = "io.get-coursier" %% "coursier-cats-interop" % coursierCore.revision
   val cron4sCore = "com.github.alonsodomin.cron4s" %% "cron4s-core" % "0.6.1"
-  val disciplineScalatest = ("org.typelevel" %% "discipline-scalatest" % "2.1.0")
+  val disciplineScalatest = ("org.typelevel" %% "discipline-scalatest" % "2.0.1")
     .excludeAll(ExclusionRule().withOrganization("org.scalatest"))
   val fs2Core = "co.fs2" %% "fs2-core" % "2.4.5"
   val fs2Io = "co.fs2" %% "fs2-io" % fs2Core.revision


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala b/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala
index a8be234a..d1fcec97 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/nurture/PullRequestRepositoryTest.scala
@@ -55,10 +55,7 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
       val nextUpdate = TestData.Updates.PortableScala.copy(newerVersions = Nel.of("1.0.1"))
 
       val p = for {
-        emptyResult <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          nextUpdate
-        )
+        emptyResult <- pullRequestRepository.getObsoleteOpenPullRequests(repo, nextUpdate)
         _ <- pullRequestRepository.createOrUpdate(
           repo,
           url,
@@ -67,23 +64,15 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
           PullRequestState.Open,
           number
         )
-        result <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          nextUpdate
-        )
+        result <- pullRequestRepository.getObsoleteOpenPullRequests(repo, nextUpdate)
         _ <- pullRequestRepository.changeState(repo, url, PullRequestState.Closed)
-        closedResult <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          nextUpdate
-        )
+        closedResult <- pullRequestRepository.getObsoleteOpenPullRequests(repo, nextUpdate)
       } yield (emptyResult, result, closedResult)
       val (state, (emptyResult, result, closedResult)) = p.run(MockState.empty).unsafeRunSync()
       val store = config.workspace / "store/pull_requests/v2/typelevel/cats/pull_requests.json"
       emptyResult shouldBe List.empty
       closedResult shouldBe List.empty
-      result shouldBe List(
-        url -> TestData.Updates.PortableScala
-      )
+      result shouldBe List((number, url, TestData.Updates.PortableScala))
 
       checkCommands(
         state,
@@ -103,10 +92,7 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
       val update = TestData.Updates.PortableScala
 
       val p = for {
-        emptyResult <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          update
-        )
+        emptyResult <- pullRequestRepository.getObsoleteOpenPullRequests(repo, update)
         _ <- pullRequestRepository.createOrUpdate(
           repo,
           url,
@@ -115,10 +101,7 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
           PullRequestState.Open,
           number
         )
-        result <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          update
-        )
+        result <- pullRequestRepository.getObsoleteOpenPullRequests(repo, update)
       } yield (emptyResult, result)
       val (state, (emptyResult, result)) = p.run(MockState.empty).unsafeRunSync()
       val store = config.workspace / "store/pull_requests/v2/typelevel/cats/pull_requests.json"
@@ -141,10 +124,7 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
       val newUpdate = TestData.Updates.CatsCore
 
       val p = for {
-        emptyResult <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          updateInStore
-        )
+        emptyResult <- pullRequestRepository.getObsoleteOpenPullRequests(repo, updateInStore)
         _ <- pullRequestRepository.createOrUpdate(
           repo,
           url,
@@ -153,10 +133,7 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
           PullRequestState.Open,
           number
         )
-        result <- pullRequestRepository.getObsoleteOpenPullRequests(
-          repo,
-          newUpdate
-        )
+        result <- pullRequestRepository.getObsoleteOpenPullRequests(repo, newUpdate)
       } yield (emptyResult, result)
       val (state, (emptyResult, result)) = p.run(MockState.empty).unsafeRunSync()
       val store = config.workspace / "store/pull_requests/v2/typelevel/cats/pull_requests.json"
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSExtraAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSExtraAlgTest.scala
index 9c8d213f..75a66a03 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSExtraAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSExtraAlgTest.scala
@@ -22,8 +22,8 @@ class VCSExtraAlgTest extends AnyFunSuite with Matchers {
       case _                                                            => NotFound()
     }
 
-  implicit val client = Client.fromHttpApp[IO](routes.orNotFound)
-  implicit val httpExistenceClient =
+  implicit val client: Client[IO] = Client.fromHttpApp[IO](routes.orNotFound)
+  implicit val httpExistenceClient: HttpExistenceClient[IO] =
     HttpExistenceClient.create[IO](MockContext.config).allocated.map(_._1).unsafeRunSync()
 
   val updateFoo = Update.Single("com.example" % "foo" % "0.1.0", Nel.of("0.2.0"))
@@ -69,30 +69,4 @@ class VCSExtraAlgTest extends AnyFunSuite with Matchers {
       .getReleaseRelatedUrls(uri"https://github.on-prem.com/foo/buz", updateFoo)
       .unsafeRunSync() shouldBe List.empty
   }
-
-  val validExtractPRTestCases = List(
-    SupportedVCS.Bitbucket -> uri"https://api.bitbucket.org/2.0/repositories/fthomas/base.g8/pullrequests/13",
-    SupportedVCS.BitbucketServer -> uri"https://api.bitbucket.org/2.0/repositories/fthomas/base.g8/pullrequests/13",
-    SupportedVCS.GitHub -> uri"https://github.com/scala-steward-org/scala-steward/pull/13",
-    SupportedVCS.GitLab -> uri"https://gitlab.com/inkscape/inkscape/-/merge_requests/13"
-  )
-  validExtractPRTestCases.foreach { case (vcs, uri) =>
-    test(s"valid - extractPRIdFromUrls for ${vcs.asString}") {
-      val vcsExtraAlg = VCSExtraAlg.create[IO](MockContext.config)
-      vcsExtraAlg.extractPRIdFromUrls(vcs, uri) shouldBe Some(13)
-    }
-  }
-
-  val invalidExtractPRTestCases = List(
-    SupportedVCS.Bitbucket -> uri"https://api.bitbucket.org/2.0/repositories/fthomas/base.g8/pullrequests/",
-    SupportedVCS.BitbucketServer -> uri"https://api.bitbucket.org/2.0/repositories/fthomas/base.g8/pullrequests/",
-    SupportedVCS.GitHub -> uri"https://github.com/scala-steward-org/scala-steward/pull/",
-    SupportedVCS.GitLab -> uri"https://gitlab.com/inkscape/inkscape/-/merge_requests/"
-  )
-  invalidExtractPRTestCases.foreach { case (vcs, uri) =>
-    test(s"invalid - extractPRIdFromUrls for ${vcs.asString}") {
-      val vcsExtraAlg = VCSExtraAlg.create[IO](MockContext.config)
-      vcsExtraAlg.extractPRIdFromUrls(vcs, uri) shouldBe None
-    }
-  }
 }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSPackageTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSPackageTest.scala
index 6cf125b8..5e3ad52a 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSPackageTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/VCSPackageTest.scala
@@ -6,11 +6,33 @@ import org.scalasteward.core.application.SupportedVCS
 import org.scalasteward.core.application.SupportedVCS.{GitHub, GitLab}
 import org.scalasteward.core.data.Update
 import org.scalasteward.core.util.Nel
-import org.scalasteward.core.vcs.data.Repo
+import org.scalasteward.core.vcs.data.{PullRequestNumber, Repo}
 import org.scalatest.funsuite.AnyFunSuite
 import org.scalatest.matchers.should.Matchers
 
 class VCSPackageTest extends AnyFunSuite with Matchers {
+  test(s"extractPullRequestNumberFrom: valid") {
+    val urls = List(
+      uri"https://api.bitbucket.org/2.0/repositories/fthomas/base.g8/pullrequests/13",
+      uri"https://github.com/scala-steward-org/scala-steward/pull/13",
+      uri"https://gitlab.com/inkscape/inkscape/-/merge_requests/13"
+    )
+    urls.foreach { uri =>
+      extractPullRequestNumberFrom(uri) shouldBe Some(PullRequestNumber(13))
+    }
+  }
+
+  test(s"extractPullRequestNumberFrom: invalid") {
+    val urls = List(
+      uri"https://api.bitbucket.org/2.0/repositories/fthomas/base.g8/pullrequests/",
+      uri"https://github.com/scala-steward-org/scala-steward/pull/",
+      uri"https://gitlab.com/inkscape/inkscape/-/merge_requests/"
+    )
+    urls.foreach { uri =>
+      extractPullRequestNumberFrom(uri) shouldBe None
+    }
+  }
+
   val repo: Repo = Repo("foo", "bar")
   val update: Update.Single =
     Update.Single("ch.qos.logback" % "logback-classic" % "1.2.0", Nel.of("1.2.3"))
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
index 343e36cd..3e0a9ccc 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
@@ -217,7 +217,7 @@ class BitbucketApiAlgTest extends AnyFunSuite with Matchers {
   }
 
   test("closePullRequest") {
-    val pr = bitbucketApiAlg.closePullRequest(repo, 1).unsafeRunSync()
+    val pr = bitbucketApiAlg.closePullRequest(repo, PullRequestNumber(1)).unsafeRunSync()
     pr shouldBe pr.copy(state = PullRequestState.Closed)
   }
 }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala
index 169d5750..135c2197 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala
@@ -129,7 +129,7 @@ class GitHubApiAlgTest extends AnyFunSuite with Matchers {
   }
 
   test("closePullRequest") {
-    val prOut = gitHubApiAlg.closePullRequest(repo, 1347).unsafeRunSync()
+    val prOut = gitHubApiAlg.closePullRequest(repo, PullRequestNumber(1347)).unsafeRunSync()
     prOut.state shouldBe PullRequestState.Closed
   }
 }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala
index eec078c9..f9c372c1 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/gitlab/GitLabApiAlgTest.scala
@@ -137,7 +137,7 @@ class GitLabApiAlgTest extends AnyFunSuite with Matchers {
   test("closePullRequest") {
     val prOut =
       gitlabApiAlg
-        .closePullRequest(Repo("foo", "bar"), 7115)
+        .closePullRequest(Repo("foo", "bar"), PullRequestNumber(7115))
         .unsafeRunSync()
 
     prOut shouldBe PullRequestOut(