Repository: scala-steward-org/scala-steward
Pull request #2328 (2021-11-02 19:30:11)

Allow running Scalafix migrations after version bump
This allows to run Scalafix migrations after the version bump. To run a
migration after the version bump, the `executionOrder` field has to be
set to `"post-update"`. The effective default value for this field is
`"pre-update"`.

Post-update migrations are useful if they consist of multiple rules
that only produce compiling code if the version is bumped before. One
example is the FS2 migration added here that consists of multiple
`replace:` rules. The symbols it renames are still present after the
bump to > 3.0.7. So it is OK to apply them successively after the
version bump but it would lead to compile errors if they were applied
before the bump because each rule could produce code that does not
compile with the old version. Running subsequent rules would then fail
with compile errors.

Here is an example PR where two rules where applied and that would have
failed without being `post-update`: https://github.com/scala-steward-org/test-repo-2/pull/71

codecov[bot]: # [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) Report
> Merging [#2328](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) (7c69400) into [master](https://codecov.io/gh/scala-steward-org/scala-steward/commit/8e4970db22ed448af068371a1e7cac4f1ea594b8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) (8e4970d) will **increase** coverage by `0.00%`.
> The diff coverage is `84.21%`.

[![Impacted file tree graph](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328/graphs/tree.svg?width=650&height=150&src=pr&token=udzMsc1Dne&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)

```diff
@@           Coverage Diff           @@
##           master    #2328   +/-   ##
=======================================
  Coverage   79.71%   79.72%           
=======================================
  Files         137      136    -1     
  Lines        2376     2382    +6     
  Branches       59       64    +5     
=======================================
+ Hits         1894     1899    +5     
- Misses        482      483    +1     
```


| [Impacted Files](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) | Coverage Δ | |
|---|---|---|
| [...steward/core/edit/scalafix/ScalafixMigration.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L3NjYWxhZml4L1NjYWxhZml4TWlncmF0aW9uLnNjYWxh) | `80.00% <57.14%> (-8.89%)` | :arrow_down: |
| [...ain/scala/org/scalasteward/core/edit/EditAlg.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L0VkaXRBbGcuc2NhbGE=) | `94.59% <100.00%> (+0.15%)` | :arrow_up: |
| [.../core/edit/scalafix/ScalafixMigrationsFinder.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9jb3JlL3NyYy9tYWluL3NjYWxhL29yZy9zY2FsYXN0ZXdhcmQvY29yZS9lZGl0L3NjYWxhZml4L1NjYWxhZml4TWlncmF0aW9uc0ZpbmRlci5zY2FsYQ==) | `100.00% <100.00%> (ø)` | |
| [...la-2.12/org/scalasteward/mill/plugin/package.scala](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org#diff-bW9kdWxlcy9taWxsLXBsdWdpbi9zcmMvbWFpbi9zY2FsYS0yLjEyL29yZy9zY2FsYXN0ZXdhcmQvbWlsbC9wbHVnaW4vcGFja2FnZS5zY2FsYQ==) | | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org). Last update [8e4970d...7c69400](https://codecov.io/gh/scala-steward-org/scala-steward/pull/2328?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fd75fcc7..4b9b3575 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -59,7 +59,7 @@ jobs:
         uses: codecov/codecov-action@v1
 
       - name: Compress target directories
-        run: tar cf targets.tar target modules/mill-plugin/.jvm/target modules/sbt-plugin/.jvm/target modules/core/.jvm/target modules/benchmark/.jvm/target project/target
+        run: tar cf targets.tar target modules/docs/.jvm/target modules/mill-plugin/.jvm/target modules/sbt-plugin/.jvm/target modules/core/.jvm/target modules/benchmark/.jvm/target project/target
 
       - name: Upload target directories
         uses: actions/upload-artifact@v2
diff --git a/build.sbt b/build.sbt
index 11d1b458..3c895c65 100644
--- a/build.sbt
+++ b/build.sbt
@@ -7,10 +7,13 @@ val groupId = "org.scala-steward"
 val projectName = "scala-steward"
 val rootPkg = groupId.replace("-", "")
 val gitHubOwner = "scala-steward-org"
+val gitHubUrl = s"https://github.com/$gitHubOwner/$projectName"
+val mainBranch = "master"
 
 val moduleCrossPlatformMatrix: Map[String, List[Platform]] = Map(
   "benchmark" -> List(JVMPlatform),
   "core" -> List(JVMPlatform),
+  "docs" -> List(JVMPlatform),
   "sbt-plugin" -> List(JVMPlatform),
   "mill-plugin" -> List(JVMPlatform)
 )
@@ -23,7 +26,7 @@ val Scala213 = "2.13.5"
 ThisBuild / crossScalaVersions := Seq(Scala212, Scala213)
 ThisBuild / githubWorkflowTargetTags ++= Seq("v*")
 ThisBuild / githubWorkflowPublishTargetBranches := Seq(
-  RefPredicate.Equals(Ref.Branch("master")),
+  RefPredicate.Equals(Ref.Branch(mainBranch)),
   RefPredicate.StartsWith(Ref.Tag("v"))
 )
 ThisBuild / githubWorkflowPublish := Seq(
@@ -56,7 +59,7 @@ ThisBuild / githubWorkflowBuild :=
 
 lazy val root = project
   .in(file("."))
-  .aggregate(benchmark.jvm, core.jvm, `sbt-plugin`.jvm, `mill-plugin`.jvm)
+  .aggregate(benchmark.jvm, core.jvm, docs.jvm, `sbt-plugin`.jvm, `mill-plugin`.jvm)
   .settings(commonSettings)
   .settings(noPublishSettings)
 
@@ -135,7 +138,9 @@ lazy val core = myCrossProject("core")
       scalaVersion,
       scalaBinaryVersion,
       sbtVersion,
-      BuildInfoKey.map(git.gitHeadCommit) { case (k, v) => k -> v.getOrElse("master") },
+      BuildInfoKey("gitHubUrl" -> gitHubUrl),
+      BuildInfoKey("mainBranch" -> mainBranch),
+      BuildInfoKey.map(git.gitHeadCommit) { case (k, v) => k -> v.getOrElse(mainBranch) },
       BuildInfoKey.map(`sbt-plugin`.jvm / moduleRootPkg) { case (_, v) =>
         "sbtPluginModuleRootPkg" -> v
       },
@@ -178,6 +183,34 @@ lazy val core = myCrossProject("core")
     Compile / unmanagedResourceDirectories ++= (`sbt-plugin`.jvm / Compile / unmanagedSourceDirectories).value
   )
 
+lazy val docs = myCrossProject("docs")
+  .dependsOn(core)
+  .enablePlugins(MdocPlugin)
+  .settings(noPublishSettings)
+  .settings(
+    mdocIn := baseDirectory.value / ".." / "mdoc",
+    mdocOut := (LocalRootProject / baseDirectory).value / "docs",
+    mdocVariables := Map(
+      "GITHUB_URL" -> gitHubUrl,
+      "MAIN_BRANCH" -> mainBranch
+    ),
+    checkDocs := {
+      val inDir = mdocIn.value.getCanonicalPath
+      val outDir = mdocOut.value.getCanonicalPath
+      val rootDir = (LocalRootProject / baseDirectory).value
+      try git.runner.value.apply("diff", "--quiet", outDir)(rootDir, streams.value.log)
+      catch {
+        case t: Throwable =>
+          val msg = s"Docs in $inDir and $outDir are out of sync." +
+            " Run 'sbt docs/mdoc' and commit the changes to fix this."
+          throw new Throwable(msg, t)
+      }
+      ()
+    },
+    coverageEnabled := false,
+    unusedCompileDependencies := Set.empty
+  )
+
 lazy val `sbt-plugin` = myCrossProject("sbt-plugin")
   .settings(noPublishSettings)
   .settings(
@@ -230,12 +263,10 @@ lazy val compileSettings = Def.settings(
 lazy val metadataSettings = Def.settings(
   name := projectName,
   organization := groupId,
-  homepage := Some(url(s"https://github.com/$gitHubOwner/$projectName")),
+  homepage := Some(url(gitHubUrl)),
   startYear := Some(2018),
   licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")),
-  scmInfo := Some(
-    ScmInfo(homepage.value.get, s"scm:git:https://github.com/$gitHubOwner/$projectName.git")
-  ),
+  scmInfo := Some(ScmInfo(homepage.value.get, s"scm:git:$gitHubUrl.git")),
   headerLicense := Some(HeaderLicense.ALv2("2018-2021", "Scala Steward contributors")),
   developers := List(
     Developer(
@@ -281,7 +312,7 @@ lazy val noPublishSettings = Def.settings(
 lazy val scaladocSettings = Def.settings(
   Compile / doc / scalacOptions ++= {
     val tag = s"v${version.value}"
-    val tree = if (isSnapshot.value) git.gitHeadCommit.value.getOrElse("master") else tag
+    val tree = if (isSnapshot.value) git.gitHeadCommit.value.getOrElse(mainBranch) else tag
     Seq(
       "-doc-source-url",
       s"${scmInfo.value.get.browseUrl}/blob/$tree€{FILE_PATH}.scala",
@@ -293,6 +324,8 @@ lazy val scaladocSettings = Def.settings(
 
 /// setting keys
 
+lazy val checkDocs = taskKey[Unit]("")
+
 lazy val installPlugin = taskKey[Unit]("Copies StewardPlugin.scala into global plugins directory.")
 installPlugin := {
   val name = "StewardPlugin.scala"
@@ -345,6 +378,8 @@ addCommandsAlias(
     "test",
     "coverageReport",
     "doc",
+    "docs/mdoc",
+    "docs/checkDocs",
     "package",
     "packageSrc",
     "core/assembly",
diff --git a/docs/faq.md b/docs/faq.md
index 00b191cc..e7bb041d 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -46,7 +46,7 @@ Updates for `sbt` and `scalafmt` can be controlled by using the following `group
 ## Can Scala Steward update multiple branches in a repository?
 
 Yes! You can update multiple branches of a repository by adding it several times to the "repos.md" file
-and a suffix with a different branch on each occurence (`owner/repo:branch`). For example:
+and a suffix with a different branch on each occurrence (`owner/repo:branch`). For example:
 
 ```md
 // repos.md
diff --git a/docs/running.md b/docs/running.md
index 733d2120..5519b270 100644
--- a/docs/running.md
+++ b/docs/running.md
@@ -41,7 +41,7 @@ The [`git-ask-pass` option](https://git-scm.com/docs/gitcredentials) must specif
 - either the plain text password corresponding to the configured `${LOGIN}`,
 - or (recommended) an authentication token corresponding to `${LOGIN}` (with appropriate permissions to watch the repositories; e.g. [Create a personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) for GitHub).
 
-**Note about git-ask-pass option**: The provided script must start with a valid shebang like `#!/bin/sh`, see issue [#1374](/../../issues/1374)
+**Note about git-ask-pass option**: The provided script must start with a valid shebang like `#!/bin/sh`, see issue [#1374](https://github.com/scala-steward-org/scala-steward/issues/1374)
 
 More information about using the `--scalafix-migrations` and `--artifact-migrations` options can be found 
 [here][scalafixmigrations] and [here][artifactmigrations].
diff --git a/modules/core/src/main/resources/scalafix-migrations.conf b/modules/core/src/main/resources/scalafix-migrations.conf
index 772ace6f..067fd34d 100644
--- a/modules/core/src/main/resources/scalafix-migrations.conf
+++ b/modules/core/src/main/resources/scalafix-migrations.conf
@@ -5,6 +5,20 @@ migrations = [
     newVersion: "1.0.0",
     rewriteRules: ["github:functional-streams-for-scala/fs2/v1?sha=v1.0.5"]
   },
+  {
+    groupId: "co.fs2",
+    artifactIds: ["fs2-.*"],
+    newVersion: "3.0.7",
+    // Note that the replace rule can generate broken code in some cases.
+    // See https://github.com/scalacenter/scalafix/issues/1168 for details.
+    rewriteRules: [
+      "replace:fs2.text.utf8Decode/fs2.text.utf8.decode",
+      "replace:fs2.text.utf8DecodeC/fs2.text.utf8.decodeC",
+      "replace:fs2.text.utf8Encode/fs2.text.utf8.encode",
+      "replace:fs2.text.utf8EncodeC/fs2.text.utf8.encodeC"
+    ],
+    executionOrder: "post-update"
+  },
   {
     groupId: "com.github.fd4s",
     artifactIds: ["fs2-kafka"],
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 c2b7bbf8..67218137 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
@@ -16,11 +16,13 @@
 
 package org.scalasteward.core.application
 
+import cats.ApplicativeThrow
 import cats.effect._
 import cats.effect.implicits._
 import cats.syntax.all._
 import org.http4s.Uri
 import org.http4s.client.Client
+import org.http4s.headers.`User-Agent`
 import org.http4s.okhttp.client.OkHttpBuilder
 import org.scalasteward.core.buildtool.BuildToolDispatcher
 import org.scalasteward.core.buildtool.maven.MavenAlg
@@ -80,7 +82,11 @@ object Context {
       logger <- Resource.eval(Slf4jLogger.fromName[F]("org.scalasteward.core"))
       _ <- Resource.eval(printBanner(logger))
       config = Config.from(args)
-      client <- OkHttpBuilder.withDefaultClient[F].flatMap(_.resource)
+      userAgent <- Resource.eval(mkUserAgent[F](config.vcsCfg.login))
+      client <- OkHttpBuilder
+        .withDefaultClient[F]
+        .flatMap(_.resource)
+        .map(c => Client[F](req => c.run(req.putHeaders(userAgent))))
       fileAlg = FileAlg.create(logger, F)
       processAlg = ProcessAlg.create(config.processCfg)(logger, F)
       workspaceAlg = WorkspaceAlg.create(config)(fileAlg, logger, F)
@@ -166,4 +172,10 @@ object Context {
       .mkString(System.lineSeparator())
     logger.info(msg)
   }
+
+  private def mkUserAgent[F[_]](login: String)(implicit F: ApplicativeThrow[F]): F[`User-Agent`] = {
+    val s = s"Scala-Steward/${org.scalasteward.core.BuildInfo.version}" +
+      s" (operated by $login; ${org.scalasteward.core.BuildInfo.gitHubUrl})"
+    F.fromEither(`User-Agent`.parse(s))
+  }
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala
index 0362750e..11b355b2 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala
@@ -61,20 +61,21 @@ final class EditAlg[F[_]](implicit
             for {
               _ <- preCommit
               repo = data.repo
-              migrations = scalafixMigrationsFinder.findMigrations(update)
-              scalafixEdits <-
-                if (migrations.isEmpty) F.pure(Nil)
+              (preMigrations, postMigrations) = scalafixMigrationsFinder.findMigrations(update)
+              preScalafixEdits <-
+                if (preMigrations.isEmpty) F.pure(Nil)
                 else
                   gitAlg.discardChanges(repo) *>
-                    runScalafixMigrations(repo, data.config, migrations) <*
+                    runScalafixMigrations(repo, data.config, preMigrations) <*
                     bumpVersion(update, files)
               _ <- reformatChangedFiles(repo, data.cache)
               updateCommitMsg = git.commitMsgFor(update, data.config.commits, data.repo.branch)
               updateEdit <- gitAlg
                 .commitAllIfDirty(repo, updateCommitMsg)
                 .map(_.map(commit => UpdateEdit(update, commit)))
+              postScalafixEdits <- runScalafixMigrations(repo, data.config, postMigrations)
               hooksEdits <- hookExecutor.execPostUpdateHooks(data, update)
-            } yield scalafixEdits ++ updateEdit ++ hooksEdits
+            } yield preScalafixEdits ++ updateEdit ++ postScalafixEdits ++ hooksEdits
         }
     }
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigration.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigration.scala
index d023bbf7..108dafc6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigration.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigration.scala
@@ -16,13 +16,14 @@
 
 package org.scalasteward.core.edit.scalafix
 
+import cats.Eq
 import cats.syntax.all._
 import io.circe.Decoder
 import io.circe.generic.semiauto._
 import org.scalasteward.core.data.{GroupId, Version}
-import org.scalasteward.core.edit.scalafix.ScalafixMigration.Target
+import org.scalasteward.core.edit.scalafix.ScalafixMigration.{ExecutionOrder, Target}
 import org.scalasteward.core.git.{Author, CommitMsg}
-import org.scalasteward.core.util.Nel
+import org.scalasteward.core.util.{unexpectedString, Nel}
 
 final case class ScalafixMigration(
     groupId: GroupId,
@@ -32,7 +33,8 @@ final case class ScalafixMigration(
     doc: Option[String] = None,
     scalacOptions: Option[Nel[String]] = None,
     authors: Option[Nel[Author]] = None,
-    target: Option[Target] = None
+    target: Option[Target] = None,
+    executionOrder: Option[ExecutionOrder] = None
 ) {
   def commitMessage(result: Either[Throwable, Unit]): CommitMsg = {
     val verb = if (result.isRight) "Applied" else "Failed"
@@ -41,12 +43,31 @@ final case class ScalafixMigration(
     CommitMsg(title, body.toList, authors.foldMap(_.toList))
   }
 
+  def executionOrderOrDefault: ExecutionOrder =
+    executionOrder.getOrElse(ExecutionOrder.PreUpdate)
+
   def targetOrDefault: Target =
     target.getOrElse(Target.Sources)
 }
 
 object ScalafixMigration {
-  sealed trait Target
+  sealed trait ExecutionOrder extends Product with Serializable
+  object ExecutionOrder {
+    case object PreUpdate extends ExecutionOrder
+    case object PostUpdate extends ExecutionOrder
+
+    implicit val executionOrderEq: Eq[ExecutionOrder] =
+      Eq.fromUniversalEquals
+
+    implicit val executionOrderDecoder: Decoder[ExecutionOrder] =
+      Decoder[String].emap {
+        case "pre-update"  => Right(PreUpdate)
+        case "post-update" => Right(PostUpdate)
+        case s             => unexpectedString(s, List("pre-update", "post-update"))
+      }
+  }
+
+  sealed trait Target extends Product with Serializable
   object Target {
     case object Sources extends Target
     case object Build extends Target
@@ -55,7 +76,7 @@ object ScalafixMigration {
       Decoder[String].emap {
         case "sources" => Right(Sources)
         case "build"   => Right(Build)
-        case unknown   => Left(s"Unexpected string '$unknown'. Expected 'sources' or 'build'.")
+        case s         => unexpectedString(s, List("sources", "build"))
       }
   }
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinder.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinder.scala
index 5eef38b4..bb59bc05 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinder.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinder.scala
@@ -18,15 +18,18 @@ package org.scalasteward.core.edit.scalafix
 
 import cats.syntax.all._
 import org.scalasteward.core.data.{Update, Version}
+import org.scalasteward.core.edit.scalafix.ScalafixMigration.ExecutionOrder
 
 final class ScalafixMigrationsFinder(migrations: List[ScalafixMigration]) {
-  def findMigrations(update: Update): List[ScalafixMigration] =
-    migrations.filter { migration =>
-      update.groupId === migration.groupId &&
-      migration.artifactIds.exists { re =>
-        update.artifactIds.exists(artifactId => re.r.findFirstIn(artifactId.name).isDefined)
-      } &&
-      Version(update.currentVersion) < migration.newVersion &&
-      Version(update.nextVersion) >= migration.newVersion
-    }
+  def findMigrations(update: Update): (List[ScalafixMigration], List[ScalafixMigration]) =
+    migrations
+      .filter { migration =>
+        update.groupId === migration.groupId &&
+        migration.artifactIds.exists { re =>
+          update.artifactIds.exists(artifactId => re.r.findFirstIn(artifactId.name).isDefined)
+        } &&
+        Version(update.currentVersion) < migration.newVersion &&
+        Version(update.nextVersion) >= migration.newVersion
+      }
+      .partition(_.executionOrderOrDefault === ExecutionOrder.PreUpdate)
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoader.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoader.scala
index a4d981ed..41064c9d 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoader.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoader.scala
@@ -20,7 +20,6 @@ import cats.MonadThrow
 import cats.syntax.all._
 import io.circe.config.parser.decode
 import org.http4s.Uri
-import org.http4s.implicits.http4sLiteralsSyntax
 import org.scalasteward.core.application.Config.ScalafixCfg
 import org.scalasteward.core.edit.scalafix.ScalafixMigrationsLoader._
 import org.scalasteward.core.io.FileAlg
@@ -52,6 +51,7 @@ final class ScalafixMigrationsLoader[F[_]](implicit
 }
 
 object ScalafixMigrationsLoader {
-  val defaultScalafixMigrationsUrl: Uri =
-    uri"https://raw.githubusercontent.com/scala-steward-org/scala-steward/master/modules/core/src/main/resources/scalafix-migrations.conf"
+  val defaultScalafixMigrationsUrl: Uri = Uri.unsafeFromString(
+    s"https://raw.githubusercontent.com/scala-steward-org/scala-steward/${org.scalasteward.core.BuildInfo.mainBranch}/modules/core/src/main/resources/scalafix-migrations.conf"
+  )
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/update/artifact/ArtifactMigrationsLoader.scala b/modules/core/src/main/scala/org/scalasteward/core/update/artifact/ArtifactMigrationsLoader.scala
index 057ff472..5a2a59a9 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/update/artifact/ArtifactMigrationsLoader.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/update/artifact/ArtifactMigrationsLoader.scala
@@ -20,7 +20,6 @@ import cats.MonadThrow
 import cats.syntax.all._
 import io.circe.config.parser.decode
 import org.http4s.Uri
-import org.http4s.implicits.http4sLiteralsSyntax
 import org.scalasteward.core.application.Config.ArtifactCfg
 import org.scalasteward.core.io.FileAlg
 import org.scalasteward.core.update.artifact.ArtifactMigrationsLoader.defaultArtifactMigrationsUrl
@@ -52,6 +51,7 @@ final class ArtifactMigrationsLoader[F[_]](implicit
 }
 
 object ArtifactMigrationsLoader {
-  val defaultArtifactMigrationsUrl: Uri =
-    uri"https://raw.githubusercontent.com/scala-steward-org/scala-steward/master/modules/core/src/main/resources/artifact-migrations.conf"
+  val defaultArtifactMigrationsUrl: Uri = Uri.unsafeFromString(
+    s"https://raw.githubusercontent.com/scala-steward-org/scala-steward/${org.scalasteward.core.BuildInfo.mainBranch}/modules/core/src/main/resources/artifact-migrations.conf"
+  )
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/util/package.scala b/modules/core/src/main/scala/org/scalasteward/core/util/package.scala
index b4d796d7..f2410264 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/util/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/util/package.scala
@@ -87,4 +87,7 @@ package object util {
       _.mapAccumulate(init) { case (i, a) => (N.plus(i, weight(a)), a) }
         .takeThrough { case (total, _) => N.lt(total, limit) }
         .map { case (_, a) => a }
+
+  def unexpectedString(s: String, expected: List[String]): Left[String, Nothing] =
+    Left(s"Unexpected string '$s'. Expected one of: ${expected.mkString(", ")}.")
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSType.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSType.scala
index 65b153e1..e92c3f9a 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSType.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSType.scala
@@ -19,6 +19,7 @@ package org.scalasteward.core.vcs
 import cats.Eq
 import cats.syntax.all._
 import org.http4s.syntax.literals._
+import org.scalasteward.core.util.unexpectedString
 import org.scalasteward.core.vcs.VCSType._
 
 sealed trait VCSType {
@@ -57,8 +58,7 @@ object VCSType {
   def parse(s: String): Either[String, VCSType] =
     all.find(_.asString === s) match {
       case Some(value) => Right(value)
-      case None =>
-        Left(s"Unexpected string '$s'. Expected one of: ${all.map(_.asString).mkString(", ")}.")
+      case None        => unexpectedString(s, all.map(_.asString))
     }
 
   def fromPublicWebHost(host: String): Option[VCSType] =
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 77166f56..0b6eb011 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
@@ -19,6 +19,7 @@ package org.scalasteward.core.vcs.bitbucket
 import io.circe.Decoder
 import org.http4s.Uri
 import org.scalasteward.core.git.{Branch, Sha1}
+import org.scalasteward.core.util.unexpectedString
 import org.scalasteward.core.util.uri.uriDecoder
 import org.scalasteward.core.vcs.data._
 
@@ -34,7 +35,7 @@ private[bitbucket] object json {
     Decoder[String].emap {
       case "OPEN"                               => Right(PullRequestState.Open)
       case "MERGED" | "SUPERSEDED" | "DECLINED" => Right(PullRequestState.Closed)
-      case unknown                              => Left(s"Unexpected string '$unknown'")
+      case s => unexpectedString(s, List("OPEN", "MERGED", "SUPERSEDED", "DECLINED"))
     }
 
   implicit val pullRequestOutDecoder: Decoder[PullRequestOut] = Decoder.instance { c =>
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestState.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestState.scala
index 9a184028..439749c1 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestState.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/data/PullRequestState.scala
@@ -18,6 +18,7 @@ package org.scalasteward.core.vcs.data
 
 import cats.Eq
 import io.circe.{Decoder, Encoder}
+import org.scalasteward.core.util.unexpectedString
 import org.scalasteward.core.vcs.data.PullRequestState.{Closed, Open}
 
 sealed trait PullRequestState {
@@ -40,7 +41,7 @@ object PullRequestState {
       _.toLowerCase match {
         case "open" | "opened"                => Right(Open)
         case "closed" | "merged" | "declined" => Right(Closed)
-        case unknown                          => Left(s"Unexpected string '$unknown'")
+        case s => unexpectedString(s, List("open", "opened", "closed", "merged", "declined"))
       }
     }
 
diff --git a/modules/docs/mdoc/artifact-migrations.md b/modules/docs/mdoc/artifact-migrations.md
new file mode 100644
index 00000000..6a2c37a5
--- /dev/null
+++ b/modules/docs/mdoc/artifact-migrations.md
@@ -0,0 +1,35 @@
+# Artifact Migrations
+
+Scala Steward can look for newer versions of artifacts with different group Ids, artifact ids, or both different.
+
+## Adding artifact migrations to Scala Steward
+
+By default, Scala Steward applies the artifact migrations defined in the
+[default list][migrations]. When running Scala Steward you can also specify
+files or URLs (via the `--artifact-migrations` command-line option) that
+contain 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.
+
+Pull requests that added artifact migrations can be found [here][migration-prs].
+
+[migrations]: @GITHUB_URL@/blob/@MAIN_BRANCH@/modules/core/src/main/resources/artifact-migrations.conf
+[migration-prs]: @GITHUB_URL@/pulls?q=label%3Aartifact-migration
+[HOCON]: https://github.com/lightbend/config/blob/master/HOCON.md
diff --git a/modules/docs/mdoc/faq.md b/modules/docs/mdoc/faq.md
new file mode 100644
index 00000000..3147b53d
--- /dev/null
+++ b/modules/docs/mdoc/faq.md
@@ -0,0 +1,96 @@
+# Frequently Asked Questions
+
+## How does Scala Steward decide what version it is updating to?
+
+Scala Steward first proposes an update to the latest patch version at the
+same minor and major version. If the dependency is already on the latest patch
+version, it proposes an update to the latest minor version at the same major
+version. And if the dependency is already on the latest minor version, it
+proposes an update to the latest major version.
+
+Here is an example. Suppose you are depending on a library foo, which has been
+published for the following versions: 1.0.0, 1.0.1, 1.0.2, 1.1.0, 1.2.0, 2.0.0,
+and 3.0.0.
+
+* If your version is 1.0.0 or 1.0.1, Scala Steward would create a PR updating it to 1.0.2
+* If your version is 1.0.2 or 1.1.0, Scala Steward would create a PR updating it to 1.2.0
+* If your version is 1.2.0 or 2.0.0, Scala Steward would create a PR updating it to 3.0.0
+
+Of course, once you merge a Scala Steward PR, you've updated your version,
+which can result in Scala Steward sending another PR making the next update.
+
+## How can version updates be controlled
+
+You can control the update of individual dependencies or of all dependencies
+belonging to a group by specifying version prefixes for them:
+
+```properties
+updates.pin = [
+  # Allow x.y to x.(y+1), but not to (x+1).y
+  { groupId = "a.group.id", artifactId="a.name", version="x." },
+  # Allow x.y.z to x.y.(z+1), but not to x.(y+1)
+  { groupId = "a.group.id", artifactId="another.name", version="x.y." }
+
+
+  # Fix the major version for all dependencies with this groupId
+  { groupId = "a.group.id", version="x." },
+]
+```
+
+Updates for `sbt` and `scalafmt` can be controlled by using the following `groupId` and `artifactId`:
+```properties
+{ groupId = "org.scala-sbt", artifactId = "sbt" }
+{ groupId = "org.scalameta", artifactId = "scalafmt-core" }
+```
+
+## Can Scala Steward update multiple branches in a repository?
+
+Yes! You can update multiple branches of a repository by adding it several times to the "repos.md" file
+and a suffix with a different branch on each occurrence (`owner/repo:branch`). For example:
+
+```md
+// repos.md
+- owner/repo
+- owner/repo:0.1.x
+- owner/repo:0.2.x
+```
+
+This configuration will update the default branch, as well as the branches `0.1.x` and `0.2.x` in the repo `owner/repo`.
+
+## Can Scala Steward update dependencies in giter8 templates ?
+
+Scala Steward can update versions in giter8 templates if the dependencies of the template 
+are also added as dependencies of the template build.
+An example is [library.g8](https://github.com/ChristopherDavenport/library.g8) ([example PR](https://github.com/ChristopherDavenport/library.g8/pull/100/files))
+
+## Why do Scala Steward updates provide no URLs in PRs?
+
+Scala Steward updates can only provide links to release notes, diffs and changelogs if
+the `ivy.xml` file contain the `homepage` attribute or the `pom.xml` contains either
+a  `scm.url` or an `url` attribute.
+
+## How can Scala Steward's PRs be merged automatically?
+
+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](@GITHUB_URL@/blob/@MAIN_BRANCH@/.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).
+
+Other examples of Mergify rules for Scala Steward can be found via
+[GitHub's code search](https://github.com/search?p=6&q=%22author%3Dscala-steward%22+filename%3A.mergify.yml&type=Code).
+
+An alternative is the [merge PR github action](https://github.com/marketplace/actions/merge-dependency-update-prs)
+which you can include in a GitHub workflow to automatically merge Scala Steward's 
+pull requests.
+
+## Where can Scala Steward's PGP key be found?
+
+Scala Steward signs commits with a PGP key. The fingerprint of that key is
+774C3674392662AE645C9B8396BDF10FFAB8B6A6 and it can be found at
+https://keys.openpgp.org/search?q=me@scala-steward.org.
+
+## How can I change log levels?
+
+If you want to switch from the standard INFO log level you can call scala steward with `-DLOG_LEVEL=DEBUG` for example: `scala-steward -DLOG_LEVEL=DEBUG  --workspace  "$STEWARD_DIR/workspace" ...` 
diff --git a/modules/docs/mdoc/running.md b/modules/docs/mdoc/running.md
new file mode 100644
index 00000000..f03a1034
--- /dev/null
+++ b/modules/docs/mdoc/running.md
@@ -0,0 +1,265 @@
+## Running Scala Steward
+
+A complete list of all command line arguments can be found [here](help.md).
+
+```bash
+sbt stage
+
+./modules/core/.jvm/target/universal/stage/bin/scala-steward \
+  --workspace  "$STEWARD_DIR/workspace" \
+  --repos-file "$STEWARD_DIR/repos.md" \
+  --default-repo-conf "$STEWARD_DIR/default.scala-steward.conf" \
+  --git-author-email ${EMAIL} \
+  --vcs-api-host "https://api.github.com" \
+  --vcs-login ${LOGIN} \
+  --git-ask-pass "$STEWARD_DIR/.github/askpass/$LOGIN.sh" \
+  --sign-commits \
+  --env-var FOO=BAR
+```
+
+Or as a [Docker](https://www.docker.com/) container:
+
+```bash
+sbt docker:publishLocal
+
+docker run -v $STEWARD_DIR:/opt/scala-steward -it fthomas/scala-steward:latest \
+  --workspace  "/opt/scala-steward/workspace" \
+  --repos-file "/opt/scala-steward/repos.md" \
+  --default-repo-conf "/opt/scala-steward/default.scala-steward.conf" \
+  --git-author-email ${EMAIL} \
+  --vcs-api-host "https://api.github.com" \
+  --vcs-login ${LOGIN} \
+  --git-ask-pass "/opt/scala-steward/.github/askpass/$LOGIN.sh" \
+  --sign-commits \
+  --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),
+
+- either the plain text password corresponding to the configured `${LOGIN}`,
+- or (recommended) an authentication token corresponding to `${LOGIN}` (with appropriate permissions to watch the repositories; e.g. [Create a personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) for GitHub).
+
+**Note about git-ask-pass option**: The provided script must start with a valid shebang like `#!/bin/sh`, see issue [#1374](@GITHUB_URL@/issues/1374)
+
+More information about using the `--scalafix-migrations` and `--artifact-migrations` options can be found 
+[here][scalafixmigrations] and [here][artifactmigrations].
+
+### Workspace
+
+The workspace directory (specified with `--workspace`) provides a location for cache and temporary files.  
+
+It is important to persist this workspace between runs.  Without this, Scala Steward will be unable to observe 
+repo-specific preferences (such as [pullRequests.frequency](repo-specific-configuration.md)) correctly.   
+
+### Private repositories
+
+If you run Scala Steward for your own private projects, the option `--do-not-fork` can be required, not to fork.
+Instead it will create pull requests directly on the private repository (as soon as the `${LOGIN}` can).
+
+#### Credentials using environment variables
+
+It can also be useful to pass additional environment variables from the command line using the `--env-var` flag as shown in the examples above. You can use this to pass any credentials required by your projects to resolve any private dependencies, e.g.:
+
+```bash
+--env-var BINTRAY_USER=username \
+--env-var BINTRAY_PASS=password
+```
+
+These variables will be accessible (in sbt) to all of the projects that Scala Steward checks dependencies for.
+
+#### Credentials using a credentials.sbt file
+
+If your projects require credentials, you can also provide global credentials in the `$HOME/.sbt/1.0/credentials.sbt` file. 
+The file should contain a single line: `credentials += Credentials("Some Nexus Repository Manager", "my.artifact.repo.net", "admin", "admin123")`.
+
+#### sbt 0.13 workaround
+For sbt 0.13 builds, scala-steward [may be unable](https://gitter.im/fthomas/scala-steward?at=5f0573dac7d15f7d0f7b15ac) to extract credentials for private resolvers. Instead, you can [configure coursier directly](https://get-coursier.io/docs/other-credentials) by adding `~/.config/coursier/credentials.properties`:
+```scala
+example1.username=username
+example1.password=password
+example1.host=artifacts.example.com
+example1.realm=Example Realm
+```
+
+### Running locally from sbt
+
+#### Sample run for GitLab
+
+```
+sbt
+project core
+run --do-not-fork --workspace "/path/workspace" --repos-file "/path/repos.md" --default-repo-conf "/path/default.scala-steward.conf" --git-ask-pass "/path/pass.sh" --git-author-email "email@example.org" --vcs-type "gitlab" --vcs-api-host "https://gitlab.com/api/v4/" --vcs-login "gitlab.steward"
+```
+
+
+### Running on Docker for Bitbucket
+
+* Create a file `repos.md` that will be injected into the container as as volume.
+* Create a file `run.sh` with this content:
+
+```
+echo "#!/bin/sh"                  >> pass.sh
+echo "echo '$BITBUCKET_PASSWORD'" >> pass.sh
+
+chmod +x pass.sh
+
+docker run -v $PWD:/opt/scala-steward \
+    -v ~/.sbt/:/root/.sbt \
+    -it fthomas/scala-steward:latest \
+    -DLOG_LEVEL=TRACE \
+    --do-not-fork \
+    --workspace "/opt/scala-steward/workspace" \
+    --repos-file "/opt/scala-steward/repos.md" \
+    --default-repo-conf "/opt/scala-steward/default.scala-steward.conf" \
+    --git-ask-pass "/opt/scala-steward/pass.sh" \
+    --git-author-email "myemail@company.xyz" \
+    --vcs-type "bitbucket" \
+    --vcs-api-host "https://api.bitbucket.org/2.0" \
+    --vcs-login "$BITBUCKET_USERNAME"
+```
+
+* Run it from a CI tool or manually using with this command:
+
+`BITBUCKET_USERNAME=<myuser> BITBUCKET_PASSWORD=<mypass> ./run.sh`
+
+### Running in a Bitbucket pipeline to update Bitbucket repos
+
+* Create a file `repos.md` that will be injected into the container as a volume.
+* Create a file `run.sh` with this content:
+
+```
+echo "#!/bin/sh"                  >> pass.sh
+echo "echo '$BITBUCKET_PASSWORD'" >> pass.sh
+
+chmod +x pass.sh
+
+docker run -v $PWD:/opt/scala-steward \
+    -i fthomas/scala-steward:latest \
+    -DLOG_LEVEL=TRACE \
+    --do-not-fork \
+    --workspace "/opt/scala-steward/workspace" \
+    --repos-file "/opt/scala-steward/repos.md" \
+    --default-repo-conf "/opt/scala-steward/default.scala-steward.conf" \
+    --git-ask-pass "/opt/scala-steward/pass.sh" \
+    --git-author-email "myemail@company.xyz" \
+    --vcs-type "bitbucket" \
+    --vcs-api-host "https://api.bitbucket.org/2.0" \
+    --vcs-login "$BITBUCKET_USERNAME"
+```
+
+NOTE: This script is slightly different to the one in the previous Bitbucket
+example, because it needs to run in a Bitbucket Pipeline. The `-t` flag has been
+removed, and we do mount `~/.sbt` as a volume.
+
+* Prepare an S3 bucket (or similar storage) to persist the Scala Steward
+  workspace between runs
+* Set some repository variables: AWS credentials, plus the S3 bucket name
+* Create a pipeline to run Scala Steward and sync the workspace to S3:
+
+```
+image:
+  name: <any Linux image with AWS CLI installed>
+
+options:
+  docker: true
+
+definitions:
+  services:
+    docker:
+      memory: 4096
+
+pipelines:
+  custom:
+    run-scala-steward:
+      - step:
+          name: Run Scala Steward
+          size: 2x
+          script:
+            - aws s3 sync s3://${WORKSPACE_BUCKET}/workspace ./workspace
+            - ./run.sh
+            - aws s3 sync ./workspace s3://${WORKSPACE_BUCKET}/workspace
+```
+
+* In the Pipelines UI, configure the pipeline to run on a schedule (e.g. daily)
+
+### Running On-premise
+
+#### GitHub Enterprise
+
+There is multiple articles on how to run Scala Steward on-premise:
+
+* [Running Scala Steward On-premise](https://engineering.avast.io/running-scala-steward-on-premise)
+* [Running scala-steward periodically on AWS Fargate](https://medium.com/@tanishiking/running-scala-steward-periodically-on-aws-fargate-3d3d202f0f7)
+* [Scala StewardとGitHub Actionsで依存ライブラリの更新を自動化する](https://scalapedia.com/articles/145/Scala+Steward%E3%81%A8GitHub+Actions%E3%81%A7%E4%BE%9D%E5%AD%98%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E6%9B%B4%E6%96%B0%E3%82%92%E8%87%AA%E5%8B%95%E5%8C%96%E3%81%99%E3%82%8B)
+* [Centralized Scala Steward with GitHub Actions](https://hector.dev/2020/11/18/centralized-scala-steward-with-github-actions)
+* [Big Timesavers for Busy Scala Developers](https://speakerdeck.com/exoego/big-timesavers-for-busy-scala-developers)
+* [Running scala steward on private repos](http://www.roundcrisis.com/2020/08/15/Scala-Steward-locally/)
+  
+#### GitLab
+
+The following describes a setup using GitLab Docker runner, which you have to setup seperately.
+
+1. create a "scalasteward" user in GitLab
+2. assign that user "Developer" permissions in every project that should be managed by Scala Steward
+3. login as that user and create a Personal Access Token with `api`, `read_repository` and `write_repository` scopes
+4. create a project and add the following GitLab CI config
+
+```yaml
+check:
+  rules:
+    # only run when scheduled, or when pushing a commit to the default
+    # branch which changed the repos.md file
+    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+      when: never
+    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+      changes:
+        - repos.md
+      when: on_success
+    - if: '$CI_PIPELINE_SOURCE == "schedule"'
+      when: on_success
+  image:
+    name: fthomas/scala-steward:latest
+    entrypoint: [""]
+  variables:
+    # change values here, if needed;
+    EMAIL: 'me@scala-steward.org'
+    LOGIN: 'scalasteward'
+  script:
+    - mkdir --parents "$CI_PROJECT_DIR/.sbt" "$CI_PROJECT_DIR/.ivy2"
+    - ln -sfT "$CI_PROJECT_DIR/.sbt"  "$HOME/.sbt"
+    - ln -sfT "$CI_PROJECT_DIR/.ivy2" "$HOME/.ivy2"
+    - >-
+      /opt/docker/bin/scala-steward
+        --workspace  "$CI_PROJECT_DIR/workspace"
+        --process-timeout 30min
+        --do-not-fork
+        --repos-file "$CI_PROJECT_DIR/repos.md"
+        --default-repo-conf "$CI_PROJECT_DIR/default.scala-steward.conf"
+        --git-author-email "${EMAIL}"
+        --vcs-type "gitlab"
+        --vcs-api-host "${CI_API_V4_URL}"
+        --vcs-login "${LOGIN}"
+        --git-ask-pass "$CI_PROJECT_DIR/askpass.sh"
+  cache:
+    key: scala-steward
+    paths:
+      - .ivy2/cache
+      - .sbt/boot/scala*
+      - workspace/store
+```
+5. add a masked CI variable `SCALA_STEWARD_TOKEN` in "Settings > CI / CD : Variables" for the access token
+6. add the `askpass.sh` script to the repository:
+
+```bash
+#!/usr/bin/env bash
+
+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]: @GITHUB_URL@/blob/@MAIN_BRANCH@/docs/scalafix-migrations.md
+[artifactmigrations]: @GITHUB_URL@/blob/@MAIN_BRANCH@/docs/artifact-migrations.md
diff --git a/modules/docs/mdoc/scalafix-migrations.md b/modules/docs/mdoc/scalafix-migrations.md
new file mode 100644
index 00000000..1f6b018a
--- /dev/null
+++ b/modules/docs/mdoc/scalafix-migrations.md
@@ -0,0 +1,69 @@
+# Scalafix Migrations
+
+Scala Steward can run [Scalafix][Scalafix] rewrite rules for version updates
+to not only bump version numbers but to also adapt the code for the new
+version. This allows to automatically rewrite deprecated code or code that
+would not compile with the new version of the dependency.
+
+Here are two pull requests that demonstrate this feature:
+
+* https://github.com/barambani/http4s-extend/pull/67/files
+* https://github.com/fthomas/scalafix-test/pull/6/files
+
+And here are all pull requests where Scala Steward applied Scalafix
+migrations:
+
+* [Created pull requests](https://github.com/search?q=author%3Ascala-steward+is%3Apr+scalafix-migrations)
+  ([compact](             https://github.com/pulls?q=author%3Ascala-steward+is%3Apr+scalafix-migrations))
+* [Merged pull requests]( https://github.com/search?q=author%3Ascala-steward+is%3Amerged+sort%3Aupdated-desc+scalafix-migrations)
+  ([compact](             https://github.com/pulls?q=author%3Ascala-steward+is%3Amerged+sort%3Aupdated-desc+scalafix-migrations))
+
+## How does this work?
+
+Scala Steward contains a list of [migration rules][migrations] combined with
+metadata that describes to which updates they are applicable. When it prepares
+a pull request and finds that one or more rules match the current update,
+Scala Steward applies these migration rules via the sbt-scalafix plugin
+which resolves those rules and edits the source code accordingly.
+
+## Writing migration rules for your project
+
+See the [Scalafix Developer Guide][scalafix-dev-guide] for more information
+about writing rewrite rules or have a look at the existing
+[migration rules][migrations] for inspiration. Rules in Scala Steward must be
+accessible via the [github:][using-github], the [http:][using-http] or the
+[dependency:][using-dependency] schemes.
+
+## Adding migration rules to Scala Steward
+
+After you have written a new migration rule for a new version of your project,
+Scala Steward needs to be made aware of it. Creating a pull request that adds
+the new rule to the list of [migrations][migrations] is enough for that. Once
+that pull request is merged, Scala Steward will start using this migration.
+
+When running Scala Steward you can also specify files or URLs (via the
+`--scalafix-migrations` command-line option) that contain additional
+migrations which are not present in the [default list][migrations].
+These files are in [HOCON][HOCON] format and should look like this:
+```hocon
+migrations = [
+  {
+    groupId: "org.http4s",
+    artifactIds: ["http4s-.*"],
+    newVersion: "0.20.0",
+    rewriteRules: ["github:http4s/http4s/v0_20?sha=v0.20.11"],
+    doc: "https://github.com/http4s/http4s/blob/v0.20.0/docs/src/main/tut/upgrading.md"
+  }
+]
+```
+
+Pull requests that added migration rules can be found [here][scalafix-prs].
+
+[Scalafix]: https://scalacenter.github.io/scalafix/
+[migrations]: @GITHUB_URL@/blob/@MAIN_BRANCH@/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]: @GITHUB_URL@/pulls?q=label%3Ascalafix-migration
+[HOCON]: https://github.com/lightbend/config/blob/master/HOCON.md
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 3fcc9741..006bb592 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -10,7 +10,8 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0")
 addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.1.20")
 addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.3")
 addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.1.0")
-addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.1")
+addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.2.24")
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.2")
 
 // This is only here so that Scala Steward updates the version in sbt/package.scala too.
 addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.31")


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationTest.scala
index 25867cf7..48845ee9 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationTest.scala
@@ -2,6 +2,7 @@ package org.scalasteward.core.edit.scalafix
 
 import io.circe.config.parser
 import munit.FunSuite
+import org.scalasteward.core.edit.scalafix.ScalafixMigration.{ExecutionOrder, Target}
 import org.scalasteward.core.util.Nel
 
 class ScalafixMigrationTest extends FunSuite {
@@ -25,4 +26,12 @@ class ScalafixMigrationTest extends FunSuite {
     )
     assertEquals(obtained, Right(expected))
   }
+
+  test("decode unknown executionOrder") {
+    assert(io.circe.parser.decode[ExecutionOrder](""""foo"""").isLeft)
+  }
+
+  test("decode unknown target") {
+    assert(io.circe.parser.decode[Target](""""foo"""").isLeft)
+  }
 }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinderTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinderTest.scala
index 2b22f7d8..fe5dc9af 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinderTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsFinderTest.scala
@@ -10,17 +10,20 @@ class ScalafixMigrationsFinderTest extends FunSuite {
   test("findMigrations") {
     val update = Update.Single("org.typelevel" % "cats-core" % "2.1.0", Nel.of("2.2.0"))
     val migrations = scalafixMigrationsFinder.findMigrations(update)
-    val expected = List(
-      ScalafixMigration(
-        GroupId("org.typelevel"),
-        Nel.of("cats-core"),
-        Version("2.2.0"),
-        Nel.of("github:typelevel/cats/Cats_v2_2_0?sha=v2.2.0"),
-        Some(
-          "https://github.com/typelevel/cats/blob/v2.2.0/scalafix/README.md#migration-to-cats-v220"
-        ),
-        Some(Nel.of("-P:semanticdb:synthetics:on"))
-      )
+    val expected = (
+      List(
+        ScalafixMigration(
+          GroupId("org.typelevel"),
+          Nel.of("cats-core"),
+          Version("2.2.0"),
+          Nel.of("github:typelevel/cats/Cats_v2_2_0?sha=v2.2.0"),
+          Some(
+            "https://github.com/typelevel/cats/blob/v2.2.0/scalafix/README.md#migration-to-cats-v220"
+          ),
+          Some(Nel.of("-P:semanticdb:synthetics:on"))
+        )
+      ),
+      List()
     )
     assertEquals(migrations, expected)
   }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoaderTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoaderTest.scala
index 7fca4456..733dc72b 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoaderTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/scalafix/ScalafixMigrationsLoaderTest.scala
@@ -5,6 +5,7 @@ import munit.FunSuite
 import org.http4s.Uri
 import org.scalasteward.core.application.Config.ScalafixCfg
 import org.scalasteward.core.data.{GroupId, Version}
+import org.scalasteward.core.edit.scalafix.ScalafixMigration.ExecutionOrder.PreUpdate
 import org.scalasteward.core.edit.scalafix.ScalafixMigration.Target.Sources
 import org.scalasteward.core.git.Author
 import org.scalasteward.core.mock.MockConfig.mockRoot
@@ -23,7 +24,8 @@ class ScalafixMigrationsLoaderTest extends FunSuite {
        |    rewriteRules: ["awesome rewrite rule"],
        |    doc: "https://scalacenter.github.io/scalafix/",
        |    authors: ["Jane Doe <jane@example.com>"],
-       |    target: "sources"
+       |    target: "sources",
+       |    executionOrder: "pre-update"
        |  }
        |]""".stripMargin
   val migration: ScalafixMigration = ScalafixMigration(
@@ -33,7 +35,8 @@ class ScalafixMigrationsLoaderTest extends FunSuite {
     Nel.of("awesome rewrite rule"),
     Some("https://scalacenter.github.io/scalafix/"),
     authors = Some(Nel.of(Author("Jane Doe", "jane@example.com"))),
-    target = Some(Sources)
+    target = Some(Sources),
+    executionOrder = Some(PreUpdate)
   )
 
   test("loadAll: without extra file, without defaults") {