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

Issue:
Look for corresponding changes in things like '.md' files
I don't know how common this use case is, but I have some dependencies that also appear in my markdown docs, for example:

```
how to install:

```scala
resolvers += "manyangled" at "https://dl.bintray.com/manyangled/maven/"

libraryDependencies ++= Seq(
  // basic coulomb dependencies
  "com.manyangled" %% "coulomb" % "0.3.6",
  "org.typelevel" %% "spire" % "0.17.0-M1",
  "eu.timepit" %% "singleton-ops" % "0.4.0",
  // pureconfig integration:
  "com.manyangled" %% "coulomb-pureconfig" % "0.3.5",
  "com.github.pureconfig" %% "pureconfig" % "0.12.0"
)
```

Using the example above, if scala steward updated the pureconfig dep in my sbt build, in theory it also might update it in my markdown.

Pull request description:
A user option to make it possible to change version numbers in text files. This will hopefully create not as many unrelated PRs as the last attempt.

fixes #1376

Patch:
diff --git a/docs/repo-specific-configuration.md b/docs/repo-specific-configuration.md
index b834d84205..ce1698db16 100644
--- a/docs/repo-specific-configuration.md
+++ b/docs/repo-specific-configuration.md
@@ -63,6 +63,10 @@ updates.limit = 5
 # Default: false
 updates.includeScala = true
 
+# The extensions of files that should be updated.
+# Default: [".scala", ".sbt", ".sbt.shared", ".sc", ".yml"]
+updates.fileExtensions = [".scala", ".sbt", ".sbt.shared", ".sc", ".yml", ".md", ".markdown", ".txt"]
+
 # If "on-conflicts", Scala Steward will update the PR it created to resolve conflicts as
 # long as you don't change it yourself.
 # If "always", Scala Steward will always update the PR it created as long as
diff --git a/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala
index eb39d90853..a462c757f6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/edit/EditAlg.scala
@@ -38,13 +38,17 @@ final class EditAlg[F[_]](
     workspaceAlg: WorkspaceAlg[F],
     F: MonadThrowable[F]
 ) {
-  def applyUpdate(repo: Repo, update: Update): F[Unit] =
+  def applyUpdate(repo: Repo, update: Update, fileExtensions: Set[String]): F[Unit] =
     for {
       _ <- applyScalafixMigrations(repo, update).handleErrorWith(e =>
         logger.warn(s"Could not apply ${update.show} : $e")
       )
       repoDir <- workspaceAlg.repoDir(repo)
-      files <- fileAlg.findFilesContaining(repoDir, update.currentVersion, isSourceFile(update))
+      files <- fileAlg.findFilesContaining(
+        repoDir,
+        update.currentVersion,
+        isSourceFile(update, fileExtensions)
+      )
       noFilesFound = logger.warn("No files found that contain the current version")
       _ <- files.toNel.fold(noFilesFound)(applyUpdateTo(_, update))
     } yield ()
diff --git a/modules/core/src/main/scala/org/scalasteward/core/io/package.scala b/modules/core/src/main/scala/org/scalasteward/core/io/package.scala
index 2768f1025a..73cb73b0d7 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/io/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/io/package.scala
@@ -21,24 +21,23 @@ import cats.implicits._
 import org.scalasteward.core.data.{GroupId, Update}
 
 package object io {
-  def isSourceFile(update: Update)(file: File): Boolean = {
+  def isSourceFile(update: Update, fileExtensions: Set[String])(file: File): Boolean = {
     val notInGitDir = !file.pathAsString.contains(".git/")
-    notInGitDir && isSpecificOrGenericSourceFile(update)(file)
+    notInGitDir && isSpecificOrGenericSourceFile(update, fileExtensions)(file)
   }
 
-  private def isSpecificOrGenericSourceFile(update: Update)(file: File): Boolean =
+  private def isSpecificOrGenericSourceFile(update: Update, fileExtensions: Set[String])(
+      file: File
+  ): Boolean =
     () match {
       case _ if isSbtUpdate(update)          => file.name === "build.properties"
       case _ if isScalafmtCoreUpdate(update) => file.name === ".scalafmt.conf"
-      case _                                 => isGenericSourceFile(file)
+      case _                                 => isGenericSourceFile(file, fileExtensions)
     }
 
-  private def isGenericSourceFile(file: File): Boolean = {
+  private def isGenericSourceFile(file: File, fileExtensions: Set[String]): Boolean = {
     val name = file.name
-    val allowedByExtension = file.extension.exists(Set(".scala", ".sbt", ".sc", ".yml"))
-    def allowedBySuffix: Boolean =
-      Set(".sbt.shared").exists(suffix => name.endsWith(suffix) && !name.startsWith(suffix))
-    allowedByExtension || allowedBySuffix
+    fileExtensions.exists(suffix => name.endsWith(suffix) && !name.startsWith(suffix))
   }
 
   private def isSbtUpdate(update: Update): Boolean =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
index 74099190c5..61dca8c77c 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/nurture/NurtureAlg.scala
@@ -120,7 +120,8 @@ final class NurtureAlg[F[_]](
     } yield result
 
   def applyNewUpdate(data: UpdateData): F[ProcessResult] =
-    (editAlg.applyUpdate(data.repo, data.update) >> gitAlg.containsChanges(data.repo)).ifM(
+    (editAlg.applyUpdate(data.repo, data.update, data.repoConfig.updates.fileExtensionsOrDefault) >> gitAlg
+      .containsChanges(data.repo)).ifM(
       gitAlg.returnToCurrentBranch(data.repo) {
         for {
           _ <- logger.info(s"Create branch ${data.updateBranch.name}")
@@ -212,7 +213,11 @@ final class NurtureAlg[F[_]](
         s"Merge branch '${data.baseBranch.name}' into ${data.updateBranch.name} and apply again"
       )
       _ <- gitAlg.mergeTheirs(data.repo, data.baseBranch)
-      _ <- editAlg.applyUpdate(data.repo, data.update)
+      _ <- editAlg.applyUpdate(
+        data.repo,
+        data.update,
+        data.repoConfig.updates.fileExtensionsOrDefault
+      )
       containsChanges <- gitAlg.containsChanges(data.repo)
       result <- if (containsChanges) commitAndPush(data) else F.pure[ProcessResult](Ignored)
     } yield result
diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala
index e996f7b5f0..155ea026cb 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala
@@ -35,11 +35,18 @@ final case class UpdatesConfig(
     allow: List[UpdatePattern] = List.empty,
     ignore: List[UpdatePattern] = List.empty,
     limit: Option[PosInt] = None,
-    includeScala: Option[Boolean] = None
+    includeScala: Option[Boolean] = None,
+    fileExtensions: List[String] = List.empty
 ) {
   def keep(update: Update.Single): FilterResult =
     isAllowed(update).flatMap(isPinned).flatMap(isIgnored)
 
+  def fileExtensionsOrDefault: Set[String] =
+    if (fileExtensions.isEmpty)
+      UpdatesConfig.defaultFileExtensions
+    else
+      fileExtensions.toSet
+
   private def isAllowed(update: Update.Single): FilterResult = {
     val m = UpdatePattern.findMatch(allow, update, include = true)
     if (m.filteredVersions.nonEmpty) {
@@ -79,6 +86,8 @@ object UpdatesConfig {
 
   val defaultIncludeScala: Boolean = false
 
+  val defaultFileExtensions: Set[String] = Set(".scala", ".sbt", ".sbt.shared", ".sc", ".yml")
+
   // prevent IntelliJ from removing the import of io.circe.refined._
   locally(refinedDecoder: Decoder[PosInt])
 }


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
index 899a550302..b68b2e32a3 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/edit/EditAlgTest.scala
@@ -5,6 +5,7 @@ import org.scalasteward.core.TestSyntax._
 import org.scalasteward.core.data.Update
 import org.scalasteward.core.mock.MockContext.editAlg
 import org.scalasteward.core.mock.MockState
+import org.scalasteward.core.repoconfig.UpdatesConfig
 import org.scalasteward.core.util.Nel
 import org.scalasteward.core.vcs.data.Repo
 import org.scalatest.funsuite.AnyFunSuite
@@ -18,7 +19,7 @@ class EditAlgTest extends AnyFunSuite with Matchers {
     val file2 = File.temp / "ws/fthomas/scala-steward/project/Dependencies.scala"
 
     val state = editAlg
-      .applyUpdate(repo, update)
+      .applyUpdate(repo, update, UpdatesConfig.defaultFileExtensions)
       .runS(MockState.empty.add(file1, """val catsVersion = "1.2.0"""").add(file2, ""))
       .unsafeRunSync()
 
@@ -47,7 +48,7 @@ class EditAlgTest extends AnyFunSuite with Matchers {
     val file1 = File.temp / "ws/fthomas/scala-steward/build.sbt"
 
     val state = editAlg
-      .applyUpdate(repo, update)
+      .applyUpdate(repo, update, UpdatesConfig.defaultFileExtensions)
       .runS(
         MockState.empty
           .add(
@@ -102,7 +103,7 @@ class EditAlgTest extends AnyFunSuite with Matchers {
     val file2 = File.temp / "ws/fthomas/scala-steward/build.sbt"
 
     val state = editAlg
-      .applyUpdate(repo, update)
+      .applyUpdate(repo, update, UpdatesConfig.defaultFileExtensions)
       .runS(
         MockState.empty
           .add(file1, """import $ivy.`org.typelevel::cats-core:1.2.0`, cats.implicits._"""")
@@ -133,7 +134,7 @@ class EditAlgTest extends AnyFunSuite with Matchers {
     val repoDir = File.temp / "ws/owner/repo"
     val filesInRepoDir = files.map { case (file, content) => repoDir / file -> content }
     editAlg
-      .applyUpdate(Repo("owner", "repo"), update)
+      .applyUpdate(Repo("owner", "repo"), update, UpdatesConfig.defaultFileExtensions)
       .runS(MockState.empty.addFiles(filesInRepoDir))
       .map(_.files)
       .unsafeRunSync()
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 3f59da81fd..8db664c590 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
@@ -26,6 +26,7 @@ class RepoConfigAlgTest extends AnyFunSuite with Matchers {
          |updates.ignore = [ { groupId = "org.acme", version = "1.0" } ]
          |updates.limit = 4
          |updates.includeScala = true
+         |updates.fileExtensions = [ ".txt" ]
          |pullRequests.frequency = "@weekly"
          |commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}"
          |""".stripMargin
@@ -62,7 +63,8 @@ class RepoConfigAlgTest extends AnyFunSuite with Matchers {
           UpdatePattern(GroupId("org.acme"), None, Some(UpdatePattern.Version(Some("1.0"), None)))
         ),
         limit = Some(PosInt.unsafeFrom(4)),
-        includeScala = Some(true)
+        includeScala = Some(true),
+        fileExtensions = List(".txt")
       ),
       commits = CommitsConfig(
         message = Some("Update ${artifactName} from ${currentVersion} to ${nextVersion}")