Repository: scala-steward-org/scala-steward (scala)

Issue:
Fallback to `.github/.scala-steward.conf` if config not found in root
I think it's a good idea to fall back to the `.github` directory when loading the config.
It's getting more and more a convention to put things GitHub related in that folder. E.g. see our repo: https://github.com/playframework/playframework/tree/main/.github
~We will also move the `.mergify.yml` from the root to `.github` soon, so `.scala-steward.conf` is the last one missing.~ Done!

The change needs to be done in
* https://github.com/scala-steward-org/scala-steward/blob/main/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfigAlg.scala
* maybe here too (?): https://github.com/scala-steward-org/scala-steward/blob/main/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfigLoader.scala
* Update docs: https://github.com/scala-steward-org/scala-steward/blob/main/docs/repo-specific-configuration.md (maybe there is more, not sure)

Pull request:
Fixes #2735

Patch:
diff --git a/docs/repo-specific-configuration.md b/docs/repo-specific-configuration.md
index cac193ba60..47715924de 100644
--- a/docs/repo-specific-configuration.md
+++ b/docs/repo-specific-configuration.md
@@ -1,6 +1,8 @@
 # Repository-specific configuration
 
-You can add `<YOUR_REPO>/.scala-steward.conf` to configure how Scala Steward updates your repository.
+You can add a configuration file `.scala-steward.conf` to configure how Scala Steward updates your repository.
+The `.scala-steward.conf` configuration file can be located in the root of your repository, in `.github` directory or in `.config` directory (searched in this order).
+If a configuration file exists in more than one location, only the first found file is taken into account.
 
 ```properties
 # pullRequests.frequency allows to control how often or when Scala Steward
@@ -40,14 +42,14 @@ pullRequests.frequency = "7 days"
 # the default procedure (one PR per update).
 #
 # Each element in the array will have the following schema:
-# 
+#
 #   - name (mandatory): the name of the group, will be used for things like naming the branch
 #   - title (optional): if provided it will be used as the title for the PR
-#   - filter (mandatory): a non-empty list containing the filters to use to know 
+#   - filter (mandatory): a non-empty list containing the filters to use to know
 #                         if an update falls into this group.
 #
 # `filter` properties would have this format:
-#   
+#
 #    {
 #       version = "major" | "minor" | "patch" | "pre-release" | "build-metadata",
 #       group = "{group}",
@@ -55,7 +57,7 @@ pullRequests.frequency = "7 days"
 #    }
 #
 # For more information on the values for the `version` filter visit https://semver.org/
-# 
+#
 # Every field in a `filter` is optional but at least one must be provided.
 #
 # For grouping every update togeher a filter like {group = "*"} can be # provided.
@@ -69,7 +71,7 @@ pullRequests.grouping = [
   { name = "all", "title" = "Dependency updates", "filter" = [{"group" = "*"}] }
 ]
 
-# pullRequests.includeMatchedLabels allows to control which labels are added to PRs 
+# pullRequests.includeMatchedLabels allows to control which labels are added to PRs
 # via a regex check each label is checked against.
 # Defaults to no regex (all labels are added) which is equivalent to ".*".
 pullRequests.includeMatchedLabels = "(.*semver.*)|(commit-count:n:.*)"
@@ -120,10 +122,10 @@ updatePullRequests = "always" | "on-conflicts" | "never"
 
 # If set, Scala Steward will use this message template for the commit messages and PR titles.
 # Supported variables: ${artifactName}, ${currentVersion}, ${nextVersion} and ${default}
-# Default: "${default}" which is equivalent to "Update ${artifactName} to ${nextVersion}" 
+# Default: "${default}" which is equivalent to "Update ${artifactName} to ${nextVersion}"
 commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
 
-# If true and when upgrading version in .scalafmt.conf, Scala Steward will perform scalafmt 
+# If true and when upgrading version in .scalafmt.conf, Scala Steward will perform scalafmt
 # and add a separate commit when format changed. So you don't need reformat manually and can merge PR.
 # If false, Scala Steward will not perform scalafmt, so your CI may abort when reformat needed.
 # Default: true
@@ -144,7 +146,7 @@ postUpdateHooks = [{
 }]
 
 # You can override some config options for dependencies that matches the given pattern.
-# Currently, "pullRequests" can be overridden.  
+# Currently, "pullRequests" can be overridden.
 # Each pattern must have `groupId`, and may have `artifactId` and `version`.
 # First-matched entry is used.
 # More-specific entry should be placed before less-specific entry.
@@ -204,7 +206,7 @@ libraryDependencies ++= Seq(
   // scala-steward:off
   "com.github.pathikrit" %% "better-files" % "3.8.0",
   "com.olegpy" %% "better-monadic-for" % "0.3.1",
-  // scala-steward:on 
+  // scala-steward:on
   "org.typelevel" %% "cats-effect" % "1.3.1",  // This and subsequent will get updated
   "org.typelevel" %% "cats-kernel-laws" % "1.6.1"
 )
diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfigAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfigAlg.scala
index 8ae9810663..68a5d4ddef 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfigAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/RepoConfigAlg.scala
@@ -18,7 +18,7 @@ package org.scalasteward.core.repoconfig
 
 import better.files.File
 import cats.syntax.all._
-import cats.{Functor, MonadThrow}
+import cats.{Functor, Monad, MonadThrow}
 import io.circe.config.parser
 import org.scalasteward.core.data.{Repo, Update}
 import org.scalasteward.core.io.{FileAlg, WorkspaceAlg}
@@ -37,7 +37,10 @@ final class RepoConfigAlg[F[_]](maybeGlobalRepoConfig: Option[RepoConfig])(impli
   def readRepoConfig(repo: Repo): F[ConfigParsingResult] =
     for {
       repoDir <- workspaceAlg.repoDir(repo)
-      configParsingResult <- readRepoConfigFromFile(repoDir / repoConfigBasename)
+      activeConfigFile <- activeConfigFile(repoDir)
+      configParsingResult <- activeConfigFile.fold(
+        F.pure[ConfigParsingResult](ConfigParsingResult.FileDoesNotExist)
+      )(readRepoConfigFromFile(_))
       _ <- configParsingResult.fold(
         F.unit,
         error => logger.info(s"Failed to parse $repoConfigBasename: ${error.getMessage}"),
@@ -77,6 +80,29 @@ object RepoConfigAlg {
   def parseRepoConfig(input: String): Either[io.circe.Error, RepoConfig] =
     parser.decode[RepoConfig](input)
 
+  private val repoConfigFileSearchPath: List[List[String]] =
+    List(List.empty, List(".github"), List(".config"))
+
+  private def activeConfigFile[F[_]](
+      repoDir: File
+  )(implicit fileAlg: FileAlg[F], logger: Logger[F], F: Monad[F]): F[Option[File]] = {
+    val configFileCandidates: F[List[File]] = repoConfigFileSearchPath
+      .map(_ :+ repoConfigBasename)
+      .map(path => path.foldLeft(repoDir)(_ / _))
+      .filterA(fileAlg.isRegularFile)
+
+    configFileCandidates.flatMap {
+      case Nil => F.pure(None)
+      case active :: remaining =>
+        F.pure(active.some)
+          .productL(
+            remaining.traverse_(file =>
+              logger.warn(s"""Ignored config file "${file.pathAsString}"""")
+            )
+          )
+    }
+  }
+
   def readRepoConfigFromFile[F[_]](
       configFile: File
   )(implicit fileAlg: FileAlg[F], F: Functor[F]): F[ConfigParsingResult] =


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala
index 025ea979b8..76077e33ee 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/repoconfig/RepoConfigAlgTest.scala
@@ -264,6 +264,52 @@ class RepoConfigAlgTest extends FunSuite {
     assert(clue(log).contains(startOfErrorMsg))
   }
 
+  test("config file in .github/") {
+    val repo = Repo("test", "dot-github-config")
+    val repoDir = workspaceAlg.repoDir(repo).unsafeRunSync()
+    val rootConfigFile = repoDir / ".scala-steward.conf"
+    val dotGithubConfigFile = repoDir / ".github" / ".scala-steward.conf"
+    val initialState = MockState.empty.addFiles(dotGithubConfigFile -> "").unsafeRunSync()
+    val config = repoConfigAlg.readRepoConfig(repo).runA(initialState).unsafeRunSync()
+
+    assert(!fileAlg.isRegularFile(rootConfigFile).unsafeRunSync())
+    assert(fileAlg.isRegularFile(dotGithubConfigFile).unsafeRunSync())
+
+    assert(config.maybeRepoConfig.isDefined)
+  }
+
+  test("config file in .config/") {
+    val repo = Repo("test", "dot-config-config")
+    val repoDir = workspaceAlg.repoDir(repo).unsafeRunSync()
+    val rootConfigFile = repoDir / ".scala-steward.conf"
+    val dotConfigConfigFile = repoDir / ".config" / ".scala-steward.conf"
+    val initialState = MockState.empty.addFiles(dotConfigConfigFile -> "").unsafeRunSync()
+    val config = repoConfigAlg.readRepoConfig(repo).runA(initialState).unsafeRunSync()
+
+    assert(!fileAlg.isRegularFile(rootConfigFile).unsafeRunSync())
+    assert(fileAlg.isRegularFile(dotConfigConfigFile).unsafeRunSync())
+
+    assert(config.maybeRepoConfig.isDefined)
+  }
+
+  test("log warning on multiple config files") {
+    val repo = Repo("test", "multiple-config")
+    val repoDir = workspaceAlg.repoDir(repo).unsafeRunSync()
+    val rootConfigFile = repoDir / ".scala-steward.conf"
+    val dotConfigConfigFile = repoDir / ".config" / ".scala-steward.conf"
+    val initialState =
+      MockState.empty.addFiles(rootConfigFile -> "", dotConfigConfigFile -> "").unsafeRunSync()
+    val (state, config) = repoConfigAlg.readRepoConfig(repo).runSA(initialState).unsafeRunSync()
+
+    assert(fileAlg.isRegularFile(rootConfigFile).unsafeRunSync())
+    assert(fileAlg.isRegularFile(dotConfigConfigFile).unsafeRunSync())
+
+    assert(config.maybeRepoConfig.isDefined)
+
+    val log = state.trace.collectFirst { case Log((_, msg)) => msg }.getOrElse("")
+    assert(clue(log).contains("Ignored config file"))
+  }
+
   test("configToIgnoreFurtherUpdates with single update") {
     val update = ("a".g % "b".a % "c" %> "d").single
     val config = RepoConfigAlg