From 7a7bd8625a280c2119581231277358868816a0b2 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 15 Dec 2023 10:34:35 +0100
Subject: [PATCH 01/21] Remove `StewardContext` hierarchy

This removes the `Context.StewardContext` hierarchy. IMO `StewardContext`
makes `Context.scala` unnecessarily more complicated only for saving one
`case` in `Main.run`.
---
 build.sbt                                     |  9 ++
 docs/help.md                                  |  2 +-
 .../scala/org/scalasteward/core/Main.scala    | 11 ++-
 .../scalasteward/core/application/Cli.scala   | 44 +++++-----
 .../core/application/Config.scala             |  6 --
 .../core/application/Context.scala            | 83 +++++--------------
 .../ValidateRepoConfigContext.scala           | 34 ++++++++
 .../core/application/CliTest.scala            | 19 ++---
 .../scalasteward/core/mock/MockConfig.scala   |  4 +-
 .../scalasteward/core/mock/MockContext.scala  |  7 +-
 .../ValidateRepoConfigAlgTest.scala           | 19 ++---
 11 files changed, 108 insertions(+), 130 deletions(-)
 create mode 100644 modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala

diff --git a/build.sbt b/build.sbt
index 8b245e58ac..ce0a455ec4 100644
--- a/build.sbt
+++ b/build.sbt
@@ -446,6 +446,15 @@ runSteward := Def.taskDyn {
   (core.jvm / Compile / run).toTask(args)
 }.value
 
+lazy val runValidateRepoConfig = taskKey[Unit]("")
+runValidateRepoConfig := Def.taskDyn {
+  val projectDir = (LocalRootProject / baseDirectory).value
+  val args = Seq(
+    Seq("validate-repo-config", s"$projectDir/.scala-steward.conf")
+  ).flatten.mkString(" ", " ", "")
+  (core.jvm / Compile / run).toTask(args)
+}.value
+
 /// commands
 
 def addCommandsAlias(name: String, cmds: Seq[String]) =
diff --git a/docs/help.md b/docs/help.md
index 2c05cb3bf1..b114ff4f33 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -4,8 +4,8 @@ All command line arguments for the `scala-steward` application.
 
 ```
 Usage:
-    scala-steward validate-repo-config
     scala-steward --workspace <file> --repos-file <uri> [--repos-file <uri>]... [--git-author-name <string>] --git-author-email <string> [--git-author-signing-key <string>] --git-ask-pass <file> [--sign-commits] [--forge-type <forge-type>] [--forge-api-host <uri>] --forge-login <string> [--do-not-fork] [--add-labels] [--ignore-opts-files] [--env-var <name=value>]... [--process-timeout <duration>] [--whitelist <string>]... [--read-only <string>]... [--enable-sandbox | --disable-sandbox] [--max-buffer-size <integer>] [--repo-config <uri>]... [--disable-default-repo-config] [--scalafix-migrations <uri>]... [--disable-default-scalafix-migrations] [--artifact-migrations <uri>]... [--disable-default-artifact-migrations] [--cache-ttl <duration>] [--bitbucket-use-default-reviewers] [--bitbucket-server-use-default-reviewers] [--gitlab-merge-when-pipeline-succeeds] [--gitlab-required-reviewers <integer>] [--gitlab-remove-source-branch] [--azure-repos-organization <string>] [--github-app-id <integer> --github-app-key-file <file>] [--url-checker-test-url <uri>]... [--default-maven-repo <string>] [--refresh-backoff-period <duration>]
+    scala-steward validate-repo-config
 
 
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/Main.scala b/modules/core/src/main/scala/org/scalasteward/core/Main.scala
index 3a6be45be5..60cc23842d 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/Main.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/Main.scala
@@ -18,13 +18,16 @@ package org.scalasteward.core
 
 import cats.effect.std.Console
 import cats.effect.{ExitCode, IO, IOApp}
-import org.scalasteward.core.application.{Cli, Context}
+import org.scalasteward.core.application.{Cli, Context, ValidateRepoConfigContext}
 
 object Main extends IOApp {
   override def run(args: List[String]): IO[ExitCode] =
     Cli.parseArgs(args) match {
-      case Cli.ParseResult.Success(config) => Context.step0[IO](config).use(_.runF)
-      case Cli.ParseResult.Help(help)      => Console[IO].println(help).as(ExitCode.Success)
-      case Cli.ParseResult.Error(error)    => Console[IO].errorln(error).as(ExitCode.Error)
+      case Cli.ParseResult.Success(Cli.Usage.Regular(config)) =>
+        Context.step0[IO](config).use(_.stewardAlg.runF)
+      case Cli.ParseResult.Success(Cli.Usage.ValidateRepoConfig(file)) =>
+        ValidateRepoConfigContext.run[IO](file)
+      case Cli.ParseResult.Help(help)   => Console[IO].println(help).as(ExitCode.Success)
+      case Cli.ParseResult.Error(error) => Console[IO].errorln(error).as(ExitCode.Error)
     }
 }
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 2191e4160f..8b53045a9b 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
@@ -337,16 +337,7 @@ object Cli {
       .withDefault(default)
   }
 
-  private val configFile: Opts[File] =
-    Opts.argument[File]()
-
-  private val validateConfigFile: Opts[File] =
-    Opts.subcommand(
-      name = "validate-repo-config",
-      help = "Validate the repo config file and exit; report errors if any"
-    )(configFile)
-
-  private val configOpts: Opts[Config] = (
+  private val regular: Opts[Usage] = (
     workspace,
     reposFiles,
     gitCfg,
@@ -365,29 +356,36 @@ object Cli {
     urlCheckerTestUrls,
     defaultMavenRepo,
     refreshBackoffPeriod
-  ).mapN(Config.apply)
-
-  val command: Command[StewardUsage] =
-    Command("scala-steward", "")(
-      validateConfigFile
-        .map(StewardUsage.ValidateRepoConfig)
-        .orElse(
-          configOpts
-            .map(StewardUsage.Regular)
-        )
-    )
+  ).mapN(Config.apply).map(Usage.Regular.apply)
+
+  private val validateRepoConfig: Opts[Usage] =
+    Opts
+      .subcommand(
+        name = "validate-repo-config",
+        help = "Validate the repo config file and exit; report errors if any"
+      )(Opts.argument[File]())
+      .map(Usage.ValidateRepoConfig.apply)
+
+  val command: Command[Usage] =
+    Command("scala-steward", "")(regular.orElse(validateRepoConfig))
 
   sealed trait ParseResult extends Product with Serializable
   object ParseResult {
-    final case class Success(config: StewardUsage) extends ParseResult
+    final case class Success(usage: Usage) extends ParseResult
     final case class Help(help: String) extends ParseResult
     final case class Error(error: String) extends ParseResult
   }
 
+  sealed trait Usage extends Product with Serializable
+  object Usage {
+    final case class Regular(config: Config) extends Usage
+    final case class ValidateRepoConfig(file: File) extends Usage
+  }
+
   def parseArgs(args: List[String]): ParseResult =
     command.parse(args) match {
       case Left(help) if help.errors.isEmpty => ParseResult.Help(help.toString)
       case Left(help)                        => ParseResult.Error(help.toString)
-      case Right(config)                     => ParseResult.Success(config)
+      case Right(usage)                      => ParseResult.Success(usage)
     }
 }
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 398ba32f16..208ed68562 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
@@ -164,10 +164,4 @@ object Config {
 
   final case class GiteaCfg(
   ) extends ForgeSpecificCfg
-
-  sealed trait StewardUsage
-  object StewardUsage {
-    final case class Regular(config: Config) extends StewardUsage
-    final case class ValidateRepoConfig(file: File) extends StewardUsage
-  }
 }
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 0d397bf008..7ba009548b 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,8 +16,6 @@
 
 package org.scalasteward.core.application
 
-import better.files.File
-import cats.MonadThrow
 import cats.effect._
 import cats.effect.implicits._
 import cats.syntax.all._
@@ -25,7 +23,7 @@ import eu.timepit.refined.auto._
 import org.http4s.Uri
 import org.http4s.client.Client
 import org.http4s.headers.`User-Agent`
-import org.scalasteward.core.application.Config.{ForgeCfg, StewardUsage}
+import org.scalasteward.core.application.Config.ForgeCfg
 import org.scalasteward.core.buildtool.BuildToolDispatcher
 import org.scalasteward.core.buildtool.maven.MavenAlg
 import org.scalasteward.core.buildtool.mill.MillAlg
@@ -45,7 +43,7 @@ import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.nurture.{NurtureAlg, PullRequestRepository, UpdateInfoUrlFinder}
 import org.scalasteward.core.persistence.{CachingKeyValueStore, JsonKeyValueStore}
 import org.scalasteward.core.repocache._
-import org.scalasteward.core.repoconfig.{RepoConfigAlg, RepoConfigLoader, ValidateRepoConfigAlg}
+import org.scalasteward.core.repoconfig.{RepoConfigAlg, RepoConfigLoader}
 import org.scalasteward.core.scalafmt.ScalafmtAlg
 import org.scalasteward.core.update.artifact.{ArtifactMigrationsFinder, ArtifactMigrationsLoader}
 import org.scalasteward.core.update.{FilterAlg, PruningAlg, UpdateAlg}
@@ -89,29 +87,10 @@ final class Context[F[_]](implicit
 )
 
 object Context {
-
-  sealed trait StewardContext[F[_]] {
-    def runF: F[ExitCode]
-  }
-  object StewardContext {
-    final case class Regular[F[_]](context: Context[F]) extends StewardContext[F] {
-      override def runF: F[ExitCode] = context.stewardAlg.runF
-    }
-
-    final case class ValidateRepoConfig[F[_]](file: File)(implicit
-        val validateRepoConfigAlg: ValidateRepoConfigAlg[F],
-        val logger: Logger[F]
-    ) extends StewardContext[F] {
-      override def runF: F[ExitCode] = validateRepoConfigAlg.validateAndReport(file)
-    }
-  }
-
-  def step0[F[_]](
-      usage: Config.StewardUsage
-  )(implicit F: Async[F]): Resource[F, StewardContext[F]] =
+  def step0[F[_]](config: Config)(implicit F: Async[F]): Resource[F, Context[F]] =
     for {
-      logger0 <- Resource.eval(Slf4jLogger.fromName[F]("org.scalasteward.core"))
-      _ <- Resource.eval(logger0.info(banner))
+      logger <- Resource.eval(Slf4jLogger.fromName[F]("org.scalasteward.core"))
+      _ <- Resource.eval(logger.info(banner))
       _ <- Resource.eval(F.delay(System.setProperty("http.agent", userAgentString)))
       userAgent <- Resource.eval(F.fromEither(`User-Agent`.parse(1)(userAgentString)))
       middleware = ClientConfiguration
@@ -125,46 +104,22 @@ object Context {
         ClientConfiguration.disableFollowRedirect,
         middleware
       )
-      fileAlg0 = FileAlg.create(logger0, F)
-      context <- usage match {
-        case StewardUsage.Regular(config) =>
-          initRegular(config)(
-            defaultClient,
-            UrlCheckerClient(urlCheckerClient),
-            fileAlg0,
-            logger0,
-            F
-          ).map(StewardContext.Regular(_))
-
-        case StewardUsage.ValidateRepoConfig(file) =>
-          implicit val fileAlg: FileAlg[F] = fileAlg0
-          implicit val logger: Logger[F] = logger0
-          Resource.pure[F, StewardContext[F]](initValidateRepoConfig(file))
+      fileAlg = FileAlg.create(logger, F)
+      processAlg = ProcessAlg.create(config.processCfg)(logger, F)
+      workspaceAlg = WorkspaceAlg.create(config)(fileAlg, logger, F)
+      context <- Resource.eval {
+        step1(config)(
+          defaultClient,
+          UrlCheckerClient(urlCheckerClient),
+          fileAlg,
+          logger,
+          processAlg,
+          workspaceAlg,
+          F
+        )
       }
-
     } yield context
 
-  def initRegular[F[_]](config: Config)(implicit
-      client: Client[F],
-      urlCheckerClient: UrlCheckerClient[F],
-      fileAlg: FileAlg[F],
-      logger: Logger[F],
-      F: Async[F]
-  ): Resource[F, Context[F]] = {
-    implicit val processAlg = ProcessAlg.create(config.processCfg)
-    implicit val workspaceAlg = WorkspaceAlg.create(config)
-    Resource.eval(step1(config))
-  }
-
-  def initValidateRepoConfig[F[_]](file: File)(implicit
-      fileAlg: FileAlg[F],
-      logger: Logger[F],
-      F: MonadThrow[F]
-  ): StewardContext.ValidateRepoConfig[F] = {
-    implicit val validateRepoConfigAlg = new ValidateRepoConfigAlg[F]()
-    StewardContext.ValidateRepoConfig[F](file)
-  }
-
   def step1[F[_]](config: Config)(implicit
       client: Client[F],
       urlCheckerClient: UrlCheckerClient[F],
@@ -235,7 +190,7 @@ object Context {
       implicit val editAlg: EditAlg[F] = new EditAlg[F]
       implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config.forgeCfg)
       implicit val pruningAlg: PruningAlg[F] = new PruningAlg[F]
-      implicit val reposFilesLoader: ReposFilesLoader[F] = new ReposFilesLoader[F]()
+      implicit val reposFilesLoader: ReposFilesLoader[F] = new ReposFilesLoader[F]
       implicit val gitHubAppApiAlg: GitHubAppApiAlg[F] =
         new GitHubAppApiAlg[F](config.forgeCfg.apiHost)
       implicit val stewardAlg: StewardAlg[F] = new StewardAlg[F](config)
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala b/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
new file mode 100644
index 0000000000..51265fbceb
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2018-2023 Scala Steward contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.scalasteward.core.application
+
+import better.files.File
+import cats.effect.{ExitCode, Sync}
+import cats.syntax.all._
+import org.scalasteward.core.io.FileAlg
+import org.scalasteward.core.repoconfig.ValidateRepoConfigAlg
+import org.typelevel.log4cats.slf4j.Slf4jLogger
+
+object ValidateRepoConfigContext {
+  def run[F[_]](repoConfigFile: File)(implicit F: Sync[F]): F[ExitCode] =
+    for {
+      logger <- Slf4jLogger.fromName[F]("org.scalasteward.core")
+      fileAlg = FileAlg.create(logger, F)
+      validateRepoConfigAlg = new ValidateRepoConfigAlg()(fileAlg, logger, F)
+      exitCode <- validateRepoConfigAlg.validateAndReport(repoConfigFile)
+    } yield exitCode
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
index 0ec3a3ddc9..2f8e3598c4 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
@@ -4,9 +4,8 @@ import better.files.File
 import cats.data.Validated.Valid
 import munit.FunSuite
 import org.http4s.syntax.literals._
-import org.scalasteward.core.application.Cli.EnvVar
 import org.scalasteward.core.application.Cli.ParseResult._
-import org.scalasteward.core.application.Config.StewardUsage
+import org.scalasteward.core.application.Cli.{EnvVar, Usage}
 import org.scalasteward.core.forge.ForgeType
 import org.scalasteward.core.forge.github.GitHubApp
 import org.scalasteward.core.util.Nel
@@ -14,7 +13,7 @@ import scala.concurrent.duration._
 
 class CliTest extends FunSuite {
   test("parseArgs: example") {
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(
       List(
         List("--workspace", "a"),
         List("--repos-file", "b"),
@@ -79,7 +78,7 @@ class CliTest extends FunSuite {
   )
 
   test("parseArgs: minimal example") {
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(
       minimumRequiredParams.flatten
     )
 
@@ -92,7 +91,7 @@ class CliTest extends FunSuite {
   }
 
   test("parseArgs: enable sandbox") {
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(
       List(
         List("--workspace", "a"),
         List("--repos-file", "b"),
@@ -123,7 +122,7 @@ class CliTest extends FunSuite {
   }
 
   test("parseArgs: disable sandbox") {
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(
       List(
         List("--workspace", "a"),
         List("--repos-file", "b"),
@@ -157,7 +156,7 @@ class CliTest extends FunSuite {
       List("--gitlab-merge-when-pipeline-succeeds"),
       List("--gitlab-required-reviewers", "5")
     )
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(params.flatten)
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten)
 
     assert(obtained.gitLabCfg.mergeWhenPipelineSucceeds)
     assertEquals(obtained.gitLabCfg.requiredReviewers, Some(5))
@@ -174,7 +173,7 @@ class CliTest extends FunSuite {
   }
 
   test("parseArgs: validate-repo-config") {
-    val Success(StewardUsage.ValidateRepoConfig(file)) = Cli.parseArgs(
+    val Success(Usage.ValidateRepoConfig(file)) = Cli.parseArgs(
       List(
         List("validate-repo-config", "file.conf")
       ).flatten
@@ -188,7 +187,7 @@ class CliTest extends FunSuite {
       List("--forge-type", "azure-repos"),
       List("--do-not-fork")
     )
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(params.flatten)
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten)
     assert(obtained.forgeCfg.doNotFork)
   }
 
@@ -204,7 +203,7 @@ class CliTest extends FunSuite {
     val params = minimumRequiredParams ++ List(
       List("--forge-type", "bitbucket")
     )
-    val Success(StewardUsage.Regular(obtained)) = Cli.parseArgs(params.flatten)
+    val Success(Usage.Regular(obtained)) = Cli.parseArgs(params.flatten)
     assert(!obtained.forgeCfg.addLabels)
   }
 
diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala
index 4e5b7f800a..955eb6641c 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockConfig.scala
@@ -2,8 +2,8 @@ package org.scalasteward.core.mock
 
 import better.files.File
 import org.http4s.Uri
+import org.scalasteward.core.application.Cli
 import org.scalasteward.core.application.Cli.ParseResult.Success
-import org.scalasteward.core.application.{Cli, Config}
 
 object MockConfig {
   val mockRoot: File = File.temp / "scala-steward"
@@ -24,5 +24,5 @@ object MockConfig {
     "--add-labels",
     "--refresh-backoff-period=1hour"
   )
-  val Success(Config.StewardUsage.Regular(config)) = Cli.parseArgs(args)
+  val Success(Cli.Usage.Regular(config)) = Cli.parseArgs(args)
 }
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 b0733d82d3..63564347a5 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
@@ -1,12 +1,10 @@
 package org.scalasteward.core.mock
 
-import better.files.File
 import cats.data.Kleisli
 import cats.effect.kernel.Resource
 import cats.effect.unsafe.implicits.global
 import org.http4s.client.Client
-import org.scalasteward.core.application.Context
-import org.scalasteward.core.application.Context.StewardContext
+import org.scalasteward.core.application.{Config, Context}
 import org.scalasteward.core.edit.scalafix.ScalafixMigrationsLoader
 import org.scalasteward.core.io.FileAlgTest.ioFileAlg
 import org.scalasteward.core.io._
@@ -15,7 +13,6 @@ import org.scalasteward.core.repoconfig.RepoConfigLoader
 import org.scalasteward.core.update.artifact.ArtifactMigrationsLoader
 import org.scalasteward.core.util.UrlCheckerClient
 import org.typelevel.log4cats.Logger
-import org.scalasteward.core.application.Config
 
 object MockContext {
   implicit private val client: Client[MockEff] =
@@ -47,6 +44,4 @@ object MockContext {
   val context: Context[MockEff] = context(config)
   def context(stewardConfig: Config): Context[MockEff] =
     mockState.toRef.flatMap(Context.step1(stewardConfig).run).unsafeRunSync()
-  def validateRepoConfigContext(file: File): StewardContext.ValidateRepoConfig[MockEff] =
-    Context.initValidateRepoConfig(file)
 }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
index e4def2e154..ef93c3ebee 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
@@ -1,15 +1,15 @@
 package org.scalasteward.core.repoconfig
 
 import better.files.File
-import cats.effect.ExitCode
 import cats.effect.unsafe.implicits.global
+import cats.effect.{ExitCode, IO}
 import java.nio.charset.StandardCharsets
 import java.nio.file.Files
-import org.scalasteward.core.mock.{MockContext, MockState}
+import org.scalasteward.core.application.ValidateRepoConfigContext
 
 class ValidateRepoConfigAlgTest extends munit.FunSuite {
 
-  def configFile(content: String) = FunFixture[(File, ExitCode)](
+  private def configFile(content: String) = FunFixture[(File, ExitCode)](
     setup = { _ =>
       val tmpFile =
         File(
@@ -19,12 +19,7 @@ class ValidateRepoConfigAlgTest extends munit.FunSuite {
           )
         )
 
-      val obtained = MockContext
-        .validateRepoConfigContext(tmpFile)
-        .runF
-        .runA(MockState.empty)
-        .unsafeRunSync()
-
+      val obtained = ValidateRepoConfigContext.run[IO](tmpFile).unsafeRunSync()
       (tmpFile, obtained)
     },
     teardown = { case (file, _) =>
@@ -65,11 +60,7 @@ class ValidateRepoConfigAlgTest extends munit.FunSuite {
 
   test("rejects non-existent config file") {
     val nonExistentFile = File("/", "scripts", "script")
-    val obtained = MockContext
-      .validateRepoConfigContext(nonExistentFile)
-      .runF
-      .runA(MockState.empty)
-      .unsafeRunSync()
+    val obtained = ValidateRepoConfigContext.run[IO](nonExistentFile).unsafeRunSync()
 
     assertEquals(obtained, ExitCode.Error)
   }

From a609dd69069a08e3d063aa1528740a2304dc8d6c Mon Sep 17 00:00:00 2001
From: Scala Steward <43047562+scala-steward@users.noreply.github.com>
Date: Fri, 15 Dec 2023 10:41:59 +0100
Subject: [PATCH 02/21] Update sbt to 1.9.8 (#3245)

---
 project/build.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/project/build.properties b/project/build.properties
index e8a1e246e8..abbbce5da4 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.9.7
+sbt.version=1.9.8

From 91b3c3cf9873a333d8ac07b29849c5fb5423ef90 Mon Sep 17 00:00:00 2001
From: Scala Steward <43047562+scala-steward@users.noreply.github.com>
Date: Fri, 15 Dec 2023 10:42:42 +0100
Subject: [PATCH 03/21] Update sbt-typelevel-mergify to 0.6.4 (#3246)

---
 project/plugins.sbt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/project/plugins.sbt b/project/plugins.sbt
index 4f37ede861..81dc31e691 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -11,4 +11,4 @@ addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.7")
 addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.2")
 addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1")
 addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")
-addSbtPlugin("org.typelevel" % "sbt-typelevel-mergify" % "0.6.3")
+addSbtPlugin("org.typelevel" % "sbt-typelevel-mergify" % "0.6.4")

From fa408829e929bbc506045b1bc7d881507cec4a91 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 15 Dec 2023 10:56:00 +0100
Subject: [PATCH 04/21] Test validate-repo-config via `Main`

---
 .../org/scalasteward/core/application/CliTest.scala      | 2 +-
 .../core/repoconfig/ValidateRepoConfigAlgTest.scala      | 9 ++++-----
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
index 2f8e3598c4..ffbe23d46f 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/application/CliTest.scala
@@ -68,7 +68,7 @@ class CliTest extends FunSuite {
     assert(!obtained.bitbucketServerCfg.useDefaultReviewers)
   }
 
-  val minimumRequiredParams = List(
+  private val minimumRequiredParams = List(
     List("--workspace", "a"),
     List("--repos-file", "b"),
     List("--git-author-email", "d"),
diff --git a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
index ef93c3ebee..77d48a5208 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
@@ -1,11 +1,11 @@
 package org.scalasteward.core.repoconfig
 
 import better.files.File
+import cats.effect.ExitCode
 import cats.effect.unsafe.implicits.global
-import cats.effect.{ExitCode, IO}
 import java.nio.charset.StandardCharsets
 import java.nio.file.Files
-import org.scalasteward.core.application.ValidateRepoConfigContext
+import org.scalasteward.core.Main
 
 class ValidateRepoConfigAlgTest extends munit.FunSuite {
 
@@ -19,7 +19,7 @@ class ValidateRepoConfigAlgTest extends munit.FunSuite {
           )
         )
 
-      val obtained = ValidateRepoConfigContext.run[IO](tmpFile).unsafeRunSync()
+      val obtained = Main.run(List("validate-repo-config", tmpFile.pathAsString)).unsafeRunSync()
       (tmpFile, obtained)
     },
     teardown = { case (file, _) =>
@@ -59,8 +59,7 @@ class ValidateRepoConfigAlgTest extends munit.FunSuite {
     }
 
   test("rejects non-existent config file") {
-    val nonExistentFile = File("/", "scripts", "script")
-    val obtained = ValidateRepoConfigContext.run[IO](nonExistentFile).unsafeRunSync()
+    val obtained = Main.run(List("validate-repo-config", "/scripts.script")).unsafeRunSync()
 
     assertEquals(obtained, ExitCode.Error)
   }

From 6888553f7e7b6866000e56cb8439ad82b6bcc2b3 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 15 Dec 2023 15:02:51 +0100
Subject: [PATCH 05/21] Test ValidateRepoConfigAlg within MockState

---
 .../scala/org/scalasteward/core/Main.scala    |  2 +-
 .../ValidateRepoConfigContext.scala           | 25 +++--
 .../scalasteward/core/mock/MockContext.scala  |  5 +-
 .../ValidateRepoConfigAlgTest.scala           | 95 ++++++++-----------
 4 files changed, 62 insertions(+), 65 deletions(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/Main.scala b/modules/core/src/main/scala/org/scalasteward/core/Main.scala
index 60cc23842d..17c5d9d2b6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/Main.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/Main.scala
@@ -26,7 +26,7 @@ object Main extends IOApp {
       case Cli.ParseResult.Success(Cli.Usage.Regular(config)) =>
         Context.step0[IO](config).use(_.stewardAlg.runF)
       case Cli.ParseResult.Success(Cli.Usage.ValidateRepoConfig(file)) =>
-        ValidateRepoConfigContext.run[IO](file)
+        ValidateRepoConfigContext.step0[IO].flatMap(_.validateRepoConfigAlg.validateAndReport(file))
       case Cli.ParseResult.Help(help)   => Console[IO].println(help).as(ExitCode.Success)
       case Cli.ParseResult.Error(error) => Console[IO].errorln(error).as(ExitCode.Error)
     }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala b/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
index 51265fbceb..b9d33f66ed 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
@@ -16,19 +16,32 @@
 
 package org.scalasteward.core.application
 
-import better.files.File
-import cats.effect.{ExitCode, Sync}
+import cats.effect.Sync
 import cats.syntax.all._
 import org.scalasteward.core.io.FileAlg
 import org.scalasteward.core.repoconfig.ValidateRepoConfigAlg
+import org.typelevel.log4cats.Logger
 import org.typelevel.log4cats.slf4j.Slf4jLogger
 
+final class ValidateRepoConfigContext[F[_]](implicit
+    val validateRepoConfigAlg: ValidateRepoConfigAlg[F]
+)
+
 object ValidateRepoConfigContext {
-  def run[F[_]](repoConfigFile: File)(implicit F: Sync[F]): F[ExitCode] =
+  def step0[F[_]](implicit F: Sync[F]): F[ValidateRepoConfigContext[F]] =
     for {
       logger <- Slf4jLogger.fromName[F]("org.scalasteward.core")
       fileAlg = FileAlg.create(logger, F)
-      validateRepoConfigAlg = new ValidateRepoConfigAlg()(fileAlg, logger, F)
-      exitCode <- validateRepoConfigAlg.validateAndReport(repoConfigFile)
-    } yield exitCode
+      context = step1(fileAlg, logger, F)
+    } yield context
+
+  def step1[F[_]](implicit
+      fileAlg: FileAlg[F],
+      logger: Logger[F],
+      F: Sync[F]
+  ): ValidateRepoConfigContext[F] = {
+    implicit val validateRepoConfigAlg: ValidateRepoConfigAlg[F] =
+      new ValidateRepoConfigAlg()(fileAlg, logger, F)
+    new ValidateRepoConfigContext[F]
+  }
 }
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 63564347a5..1bfcb8a466 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
@@ -4,7 +4,7 @@ import cats.data.Kleisli
 import cats.effect.kernel.Resource
 import cats.effect.unsafe.implicits.global
 import org.http4s.client.Client
-import org.scalasteward.core.application.{Config, Context}
+import org.scalasteward.core.application.{Config, Context, ValidateRepoConfigContext}
 import org.scalasteward.core.edit.scalafix.ScalafixMigrationsLoader
 import org.scalasteward.core.io.FileAlgTest.ioFileAlg
 import org.scalasteward.core.io._
@@ -44,4 +44,7 @@ object MockContext {
   val context: Context[MockEff] = context(config)
   def context(stewardConfig: Config): Context[MockEff] =
     mockState.toRef.flatMap(Context.step1(stewardConfig).run).unsafeRunSync()
+
+  val validateRepoConfigContext: ValidateRepoConfigContext[MockEff] =
+    ValidateRepoConfigContext.step1
 }
diff --git a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
index 77d48a5208..b2a1c498d6 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/ValidateRepoConfigAlgTest.scala
@@ -1,66 +1,47 @@
 package org.scalasteward.core.repoconfig
 
-import better.files.File
 import cats.effect.ExitCode
-import cats.effect.unsafe.implicits.global
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import org.scalasteward.core.Main
-
-class ValidateRepoConfigAlgTest extends munit.FunSuite {
-
-  private def configFile(content: String) = FunFixture[(File, ExitCode)](
-    setup = { _ =>
-      val tmpFile =
-        File(
-          Files.write(
-            Files.createTempFile(".scala-steward", ".conf"),
-            content.getBytes(StandardCharsets.UTF_8)
-          )
-        )
-
-      val obtained = Main.run(List("validate-repo-config", tmpFile.pathAsString)).unsafeRunSync()
-      (tmpFile, obtained)
-    },
-    teardown = { case (file, _) =>
-      file.delete()
-    }
-  )
-
-  configFile(
-    """|updates.pin = [
-       |  { groupId = "org.scala-lang", artifactId="scala3-library", version = "3.1." },
-       |  { groupId = "org.scala-lang", artifactId="scala3-library_sjs1", version = "3.1." },
-       |  { groupId = "org.scala-js", artifactId="sbt-scalajs", version = "1.10." }
-       |]""".stripMargin
-  )
-    .test("accepts valid config") { case (_, obtained) =>
-      assertEquals(obtained, ExitCode.Success)
-    }
+import munit.CatsEffectSuite
+import org.scalasteward.core.mock.MockConfig.mockRoot
+import org.scalasteward.core.mock.MockContext.validateRepoConfigContext.validateRepoConfigAlg
+import org.scalasteward.core.mock.MockState
+
+class ValidateRepoConfigAlgTest extends CatsEffectSuite {
+  test("accepts valid config") {
+    val file = mockRoot / ".scala-steward.conf"
+    val content =
+      """|updates.pin = [
+         |  { groupId = "org.scala-lang", artifactId="scala3-library", version = "3.1." },
+         |  { groupId = "org.scala-js", artifactId="sbt-scalajs", version = "1.10." }
+         |]""".stripMargin
+    val state = MockState.empty.addFiles(file -> content)
+    val obtained = state.flatMap(validateRepoConfigAlg.validateAndReport(file).runA)
+    assertIO(obtained, ExitCode.Success)
+  }
 
-  configFile(
-    """|updates.pin  =? [
-       |  { groupId = "org.scala-lang", artifactId="scala3-library", version = "3.1." },
-       |  { groupId = "org.scala-lang", artifactId="scala3-library_sjs1", version = "3.1." },
-       |  { groupId = "org.scala-js", artifactId="sbt-scalajs", version = "1.10." },
-       |]""".stripMargin
-  )
-    .test("rejects config with a parsing failure") { case (_, obtained) =>
-      assertEquals(obtained, ExitCode.Error)
-    }
+  test("rejects config with a parsing failure") {
+    val file = mockRoot / ".scala-steward.conf"
+    val content =
+      """|updates.pin  =? [
+         |  { groupId = "org.scala-lang", artifactId="scala3-library", version = "3.1." },
+         |  { groupId = "org.scala-js", artifactId="sbt-scalajs", version = "1.10." },
+         |]""".stripMargin
+    val state = MockState.empty.addFiles(file -> content)
+    val obtained = state.flatMap(validateRepoConfigAlg.validateAndReport(file).runA)
+    assertIO(obtained, ExitCode.Error)
+  }
 
-  configFile(
-    """|updatePullRequests = 123
-       |
-       |""".stripMargin
-  )
-    .test("rejects config with a decoding failure") { case (_, obtained) =>
-      assertEquals(obtained, ExitCode.Error)
-    }
+  test("rejects config with a decoding failure") {
+    val file = mockRoot / ".scala-steward.conf"
+    val content = """updatePullRequests = 123""".stripMargin
+    val state = MockState.empty.addFiles(file -> content)
+    val obtained = state.flatMap(validateRepoConfigAlg.validateAndReport(file).runA)
+    assertIO(obtained, ExitCode.Error)
+  }
 
   test("rejects non-existent config file") {
-    val obtained = Main.run(List("validate-repo-config", "/scripts.script")).unsafeRunSync()
-
-    assertEquals(obtained, ExitCode.Error)
+    val file = mockRoot / ".scala-steward.conf"
+    val obtained = validateRepoConfigAlg.validateAndReport(file).runA(MockState.empty)
+    assertIO(obtained, ExitCode.Error)
   }
 }

From bfb1b07e44342e33333281ae8e4529dc03aa4cfe Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 15 Dec 2023 15:06:38 +0100
Subject: [PATCH 06/21] Don't pass implicits explicitly

---
 .../core/application/ValidateRepoConfigContext.scala           | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala b/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
index b9d33f66ed..3439427e94 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/application/ValidateRepoConfigContext.scala
@@ -40,8 +40,7 @@ object ValidateRepoConfigContext {
       logger: Logger[F],
       F: Sync[F]
   ): ValidateRepoConfigContext[F] = {
-    implicit val validateRepoConfigAlg: ValidateRepoConfigAlg[F] =
-      new ValidateRepoConfigAlg()(fileAlg, logger, F)
+    implicit val validateRepoConfigAlg: ValidateRepoConfigAlg[F] = new ValidateRepoConfigAlg
     new ValidateRepoConfigContext[F]
   }
 }

From 35ee8d5267aee698a9e1e9826da63f5f3ff986e6 Mon Sep 17 00:00:00 2001
From: Frank Thomas <frank@timepit.eu>
Date: Wed, 20 Dec 2023 08:06:38 +0100
Subject: [PATCH 07/21] Update short description of Scala Steward

---
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 9c23b122d8..3ad33357fa 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,8 @@
 [![Scala Steward badge](https://img.shields.io/badge/Scala_Steward-helping-blue.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=)](https://scala-steward.org)
 [![Docker Pulls](https://img.shields.io/docker/pulls/fthomas/scala-steward.svg?style=flat&color=blue)](https://hub.docker.com/r/fthomas/scala-steward/)
 
-Scala Steward is a bot that helps you keep your library dependencies, sbt plugins, and Scala and sbt versions up-to-date.
+Scala Steward is a bot that helps you keep your library dependencies and build plugins up-to-date.
+It works with [Maven](https://maven.apache.org/), [Mill](https://mill-build.com/), [sbt](https://www.scala-sbt.org/), and [Scala CLI](https://scala-cli.virtuslab.org/).
 
 See also the announcement blog post:
 [*Keep your projects up-to-date with Scala Steward*](https://www.scala-lang.org/blog/2019/07/10/announcing-scala-steward.html)

From ff2098adbc93249e32c662592d53cf90baa9d6a1 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 22 Dec 2023 08:23:42 +0100
Subject: [PATCH 08/21] Do not cache the output of `gitAskPass`

Prior to this change, the `gitAskPass` program was called once on
start-up and its output was cached and used for API calls to the forge
while Git itself does not cache its output but calls it anytime a password
is needed. This means if the output of `gitAskPass` changes during a Scala
Steward run, the new password is only used for Git operations but not for
forge API calls.

With this change we now do the same as Git and call `gitAskPass`
everytime the password is needed. This should make it easier to support
GitHub Apps proper which require different access tokens during a run.
See also https://github.com/scala-steward-org/scala-steward/issues/2973#issuecomment-1866224921.
---
 .../core/application/Config.scala             | 20 ---------
 .../core/application/Context.scala            |  5 ++-
 .../core/forge/ForgeAuthAlg.scala             | 42 +++++++++++++++++++
 .../core/forge/ForgeSelection.scala           | 29 +++++++------
 .../azurerepos/AzureReposApiAlgTest.scala     |  3 +-
 .../forge/bitbucket/BitbucketApiAlgTest.scala |  3 +-
 .../BitbucketServerApiAlgTest.scala           |  5 ++-
 .../core/forge/gitea/GiteaApiAlgTest.scala    |  3 +-
 .../core/forge/github/GitHubApiAlgTest.scala  |  3 +-
 .../core/forge/gitlab/GitLabApiAlgTest.scala  | 13 +++---
 10 files changed, 79 insertions(+), 47 deletions(-)
 create mode 100644 modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala

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 208ed68562..3385af7203 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
@@ -17,19 +17,13 @@
 package org.scalasteward.core.application
 
 import better.files.File
-import cats.Monad
-import cats.syntax.all._
 import org.http4s.Uri
-import org.http4s.Uri.UserInfo
 import org.scalasteward.core.application.Cli.EnvVar
 import org.scalasteward.core.application.Config._
 import org.scalasteward.core.data.Resolver
 import org.scalasteward.core.forge.ForgeType
-import org.scalasteward.core.forge.data.AuthenticatedUser
 import org.scalasteward.core.forge.github.GitHubApp
 import org.scalasteward.core.git.Author
-import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
-import org.scalasteward.core.util
 import org.scalasteward.core.util.Nel
 import scala.concurrent.duration.FiniteDuration
 
@@ -71,20 +65,6 @@ final case class Config(
     defaultResolver: Resolver,
     refreshBackoffPeriod: FiniteDuration
 ) {
-  def forgeUser[F[_]](implicit
-      processAlg: ProcessAlg[F],
-      workspaceAlg: WorkspaceAlg[F],
-      F: Monad[F]
-  ): F[AuthenticatedUser] =
-    for {
-      rootDir <- workspaceAlg.rootDir
-      urlWithUser = util.uri.withUserInfo
-        .replace(UserInfo(forgeCfg.login, None))(forgeCfg.apiHost)
-        .renderString
-      prompt = s"Password for '$urlWithUser': "
-      password <- processAlg.exec(Nel.of(gitCfg.gitAskPass.pathAsString, prompt), rootDir)
-    } yield AuthenticatedUser(forgeCfg.login, password.mkString.trim)
-
   def forgeSpecificCfg: ForgeSpecificCfg =
     forgeCfg.tpe match {
       case ForgeType.AzureRepos      => azureReposCfg
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 7ba009548b..cb18337892 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.edit.hooks.HookExecutor
 import org.scalasteward.core.edit.scalafix._
 import org.scalasteward.core.edit.update.ScannerAlg
 import org.scalasteward.core.forge.github.{GitHubAppApiAlg, GitHubAuthAlg}
-import org.scalasteward.core.forge.{ForgeApiAlg, ForgeRepoAlg, ForgeSelection}
+import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection}
 import org.scalasteward.core.git.{GenGitAlg, GitAlg}
 import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
 import org.scalasteward.core.nurture.{NurtureAlg, PullRequestRepository, UpdateInfoUrlFinder}
@@ -130,7 +130,8 @@ object Context {
       F: Async[F]
   ): F[Context[F]] =
     for {
-      forgeUser <- config.forgeUser[F]
+      _ <- F.unit
+      forgeUser = new ForgeAuthAlg[F](config.gitCfg, config.forgeCfg).forgeUser
       artifactMigrationsLoader0 = new ArtifactMigrationsLoader[F]
       artifactMigrationsFinder0 <- artifactMigrationsLoader0.createFinder(config.artifactCfg)
       scalafixMigrationsLoader0 = new ScalafixMigrationsLoader[F]
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala
new file mode 100644
index 0000000000..65debaa02e
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2018-2023 Scala Steward contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.scalasteward.core.forge
+
+import cats.Monad
+import cats.syntax.all._
+import org.http4s.Uri.UserInfo
+import org.scalasteward.core.application.Config.{ForgeCfg, GitCfg}
+import org.scalasteward.core.forge.data.AuthenticatedUser
+import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
+import org.scalasteward.core.util
+import org.scalasteward.core.util.Nel
+
+final class ForgeAuthAlg[F[_]](gitCfg: GitCfg, forgeCfg: ForgeCfg)(implicit
+    processAlg: ProcessAlg[F],
+    workspaceAlg: WorkspaceAlg[F],
+    F: Monad[F]
+) {
+  def forgeUser: F[AuthenticatedUser] =
+    for {
+      rootDir <- workspaceAlg.rootDir
+      userInfo = UserInfo(forgeCfg.login, None)
+      urlWithUser = util.uri.withUserInfo.replace(userInfo)(forgeCfg.apiHost).renderString
+      prompt = s"Password for '$urlWithUser': "
+      output <- processAlg.exec(Nel.of(gitCfg.gitAskPass.pathAsString, prompt), rootDir)
+      password = output.mkString.trim
+    } yield AuthenticatedUser(forgeCfg.login, password)
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
index e53fe350b2..bfe35fafca 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
@@ -18,7 +18,7 @@ package org.scalasteward.core.forge
 
 import cats.effect.Temporal
 import cats.syntax.all._
-import cats.{Applicative, Parallel}
+import cats.{Applicative, Functor, Parallel}
 import org.http4s.headers.Authorization
 import org.http4s.{BasicCredentials, Header, Request}
 import org.scalasteward.core.application.Config
@@ -39,7 +39,7 @@ object ForgeSelection {
   def forgeApiAlg[F[_]: Parallel](
       forgeCfg: ForgeCfg,
       forgeSpecificCfg: ForgeSpecificCfg,
-      user: AuthenticatedUser
+      user: F[AuthenticatedUser]
   )(implicit
       httpJsonClient: HttpJsonClient[F],
       logger: Logger[F],
@@ -64,16 +64,19 @@ object ForgeSelection {
 
   def authenticate[F[_]](
       forgeType: ForgeType,
-      user: AuthenticatedUser
-  )(implicit F: Applicative[F]): Request[F] => F[Request[F]] =
-    forgeType match {
-      case AzureRepos      => _.putHeaders(basicAuth(user)).pure[F]
-      case Bitbucket       => _.putHeaders(basicAuth(user)).pure[F]
-      case BitbucketServer => _.putHeaders(basicAuth(user), xAtlassianToken).pure[F]
-      case GitHub          => _.putHeaders(basicAuth(user)).pure[F]
-      case GitLab          => _.putHeaders(Header.Raw(ci"Private-Token", user.accessToken)).pure[F]
-      case Gitea           => _.putHeaders(basicAuth(user)).pure[F]
-    }
+      user: F[AuthenticatedUser]
+  )(implicit F: Functor[F]): Request[F] => F[Request[F]] =
+    req =>
+      user.map { user =>
+        forgeType match {
+          case AzureRepos      => req.putHeaders(basicAuth(user))
+          case Bitbucket       => req.putHeaders(basicAuth(user))
+          case BitbucketServer => req.putHeaders(basicAuth(user), xAtlassianToken)
+          case GitHub          => req.putHeaders(basicAuth(user))
+          case GitLab          => req.putHeaders(Header.Raw(ci"Private-Token", user.accessToken))
+          case Gitea           => req.putHeaders(basicAuth(user))
+        }
+      }
 
   private def basicAuth(user: AuthenticatedUser): Authorization =
     Authorization(BasicCredentials(user.login, user.accessToken))
@@ -84,7 +87,7 @@ object ForgeSelection {
 
   def authenticateIfApiHost[F[_]](
       forgeCfg: ForgeCfg,
-      user: AuthenticatedUser
+      user: F[AuthenticatedUser]
   )(implicit F: Applicative[F]): Request[F] => F[Request[F]] =
     req => {
       val sameScheme = req.uri.scheme === forgeCfg.apiHost.scheme
diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala
index 70b11e548b..c30d67fde7 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlgTest.scala
@@ -18,6 +18,7 @@ import org.scalasteward.core.mock.{MockEff, MockState}
 
 class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
   private val user = AuthenticatedUser("user", "pass")
+  private val userM = MockEff.pure(user)
   private val repo = Repo("scala-steward-org", "scala-steward")
   private val apiHost = uri"https://dev.azure.com"
 
@@ -175,7 +176,7 @@ class AzureReposApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
 
   private val forgeCfg = config.forgeCfg.copy(apiHost = apiHost, tpe = ForgeType.AzureRepos)
   private val azureReposCfg = AzureReposCfg(organization = Some("azure-org"))
-  private val azureReposApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, azureReposCfg, user)
+  private val azureReposApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, azureReposCfg, userM)
 
   test("getRepo") {
     val obtained = azureReposApiAlg.getRepo(repo).runA(state)
diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala
index be66fa5d47..4c06bc6824 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlgTest.scala
@@ -21,6 +21,7 @@ import org.scalasteward.core.mock.{MockEff, MockState}
 class BitbucketApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
 
   private val user = AuthenticatedUser("user", "pass")
+  private val userM = MockEff.pure(user)
 
   private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken))
   private val auth = HttpApp[MockEff] { request =>
@@ -209,7 +210,7 @@ class BitbucketApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
 
   private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.Bitbucket)
   private val bitbucketCfg = BitbucketCfg(useDefaultReviewers = true)
-  private val bitbucketApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, user)
+  private val bitbucketApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, bitbucketCfg, userM)
 
   private val prUrl = uri"https://bitbucket.org/fthomas/base.g8/pullrequests/2"
   private val repo = Repo("fthomas", "base.g8")
diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala
index 5b33d287a8..a8a6bf691e 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlgTest.scala
@@ -22,6 +22,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff]
   private val repo = Repo("scala-steward-org", "scala-steward")
   private val main = Branch("main")
   private val user = AuthenticatedUser("user", "pass")
+  private val userM = MockEff.pure(user)
 
   private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken))
   private val auth = HttpApp[MockEff] { request =>
@@ -112,7 +113,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff]
 
   private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.BitbucketServer)
   private val bitbucketServerApiAlg = ForgeSelection
-    .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = false), user)
+    .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = false), userM)
 
   test("createPullRequest") {
     val data = NewPullRequestData(
@@ -146,7 +147,7 @@ class BitbucketServerApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff]
       reviewers = Nil
     )
     val apiAlg = ForgeSelection
-      .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = true), user)
+      .forgeApiAlg[MockEff](forgeCfg, BitbucketServerCfg(useDefaultReviewers = true), userM)
     val pr = apiAlg.createPullRequest(repo, data).runA(state)
     val expected =
       PullRequestOut(
diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala
index 80f066bd0c..dcfa749814 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/forge/gitea/GiteaApiAlgTest.scala
@@ -20,6 +20,7 @@ import org.scalasteward.core.mock.{MockEff, MockState}
 
 class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
   private val user = AuthenticatedUser("user", "pass")
+  private val userM = MockEff.pure(user)
   private val repo = Repo("foo", "baz")
 
   private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken))
@@ -61,7 +62,7 @@ class GiteaApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
     tpe = ForgeType.Gitea,
     apiHost = config.forgeCfg.apiHost / "api" / "v1"
   )
-  private val giteaAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), user)
+  private val giteaAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GiteaCfg(), userM)
 
   test("getRepo") {
     giteaAlg
diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala
index e6f15da442..0c48e4a334 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubApiAlgTest.scala
@@ -23,6 +23,7 @@ import org.scalasteward.core.mock.{MockEff, MockState}
 class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
 
   private val user = AuthenticatedUser("user", "pass")
+  private val userM = MockEff.pure(user)
 
   private val basicAuth = Authorization(BasicCredentials(user.login, user.accessToken))
   private val auth = HttpApp[MockEff] { request =>
@@ -198,7 +199,7 @@ class GitHubApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
   private val state = MockState.empty.copy(clientResponses = auth <+> httpApp)
 
   private val forgeCfg = config.forgeCfg.copy(tpe = ForgeType.GitHub)
-  private val gitHubApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), user)
+  private val gitHubApiAlg = ForgeSelection.forgeApiAlg[MockEff](forgeCfg, GitHubCfg(), userM)
 
   private val repo = Repo("fthomas", "base.g8")
 
diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala
index b6a224456a..20c9fa128b 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlgTest.scala
@@ -27,6 +27,7 @@ import org.typelevel.ci.CIStringSyntax
 class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
 
   private val user = AuthenticatedUser("user", "pass")
+  private val userM = MockEff.pure(user)
 
   object MergeWhenPipelineSucceedsMatcher
       extends QueryParamDecoderMatcher[Boolean]("merge_when_pipeline_succeeds")
@@ -127,7 +128,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
       requiredReviewers = None,
       removeSourceBranch = false
     ),
-    user
+    userM
   )
 
   private val gitlabApiAlgNoFork = ForgeSelection.forgeApiAlg[MockEff](
@@ -137,7 +138,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
       requiredReviewers = None,
       removeSourceBranch = false
     ),
-    user
+    userM
   )
 
   private val gitlabApiAlgAutoMerge = ForgeSelection.forgeApiAlg[MockEff](
@@ -147,7 +148,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
       requiredReviewers = None,
       removeSourceBranch = false
     ),
-    user
+    userM
   )
 
   private val gitlabApiAlgRemoveSourceBranch = ForgeSelection.forgeApiAlg[MockEff](
@@ -157,7 +158,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
       requiredReviewers = None,
       removeSourceBranch = true
     ),
-    user
+    userM
   )
 
   private val gitlabApiAlgLessReviewersRequired = ForgeSelection.forgeApiAlg[MockEff](
@@ -167,7 +168,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
       requiredReviewers = Some(0),
       removeSourceBranch = false
     ),
-    user
+    userM
   )
 
   private val gitlabApiAlgWithAssigneeAndReviewers = ForgeSelection.forgeApiAlg[MockEff](
@@ -177,7 +178,7 @@ class GitLabApiAlgTest extends CatsEffectSuite with Http4sDsl[MockEff] {
       requiredReviewers = Some(0),
       removeSourceBranch = false
     ),
-    user
+    userM
   )
 
   private val data = UpdateData(

From 5bc70dfee4fb57491c9c67dff4b5dd3e4bf32469 Mon Sep 17 00:00:00 2001
From: Matthias Kurz <m.kurz@irregular.at>
Date: Fri, 22 Dec 2023 12:02:23 +0100
Subject: [PATCH 09/21] play-grpc changed groupid to org.playframework

---
 .../resources/artifact-migrations.v2.conf     | 25 +++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/modules/core/src/main/resources/artifact-migrations.v2.conf b/modules/core/src/main/resources/artifact-migrations.v2.conf
index 74fdc0f64a..cfa9ef3d5b 100644
--- a/modules/core/src/main/resources/artifact-migrations.v2.conf
+++ b/modules/core/src/main/resources/artifact-migrations.v2.conf
@@ -727,6 +727,31 @@ changes = [
     groupIdAfter = com.typesafe.play
     artifactIdAfter = play-grpc-testkit
   },
+  {
+    groupIdBefore = com.typesafe.play
+    groupIdAfter = org.playframework
+    artifactIdAfter = play-grpc-generators
+  },
+  {
+    groupIdBefore = com.typesafe.play
+    groupIdAfter = org.playframework
+    artifactIdAfter = play-grpc-runtime
+  },
+  {
+    groupIdBefore = com.typesafe.play
+    groupIdAfter = org.playframework
+    artifactIdAfter = play-grpc-scalatest
+  },
+  {
+    groupIdBefore = com.typesafe.play
+    groupIdAfter = org.playframework
+    artifactIdAfter = play-grpc-specs2
+  },
+  {
+    groupIdBefore = com.typesafe.play
+    groupIdAfter = org.playframework
+    artifactIdAfter = play-grpc-testkit
+  },
   {
     groupIdBefore = com.lightbend.sbt
     groupIdAfter = com.github.sbt

From d23c691cc70e1e46524d1b0192d9dfcfaddcff1e Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Sat, 23 Dec 2023 09:31:12 +0100
Subject: [PATCH 10/21] Do not capture forgeType in closure

---
 .../core/forge/ForgeSelection.scala           | 22 +++++++++----------
 1 file changed, 11 insertions(+), 11 deletions(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
index bfe35fafca..ad14d84b85 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
@@ -66,21 +66,21 @@ object ForgeSelection {
       forgeType: ForgeType,
       user: F[AuthenticatedUser]
   )(implicit F: Functor[F]): Request[F] => F[Request[F]] =
-    req =>
-      user.map { user =>
-        forgeType match {
-          case AzureRepos      => req.putHeaders(basicAuth(user))
-          case Bitbucket       => req.putHeaders(basicAuth(user))
-          case BitbucketServer => req.putHeaders(basicAuth(user), xAtlassianToken)
-          case GitHub          => req.putHeaders(basicAuth(user))
-          case GitLab          => req.putHeaders(Header.Raw(ci"Private-Token", user.accessToken))
-          case Gitea           => req.putHeaders(basicAuth(user))
-        }
-      }
+    forgeType match {
+      case AzureRepos      => req => user.map(u => req.putHeaders(basicAuth(u)))
+      case Bitbucket       => req => user.map(u => req.putHeaders(basicAuth(u)))
+      case BitbucketServer => req => user.map(u => req.putHeaders(basicAuth(u), xAtlassianToken))
+      case GitHub          => req => user.map(u => req.putHeaders(basicAuth(u)))
+      case GitLab          => req => user.map(u => req.putHeaders(privateToken(u)))
+      case Gitea           => req => user.map(u => req.putHeaders(basicAuth(u)))
+    }
 
   private def basicAuth(user: AuthenticatedUser): Authorization =
     Authorization(BasicCredentials(user.login, user.accessToken))
 
+  private def privateToken(user: AuthenticatedUser): Header.Raw =
+    Header.Raw(ci"Private-Token", user.accessToken)
+
   // Bypass the server-side XSRF check, see
   // https://github.com/scala-steward-org/scala-steward/pull/1863#issuecomment-754538364
   private val xAtlassianToken = Header.Raw(ci"X-Atlassian-Token", "no-check")

From 881d3223587afb81410d6587363422c862a4ee89 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Sat, 23 Dec 2023 14:55:56 +0100
Subject: [PATCH 11/21] Remove repo from function for modifying requests

This removes the unnecessary `Repo` parameter from the function for
modifying requests to add authentication headers. This parameter was
added in #373 but wasn't ever used as can be observed in the diff of
`ForgeSelection`. My motivation for adding `Repo` back then was to
eventually use different credentials for different repositories
for proper GitHub App support. But #3250 demonstrates that we can
achieve this without this extra parameter.
---
 .../core/forge/ForgeSelection.scala           |  2 +-
 .../forge/azurerepos/AzureReposApiAlg.scala   | 14 +++----
 .../forge/bitbucket/BitbucketApiAlg.scala     | 24 ++++++------
 .../BitbucketServerApiAlg.scala               | 20 +++++-----
 .../core/forge/gitea/GiteaApiAlg.scala        | 20 +++++-----
 .../core/forge/github/GitHubApiAlg.scala      | 26 ++++++-------
 .../core/forge/gitlab/GitLabApiAlg.scala      | 37 +++++++++----------
 7 files changed, 71 insertions(+), 72 deletions(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
index ad14d84b85..9fb7c0ca1f 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala
@@ -45,7 +45,7 @@ object ForgeSelection {
       logger: Logger[F],
       F: Temporal[F]
   ): ForgeApiAlg[F] = {
-    val auth = (_: Any) => authenticate(forgeCfg.tpe, user)
+    val auth = authenticate(forgeCfg.tpe, user)
     forgeSpecificCfg match {
       case specificCfg: Config.AzureReposCfg =>
         new AzureReposApiAlg(forgeCfg.apiHost, specificCfg, auth)
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala
index c399094026..4dd5698910 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/azurerepos/AzureReposApiAlg.scala
@@ -31,7 +31,7 @@ import org.typelevel.log4cats.Logger
 final class AzureReposApiAlg[F[_]](
     azureAPiHost: Uri,
     config: AzureReposCfg,
-    modify: Repo => Request[F] => F[Request[F]]
+    modify: Request[F] => F[Request[F]]
 )(implicit client: HttpJsonClient[F], logger: Logger[F], F: MonadThrow[F])
     extends ForgeApiAlg[F] {
 
@@ -45,7 +45,7 @@ final class AzureReposApiAlg[F[_]](
     val create = client.postWithBody[PullRequestOut, PullRequestPayload](
       url.pullRequests(repo),
       PullRequestPayload.from(data),
-      modify(repo)
+      modify
     )
     for {
       _ <- F.whenA(data.assignees.nonEmpty)(warnIfAssigneesAreUsed)
@@ -66,21 +66,21 @@ final class AzureReposApiAlg[F[_]](
     client.patchWithBody[PullRequestOut, ClosePullRequestPayload](
       url.closePullRequest(repo, number),
       ClosePullRequestPayload("abandoned"),
-      modify(repo)
+      modify
     )
 
   // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/stats/get?view=azure-devops-rest-7.1
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
-    client.get[BranchOut](url.getBranch(repo, branch), modify(repo))
+    client.get[BranchOut](url.getBranch(repo, branch), modify)
 
   // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/repositories/get-repository-with-parent?view=azure-devops-rest-7.1
   override def getRepo(repo: Repo): F[RepoOut] =
-    client.get[RepoOut](url.getRepo(repo), modify(repo))
+    client.get[RepoOut](url.getRepo(repo), modify)
 
   // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-requests/get-pull-requests?view=azure-devops-rest-7.1
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
     client
-      .get[Paginated[PullRequestOut]](url.listPullRequests(repo, head, base), modify(repo))
+      .get[Paginated[PullRequestOut]](url.listPullRequests(repo, head, base), modify)
       .map(_.value)
 
   // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/pull-request-threads/create?view=azure-devops-rest-7.1
@@ -92,7 +92,7 @@ final class AzureReposApiAlg[F[_]](
     client.postWithBody[Comment, PullRequestCommentPayload](
       url.commentPullRequest(repo, number),
       PullRequestCommentPayload.createComment(comment),
-      modify(repo)
+      modify
     )
 
   private def warnIfAssigneesAreUsed =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala
index fe81d9fb07..65af40706e 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucket/BitbucketApiAlg.scala
@@ -32,7 +32,7 @@ import org.typelevel.log4cats.Logger
 class BitbucketApiAlg[F[_]](
     config: ForgeCfg,
     bitbucketCfg: BitbucketCfg,
-    modify: Repo => Request[F] => F[Request[F]]
+    modify: Request[F] => F[Request[F]]
 )(implicit
     client: HttpJsonClient[F],
     logger: Logger[F],
@@ -42,13 +42,13 @@ class BitbucketApiAlg[F[_]](
 
   override def createFork(repo: Repo): F[RepoOut] =
     for {
-      fork <- client.post[RepositoryResponse](url.forks(repo), modify(repo)).recoverWith {
+      fork <- client.post[RepositoryResponse](url.forks(repo), modify).recoverWith {
         case UnexpectedResponse(_, _, _, Status.BadRequest, _) =>
-          client.get(url.repo(repo.copy(owner = config.login)), modify(repo))
+          client.get(url.repo(repo.copy(owner = config.login)), modify)
       }
       maybeParent <-
         fork.parent
-          .map(n => client.get[RepositoryResponse](url.repo(n), modify(n)))
+          .map(n => client.get[RepositoryResponse](url.repo(n), modify))
           .sequence[F, RepositoryResponse]
     } yield mapToRepoOut(fork, maybeParent)
 
@@ -65,7 +65,7 @@ class BitbucketApiAlg[F[_]](
     )
 
   private def getDefaultReviewers(repo: Repo): F[List[Reviewer]] =
-    client.get[DefaultReviewers](url.defaultReviewers(repo), modify(repo)).map(_.values)
+    client.get[DefaultReviewers](url.defaultReviewers(repo), modify).map(_.values)
 
   override def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = {
     val sourceBranchOwner = if (config.doNotFork) repo.owner else config.login
@@ -86,7 +86,7 @@ class BitbucketApiAlg[F[_]](
           )
         )
         .flatMap { payload =>
-          client.postWithBody(url.pullRequests(repo), payload, modify(repo))
+          client.postWithBody(url.pullRequests(repo), payload, modify)
         }
 
     for {
@@ -105,26 +105,26 @@ class BitbucketApiAlg[F[_]](
     logger.warn("Updating PRs is not yet supported for Bitbucket")
 
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
-    client.get(url.branch(repo, branch), modify(repo))
+    client.get(url.branch(repo, branch), modify)
 
   override def getRepo(repo: Repo): F[RepoOut] =
     for {
-      repo <- client.get[RepositoryResponse](url.repo(repo), modify(repo))
+      repo <- client.get[RepositoryResponse](url.repo(repo), modify)
       maybeParent <-
         repo.parent
-          .map(n => client.get[RepositoryResponse](url.repo(n), modify(n)))
+          .map(n => client.get[RepositoryResponse](url.repo(n), modify))
           .sequence[F, RepositoryResponse]
     } yield mapToRepoOut(repo, maybeParent)
 
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
     client
-      .get[Page[PullRequestOut]](url.listPullRequests(repo, head), modify(repo))
+      .get[Page[PullRequestOut]](url.listPullRequests(repo, head), modify)
       .map(_.values)
 
   override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client.post[PullRequestOut](
       url.decline(repo, number),
-      modify(repo)
+      modify
     )
 
   override def commentPullRequest(
@@ -136,7 +136,7 @@ class BitbucketApiAlg[F[_]](
       .postWithBody[CreateComment, CreateComment](
         url.comments(repo, number),
         CreateComment(comment),
-        modify(repo)
+        modify
       )
       .map((cc: CreateComment) => Comment(cc.content.raw))
 
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala
index 8120a16e6e..71cac81c6c 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/bitbucketserver/BitbucketServerApiAlg.scala
@@ -33,7 +33,7 @@ import org.typelevel.log4cats.Logger
 final class BitbucketServerApiAlg[F[_]](
     bitbucketApiHost: Uri,
     config: BitbucketServerCfg,
-    modify: Repo => Request[F] => F[Request[F]]
+    modify: Request[F] => F[Request[F]]
 )(implicit client: HttpJsonClient[F], logger: Logger[F], F: MonadThrow[F])
     extends ForgeApiAlg[F] {
   private val url = new Url(bitbucketApiHost)
@@ -53,7 +53,7 @@ final class BitbucketServerApiAlg[F[_]](
       .postWithBody[Json.Comment, Json.Comment](
         url.comments(repo, number),
         Json.Comment(comment),
-        modify(repo)
+        modify
       )
       .map(comment => Comment(comment.text))
 
@@ -82,7 +82,7 @@ final class BitbucketServerApiAlg[F[_]](
         locked = false,
         reviewers = reviewers
       )
-      pr <- client.postWithBody[Json.PR, Json.NewPR](url.pullRequests(repo), req, modify(repo))
+      pr <- client.postWithBody[Json.PR, Json.NewPR](url.pullRequests(repo), req, modify)
     } yield pr.toPullRequestOut
   }
 
@@ -97,10 +97,10 @@ final class BitbucketServerApiAlg[F[_]](
     if (config.useDefaultReviewers) getDefaultReviewers(repo) else F.pure(List.empty[Reviewer])
 
   private def declinePullRequest(repo: Repo, number: PullRequestNumber, version: Int): F[Unit] =
-    client.post_(url.declinePullRequest(repo, number, version), modify(repo))
+    client.post_(url.declinePullRequest(repo, number, version), modify)
 
   private def getDefaultReviewers(repo: Repo): F[List[Reviewer]] =
-    client.get[List[Json.Condition]](url.reviewers(repo), modify(repo)).map { conditions =>
+    client.get[List[Json.Condition]](url.reviewers(repo), modify).map { conditions =>
       conditions.flatMap { condition =>
         condition.reviewers.map(reviewer => Reviewer(User(reviewer.name)))
       }
@@ -108,25 +108,25 @@ final class BitbucketServerApiAlg[F[_]](
 
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
     client
-      .get[Json.Branches](url.listBranch(repo, branch), modify(repo))
+      .get[Json.Branches](url.listBranch(repo, branch), modify)
       .map(_.values.head.toBranchOut)
 
   private def getDefaultBranch(repo: Repo): F[Json.Branch] =
-    client.get[Json.Branch](url.defaultBranch(repo), modify(repo))
+    client.get[Json.Branch](url.defaultBranch(repo), modify)
 
   private def getPullRequest(repo: Repo, number: PullRequestNumber): F[PR] =
-    client.get[Json.PR](url.pullRequest(repo, number), modify(repo))
+    client.get[Json.PR](url.pullRequest(repo, number), modify)
 
   override def getRepo(repo: Repo): F[RepoOut] =
     for {
-      jRepo <- client.get[Json.Repo](url.repos(repo), modify(repo))
+      jRepo <- client.get[Json.Repo](url.repos(repo), modify)
       cloneUrl <- jRepo.cloneUrlOrRaise[F]
       defaultBranch <- getDefaultBranch(repo)
     } yield RepoOut(jRepo.slug, UserOut(repo.owner), None, cloneUrl, defaultBranch.displayId)
 
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
     client
-      .get[Json.Page[Json.PR]](url.listPullRequests(repo, s"refs/heads/$head"), modify(repo))
+      .get[Json.Page[Json.PR]](url.listPullRequests(repo, s"refs/heads/$head"), modify)
       .map(_.values.map(_.toPullRequestOut))
 
   private def warnIfLabelsAreUsed =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala
index 36faa27948..a9b9b32a03 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala
@@ -120,7 +120,7 @@ object GiteaApiAlg {
 
 final class GiteaApiAlg[F[_]: HttpJsonClient](
     vcs: ForgeCfg,
-    modify: Repo => Request[F] => F[Request[F]]
+    modify: Request[F] => F[Request[F]]
 )(implicit logger: Logger[F], F: MonadThrow[F])
     extends ForgeApiAlg[F] {
   import GiteaApiAlg._
@@ -158,7 +158,7 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
       .postWithBody[Repository, CreateForkOption](
         url.forks(repo),
         CreateForkOption(name = none, organization = none),
-        modify(repo)
+        modify
       )
       .map(repoOut(_))
 
@@ -182,7 +182,7 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
         .postWithBody[PullRequestResp, CreatePullRequestOption](
           url.pulls(repo),
           create,
-          modify(repo)
+          modify
         )
     } yield pullRequestOut(resp)
 
@@ -199,21 +199,21 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
       .patchWithBody[PullRequestResp, EditPullRequestOption](
         url.pull(repo, number),
         edit,
-        modify(repo)
+        modify
       )
       .map(pullRequestOut(_))
   }
 
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
     client
-      .get[BranchResp](url.repoBranch(repo, branch), modify(repo))
+      .get[BranchResp](url.repoBranch(repo, branch), modify)
       .map { b =>
         BranchOut(branch, CommitOut(b.commit.id))
       }
 
   override def getRepo(repo: Repo): F[RepoOut] =
     client
-      .get[Repository](url.repos(repo), modify(repo))
+      .get[Repository](url.repos(repo), modify)
       .map(repoOut(_))
 
   override def listPullRequests(
@@ -228,7 +228,7 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
             .pulls(repo)
             .withQueryParam("page", page)
             .withQueryParam("limit", PULL_REQUEST_PAGE_SIZE),
-          modify(repo)
+          modify
         )
 
     // basically unfoldEval
@@ -255,7 +255,7 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
       .postWithBody[CommentResp, CreateIssueCommentOption](
         url.comments(repo, number),
         create,
-        modify(repo)
+        modify
       )
       .map { x =>
         Comment(x.body)
@@ -277,14 +277,14 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
     client.postWithBody[Label, CreateLabelReq](
       url.labels(repo),
       CreateLabelReq(name, DefaultLabelColor),
-      modify(repo)
+      modify
     )
 
   def listLabels(repo: Repo): F[Vector[Label]] = {
     def paging(page: Int) =
       client.get[Vector[Label]](
         url.labels(repo).withQueryParam("page", page),
-        modify(repo)
+        modify
       )
 
     def go(page: Int, accu: Vector[Label]): F[Vector[Label]] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala
index 41bc2a2921..18c4a1f7c6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApiAlg.scala
@@ -18,6 +18,7 @@ package org.scalasteward.core.forge.github
 
 import cats.MonadThrow
 import cats.syntax.all._
+import io.circe.Json
 import org.http4s.{Request, Uri}
 import org.scalasteward.core.data.Repo
 import org.scalasteward.core.forge.ForgeApiAlg
@@ -26,11 +27,10 @@ import org.scalasteward.core.forge.github.GitHubException._
 import org.scalasteward.core.git.Branch
 import org.scalasteward.core.util.HttpJsonClient
 import org.typelevel.log4cats.Logger
-import io.circe.Json
 
 final class GitHubApiAlg[F[_]](
     gitHubApiHost: Uri,
-    modify: Repo => Request[F] => F[Request[F]]
+    modify: Request[F] => F[Request[F]]
 )(implicit
     client: HttpJsonClient[F],
     logger: Logger[F],
@@ -40,7 +40,7 @@ final class GitHubApiAlg[F[_]](
 
   /** https://docs.github.com/en/rest/repos/forks?apiVersion=2022-11-28#create-a-fork */
   override def createFork(repo: Repo): F[RepoOut] =
-    client.post[RepoOut](url.forks(repo), modify(repo)).flatTap { repoOut =>
+    client.post[RepoOut](url.forks(repo), modify).flatTap { repoOut =>
       F.raiseWhen(repoOut.parent.exists(_.archived))(RepositoryArchived(repo))
     }
 
@@ -51,7 +51,7 @@ final class GitHubApiAlg[F[_]](
       .postWithBody[PullRequestOut, CreatePullRequestPayload](
         uri = url.pulls(repo),
         body = payload,
-        modify = modify(repo)
+        modify = modify
       )
       .adaptErr(SecondaryRateLimitExceeded.fromThrowable)
 
@@ -77,7 +77,7 @@ final class GitHubApiAlg[F[_]](
       .patchWithBody[PullRequestOut, UpdatePullRequestPayload](
         uri = url.pull(repo, number),
         body = payload,
-        modify = modify(repo)
+        modify = modify
       )
       .adaptErr(SecondaryRateLimitExceeded.fromThrowable)
 
@@ -91,24 +91,24 @@ final class GitHubApiAlg[F[_]](
 
   /** https://docs.github.com/en/rest/repos/branches?apiVersion=2022-11-28#get-branch */
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
-    client.get(url.branches(repo, branch), modify(repo))
+    client.get(url.branches(repo, branch), modify)
 
   /** https://docs.github.com/en/rest/repos?apiVersion=2022-11-28#get */
   override def getRepo(repo: Repo): F[RepoOut] =
-    client.get[RepoOut](url.repos(repo), modify(repo)).flatTap { repoOut =>
+    client.get[RepoOut](url.repos(repo), modify).flatTap { repoOut =>
       F.raiseWhen(repoOut.archived)(RepositoryArchived(repo))
     }
 
   /** https://docs.github.com/en/rest/pulls?apiVersion=2022-11-28#list-pull-requests */
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
-    client.get(url.listPullRequests(repo, head, base), modify(repo))
+    client.get(url.listPullRequests(repo, head, base), modify)
 
   /** https://docs.github.com/en/rest/pulls?apiVersion=2022-11-28#update-a-pull-request */
   override def closePullRequest(repo: Repo, number: PullRequestNumber): F[PullRequestOut] =
     client.patchWithBody[PullRequestOut, UpdateState](
       url.pull(repo, number),
       UpdateState(PullRequestState.Closed),
-      modify(repo)
+      modify
     )
 
   /** https://docs.github.com/en/rest/issues?apiVersion=2022-11-28#create-an-issue-comment */
@@ -118,7 +118,7 @@ final class GitHubApiAlg[F[_]](
       comment: String
   ): F[Comment] =
     client
-      .postWithBody(url.comments(repo, number), Comment(comment), modify(repo))
+      .postWithBody(url.comments(repo, number), Comment(comment), modify)
 
   /** https://docs.github.com/en/rest/reference/issues?apiVersion=2022-11-28#add-labels-to-an-issue
     */
@@ -131,7 +131,7 @@ final class GitHubApiAlg[F[_]](
       .postWithBody[io.circe.Json, GitHubLabels](
         url.issueLabels(repo, number),
         GitHubLabels(labels),
-        modify(repo)
+        modify
       )
       .adaptErr(SecondaryRateLimitExceeded.fromThrowable)
       .void
@@ -145,7 +145,7 @@ final class GitHubApiAlg[F[_]](
       .postWithBody[Json, GitHubAssignees](
         url.assignees(repo, number),
         GitHubAssignees(assignees),
-        modify(repo)
+        modify
       )
       .void
       .handleErrorWith { error =>
@@ -161,7 +161,7 @@ final class GitHubApiAlg[F[_]](
       .postWithBody[Json, GitHubReviewers](
         url.reviewers(repo, number),
         GitHubReviewers(reviewers),
-        modify(repo)
+        modify
       )
       .void
       .handleErrorWith { error =>
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala
index 53e543a5ed..63da7a5427 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/gitlab/GitLabApiAlg.scala
@@ -16,8 +16,8 @@
 
 package org.scalasteward.core.forge.gitlab
 
-import cats.effect.Temporal
 import cats.Parallel
+import cats.effect.Temporal
 import cats.syntax.all._
 import io.circe._
 import io.circe.generic.semiauto._
@@ -31,7 +31,6 @@ import org.scalasteward.core.git.{Branch, Sha1}
 import org.scalasteward.core.util.uri.uriDecoder
 import org.scalasteward.core.util.{intellijThisImportIsUsed, HttpJsonClient, UnexpectedResponse}
 import org.typelevel.log4cats.Logger
-
 import scala.concurrent.duration.{Duration, DurationInt}
 
 final private[gitlab] case class ForkPayload(id: String, namespace: String)
@@ -161,7 +160,7 @@ private[gitlab] object GitLabJsonCodec {
 final class GitLabApiAlg[F[_]: Parallel](
     forgeCfg: ForgeCfg,
     gitLabCfg: GitLabCfg,
-    modify: Repo => Request[F] => F[Request[F]]
+    modify: Request[F] => F[Request[F]]
 )(implicit
     client: HttpJsonClient[F],
     logger: Logger[F],
@@ -172,13 +171,13 @@ final class GitLabApiAlg[F[_]: Parallel](
   private val url = new Url(forgeCfg.apiHost)
 
   override def listPullRequests(repo: Repo, head: String, base: Branch): F[List[PullRequestOut]] =
-    client.get(url.listMergeRequests(repo, head, base.name), modify(repo))
+    client.get(url.listMergeRequests(repo, head, base.name), modify)
 
   override def createFork(repo: Repo): F[RepoOut] = {
     val userOwnedRepo = repo.copy(owner = forgeCfg.login)
     val data = ForkPayload(url.encodedProjectId(userOwnedRepo), forgeCfg.login)
     client
-      .postWithBody[RepoOut, ForkPayload](url.createFork(repo), data, modify(repo))
+      .postWithBody[RepoOut, ForkPayload](url.createFork(repo), data, modify)
       .recoverWith {
         case UnexpectedResponse(_, _, _, Status.Conflict, _) => getRepo(userOwnedRepo)
         // workaround for https://gitlab.com/gitlab-org/gitlab-ce/issues/65275
@@ -190,8 +189,8 @@ final class GitLabApiAlg[F[_]: Parallel](
   override def createPullRequest(repo: Repo, data: NewPullRequestData): F[PullRequestOut] = {
     val targetRepo = if (forgeCfg.doNotFork) repo else repo.copy(owner = forgeCfg.login)
     val mergeRequest = for {
-      projectId <- client.get[ProjectId](url.repos(repo), modify(repo))
-      usernameMapping <- getUsernameToUserIdsMapping(repo, (data.assignees ++ data.reviewers).toSet)
+      projectId <- client.get[ProjectId](url.repos(repo), modify)
+      usernameMapping <- getUsernameToUserIdsMapping((data.assignees ++ data.reviewers).toSet)
       payload = MergeRequestPayload(
         id = url.encodedProjectId(targetRepo),
         projectId = projectId.id,
@@ -202,7 +201,7 @@ final class GitLabApiAlg[F[_]: Parallel](
       res <- client.postWithBody[MergeRequestOut, MergeRequestPayload](
         uri = url.mergeRequest(targetRepo),
         body = payload,
-        modify = modify(repo)
+        modify = modify
       )
     } yield res
 
@@ -213,7 +212,7 @@ final class GitLabApiAlg[F[_]: Parallel](
         backoffMultiplier: Double = 2.0
     ): F[MergeRequestOut] =
       client
-        .get[MergeRequestOut](url.existingMergeRequest(repo, number), modify(repo))
+        .get[MergeRequestOut](url.existingMergeRequest(repo, number), modify)
         .flatMap {
           case mr if mr.mergeStatus =!= GitLabMergeStatus.Checking => F.pure(mr)
           case mr if retries > 0 =>
@@ -264,7 +263,7 @@ final class GitLabApiAlg[F[_]: Parallel](
             client
               .put[MergeRequestOut](
                 url.mergeWhenPiplineSucceeds(repo, mr.iid),
-                modify(repo)
+                modify
               )
               // it's possible that our status changed from can be merged already,
               // so just handle it gracefully and proceed without setting auto merge.
@@ -289,7 +288,7 @@ final class GitLabApiAlg[F[_]: Parallel](
             client
               .put[MergeRequestApprovalsOut](
                 url.requiredApprovals(repo, mrOut.iid, requiredReviewers),
-                modify(repo)
+                modify
               )
               .map(_ => ())
               .recoverWith { case UnexpectedResponse(_, _, _, status, body) =>
@@ -301,18 +300,18 @@ final class GitLabApiAlg[F[_]: Parallel](
       case None => F.pure(mrOut)
     }
 
-  private def getUsernameToUserIdsMapping(repo: Repo, usernames: Set[String]): F[Map[String, Int]] =
+  private def getUsernameToUserIdsMapping(usernames: Set[String]): F[Map[String, Int]] =
     usernames.toList
       .parTraverse { username =>
-        getUserIdForUsername(repo, username).map { userIdOpt =>
+        getUserIdForUsername(username).map { userIdOpt =>
           userIdOpt.map(userId => (username, userId))
         }
       }
       .map(_.flatten.toMap)
 
-  private def getUserIdForUsername(repo: Repo, username: String): F[Option[Int]] = {
+  private def getUserIdForUsername(username: String): F[Option[Int]] = {
     val userIdOrError: F[Decoder.Result[Int]] = client
-      .get[Json](url.users.withQueryParam("username", username), modify(repo))
+      .get[Json](url.users.withQueryParam("username", username), modify)
       .flatMap { usersReponse =>
         usersReponse.hcursor.values match {
           case Some(users) =>
@@ -341,15 +340,15 @@ final class GitLabApiAlg[F[_]: Parallel](
       .putWithBody[MergeRequestOut, UpdateState](
         url.existingMergeRequest(repo, number),
         UpdateState(PullRequestState.Closed),
-        modify(repo)
+        modify
       )
       .map(_.pullRequestOut)
 
   override def getBranch(repo: Repo, branch: Branch): F[BranchOut] =
-    client.get(url.getBranch(repo, branch), modify(repo))
+    client.get(url.getBranch(repo, branch), modify)
 
   override def getRepo(repo: Repo): F[RepoOut] =
-    client.get(url.repos(repo), modify(repo))
+    client.get(url.repos(repo), modify)
 
   override def referencePullRequest(number: PullRequestNumber): String =
     s"!${number.value}"
@@ -360,6 +359,6 @@ final class GitLabApiAlg[F[_]: Parallel](
       number: PullRequestNumber,
       comment: String
   ): F[Comment] =
-    client.postWithBody(url.comments(repo, number), Comment(comment), modify(repo))
+    client.postWithBody(url.comments(repo, number), Comment(comment), modify)
 
 }

From a3c015a714e022ffae74ee4591ff8bbafe5ffd2c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Zu=CC=88hlke?= <mzuehlke@gmail.com>
Date: Tue, 26 Dec 2023 10:09:39 +0100
Subject: [PATCH 12/21] Document how to control Scala updates

---
 docs/faq.md              | 6 ++++++
 modules/docs/mdoc/faq.md | 6 ++++++
 2 files changed, 12 insertions(+)

diff --git a/docs/faq.md b/docs/faq.md
index 3e4bc694f3..a9bec186b2 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -45,6 +45,12 @@ Updates for `sbt` and `scalafmt` can be controlled by using the following `group
 { groupId = "org.scalameta", artifactId = "scalafmt-core" }
 ```
 
+Updates for the Scala 2 or Scala 3 version can be controlled by using the respective `groupId` and `artifactId`:
+```properties
+{ groupId = "org.scala-lang", artifactId = "scala-library" }
+{ groupId = "org.scala-lang", artifactId = "scala3-library" }
+```
+
 ## 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
diff --git a/modules/docs/mdoc/faq.md b/modules/docs/mdoc/faq.md
index d242c086e8..4b0ec7f177 100644
--- a/modules/docs/mdoc/faq.md
+++ b/modules/docs/mdoc/faq.md
@@ -45,6 +45,12 @@ Updates for `sbt` and `scalafmt` can be controlled by using the following `group
 { groupId = "org.scalameta", artifactId = "scalafmt-core" }
 ```
 
+Updates for the Scala 2 or Scala 3 version can be controlled by using the respective `groupId` and `artifactId`:
+```properties
+{ groupId = "org.scala-lang", artifactId = "scala-library" }
+{ groupId = "org.scala-lang", artifactId = "scala3-library" }
+```
+
 ## 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

From 7697d2ee486cafc50ae307fc4a9f2568044dea73 Mon Sep 17 00:00:00 2001
From: Marissa | April <7505383+NthPortal@users.noreply.github.com>
Date: Fri, 22 Dec 2023 09:59:07 -0500
Subject: [PATCH 13/21] Rename `otel4s-java*` to `otel4s-oteljava*`

Rename `org.typelevel:otel4s-java*` to `org.typelevel:otel4s-oteljava*`.
---
 .../resources/artifact-migrations.v2.conf     | 20 +++++++++++++++++++
 1 file changed, 20 insertions(+)

diff --git a/modules/core/src/main/resources/artifact-migrations.v2.conf b/modules/core/src/main/resources/artifact-migrations.v2.conf
index cfa9ef3d5b..085a8ff8aa 100644
--- a/modules/core/src/main/resources/artifact-migrations.v2.conf
+++ b/modules/core/src/main/resources/artifact-migrations.v2.conf
@@ -1099,4 +1099,24 @@ changes = [
     groupIdAfter = org.playframework.silhouette
     artifactIdAfter = play-silhouette
   },
+  {
+    groupIdAfter = org.typelevel
+    artifactIdBefore = otel4s-java
+    artifactIdAfter = otel4s-oteljava
+  },
+  {
+    groupIdAfter = org.typelevel
+    artifactIdBefore = otel4s-java-common
+    artifactIdAfter = otel4s-oteljava-common
+  },
+  {
+    groupIdAfter = org.typelevel
+    artifactIdBefore = otel4s-java-metrics
+    artifactIdAfter = otel4s-oteljava-metrics
+  },
+  {
+    groupIdAfter = org.typelevel
+    artifactIdBefore = otel4s-java-trace
+    artifactIdAfter = otel4s-oteljava-trace
+  },
 ]

From c024435713879c63d06070c0825d4d144887d602 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Thu, 28 Dec 2023 08:43:06 +0100
Subject: [PATCH 14/21] Move `Encoder[Uri]` to `util.uri`

This instance is general enough to be added to the `util.uri` object.
---
 .../core/forge/gitea/GiteaApiAlg.scala         | 18 +++++++++---------
 .../scala/org/scalasteward/core/util/uri.scala |  9 ++++++---
 2 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala
index a9b9b32a03..2364afd069 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/gitea/GiteaApiAlg.scala
@@ -18,14 +18,17 @@ package org.scalasteward.core.forge.gitea
 
 import cats._
 import cats.implicits._
-import org.scalasteward.core.git.Branch
-import org.scalasteward.core.git.Sha1
-import org.scalasteward.core.util.HttpJsonClient
+import io.circe._
+import io.circe.generic.semiauto.{deriveCodec, deriveEncoder}
 import org.http4s.{Request, Uri}
 import org.scalasteward.core.application.Config.ForgeCfg
 import org.scalasteward.core.data.Repo
 import org.scalasteward.core.forge.ForgeApiAlg
 import org.scalasteward.core.forge.data._
+import org.scalasteward.core.forge.gitea.GiteaApiAlg._
+import org.scalasteward.core.git.{Branch, Sha1}
+import org.scalasteward.core.util.uri._
+import org.scalasteward.core.util.{intellijThisImportIsUsed, HttpJsonClient}
 import org.typelevel.log4cats.Logger
 
 // docs
@@ -33,10 +36,6 @@ import org.typelevel.log4cats.Logger
 // - https://try.gitea.io/api/swagger
 // - https://codeberg.org/api/swagger
 object GiteaApiAlg {
-  import io.circe._
-  import io.circe.generic.semiauto.deriveCodec
-  import org.scalasteward.core.util.uri._
-  implicit val uriEncoder: Encoder[Uri] = Encoder[String].contramap[Uri](_.renderString)
 
   val DefaultLabelColor = "#e01060"
 
@@ -44,7 +43,7 @@ object GiteaApiAlg {
       name: Option[String], // name of the forked repository
       organization: Option[String] // organization name, if forking into an organization
   )
-  implicit val createForkOptionCodec: Encoder[CreateForkOption] = deriveCodec
+  implicit val createForkOptionEncoder: Encoder[CreateForkOption] = deriveEncoder
 
   case class User(
       login: String,
@@ -116,6 +115,8 @@ object GiteaApiAlg {
 
   case class AttachLabelReq(labels: Vector[Int])
   implicit val attachLabelReqCodec: Codec[AttachLabelReq] = deriveCodec
+
+  intellijThisImportIsUsed(uriEncoder)
 }
 
 final class GiteaApiAlg[F[_]: HttpJsonClient](
@@ -123,7 +124,6 @@ final class GiteaApiAlg[F[_]: HttpJsonClient](
     modify: Request[F] => F[Request[F]]
 )(implicit logger: Logger[F], F: MonadThrow[F])
     extends ForgeApiAlg[F] {
-  import GiteaApiAlg._
 
   def client: HttpJsonClient[F] = implicitly
   val url = new Url(vcs.apiHost)
diff --git a/modules/core/src/main/scala/org/scalasteward/core/util/uri.scala b/modules/core/src/main/scala/org/scalasteward/core/util/uri.scala
index 614d8b88d3..b21dfd3865 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/util/uri.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/util/uri.scala
@@ -17,7 +17,7 @@
 package org.scalasteward.core.util
 
 import cats.syntax.all._
-import io.circe.{Decoder, KeyDecoder, KeyEncoder}
+import io.circe.{Decoder, Encoder, KeyDecoder, KeyEncoder}
 import monocle.Optional
 import org.http4s.Uri
 import org.http4s.Uri.{Authority, Scheme, UserInfo}
@@ -26,16 +26,19 @@ object uri {
   implicit val uriDecoder: Decoder[Uri] =
     Decoder[String].emap(s => Uri.fromString(s).leftMap(_.getMessage))
 
+  implicit val uriEncoder: Encoder[Uri] =
+    Encoder[String].contramap[Uri](_.renderString)
+
   implicit val uriKeyDecoder: KeyDecoder[Uri] =
     KeyDecoder.instance(Uri.fromString(_).toOption)
 
   implicit val uriKeyEncoder: KeyEncoder[Uri] =
     KeyEncoder.instance(_.renderString)
 
-  val withAuthority: Optional[Uri, Authority] =
+  private val withAuthority: Optional[Uri, Authority] =
     Optional[Uri, Authority](_.authority)(authority => _.copy(authority = Some(authority)))
 
-  val authorityWithUserInfo: Optional[Authority, UserInfo] =
+  private val authorityWithUserInfo: Optional[Authority, UserInfo] =
     Optional[Authority, UserInfo](_.userInfo)(userInfo => _.copy(userInfo = Some(userInfo)))
 
   val withUserInfo: Optional[Uri, UserInfo] =

From aee074dbf1d1001912fd2824cec110c3ae1732a5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Zu=CC=88hlke?= <mzuehlke@gmail.com>
Date: Thu, 28 Dec 2023 21:01:02 +0100
Subject: [PATCH 15/21] Ignore circe-yaml 1.15.0 See:
 https://github.com/circe/circe-yaml/issues/402

---
 modules/core/src/main/resources/default.scala-steward.conf | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/modules/core/src/main/resources/default.scala-steward.conf b/modules/core/src/main/resources/default.scala-steward.conf
index 6467fbd9da..347d36deb2 100644
--- a/modules/core/src/main/resources/default.scala-steward.conf
+++ b/modules/core/src/main/resources/default.scala-steward.conf
@@ -194,4 +194,7 @@ updates.ignore = [
 
   // https://github.com/scalameta/scalafmt/issues/3689
   { groupId = "org.scalameta", artifactId = "scalafmt-core", version = "3.7.16" },
+
+  // https://github.com/circe/circe-yaml/issues/402
+  { groupId = "io.circe", artifactId="circe-yaml", version="1.15.0" },
 ]

From 0809ada265e14ba48906026dec36e54d7b9d7ebb Mon Sep 17 00:00:00 2001
From: Tobias Schlatter <schlatter.tobias@gmail.com>
Date: Sun, 31 Dec 2023 15:38:28 +0100
Subject: [PATCH 16/21] Allow to upgrade to Scala.js 1.15.0

We add new ignore rules for the new scalajs-scalalib artifact
introduced in Scala.js 1.15.0.
---
 .../main/resources/default.scala-steward.conf | 116 +++++++++---------
 1 file changed, 60 insertions(+), 56 deletions(-)

diff --git a/modules/core/src/main/resources/default.scala-steward.conf b/modules/core/src/main/resources/default.scala-steward.conf
index 347d36deb2..8ca3c128b4 100644
--- a/modules/core/src/main/resources/default.scala-steward.conf
+++ b/modules/core/src/main/resources/default.scala-steward.conf
@@ -48,64 +48,68 @@ updates.ignore = [
   { groupId = "org.scala-lang", artifactId = "scalap",         version = { exact = "2.12.19" } },
 
   // Ignore the next Scala.js patch version until it is announced.
-  { groupId = "org.scala-js", artifactId = "sbt-scalajs",                     version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-ir",                      version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-ir_sjs1",                 version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface",        version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface_sjs1",   version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker",                  version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker_sjs1",             version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-sbt-test-adapter",        version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-compiler",                version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-javalib",                 version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-javalib-intf",            version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library",                 version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library_2.11",            version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library_2.12",            version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library_2.13",            version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface",          version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.11",     version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.12",     version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.13",     version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge",             version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.11",        version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.12",        version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.13",        version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-plugin",       version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime",      version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.11", version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.12", version = { exact = "1.14.1" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.13", version = { exact = "1.14.1" } },
+  { groupId = "org.scala-js", artifactId = "sbt-scalajs",                     version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-ir",                      version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-ir_sjs1",                 version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface",        version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface_sjs1",   version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker",                  version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker_sjs1",             version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-sbt-test-adapter",        version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-compiler",                version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-javalib",                 version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-javalib-intf",            version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library",                 version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library_2.11",            version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library_2.12",            version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library_2.13",            version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-scalalib_2.12",           version = { suffix = "+1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-scalalib_2.13",           version = { suffix = "+1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface",          version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.11",     version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.12",     version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.13",     version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge",             version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.11",        version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.12",        version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.13",        version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-plugin",       version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime",      version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.11", version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.12", version = { exact  =  "1.15.1" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.13", version = { exact  =  "1.15.1" } },
 
   // Ignore the next Scala.js minor version until it is announced.
-  { groupId = "org.scala-js", artifactId = "sbt-scalajs",                     version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-ir",                      version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-ir_sjs1",                 version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface",        version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface_sjs1",   version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker",                  version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-linker_sjs1",             version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-sbt-test-adapter",        version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-compiler",                version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-javalib",                 version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-javalib-intf",            version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library",                 version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library_2.11",            version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library_2.12",            version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-library_2.13",            version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface",          version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.11",     version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.12",     version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.13",     version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge",             version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.11",        version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.12",        version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.13",        version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-plugin",       version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime",      version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.11", version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.12", version = { exact = "1.15.0" } },
-  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.13", version = { exact = "1.15.0" } },
+  { groupId = "org.scala-js", artifactId = "sbt-scalajs",                     version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-ir",                      version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-ir_sjs1",                 version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface",        version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker-interface_sjs1",   version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker",                  version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-linker_sjs1",             version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-sbt-test-adapter",        version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-compiler",                version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-javalib",                 version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-javalib-intf",            version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library",                 version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library_2.11",            version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library_2.12",            version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-library_2.13",            version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-scalalib_2.12",           version = { suffix = "+1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-scalalib_2.13",           version = { suffix = "+1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface",          version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.11",     version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.12",     version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-interface_2.13",     version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge",             version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.11",        version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.12",        version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-test-bridge_2.13",        version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-plugin",       version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime",      version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.11", version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.12", version = { exact  =  "1.16.0" } },
+  { groupId = "org.scala-js", artifactId = "scalajs-junit-test-runtime_2.13", version = { exact  =  "1.16.0" } },
 
   // Artifacts below are ignored because they are broken or their versioning is broken.
 

From efb9275d65d28e82d0c0603d8ac7d971d283ef08 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Tue, 2 Jan 2024 16:03:29 +0100
Subject: [PATCH 17/21] Show repo owner in exception message

---
 .../main/scala/org/scalasteward/core/forge/data/RepoOut.scala   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala
index c833905145..be9abd615d 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/data/RepoOut.scala
@@ -34,7 +34,7 @@ final case class RepoOut(
     archived: Boolean = false
 ) {
   def parentOrRaise[F[_]](implicit F: ApplicativeThrow[F]): F[RepoOut] =
-    parent.fold(F.raiseError[RepoOut](new Throwable(s"repo $name has no parent")))(F.pure)
+    parent.fold(F.raiseError[RepoOut](new Throwable(s"repo ${repo.show} has no parent")))(F.pure)
 
   def repo: Repo =
     Repo(owner.login, name)

From 77d868db3f4dcb0f1488e5eb19851bc3ceba7aa6 Mon Sep 17 00:00:00 2001
From: Scala Steward <43047562+scala-steward@users.noreply.github.com>
Date: Wed, 3 Jan 2024 08:22:33 +0100
Subject: [PATCH 18/21] Update jjwt-api, jjwt-impl, jjwt-jackson to 0.12.3
 (#3186)

* Update jjwt-api, jjwt-impl, jjwt-jackson to 0.12.3

* Replace deprecated API calls

---------

Co-authored-by: Frank S. Thomas <frank@timepit.eu>
---
 build.sbt                                         |  3 +++
 .../core/forge/github/GitHubAuthAlg.scala         | 15 +++++++--------
 project/Dependencies.scala                        |  2 +-
 3 files changed, 11 insertions(+), 9 deletions(-)

diff --git a/build.sbt b/build.sbt
index ce0a455ec4..e185d9c1b9 100644
--- a/build.sbt
+++ b/build.sbt
@@ -429,12 +429,15 @@ lazy val runSteward = taskKey[Unit]("")
 runSteward := Def.taskDyn {
   val home = System.getenv("HOME")
   val projectDir = (LocalRootProject / baseDirectory).value
+  // val ghAppDir = projectDir.getParentFile / "gh-app"
   val args = Seq(
     Seq("--workspace", s"$projectDir/workspace"),
     Seq("--repos-file", s"$projectDir/repos.md"),
     Seq("--git-author-email", s"me@$projectName.org"),
     Seq("--forge-login", projectName),
     Seq("--git-ask-pass", s"$home/.github/askpass/$projectName.sh"),
+    // Seq("--github-app-id", IO.read(ghAppDir / "scala-steward.app-id.txt").trim),
+    // Seq("--github-app-key-file", s"$ghAppDir/scala-steward.private-key.pem"),
     Seq("--whitelist", s"$home/.cache/coursier"),
     Seq("--whitelist", s"$home/.cache/JNA"),
     Seq("--whitelist", s"$home/.cache/mill"),
diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala
index 0dcc6d3b7c..8ffbb10f37 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala
@@ -19,14 +19,13 @@ package org.scalasteward.core.forge.github
 import better.files.File
 import cats.effect.Sync
 import cats.implicits._
-import io.jsonwebtoken.{Jwts, SignatureAlgorithm}
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.bouncycastle.util.io.pem.PemReader
-
+import io.jsonwebtoken.Jwts
 import java.io.FileReader
 import java.security.spec.PKCS8EncodedKeySpec
 import java.security.{KeyFactory, PrivateKey, Security}
 import java.util.Date
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.io.pem.PemReader
 import scala.concurrent.duration.FiniteDuration
 import scala.util.Using
 
@@ -72,13 +71,13 @@ object GitHubAuthAlg {
           val signingKey = readPrivateKey(app.keyFile)
           val builder = Jwts
             .builder()
-            .setIssuedAt(now)
-            .setIssuer(app.id.toString)
-            .signWith(signingKey, SignatureAlgorithm.RS256)
+            .issuedAt(now)
+            .issuer(app.id.toString)
+            .signWith(signingKey, Jwts.SIG.RS256)
           if (ttlMillis > 0) {
             val expMillis = nowMillis + ttlMillis
             val exp = new Date(expMillis)
-            builder.setExpiration(exp)
+            builder.expiration(exp)
           }
           builder.compact()
         }
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 9a5b043e42..819b7993c0 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -30,7 +30,7 @@ object Dependencies {
   val http4sJdkhttpClient = "org.http4s" %% "http4s-jdk-http-client" % "1.0.0-M9"
   val log4catsSlf4j = "org.typelevel" %% "log4cats-slf4j" % "2.6.0"
   val logbackClassic = "ch.qos.logback" % "logback-classic" % "1.4.14"
-  val jjwtApi = "io.jsonwebtoken" % "jjwt-api" % "0.11.5"
+  val jjwtApi = "io.jsonwebtoken" % "jjwt-api" % "0.12.3"
   val jjwtImpl = "io.jsonwebtoken" % "jjwt-impl" % jjwtApi.revision
   val jjwtJackson = "io.jsonwebtoken" % "jjwt-jackson" % jjwtApi.revision
   val millScriptVersion = "0.11.0-M10"

From 5ee9474c4533f5978488cb0418209bf65c620ca8 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Thu, 4 Jan 2024 18:56:41 +0100
Subject: [PATCH 19/21] Use @scala-steward-dev for the runSteward task

---
 build.sbt | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/build.sbt b/build.sbt
index e185d9c1b9..4ffda8ebdc 100644
--- a/build.sbt
+++ b/build.sbt
@@ -424,20 +424,22 @@ lazy val moduleRootPkg = settingKey[String]("").withRank(KeyRanks.Invisible)
 moduleRootPkg := rootPkg
 
 // Run Scala Steward from sbt for development and testing.
-// Do not do this in production.
+// Members of the @scala-steward-org/core team can request an access token
+// of @scala-steward-dev for local development from @fthomas.
 lazy val runSteward = taskKey[Unit]("")
 runSteward := Def.taskDyn {
   val home = System.getenv("HOME")
   val projectDir = (LocalRootProject / baseDirectory).value
-  // val ghAppDir = projectDir.getParentFile / "gh-app"
+  val gitHubLogin = projectName + "-dev"
+  // val gitHubAppDir = projectDir.getParentFile / "gh-app"
   val args = Seq(
     Seq("--workspace", s"$projectDir/workspace"),
     Seq("--repos-file", s"$projectDir/repos.md"),
-    Seq("--git-author-email", s"me@$projectName.org"),
-    Seq("--forge-login", projectName),
-    Seq("--git-ask-pass", s"$home/.github/askpass/$projectName.sh"),
-    // Seq("--github-app-id", IO.read(ghAppDir / "scala-steward.app-id.txt").trim),
-    // Seq("--github-app-key-file", s"$ghAppDir/scala-steward.private-key.pem"),
+    Seq("--git-author-email", s"dev@$projectName.org"),
+    Seq("--forge-login", gitHubLogin),
+    Seq("--git-ask-pass", s"$home/.github/askpass/$gitHubLogin.sh"),
+    // Seq("--github-app-id", IO.read(gitHubAppDir / "scala-steward.app-id.txt").trim),
+    // Seq("--github-app-key-file", s"$gitHubAppDir/scala-steward.private-key.pem"),
     Seq("--whitelist", s"$home/.cache/coursier"),
     Seq("--whitelist", s"$home/.cache/JNA"),
     Seq("--whitelist", s"$home/.cache/mill"),

From 09f97493abc14723d9e616efb49585250ffc5d37 Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 5 Jan 2024 08:01:16 +0100
Subject: [PATCH 20/21] Add reproduction for #3124

---
 .../src/test/scala/org/scalasteward/core/data/VersionTest.scala  | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala b/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala
index 505bbdd330..72dbec4832 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/data/VersionTest.scala
@@ -222,6 +222,7 @@ class VersionTest extends DisciplineSuite {
       ("10000000", List("20000000"), Some("20000000")),
       ("1032048a", List("2032048a4c2"), Some("2032048a4c2")),
       ("0.1.1-3dfde9d7", List("0.2.1-485fdf3b"), None),
+      ("1.0.0+1319.ae77058", List("1.0.0+1320.38b57aa"), Some("1.0.0+1320.38b57aa")),
       ("0.1.1", List("0.2.1-485fdf3b"), None),
       ("0.1.1-ALPHA", List("0.2.1-485fdf3b"), None),
       ("0.1.1-ALPHA", List("0.2.1-BETA"), None),

From c4cc5ab8797ee4e64ca91edbdc995c6d57a6c65c Mon Sep 17 00:00:00 2001
From: "Frank S. Thomas" <frank@timepit.eu>
Date: Fri, 5 Jan 2024 08:03:46 +0100
Subject: [PATCH 21/21] Recognize hashes if they are separated with `.` and `_`

This allows hashes to also be separated with a `.` and `_` from the
preceding component.

Here is for example what `Version.Component.parse("1.0.0+1320.38b57aa")`
returned before this change and what it returns now:
```
before:
Numeric(1), Separator(.), Numeric(0), Separator(.), Numeric(0), Separator(+),
Numeric(1320), Separator(.), Numeric(38), Alpha(b), Numeric(57), Alpha(aa)

now:
Numeric(1), Separator(.), Numeric(0), Separator(.), Numeric(0), Separator(+),
Numeric(1320), Separator(.), Hash(38b57aa))
```

Btw, recognizing hashes was added in #1549 but that PR gives no
explanation why they only could preceded by `-` and `+`.

Closes #3124
---
 .../src/main/scala/org/scalasteward/core/data/Version.scala     | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala
index 9bb595ae14..22c005d6a0 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/data/Version.scala
@@ -190,7 +190,7 @@ object Version {
       val numeric = Numbers.digits.map(s => List(Numeric(s)))
       val alpha = Parser.charsWhile(c => !digits(c) && !separators(c)).map(s => List(Alpha(s)))
       val separator = Parser.charIn(separators).map(c => List(Separator(c)))
-      val hash = (Parser.charIn('-', '+') ~
+      val hash = (Parser.charIn(separators) ~
         Parser.char('g').string.? ~
         Rfc5234.hexdig.rep(6).string.filterNot(startsWithDate)).backtrack
         .map { case ((s, g), h) => List(Separator(s), Hash(g.getOrElse("") + h)) }