Repository: scala-steward-org/scala-steward
Pull request #1686 (2020-10-20 22:12:07)

Add ability to search for updates with different artifact id and / or group id
This builds from the [custom group migrations](https://github.com/scala-steward-org/scala-steward/pull/1298) work. The author there, @kubukoz, mentioned wanting to "tackle changing the artifact name... in another PR", and this is my attempt to do that.

I do not know of open source Scala projects that could be used to test upgrading with a different artifact id, or updating both the group id and artifact id. Ideally, someone in the community would know of such examples so that they can be added to tests here.

At my company, we are using scala-steward and need to be able to update a privately hosted library like this:
```
"com.company" %% "old-artifact-name" % "1.0.0" ->  "com.company" %% "new-artifact-name" % "2.0.0"
```
so that is my primary purpose for this PR.

I opted to rename the cli argument from `--group-migrations` to `--artifact-migrations`, and keep the logic for updating either / both of the artifacts in one place, since I think that was simpler and easier to handle the "both" case. However, this is a backwards incompatible change, so if this is undesired, I could break the artifact id rename into a separate cli argument and logic in code.

codecov[bot]: # [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=h1) Report
> Merging [#1686](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=desc) into [master](https://codecov.io/gh/scala-steward-org/scala-steward/commit/9b9e2132ceac83a8c7de02764605f2d01d173034?el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `89.47%`.

[![Impacted file tree graph](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/graphs/tree.svg?width=650&height=150&src=pr&token=udzMsc1Dne)](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1686      +/-   ##
==========================================
+ Coverage   73.67%   73.70%   +0.02%     
==========================================
  Files         115      115              
  Lines        1941     1947       +6     
  Branches       59       64       +5     
==========================================
+ Hits         1430     1435       +5     
- Misses        511      512       +1     
```


| [Impacted Files](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [.../scala/org/scalasteward/core/application/Cli.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9hcHBsaWNhdGlvbi9DbGkuc2NhbGE=) | `100.00% <ø> (ø)` | |
| [...la/org/scalasteward/core/application/Context.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9hcHBsaWNhdGlvbi9Db250ZXh0LnNjYWxh) | `0.00% <0.00%> (ø)` | |
| [...main/scala/org/scalasteward/core/data/Update.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9kYXRhL1VwZGF0ZS5zY2FsYQ==) | `97.67% <ø> (ø)` | |
| [.../scalasteward/core/update/ArtifactMigrations.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS91cGRhdGUvQXJ0aWZhY3RNaWdyYXRpb25zLnNjYWxh) | `89.28% <91.66%> (ø)` | |
| [...ala/org/scalasteward/core/application/Config.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9hcHBsaWNhdGlvbi9Db25maWcuc2NhbGE=) | `89.65% <100.00%> (ø)` | |
| [...a/org/scalasteward/core/edit/UpdateHeuristic.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L1VwZGF0ZUhldXJpc3RpYy5zY2FsYQ==) | `100.00% <100.00%> (ø)` | |
| [...scala/org/scalasteward/core/update/UpdateAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686/diff?src=pr&el=tree#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS91cGRhdGUvVXBkYXRlQWxnLnNjYWxh) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=footer). Last update [9b9e213...53d09d0](https://codecov.io/gh/scala-steward-org/scala-steward/pull/1686?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.gitignore b/.gitignore
index 82a911a7..f52a4a46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ workspace/
 .bloop/
 .metals/
 metals.sbt
+
+.bsp/
diff --git a/README.md b/README.md
index 384c0ed7..1b2dfba4 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 <p align="center">
-  <img src="https://github.com/fthomas/scala-steward/raw/master/data/images/scala-steward-logo-circle-0.png" height="180px">
+  <img src="https://github.com/scala-steward-org/scala-steward/raw/master/data/images/scala-steward-logo-circle-0.png" height="180px">
 </p>
 
 # Scala Steward
@@ -179,7 +179,7 @@ plugin which was used until version 0.6 to find dependency updates.
 Thanks goes also to [**@impurepics**](https://twitter.com/impurepics)
 for [**@scala-steward**][@scala-steward]'s cute profile picture and to
 the maintainers and contributors of the various
-[libraries](https://github.com/fthomas/scala-steward/blob/master/project/Dependencies.scala)
+[libraries](https://github.com/scala-steward-org/scala-steward/blob/master/project/Dependencies.scala)
 this project depends on.
 
 ## License
@@ -187,6 +187,6 @@ this project depends on.
 Scala Steward is licensed under the
 [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0).
 
-[CoC]: https://github.com/fthomas/scala-steward/blob/master/CODE_OF_CONDUCT.md
+[CoC]: https://github.com/scala-steward-org/scala-steward/blob/master/CODE_OF_CONDUCT.md
 [@scala-steward]: https://github.com/scala-steward
 [sbt-updates]: https://github.com/rtimush/sbt-updates
diff --git a/docs/artifact-migrations.md b/docs/artifact-migrations.md
new file mode 100644
index 00000000..f500936e
--- /dev/null
+++ b/docs/artifact-migrations.md
@@ -0,0 +1,32 @@
+# Artifact Migrations
+
+Scala Steward can look for newer versions of artifacts with different group Ids, artifact ids, or both different.
+
+## Adding artifact migration rules to Scala Steward
+
+By default, scala-steward applies the artifact migrations rules defined in the [default list][migrations]. When running 
+Scala Steward you can also specify a file (via the `--artifact-migrations` command-line option) that contains 
+additional migrations.
+
+These files are in [HOCON][HOCON] format and should look like this:
+```hocon
+changes = [
+  {
+    groupIdBefore = com.geirsson
+    groupIdAfter = org.scalameta
+    artifactIdAfter = sbt-scalafmt
+    initialVersion = 2.0.0
+  }
+]
+```
+In this example, scala-steward will look in the project for dependencies with the before group id, "com.geirsson", and 
+artifact id "sbt-scalafmt". If found, scala-steward will search for updates using the after group id, "org.scalameta", 
+artifact id "sbt-scalafmt", and version greater than the initial version, 2.0.0. If found, scala-steward will update
+to the after group id and latest version.
+
+The fields `groupIdBefore` and `artifactIdBefore` are optional. If just `groupIdBefore` is specified, as in the previous
+example, then only the group id will get renamed. If just `artifactIdBefore` is specified, then only the artifact id
+will get renamed. Specifying both `groupIdBefore` and `artifactIdBefore` will rename both.
+
+[migrations]: https://github.com/scala-steward-org/scala-steward/blob/master/modules/core/src/main/resources/artifact-migrations.conf
+[HOCON]: https://github.com/lightbend/config/blob/master/HOCON.md
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
index c0e4a14b..9309240f 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -59,7 +59,7 @@ a  `scm.url` or an `url` attribute.
 
 You can use [Mergify](https://mergify.io) to automatically merge Scala Steward's
 pull requests. Mergify rules that does this can be found in Scala Steward's own
-repository [here](https://github.com/fthomas/scala-steward/blob/master/.mergify.yml).
+repository [here](https://github.com/scala-steward-org/scala-steward/blob/master/.mergify.yml).
 Mergify can also be configured to only merge patch updates (in case the version
 number adheres to [Semantic Versioning](https://semver.org/)) as demonstrated
 [here](https://github.com/fthomas/refined/blob/master/.mergify.yml).
diff --git a/docs/running.md b/docs/running.md
index 8709c301..57220025 100644
--- a/docs/running.md
+++ b/docs/running.md
@@ -31,7 +31,9 @@ docker run -v $STEWARD_DIR:/opt/scala-steward -it fthomas/scala-steward:latest \
   --vcs-login ${LOGIN} \
   --git-ask-pass "/opt/scala-steward/.github/askpass/$LOGIN.sh" \
   --sign-commits \
-  --env-var FOO=BAR
+  --env-var FOO=BAR \ 
+  --scalafix-migrations "/opt/scala-steward/extra-scalafix-migrations.conf" \
+  --artifact-migrations "/opt/scala-steward/extra-artifact-migrations.conf" 
 ```
 
 The [`git-ask-pass` option](https://git-scm.com/docs/gitcredentials) must specify an executable file (script) that returns (on the stdout),
@@ -41,9 +43,8 @@ The [`git-ask-pass` option](https://git-scm.com/docs/gitcredentials) must specif
 
 **Note about git-ask-pass option**: The provided script must start with a valid shebang like `#!/bin/sh`, see issue [#1374](/../../issues/1374)
 
-
-You can also provide a `--scalafix-migrations` option with the path to a file containing scalafix migrations.
-More information can be found [here][migrations]
+More information about using the `--scalafix-migrations` and `--artifact-migrations` options can be found 
+[here][scalafixmigrations] and [here][artifactmigrations].
 
 ### Workspace
 
@@ -124,8 +125,6 @@ docker run -v $PWD:/opt/scala-steward \
 
 `BITBUCKET_USERNAME=<myuser> BITBUCKET_PASSWORD=<mypass> ./run.sh`
 
-[migrations]: https://github.com/fthomas/scala-steward/blob/master/docs/scalafix-migrations.md
-
 ### Running On-premise
 
 #### GitHub Enterprise
@@ -196,3 +195,7 @@ echo "${SCALA_STEWARD_TOKEN}"
 ```
 7. add the `repos.md` file 
 8. (*optional*) create a new schedule to trigger the pipeline on a daily/weekly basis
+
+
+[scalafixmigrations]: https://github.com/scala-steward-org/scala-steward/blob/master/docs/scalafix-migrations.md
+[artifactmigrations]: https://github.com/scala-steward-org/scala-steward/blob/master/docs/artifact-migrations.md
diff --git a/docs/scalafix-migrations.md b/docs/scalafix-migrations.md
index d1da74f8..dbca3dc3 100644
--- a/docs/scalafix-migrations.md
+++ b/docs/scalafix-migrations.md
@@ -60,10 +60,10 @@ migrations = [
 Pull requests that added migration rules can be found [here][scalafix-prs].
 
 [Scalafix]: https://scalacenter.github.io/scalafix/
-[migrations]: https://github.com/fthomas/scala-steward/blob/master/modules/core/src/main/resources/scalafix-migrations.conf
+[migrations]: https://github.com/scala-steward-org/scala-steward/blob/master/modules/core/src/main/resources/scalafix-migrations.conf
 [scalafix-dev-guide]: https://scalacenter.github.io/scalafix/docs/developers/setup.html
 [using-dependency]: https://scalacenter.github.io/scalafix/docs/rules/external-rules.html
 [using-github]: https://scalacenter.github.io/scalafix/docs/developers/sharing-rules.html#using-github
 [using-http]: https://scalacenter.github.io/scalafix/docs/developers/sharing-rules.html#using-http
-[scalafix-prs]: https://github.com/fthomas/scala-steward/pulls?q=label%3Ascalafix-migration
+[scalafix-prs]: https://github.com/scala-steward-org/scala-steward/pulls?q=label%3Ascalafix-migration
 [HOCON]: https://github.com/lightbend/config/blob/master/HOCON.md
diff --git a/modules/core/src/main/resources/artifact-migrations.conf b/modules/core/src/main/resources/artifact-migrations.conf
new file mode 100644
index 00000000..e43a1ff0
--- /dev/null
+++ b/modules/core/src/main/resources/artifact-migrations.conf
@@ -0,0 +1,26 @@
+changes = [
+  {
+    groupIdBefore = com.geirsson
+    groupIdAfter = org.scalameta
+    artifactIdAfter = sbt-scalafmt
+    initialVersion = 2.0.0
+  },
+  {
+    groupIdBefore = com.github.mpilquist
+    groupIdAfter = org.typelevel
+    artifactIdAfter = simulacrum
+    initialVersion = 1.0.0
+  },
+  {
+    groupIdBefore = net.ceedubs
+    groupIdAfter = com.iheart
+    artifactIdAfter = ficus
+    initialVersion = 1.3.4
+  },
+  {
+    groupIdBefore = org.spire-math
+    groupIdAfter = org.typelevel
+    artifactIdAfter = kind-projector
+    initialVersion = 0.10.0
+  }
+]
diff --git a/modules/core/src/main/resources/group-migrations.conf b/modules/core/src/main/resources/group-migrations.conf
deleted file mode 100644
index ebf7c797..00000000
--- a/modules/core/src/main/resources/group-migrations.conf
+++ /dev/null
@@ -1,26 +0,0 @@
-changes = [
-  {
-    before = com.geirsson
-    after = org.scalameta
-    artifactId = sbt-scalafmt
-    initialVersion = 2.0.0
-  },
-  {
-    before = com.github.mpilquist
-    after = org.typelevel
-    artifactId = simulacrum
-    initialVersion = 1.0.0
-  },
-  {
-    before = net.ceedubs
-    after = com.iheart
-    artifactId = ficus
-    initialVersion = 1.3.4
-  },
-  {
-    before = org.spire-math
-    after = org.typelevel
-    artifactId = kind-projector
-    initialVersion = 0.10.0
-  }
-]
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
index 16017fb4..b77a72e6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Cli.scala
@@ -47,7 +47,7 @@ object Cli {
       processTimeout: FiniteDuration = 10.minutes,
       scalafixMigrations: List[Uri] = Nil,
       disableDefaultScalafixMigrations: Boolean = false,
-      groupMigrations: Option[File] = None,
+      artifactMigrations: Option[File] = None,
       cacheTtl: FiniteDuration = 2.hours,
       bitbucketServerUseDefaultReviewers: Boolean = false,
       gitlabMergeWhenPipelineSucceeds: Boolean = false
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala
index cc7c1fac..eeaf0f75 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Config.scala
@@ -66,7 +66,7 @@ final case class Config(
     ignoreOptsFiles: Boolean,
     processCfg: ProcessCfg,
     scalafixCfg: ScalafixCfg,
-    groupMigrations: Option[File],
+    artifactMigrations: Option[File],
     cacheTtl: FiniteDuration,
     bitbucketServerUseDefaultReviewers: Boolean,
     gitlabMergeWhenPipelineSucceeds: Boolean
@@ -125,7 +125,7 @@ object Config {
         migrations = args.scalafixMigrations,
         disableDefaults = args.disableDefaultScalafixMigrations
       ),
-      groupMigrations = args.groupMigrations,
+      artifactMigrations = args.artifactMigrations,
       cacheTtl = args.cacheTtl,
       bitbucketServerUseDefaultReviewers = args.bitbucketServerUseDefaultReviewers,
       gitlabMergeWhenPipelineSucceeds = args.gitlabMergeWhenPipelineSucceeds
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
index 480538f8..927232ff 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala
@@ -37,7 +37,7 @@ import org.scalasteward.core.repocache.{RefreshErrorAlg, RepoCacheAlg, RepoCache
 import org.scalasteward.core.repoconfig.RepoConfigAlg
 import org.scalasteward.core.scalafix.{MigrationAlg, MigrationsLoader}
 import org.scalasteward.core.scalafmt.ScalafmtAlg
-import org.scalasteward.core.update.{FilterAlg, GroupMigrations, PruningAlg, UpdateAlg}
+import org.scalasteward.core.update.{ArtifactMigrations, FilterAlg, PruningAlg, UpdateAlg}
 import org.scalasteward.core.util._
 import org.scalasteward.core.util.uri._
 import org.scalasteward.core.vcs.data.AuthenticatedUser
@@ -58,7 +58,9 @@ object Context {
       implicit0(fileAlg: FileAlg[F]) = FileAlg.create[F]
       implicit0(migrationAlg: MigrationAlg) <-
         Resource.liftF(new MigrationsLoader[F].loadAll(config.scalafixCfg).map(new MigrationAlg(_)))
-      implicit0(groupMigration: GroupMigrations) <- Resource.liftF(GroupMigrations.create[F])
+      implicit0(artifactMigration: ArtifactMigrations) <- Resource.liftF(
+        ArtifactMigrations.create[F]
+      )
     } yield {
       val kvsPrefix = Some(config.vcsType.asString)
       implicit val dateTimeAlg: DateTimeAlg[F] = DateTimeAlg.create[F]
diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
index 2a772899..e08f748c 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/data/Update.scala
@@ -55,7 +55,7 @@ sealed trait Update extends Product with Serializable {
   }
 
   def withNewerVersions(versions: Nel[String]): Update = this match {
-    case s @ Single(_, _, _) =>
+    case s @ Single(_, _, _, _) =>
       s.copy(newerVersions = versions)
     case g @ Group(_, _) =>
       g.copy(newerVersions = versions)
@@ -66,7 +66,8 @@ object Update {
   final case class Single(
       crossDependency: CrossDependency,
       newerVersions: Nel[String],
-      newerGroupId: Option[GroupId] = None
+      newerGroupId: Option[GroupId] = None,
+      newerArtifactId: Option[String] = None
   ) extends Update {
     override def crossDependencies: Nel[CrossDependency] =
       Nel.one(crossDependency)
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 e67fe94a..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
@@ -52,19 +52,19 @@ object UpdateHeuristic {
   private def shouldBeIgnored(prefix: String): Boolean =
     prefix.toLowerCase.contains("previous") || prefix.trim.startsWith("//")
 
-  private def replaceGroupF(update: Update): String => Option[String] = { target =>
+  private def replaceArtifactF(update: Update): String => Option[String] = { target =>
     update match {
-      case s @ Update.Single(_, _, Some(newerGroupId)) =>
+      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,
           match0 => {
             val group1 = match0.group(1)
             val group2 = match0.group(2)
-            Some(s"""$group1$newerGroupId$group2""")
+            Some(s"""$group1$newerGroupId$group2$newerArtifactId""")
           }
         ).someIfChanged
       case _ => Some(target)
@@ -86,7 +86,7 @@ object UpdateHeuristic {
       }
 
     def replaceF(update: Update): String => Option[String] =
-      target => replaceVersionF(update)(target) >>= replaceGroupF(update)
+      target => replaceVersionF(update)(target) >>= replaceArtifactF(update)
 
     def replaceVersionF(update: Update): String => Option[String] =
       mkRegex(update).fold((_: String) => Option.empty[String]) { regex => target =>
@@ -151,7 +151,7 @@ object UpdateHeuristic {
                 )
             }
           ).someIfChanged
-        } >>= replaceGroupF(update)
+        } >>= replaceArtifactF(update)
   )
 
   val strict = UpdateHeuristic(
diff --git a/modules/core/src/main/scala/org/scalasteward/core/gitlab/http4s/Http4sGitlabApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/gitlab/http4s/Http4sGitlabApiAlg.scala
index 0559c63c..925a1f44 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/gitlab/http4s/Http4sGitlabApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/gitlab/http4s/Http4sGitlabApiAlg.scala
@@ -143,7 +143,7 @@ class Http4sGitLabApiAlg[F[_]](
       .recoverWith {
         case UnexpectedResponse(_, _, _, Status.Conflict, _) => getRepo(userOwnedRepo)
         // workaround for https://gitlab.com/gitlab-org/gitlab-ce/issues/65275
-        // see also https://github.com/fthomas/scala-steward/pull/761
+        // see also https://github.com/scala-steward-org/scala-steward/pull/761
         case UnexpectedResponse(_, _, _, Status.NotFound, _) => getRepo(userOwnedRepo)
       }
   }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/update/GroupMigrations.scala b/modules/core/src/main/scala/org/scalasteward/core/update/ArtifactMigrations.scala
similarity index 54%
rename from modules/core/src/main/scala/org/scalasteward/core/update/GroupMigrations.scala
rename to modules/core/src/main/scala/org/scalasteward/core/update/ArtifactMigrations.scala
index 23883edd..3b96d9c8 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/update/GroupMigrations.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/update/ArtifactMigrations.scala
@@ -25,77 +25,92 @@ import org.scalasteward.core.data._
 import org.scalasteward.core.io.FileAlg
 import org.scalasteward.core.util.{MonadThrow, Nel}
 
-trait GroupMigrations {
-  def findUpdateWithNewerGroupId(dependency: Dependency): Option[Update.Single]
+trait ArtifactMigrations {
+  def findUpdateWithRenamedArtifact(dependency: Dependency): Option[Update.Single]
 }
 
-object GroupMigrations {
+object ArtifactMigrations {
   def create[F[_]: MonadThrow](implicit
       fileAlg: FileAlg[F],
       config: Config
-  ): F[GroupMigrations] = {
+  ): F[ArtifactMigrations] = {
     val migrationSources = {
 
       val fromParameters: F[Option[String]] =
-        config.groupMigrations.traverse(fileAlg.readFile(_)).flatMap {
+        config.artifactMigrations.traverse(fileAlg.readFile).flatMap {
           case None => none[String].pure[F]
           case Some(None) =>
-            new Throwable("Couldn't read the file with custom group migrations")
+            new Throwable("Couldn't read the file with custom artifact migrations")
               .raiseError[F, Option[String]]
           case Some(Some(text)) => text.some.pure[F]
         }
 
       List(
-        fileAlg.readResource("group-migrations.conf").map(List(_)),
+        fileAlg.readResource("artifact-migrations.conf").map(List(_)),
         fromParameters.map(_.toList)
       ).flatSequence
     }
 
-    val decodeFile: String => F[GroupIdChanges] = parser
-      .decode[GroupIdChanges](_)
+    val decodeFile: String => F[ArtifactChanges] = parser
+      .decode[ArtifactChanges](_)
       .leftMap(e => new Throwable("Couldn't decode migrations file", e))
       .liftTo[F]
 
     migrationSources
       .flatMap(_.traverse(decodeFile))
       .map(_.flatMap(_.changes))
-      .map(migrations => dependency => migrateGroupId(dependency, migrations))
+      .map(migrations => dependency => migrateArtifact(dependency, migrations))
   }
 
-  def migrateGroupId(
+  def migrateArtifact(
       dependency: Dependency,
-      migrations: List[GroupIdChange]
+      migrations: List[ArtifactChange]
   ): Option[Update.Single] =
     migrations.view
-      .filter(_.before === dependency.groupId)
-      .find(_.artifactId === dependency.artifactId.name)
+      .find { migration =>
+        (migration.groupIdBefore, migration.artifactIdBefore) match {
+          case (Some(groupId), Some(artifactId)) =>
+            groupId === dependency.groupId &&
+              artifactId === dependency.artifactId.name
+          case (Some(groupId), _) =>
+            groupId === dependency.groupId &&
+              migration.artifactIdAfter === dependency.artifactId.name
+          case (_, Some(artifactId)) =>
+            migration.groupIdAfter === dependency.groupId &&
+              artifactId === dependency.artifactId.name
+          case _ => false
+        }
+      }
       .map { migration =>
         Update.Single(
           CrossDependency(dependency),
           Nel.one(migration.initialVersion),
-          Some(migration.after)
+          Some(migration.groupIdAfter),
+          Some(migration.artifactIdAfter)
         )
       }
 
   implicit val customConfig: Configuration =
     Configuration.default.withDefaults
 
-  final case class GroupIdChanges(changes: List[GroupIdChange])
-  object GroupIdChanges {
-    implicit val decoder: Decoder[GroupIdChanges] =
+  final case class ArtifactChanges(changes: List[ArtifactChange])
+
+  object ArtifactChanges {
+    implicit val decoder: Decoder[ArtifactChanges] =
       semiauto.deriveConfiguredDecoder
   }
 
-  final case class GroupIdChange(
-      before: GroupId,
-      after: GroupId,
-      artifactId: String,
+  final case class ArtifactChange(
+      groupIdBefore: Option[GroupId],
+      groupIdAfter: GroupId,
+      artifactIdBefore: Option[String],
+      artifactIdAfter: String,
       initialVersion: String
   )
 
-  object GroupIdChange {
+  object ArtifactChange {
 
-    implicit val decoder: Decoder[GroupIdChange] =
+    implicit val decoder: Decoder[ArtifactChange] =
       semiauto.deriveConfiguredDecoder
   }
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
index 3cd8b7a6..a668dc25 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/update/FilterAlg.scala
@@ -129,7 +129,7 @@ object FilterAlg {
         ).contains
       case ("com.nequissimus", "sort-imports_2.12") =>
         List(
-          // https://github.com/fthomas/scala-steward/issues/1413
+          // https://github.com/scala-steward-org/scala-steward/issues/1413
           "36845576"
         ).contains
       case ("commons-collections", "commons-collections") =>
@@ -143,7 +143,7 @@ object FilterAlg {
         ).contains
       case ("io.monix", _) =>
         List(
-          // https://github.com/fthomas/scala-steward/issues/105
+          // https://github.com/scala-steward-org/scala-steward/issues/105
           "3.0.0-fbcb270"
         ).contains
       case ("net.sourceforge.plantuml", "plantuml") =>
diff --git a/modules/core/src/main/scala/org/scalasteward/core/update/UpdateAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/update/UpdateAlg.scala
index a516d829..c2043b9a 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/update/UpdateAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/update/UpdateAlg.scala
@@ -27,7 +27,7 @@ import scala.concurrent.duration.FiniteDuration
 final class UpdateAlg[F[_]](implicit
     filterAlg: FilterAlg[F],
     versionsCache: VersionsCache[F],
-    groupMigrations: GroupMigrations,
+    artifactMigrations: ArtifactMigrations,
     F: Monad[F]
 ) {
   def findUpdate(
@@ -40,7 +40,7 @@ final class UpdateAlg[F[_]](implicit
       maybeNewerVersions = Nel.fromList(versions.filter(_ > current))
       maybeUpdate = maybeNewerVersions
         .map(vs => Update.Single(CrossDependency(dependency.value), vs.map(_.value)))
-        .orElse(groupMigrations.findUpdateWithNewerGroupId(dependency.value))
+        .orElse(artifactMigrations.findUpdateWithRenamedArtifact(dependency.value))
     } yield maybeUpdate
 
   def findUpdates(
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala
index 8b991a76..4b0a0501 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/NewPullRequestData.scala
@@ -65,7 +65,7 @@ object NewPullRequestData {
         |
         |If you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.
         |
-        |Configure Scala Steward for your repository with a [`${RepoConfigAlg.repoConfigBasename}`](https://github.com/fthomas/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.
+        |Configure Scala Steward for your repository with a [`${RepoConfigAlg.repoConfigBasename}`](https://github.com/scala-steward-org/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.
         |
         |Have a fantastic day writing Scala!
         |


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala
index a8813728..7fa1f25f 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/UpdateHeuristicTest.scala
@@ -160,7 +160,8 @@ class UpdateHeuristicTest extends AnyFunSuite with Matchers {
     Single(
       "org.spire-math" % "kind-projector" % "0.9.0",
       Nel.of("0.10.0"),
-      Some(GroupId("org.typelevel"))
+      Some(GroupId("org.typelevel")),
+      Some("kind-projector")
     ).replaceVersionIn(original) shouldBe (Some(expected) -> UpdateHeuristic.moduleId.name)
   }
 
diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
index da4c17cd..bd03b82c 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
@@ -21,7 +21,7 @@ import org.scalasteward.core.repocache.RepoCacheRepository
 import org.scalasteward.core.repoconfig.RepoConfigAlg
 import org.scalasteward.core.scalafix.{MigrationAlg, MigrationsLoader, MigrationsLoaderTest}
 import org.scalasteward.core.scalafmt.ScalafmtAlg
-import org.scalasteward.core.update.{FilterAlg, GroupMigrations, PruningAlg, UpdateAlg}
+import org.scalasteward.core.update.{ArtifactMigrations, FilterAlg, PruningAlg, UpdateAlg}
 import org.scalasteward.core.util.uri._
 import org.scalasteward.core.util.{BracketThrow, DateTimeAlg}
 import org.scalasteward.core.vcs.VCSRepoAlg
@@ -75,8 +75,8 @@ object MockContext {
   implicit val filterAlg: FilterAlg[MockEff] = new FilterAlg[MockEff]
   implicit val versionsCache: VersionsCache[MockEff] =
     new VersionsCache[MockEff](config.cacheTtl, new JsonKeyValueStore("versions", "1"))
-  implicit val groupMigrations: GroupMigrations =
-    GroupMigrations.create[MockEff].runA(MockState.empty).unsafeRunSync()
+  implicit val artifactMigrations: ArtifactMigrations =
+    ArtifactMigrations.create[MockEff].runA(MockState.empty).unsafeRunSync()
   implicit val updateAlg: UpdateAlg[MockEff] = new UpdateAlg[MockEff]
   implicit val mavenAlg: MavenAlg[MockEff] = MavenAlg.create
   implicit val sbtAlg: SbtAlg[MockEff] = SbtAlg.create
diff --git a/modules/core/src/test/scala/org/scalasteward/core/update/ArtifactMigrationsTest.scala b/modules/core/src/test/scala/org/scalasteward/core/update/ArtifactMigrationsTest.scala
new file mode 100644
index 00000000..8eab2be3
--- /dev/null
+++ b/modules/core/src/test/scala/org/scalasteward/core/update/ArtifactMigrationsTest.scala
@@ -0,0 +1,117 @@
+package org.scalasteward.core.update
+
+import org.scalasteward.core.TestSyntax._
+import org.scalasteward.core.data.{ArtifactId, GroupId, Update}
+import org.scalasteward.core.mock.MockContext._
+import org.scalasteward.core.mock.MockState
+import org.scalasteward.core.update.ArtifactMigrations.ArtifactChange
+import org.scalasteward.core.util.Nel
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+
+class ArtifactMigrationsTest extends AnyFunSuite with Matchers {
+
+  val standardGroupMigrations = List(
+    ArtifactChange(
+      groupIdBefore = Some(GroupId("org.spire-math")),
+      groupIdAfter = GroupId("org.typelevel"),
+      artifactIdBefore = None,
+      artifactIdAfter = "kind-projector",
+      initialVersion = "0.10.0"
+    )
+  )
+
+  val standardArtifactMigrations = List(
+    ArtifactChange(
+      groupIdBefore = None,
+      groupIdAfter = GroupId("com.nodifferent"),
+      artifactIdBefore = Some("artifact-before"),
+      artifactIdAfter = "artifact-after",
+      initialVersion = "0.10.0"
+    )
+  )
+
+  val standardBothMigrations = List(
+    ArtifactChange(
+      groupIdBefore = Some(GroupId("org.before")),
+      groupIdAfter = GroupId("org.after"),
+      artifactIdBefore = Some("artifact-before"),
+      artifactIdAfter = "artifact-after",
+      initialVersion = "0.10.0"
+    )
+  )
+
+  test("migrateArtifact: for groupId, returns empty if artifactIdAfter is not listed") {
+    val original = "org.spire-math" % ArtifactId("UNKNOWN", "UNKNOWN_2.12") % "1.0.0"
+    ArtifactMigrations.migrateArtifact(original, standardGroupMigrations) shouldBe None
+  }
+
+  test("migrateArtifact: for artifactId, returns empty if groupIdAfter is not listed") {
+    val original = "UNKNOWN" % ArtifactId("artifact-before", "artifact-before_2.12") % "1.0.0"
+    ArtifactMigrations.migrateArtifact(original, standardArtifactMigrations) shouldBe None
+  }
+
+  test(
+    "migrateArtifact: for both groupId and artifactId, returns empty if either of groupIdBefore or " +
+      "artifactIdBefore are not listed"
+  ) {
+    val original1 = "UNKNOWN" % ArtifactId("artifact-before", "artifact-before_2.12") % "1.0.0"
+    ArtifactMigrations.migrateArtifact(original1, standardBothMigrations) shouldBe None
+
+    val original2 = "org.before" % ArtifactId("UNKNOWN", "UNKNOWN_2.12") % "1.0.0"
+    ArtifactMigrations.migrateArtifact(original2, standardBothMigrations) shouldBe None
+  }
+
+  test("migrateArtifact: for groupId, returns Update.Single for updating groupId") {
+    val original = "org.spire-math" % ArtifactId("kind-projector", "kind-projector_2.12") % "0.9.0"
+    val expected = Update.Single(
+      original,
+      Nel.of("0.10.0"),
+      Some(GroupId("org.typelevel")),
+      Some("kind-projector")
+    )
+    ArtifactMigrations.migrateArtifact(original, standardGroupMigrations) shouldBe Some(expected)
+  }
+
+  test("migrateArtifact: for artifactId, returns Update.Single for updating artifactId") {
+    val original =
+      "com.nodifferent" % ArtifactId("artifact-before", "artifact-before_2.12") % "0.9.0"
+    val expected = Update.Single(
+      original,
+      Nel.of("0.10.0"),
+      Some(GroupId("com.nodifferent")),
+      Some("artifact-after")
+    )
+    ArtifactMigrations.migrateArtifact(original, standardArtifactMigrations) shouldBe Some(expected)
+  }
+
+  test(
+    "migrateArtifact: for both groupId and artifactId, returns Update.Single for updating both groupId " +
+      "and artifactId"
+  ) {
+    val original = "org.before" % ArtifactId("artifact-before", "artifact-before_2.12") % "0.9.0"
+    val expected = Update.Single(
+      original,
+      Nel.of("0.10.0"),
+      Some(GroupId("org.after")),
+      Some("artifact-after")
+    )
+    ArtifactMigrations.migrateArtifact(original, standardBothMigrations) shouldBe Some(expected)
+  }
+
+  test("findUpdate: newer groupId") {
+    val dependency =
+      "org.spire-math" % ArtifactId("kind-projector", "kind-projector_2.12") % "0.9.10"
+    val expected = Update.Single(
+      "org.spire-math" % ArtifactId("kind-projector", "kind-projector_2.12") % "0.9.10",
+      Nel.of("0.10.0"),
+      Some(GroupId("org.typelevel")),
+      Some("kind-projector")
+    )
+    val actual = updateAlg
+      .findUpdate(dependency.withMavenCentral, None)
+      .runA(MockState.empty)
+      .unsafeRunSync()
+    actual shouldBe Some(expected)
+  }
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/update/GroupMigrationsTest.scala b/modules/core/src/test/scala/org/scalasteward/core/update/GroupMigrationsTest.scala
deleted file mode 100644
index c8c8bb8c..00000000
--- a/modules/core/src/test/scala/org/scalasteward/core/update/GroupMigrationsTest.scala
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.scalasteward.core.update
-
-import org.scalasteward.core.TestSyntax._
-import org.scalasteward.core.data.{ArtifactId, GroupId, Update}
-import org.scalasteward.core.mock.MockContext._
-import org.scalasteward.core.mock.MockState
-import org.scalasteward.core.update.GroupMigrations.GroupIdChange
-import org.scalasteward.core.util.Nel
-import org.scalatest.funsuite.AnyFunSuite
-import org.scalatest.matchers.should.Matchers
-
-class GroupMigrationsTest extends AnyFunSuite with Matchers {
-
-  val standardGroupMigrations = List(
-    GroupIdChange(
-      GroupId("org.spire-math"),
-      GroupId("org.typelevel"),
-      "kind-projector",
-      "0.10.0"
-    )
-  )
-
-  test("migrateGroupId: returns empty if dep is not listed") {
-    val original = "org.spire-math" % ArtifactId("UNKNOWN", "UNKNOWN_2.12") % "1.0.0"
-    GroupMigrations.migrateGroupId(original, standardGroupMigrations) shouldBe None
-  }
-
-  test("migrateGroupId: returns Update.Single for updating groupId") {
-    val original = "org.spire-math" % ArtifactId("kind-projector", "kind-projector_2.12") % "0.9.0"
-    val expected = Update.Single(
-      original,
-      Nel.of("0.10.0"),
-      Some(GroupId("org.typelevel"))
-    )
-    GroupMigrations.migrateGroupId(original, standardGroupMigrations) shouldBe Some(expected)
-  }
-
-  test("findUpdate: newer groupId") {
-    val dependency =
-      "org.spire-math" % ArtifactId("kind-projector", "kind-projector_2.12") % "0.9.10"
-    val expected = Update.Single(
-      "org.spire-math" % ArtifactId("kind-projector", "kind-projector_2.12") % "0.9.10",
-      Nel.of("0.10.0"),
-      Some(GroupId("org.typelevel"))
-    )
-    val actual = updateAlg
-      .findUpdate(dependency.withMavenCentral, None)
-      .runA(MockState.empty)
-      .unsafeRunSync()
-    actual shouldBe Some(expected)
-  }
-}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala
index 77f22842..2917ba36 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/data/NewPullRequestDataTest.scala
@@ -30,7 +30,7 @@ class NewPullRequestDataTest extends AnyFunSuite with Matchers {
       .spaces2 shouldBe
       raw"""|{
             |  "title" : "Update logback-classic to 1.2.3",
-            |  "body" : "Updates ch.qos.logback:logback-classic from 1.2.0 to 1.2.3.\n\n\nI'll automatically update this PR to resolve conflicts as long as you don't change it yourself.\n\nIf you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.\n\nConfigure Scala Steward for your repository with a [`.scala-steward.conf`](https://github.com/fthomas/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.\n\nHave a fantastic day writing Scala!\n\n<details>\n<summary>Ignore future updates</summary>\n\nAdd this to your `.scala-steward.conf` file to ignore future updates of this dependency:\n```\nupdates.ignore = [ { groupId = \"ch.qos.logback\", artifactId = \"logback-classic\" } ]\n```\n</details>\n\nlabels: library-update, semver-patch",
+            |  "body" : "Updates ch.qos.logback:logback-classic from 1.2.0 to 1.2.3.\n\n\nI'll automatically update this PR to resolve conflicts as long as you don't change it yourself.\n\nIf you'd like to skip this version, you can just close this PR. If you have any feedback, just mention me in the comments below.\n\nConfigure Scala Steward for your repository with a [`.scala-steward.conf`](https://github.com/scala-steward-org/scala-steward/blob/${org.scalasteward.core.BuildInfo.gitHeadCommit}/docs/repo-specific-configuration.md) file.\n\nHave a fantastic day writing Scala!\n\n<details>\n<summary>Ignore future updates</summary>\n\nAdd this to your `.scala-steward.conf` file to ignore future updates of this dependency:\n```\nupdates.ignore = [ { groupId = \"ch.qos.logback\", artifactId = \"logback-classic\" } ]\n```\n</details>\n\nlabels: library-update, semver-patch",
             |  "head" : "scala-steward:update/logback-classic-1.2.3",
             |  "base" : "master"
             |}""".stripMargin