Repository: scala-steward-org/scala-steward
Pull request #1741 (2020-11-12 14:11:02)

Add PR number to PullRequestOut and persist it in PullRequestRepository
This contains some of the changes that were also in #1631 and is a
follow-up to #1667. This PR stores the pull request number in
`PullRequestOut` and persists it in `PullRequestRepository`. To sidestep
the issue of migrating old `pull_requests.json` files, the PR number is
added as an optional field to `PullRequestData`.

The goal here is to use the persisted PR number instead of
`VCSExtraAlg.extractPRIdFromUrls` in a future release.


Patch:
diff --git a/README.md b/README.md
index db2f6ad7..9d275aa0 100644
--- a/README.md
+++ b/README.md
@@ -57,7 +57,6 @@ The [`docs`](docs) directory contains documentation about these topics:
 * [Scalafix Migrations](docs/scalafix-migrations.md)
 * [Frequently Asked Questions](docs/faq.md)
 * [Repository-specific configuration](docs/repo-specific-configuration.md)
-* [Artifact Migrations](docs/artifact-migrations.md)
 
 ## Contributors
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
index 8822c473..0e3189f5 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/UpdateHeuristic.scala
@@ -57,7 +57,7 @@ object UpdateHeuristic {
       case s @ Update.Single(_, _, Some(newerGroupId), Some(newerArtifactId)) =>
         val currentGroupId = Regex.quote(s.groupId.value)
         val currentArtifactId = Regex.quote(s.artifactId.name)
-        val regex = s"""(?i)(.*)$currentGroupId(.*)$currentArtifactId""".r
+        val regex = s"""(?i)(.*)${currentGroupId}(.*)${currentArtifactId}""".r
         replaceSomeInAllowedParts(
           regex,
           target,
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 4737462b..4eb9e85a 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
@@ -115,7 +115,8 @@ final class NurtureAlg[F[_]](config: Config)(implicit
           pr.html_url,
           data.baseSha1,
           data.update,
-          pr.state
+          pr.state,
+          pr.number
         )
       }
     } yield result
@@ -216,7 +217,8 @@ final class NurtureAlg[F[_]](config: Config)(implicit
         pr.html_url,
         data.baseSha1,
         data.update,
-        pr.state
+        pr.state,
+        pr.number
       )
       _ <- logger.info(s"Created PR ${pr.html_url}")
     } yield ()
diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestData.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestData.scala
index 455c34e4..8ab836f2 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestData.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/PullRequestData.scala
@@ -21,13 +21,14 @@ import io.circe.generic.semiauto._
 import org.scalasteward.core.data.Update
 import org.scalasteward.core.git.Sha1
 import org.scalasteward.core.util.Timestamp
-import org.scalasteward.core.vcs.data.PullRequestState
+import org.scalasteward.core.vcs.data.{PullRequestNumber, PullRequestState}
 
 final case class PullRequestData(
     baseSha1: Sha1,
     update: Update,
     state: PullRequestState,
-    entryCreatedAt: Timestamp
+    entryCreatedAt: Timestamp,
+    number: Option[PullRequestNumber]
 )
 
 object PullRequestData {
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 906f04d3..2f386757 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
@@ -24,7 +24,7 @@ import org.scalasteward.core.git.Sha1
 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.{PullRequestState, Repo}
+import org.scalasteward.core.vcs.data.{PullRequestNumber, PullRequestState, Repo}
 
 final class PullRequestRepository[F[_]](
     kvStore: KeyValueStore[F, Repo, Map[Uri, PullRequestData]]
@@ -48,7 +48,8 @@ final class PullRequestRepository[F[_]](
       url: Uri,
       baseSha1: Sha1,
       update: Update,
-      state: PullRequestState
+      state: PullRequestState,
+      number: PullRequestNumber
   ): F[Unit] =
     kvStore
       .modifyF(repo) { maybePullRequests =>
@@ -59,7 +60,7 @@ final class PullRequestRepository[F[_]](
             pullRequests.updated(url, data).some.pure[F]
           case None =>
             dateTimeAlg.currentTimestamp.map { now =>
-              val data = PullRequestData(baseSha1, update, state, now)
+              val data = PullRequestData(baseSha1, update, state, now, Some(number))
               pullRequests.updated(url, data).some
             }
         }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/json.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/json.scala
index 32518c4e..06b5e758 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/json.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucket/json.scala
@@ -20,7 +20,7 @@ import io.circe.Decoder
 import org.http4s.Uri
 import org.scalasteward.core.git.{Branch, Sha1}
 import org.scalasteward.core.util.uri.uriDecoder
-import org.scalasteward.core.vcs.data.{BranchOut, CommitOut, PullRequestOut, PullRequestState}
+import org.scalasteward.core.vcs.data._
 
 private[bitbucket] object json {
   implicit val branchOutDecoder: Decoder[BranchOut] = Decoder.instance { c =>
@@ -39,9 +39,10 @@ private[bitbucket] object json {
 
   implicit val pullRequestOutDecoder: Decoder[PullRequestOut] = Decoder.instance { c =>
     for {
+      id <- c.downField("id").as[PullRequestNumber]
       title <- c.downField("title").as[String]
       state <- c.downField("state").as[PullRequestState]
       html_url <- c.downField("links").downField("self").downField("href").as[Uri]
-    } yield (PullRequestOut(html_url, state, title))
+    } yield PullRequestOut(html_url, state, id, title)
   }
 }
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 9ca569cb..4318fdfe 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
@@ -56,7 +56,7 @@ class BitbucketServerApiAlg[F[_]](
         reviewers = reviewers
       )
       pr <- client.postWithBody[Json.PR, Json.NewPR](url.pullRequests(repo), req, modify(repo))
-    } yield PullRequestOut(pr.links("self").head.href, pr.state, pr.title)
+    } yield pr.toPullRequestOut
   }
 
   private def useDefaultReviewers(repo: Repo): F[List[Reviewer]] =
@@ -89,7 +89,7 @@ class BitbucketServerApiAlg[F[_]](
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
     client
       .get[Json.Page[Json.PR]](url.listPullRequests(repo, s"refs/heads/$head"), modify(repo))
-      .map(_.values.map(pr => PullRequestOut(pr.links("self").head.href, pr.state, pr.title)))
+      .map(_.values.map(_.toPullRequestOut))
 
   def ni(name: String): Nothing = throw new NotImplementedError(name)
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/Json.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/Json.scala
index 9b38af61..945619d1 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/Json.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/bitbucketserver/Json.scala
@@ -21,7 +21,7 @@ import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
 import io.circe.{Decoder, Encoder}
 import org.http4s.Uri
 import org.scalasteward.core.git.Sha1
-import org.scalasteward.core.vcs.data.PullRequestState
+import org.scalasteward.core.vcs.data.{PullRequestNumber, PullRequestOut, PullRequestState}
 
 object Json {
   case class Page[A](values: List[A])
@@ -34,7 +34,10 @@ object Json {
 
   case class Link(href: Uri, name: Option[String])
 
-  case class PR(title: String, state: PullRequestState, links: Links)
+  case class PR(id: PullRequestNumber, title: String, state: PullRequestState, links: Links) {
+    def toPullRequestOut: PullRequestOut =
+      PullRequestOut(links("self").head.href, state, id, title)
+  }
 
   case class NewPR(
       title: String,
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
new file mode 100644
index 00000000..2636b066
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestNumber.scala
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2018-2020 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.vcs.data
+
+import io.circe.Codec
+import io.circe.generic.extras.semiauto.deriveUnwrappedCodec
+
+final case class PullRequestNumber(value: Int)
+
+object PullRequestNumber {
+  implicit val pullRequestNumberCodec: Codec[PullRequestNumber] =
+    deriveUnwrappedCodec
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestOut.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestOut.scala
index 81c3c6ac..dfbe43ac 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestOut.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestOut.scala
@@ -26,6 +26,7 @@ import org.scalasteward.core.vcs.data.PullRequestState.Closed
 final case class PullRequestOut(
     html_url: Uri,
     state: PullRequestState,
+    number: PullRequestNumber,
     title: String
 ) {
   def isClosed: Boolean =
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 eade26c4..8825ea48 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
@@ -50,7 +50,7 @@ final private[gitlab] case class MergeRequestOut(
     iid: Int,
     mergeStatus: String
 ) {
-  val pullRequestOut: PullRequestOut = PullRequestOut(webUrl, state, title)
+  val pullRequestOut: PullRequestOut = PullRequestOut(webUrl, state, PullRequestNumber(iid), title)
 }
 
 final private[gitlab] case class CommitId(id: Sha1) {
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index dd8e5285..19ff39e3 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -24,7 +24,7 @@ object Dependencies {
     .excludeAll(ExclusionRule().withOrganization("org.scalatest"))
   val fs2Core = "co.fs2" %% "fs2-core" % "2.4.5"
   val fs2Io = "co.fs2" %% "fs2-io" % fs2Core.revision
-  val http4sCore = "org.http4s" %% "http4s-core" % "0.21.9"
+  val http4sCore = "org.http4s" %% "http4s-core" % "0.21.8"
   val http4sCirce = "org.http4s" %% "http4s-circe" % http4sCore.revision
   val http4sClient = "org.http4s" %% "http4s-client" % http4sCore.revision
   val http4sDsl = "org.http4s" %% "http4s-dsl" % http4sCore.revision


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
index b28cff00..acdc3bd4 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
@@ -7,7 +7,6 @@ import org.scalasteward.core.application.Cli.ParseResult._
 import org.scalatest.EitherValues
 import org.scalatest.funsuite.AnyFunSuite
 import org.scalatest.matchers.should.Matchers
-
 import scala.concurrent.duration._
 
 class CliTest extends AnyFunSuite with Matchers with EitherValues {
@@ -25,9 +24,7 @@ class CliTest extends AnyFunSuite with Matchers with EitherValues {
         List("--ignore-opts-files"),
         List("--env-var", "g=h"),
         List("--env-var", "i=j"),
-        List("--process-timeout", "30min"),
-        List("--scalafix-migrations", "/opt/scala-steward/extra-scalafix-migrations.conf"),
-        List("--artifact-migrations", "/opt/scala-steward/extra-artifact-migrations.conf")
+        List("--process-timeout", "30min")
       ).flatten
     ) shouldBe Success(
       Cli.Args(
@@ -41,9 +38,7 @@ class CliTest extends AnyFunSuite with Matchers with EitherValues {
         gitAskPass = File("f"),
         ignoreOptsFiles = true,
         envVar = List(EnvVar("g", "h"), EnvVar("i", "j")),
-        processTimeout = 30.minutes,
-        scalafixMigrations = List(uri"/opt/scala-steward/extra-scalafix-migrations.conf"),
-        artifactMigrations = Some(File("/opt/scala-steward/extra-artifact-migrations.conf"))
+        processTimeout = 30.minutes
       )
     )
   }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
index ff5573f4..696e8b25 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
@@ -2,7 +2,7 @@ package org.scalasteward.core.edit
 
 import better.files.File
 import org.scalasteward.core.TestSyntax._
-import org.scalasteward.core.data.{GroupId, Update}
+import org.scalasteward.core.data.Update
 import org.scalasteward.core.mock.MockContext.{config, editAlg}
 import org.scalasteward.core.mock.MockState
 import org.scalasteward.core.repoconfig.UpdatesConfig
@@ -139,6 +139,17 @@ class EditAlgTest extends AnyFunSuite with Matchers {
     )
   }
 
+  def runApplyUpdate(update: Update, files: Map[String, String]): Map[String, String] = {
+    val repoDir = File.temp / "ws/owner/repo"
+    val filesInRepoDir = files.map { case (file, content) => repoDir / file -> content }
+    editAlg
+      .applyUpdate(Repo("owner", "repo"), update, UpdatesConfig.defaultFileExtensions)
+      .runS(MockState.empty.addFiles(filesInRepoDir))
+      .map(_.files)
+      .unsafeRunSync()
+      .map { case (file, content) => file.toString.replace(repoDir.toString + "/", "") -> content }
+  }
+
   test("reproduce https://github.com/circe/circe-config/pull/40") {
     val update = Update.Single("com.typesafe" % "config" % "1.3.3", Nel.of("1.3.4"))
     val original = Map(
@@ -180,63 +191,4 @@ class EditAlgTest extends AnyFunSuite with Matchers {
     )
     runApplyUpdate(update, original) shouldBe expected
   }
-
-  test("test updating group id and version") {
-    val update = Update.Single(
-      crossDependency = "com.github.mpilquist" % "simulacrum" % "0.19.0",
-      newerVersions = Nel.of("1.0.0"),
-      newerGroupId = Some(GroupId("org.typelevel")),
-      newerArtifactId = Some("simulacrum")
-    )
-    val original = Map(
-      "build.sbt" ->
-        """
-          |val simulacrum = "0.19.0"
-          |"com.github.mpilquist" %% "simulacrum" % simulacrum
-          |"""".stripMargin
-    )
-    val expected = Map(
-      "build.sbt" ->
-        """
-          |val simulacrum = "1.0.0"
-          |"org.typelevel" %% "simulacrum" % simulacrum
-          |"""".stripMargin // the version should have been updated here
-    )
-    runApplyUpdate(update, original) shouldBe expected
-  }
-
-  test("test updating artifact id and version") {
-    val update = Update.Single(
-      crossDependency = "com.test" % "artifact" % "1.0.0",
-      newerVersions = Nel.of("2.0.0"),
-      newerGroupId = Some(GroupId("com.test")),
-      newerArtifactId = Some("newer-artifact")
-    )
-    val original = Map(
-      "Dependencies.scala" ->
-        """
-          |private val artifactVersion = "1.0.0"
-          |val test = "com.test" %% "artifact" % testVersion 
-          |"""".stripMargin
-    )
-    val expected = Map(
-      "Dependencies.scala" ->
-        """
-          |private val artifactVersion = "2.0.0"
-          |val test = "com.test" %% "newer-artifact" % testVersion 
-          |"""".stripMargin
-    )
-    runApplyUpdate(update, original) shouldBe expected
-  }
-
-  private def runApplyUpdate(update: Update, files: Map[String, String]): Map[String, String] = {
-    val repoDir = File.temp / "ws/owner/repo"
-    val filesInRepoDir = files.map { case (file, content) => repoDir / file -> content }
-    editAlg
-      .applyUpdate(Repo("owner", "repo"), update, UpdatesConfig.defaultFileExtensions)
-      .runS(MockState.empty.addFiles(filesInRepoDir))
-      .map(_.files)
-      .unsafeRunSync()
-      .map { case (file, content) => file.toString.replace(repoDir.toString + "/", "") -> content }
-  }
 }
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 639ea21c..a8be234a 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
@@ -9,7 +9,7 @@ import org.scalasteward.core.git.Sha1.HexString
 import org.scalasteward.core.mock.MockContext.{config, pullRequestRepository}
 import org.scalasteward.core.mock.MockState
 import org.scalasteward.core.util.Nel
-import org.scalasteward.core.vcs.data.{PullRequestState, Repo}
+import org.scalasteward.core.vcs.data.{PullRequestNumber, PullRequestState, Repo}
 import org.scalatest.funsuite.AnyFunSuite
 import org.scalatest.matchers.should.Matchers
 
@@ -17,12 +17,19 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
   private def checkCommands(state: MockState, commands: Vector[List[String]]) =
     state.copy(files = Map.empty) shouldBe MockState.empty.copy(commands = commands)
 
-  withStoreData { (repo, url, sha1) =>
+  withStoreData { (repo, url, sha1, number) =>
     test("createOrUpdate >> findPullRequest >> lastPullRequestCreatedAt") {
       val update = TestData.Updates.PortableScala
 
       val p = for {
-        _ <- pullRequestRepository.createOrUpdate(repo, url, sha1, update, PullRequestState.Open)
+        _ <- pullRequestRepository.createOrUpdate(
+          repo,
+          url,
+          sha1,
+          update,
+          PullRequestState.Open,
+          number
+        )
         result <- pullRequestRepository.findPullRequest(repo, update.crossDependency, "1.0.0")
         createdAt <- pullRequestRepository.lastPullRequestCreatedAt(repo)
       } yield (result, createdAt)
@@ -52,7 +59,14 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
           repo,
           nextUpdate
         )
-        _ <- pullRequestRepository.createOrUpdate(repo, url, sha1, update, PullRequestState.Open)
+        _ <- pullRequestRepository.createOrUpdate(
+          repo,
+          url,
+          sha1,
+          update,
+          PullRequestState.Open,
+          number
+        )
         result <- pullRequestRepository.getObsoleteOpenPullRequests(
           repo,
           nextUpdate
@@ -93,7 +107,14 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
           repo,
           update
         )
-        _ <- pullRequestRepository.createOrUpdate(repo, url, sha1, update, PullRequestState.Open)
+        _ <- pullRequestRepository.createOrUpdate(
+          repo,
+          url,
+          sha1,
+          update,
+          PullRequestState.Open,
+          number
+        )
         result <- pullRequestRepository.getObsoleteOpenPullRequests(
           repo,
           update
@@ -129,7 +150,8 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
           url,
           sha1,
           updateInStore,
-          PullRequestState.Open
+          PullRequestState.Open,
+          number
         )
         result <- pullRequestRepository.getObsoleteOpenPullRequests(
           repo,
@@ -153,12 +175,13 @@ class PullRequestRepositoryTest extends AnyFunSuite with Matchers {
     }
   }
 
-  private def withStoreData(f: (Repo, Uri, Sha1) => Unit): Unit = {
+  private def withStoreData(f: (Repo, Uri, Sha1, PullRequestNumber) => Unit): Unit = {
     val repo = Repo("typelevel", "cats")
     val url = uri"https://github.com/typelevel/cats/pull/3291"
     val sha1 = Sha1(HexString("a2ced5793c2832ada8c14ba5c77e51c4bc9656a8"))
+    val number = PullRequestNumber(3291)
 
-    f(repo, url, sha1)
+    f(repo, url, sha1, number)
   }
 }
 
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 b068c5e4..343e36cd 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
@@ -98,6 +98,7 @@ class BitbucketApiAlgTest extends AnyFunSuite with Matchers {
     case POST -> Root / "repositories" / "fthomas" / "base.g8" / "pullrequests" =>
       Ok(
         json"""{
+            "id": 2,
             "title": "scala-steward-pr",
             "state": "OPEN",
             "links": {
@@ -112,6 +113,7 @@ class BitbucketApiAlgTest extends AnyFunSuite with Matchers {
         json"""{
           "values": [
               {
+                  "id": 2,
                   "title": "scala-steward-pr",
                   "state": "OPEN",
                   "links": {
@@ -126,6 +128,7 @@ class BitbucketApiAlgTest extends AnyFunSuite with Matchers {
     case PUT -> Root / "repositories" / "fthomas" / "base.g8" / "pullrequests" / IntVar(_) =>
       Ok(
         json"""{
+            "id": 2,
             "title": "scala-steward-pr",
             "state": "DECLINED",
             "links": {
@@ -170,7 +173,8 @@ class BitbucketApiAlgTest extends AnyFunSuite with Matchers {
     CommitOut(Sha1(HexString("07eb2a203e297c8340273950e98b2cab68b560c1")))
   )
 
-  val pullRequest = PullRequestOut(prUrl, PullRequestState.Open, "scala-steward-pr")
+  val pullRequest =
+    PullRequestOut(prUrl, PullRequestState.Open, PullRequestNumber(2), "scala-steward-pr")
 
   test("createForkOrGetRepo") {
     val repoOut = bitbucketApiAlg.createForkOrGetRepo(repo, doNotFork = false).unsafeRunSync()
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala
new file mode 100644
index 00000000..19e48f00
--- /dev/null
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucketserver/BitbucketServerApiAlgTest.scala
@@ -0,0 +1,48 @@
+package org.scalasteward.core.vcs.bitbucketserver
+
+import cats.effect.IO
+import org.http4s.client.Client
+import org.http4s.dsl.io._
+import org.http4s.implicits._
+import org.http4s.{HttpRoutes, Uri}
+import org.scalasteward.core.git.Branch
+import org.scalasteward.core.mock.MockContext.config
+import org.scalasteward.core.util.HttpJsonClient
+import org.scalasteward.core.vcs.data._
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+
+class BitbucketServerApiAlgTest extends AnyFunSuite with Matchers {
+  private val repo = Repo("scala-steward-org", "scala-steward")
+  private val routes: HttpRoutes[IO] = HttpRoutes.of[IO] {
+    case GET -> Root / "rest" / "api" / "1.0" / "projects" / repo.owner / "repos" / repo.repo / "pull-requests" =>
+      Ok("""{ "values": [
+           |  {
+           |    "id": 123,
+           |    "title": "Update sbt to 1.4.2",
+           |    "state": "open",
+           |    "links": { "self": [ { "href": "http://example.org" } ] }
+           |  }
+           |]}""".stripMargin)
+  }
+
+  implicit private val client: Client[IO] = Client.fromHttpApp(routes.orNotFound)
+  implicit private val httpJsonClient: HttpJsonClient[IO] = new HttpJsonClient[IO]
+  private val bitbucketServerApiAlg: BitbucketServerApiAlg[IO] =
+    new BitbucketServerApiAlg[IO](config.vcsApiHost, _ => IO.pure, useReviewers = false)
+
+  test("listPullRequests") {
+    val pullRequests = bitbucketServerApiAlg
+      .listPullRequests(repo, "update/sbt-1.4.2", Branch("main"))
+      .unsafeRunSync()
+
+    pullRequests shouldBe List(
+      PullRequestOut(
+        Uri.unsafeFromString("http://example.org"),
+        PullRequestState.Open,
+        PullRequestNumber(123),
+        "Update sbt to 1.4.2"
+      )
+    )
+  }
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/data/PullRequestOutTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/data/PullRequestOutTest.scala
index c15e48c8..31143178 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/data/PullRequestOutTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/data/PullRequestOutTest.scala
@@ -14,6 +14,7 @@ class PullRequestOutTest extends AnyFunSuite with Matchers {
         PullRequestOut(
           uri"https://github.com/octocat/Hello-World/pull/1347",
           Open,
+          PullRequestNumber(1347),
           "new-feature"
         )
       )
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 9aa00503..169d5750 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
@@ -41,6 +41,7 @@ class GitHubApiAlgTest extends AnyFunSuite with Matchers {
           json"""[{
             "html_url": "https://github.com/octocat/Hello-World/pull/1347",
             "state": "open",
+            "number": 1347,
             "title": "new-feature"
           }]"""
         )
@@ -50,6 +51,7 @@ class GitHubApiAlgTest extends AnyFunSuite with Matchers {
           json"""{
             "html_url": "https://github.com/octocat/Hello-World/pull/1347",
             "state": "closed",
+            "number": 1347,
             "title": "new-feature"
           }"""
         )
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 7067d7d8..eec078c9 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
@@ -103,6 +103,7 @@ class GitLabApiAlgTest extends AnyFunSuite with Matchers {
     prOut shouldBe PullRequestOut(
       uri"https://gitlab.com/foo/bar/merge_requests/7115",
       PullRequestState.Open,
+      PullRequestNumber(7115),
       "title"
     )
   }
@@ -124,6 +125,7 @@ class GitLabApiAlgTest extends AnyFunSuite with Matchers {
     prOut shouldBe PullRequestOut(
       uri"https://gitlab.com/foo/bar/merge_requests/150",
       PullRequestState.Open,
+      PullRequestNumber(150),
       "title"
     )
   }
@@ -141,6 +143,7 @@ class GitLabApiAlgTest extends AnyFunSuite with Matchers {
     prOut shouldBe PullRequestOut(
       uri"https://gitlab.com/foo/bar/merge_requests/7115",
       PullRequestState.Closed,
+      PullRequestNumber(7115),
       "title"
     )
   }
@@ -163,6 +166,7 @@ class GitLabApiAlgTest extends AnyFunSuite with Matchers {
     prOut shouldBe PullRequestOut(
       uri"https://gitlab.com/foo/bar/merge_requests/150",
       PullRequestState.Open,
+      PullRequestNumber(150),
       "title"
     )
   }
@@ -196,6 +200,7 @@ class GitLabApiAlgTest extends AnyFunSuite with Matchers {
     prOut shouldBe PullRequestOut(
       uri"https://gitlab.com/foo/bar/merge_requests/150",
       PullRequestState.Open,
+      PullRequestNumber(150),
       "title"
     )
   }