Repository: scala-steward-org/scala-steward
Pull request #2183 (2021-07-15 09:01:23)

Allow choosing a custom default branch
# What has been done in this PR?

A new `--default-branch` argument has been added to the Scala Steward cli. When this argument is present, its value will be used as the default branch when updating repositories instead of the repository's default branch.

# Why this change?

This change enables users who run their own instances of Scala Steward to customize the branch in which updates are performed. This will enable multiple use-cases, for example:

- Updating multiple branches in a repository.
- Merging updates into a "develop" branch regularly that will be merged into the real default branch every so often.

This change will be specially useful for users of the [Scala Steward GitHub Action](https://github.com/scala-steward-org/scala-steward-action) after scala-steward-org/scala-steward-action#268 is merged.

Example:

```yml
on:
  schedule:
    - cron: '0 0 * * 0'
name: Launch Scala Steward on `main`, `1.x` and `2.x`
jobs:
  scala-steward:
    runs-on: ubuntu-latest
    name: Launch Scala Steward
    strategy:
      matrix:
        branch: ['main', '1.x', '2.x']
    steps:
      - name: Launch Scala Steward
        uses: scala-steward-org/scala-steward-action@v2
        with:
          github-token: ${{ secrets.REPO_GITHUB_TOKEN }}
          branch: ${{ matrix.branch }}
```


Patch:
diff --git a/docs/help.md b/docs/help.md
index a8c501bc..86dff335 100644
--- a/docs/help.md
+++ b/docs/help.md
@@ -37,4 +37,5 @@ All command line arguments for the `scala-steward` application.
   --github-app-key-file  FILE                        Github application key file
   --github-app-id  ID                                Github application id
   --refresh-backoff-period DURATION                  Period of time a failed build won't be triggered again, default: "7 days"
+  --default-branch BRANCH                            A fixed branch name to use as default instead of the repository's default branch
 ```
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 6ec50447..cca00a32 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
@@ -59,7 +59,8 @@ object Cli {
       githubAppId: Option[Long] = None,
       urlCheckerTestUrl: Option[Uri] = None,
       defaultMavenRepo: Option[String] = None,
-      refreshBackoffPeriod: FiniteDuration = 7.days
+      refreshBackoffPeriod: FiniteDuration = 7.days,
+      defaultBranch: Option[String] = None
   )
 
   final case class EnvVar(name: String, value: String)
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 ead4eae5..74b2f254 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
@@ -33,6 +33,7 @@ import org.scalasteward.core.vcs.VCSType
 import org.scalasteward.core.vcs.data.AuthenticatedUser
 import org.scalasteward.core.vcs.github.GitHubApp
 import scala.concurrent.duration.FiniteDuration
+import org.scalasteward.core.git.Branch
 
 /** Configuration for scala-steward.
   *
@@ -77,7 +78,8 @@ final case class Config(
     githubApp: Option[GitHubApp],
     urlCheckerTestUrl: Uri,
     defaultResolver: Resolver,
-    refreshBackoffPeriod: FiniteDuration
+    refreshBackoffPeriod: FiniteDuration,
+    defaultBranch: Option[Branch]
 ) {
   def vcsUser[F[_]](implicit
       processAlg: ProcessAlg[F],
@@ -167,6 +169,7 @@ object Config {
       defaultResolver = args.defaultMavenRepo
         .map(url => Resolver.MavenRepository("default", url, None))
         .getOrElse(Resolver.mavenCentral),
-      refreshBackoffPeriod = args.refreshBackoffPeriod
+      refreshBackoffPeriod = args.refreshBackoffPeriod,
+      defaultBranch = args.defaultBranch.map(Branch(_))
     )
 }
diff --git a/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala
index 9c69adde..a08633dd 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/repocache/RepoCacheAlg.scala
@@ -43,7 +43,11 @@ final class RepoCacheAlg[F[_]](config: Config)(implicit
     logger.info(s"Check cache of ${repo.show}") >>
       refreshErrorAlg.skipIfFailedRecently(repo) {
         (
-          vcsApiAlg.createForkOrGetRepoWithDefaultBranch(repo, config.doNotFork),
+          vcsApiAlg.createForkOrGetRepoWithDefaultBranch(
+            repo,
+            config.doNotFork,
+            config.defaultBranch
+          ),
           repoCacheRepository.findCache(repo)
         ).parTupled.flatMap { case ((repoOut, branchOut), maybeCache) =>
           val latestSha1 = branchOut.commit.sha
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala
index 0abc9ffe..048621b0 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSApiAlg.scala
@@ -42,13 +42,26 @@ trait VCSApiAlg[F[_]] {
   final def createForkOrGetRepo(repo: Repo, doNotFork: Boolean): F[RepoOut] =
     if (doNotFork) getRepo(repo) else createFork(repo)
 
-  final def createForkOrGetRepoWithDefaultBranch(repo: Repo, doNotFork: Boolean)(implicit
+  final def createForkOrGetRepoWithDefaultBranch(
+      repo: Repo,
+      doNotFork: Boolean,
+      defaultBranch: Option[Branch]
+  )(implicit
       F: MonadThrow[F]
   ): F[(RepoOut, BranchOut)] =
     for {
       forkOrRepo <- createForkOrGetRepo(repo, doNotFork)
-      defaultBranch <- getDefaultBranchOfParentOrRepo(forkOrRepo, doNotFork)
-    } yield (forkOrRepo, defaultBranch)
+      forkOrRepoWithDefaultBranch = applyDefaultBranch(forkOrRepo, defaultBranch)
+      defaultBranch <- getDefaultBranchOfParentOrRepo(forkOrRepoWithDefaultBranch, doNotFork)
+    } yield (forkOrRepoWithDefaultBranch, defaultBranch)
+
+  final def applyDefaultBranch(repoOut: RepoOut, defaultBranch: Option[Branch]): RepoOut =
+    defaultBranch.fold(repoOut) { branch =>
+      repoOut.copy(
+        default_branch = branch,
+        parent = repoOut.parent.map(_.copy(default_branch = branch))
+      )
+    }
 
   final def getDefaultBranchOfParentOrRepo(repoOut: RepoOut, doNotFork: Boolean)(implicit
       F: MonadThrow[F]
diff --git a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala
index d34bbe4a..b2203faa 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/vcs/VCSRepoAlg.scala
@@ -40,7 +40,9 @@ final class VCSRepoAlg[F[_]](config: Config)(implicit
   private def clone(repo: Repo, repoOut: RepoOut): F[Unit] =
     logger.info(s"Clone ${repoOut.repo.show}") >>
       gitAlg.clone(repo, withLogin(repoOut.clone_url)) >>
-      gitAlg.setAuthor(repo, config.gitCfg.gitAuthor)
+      gitAlg.setAuthor(repo, config.gitCfg.gitAuthor) >> config.defaultBranch.fold(F.unit)(
+        gitAlg.checkoutBranch(repo, _)
+      )
 
   private def syncFork(repo: Repo, repoOut: RepoOut): F[Unit] =
     repoOut.parentOrRaise[F].flatMap { parent =>


Test patch:
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 4fe50b5b..f8d4fc7e 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
@@ -29,7 +29,8 @@ class CliTest extends FunSuite {
         List("--artifact-migrations", "/opt/scala-steward/extra-artifact-migrations.conf"),
         List("--github-app-id", "12345678"),
         List("--github-app-key-file", "example_app_key"),
-        List("--refresh-backoff-period", "1 day")
+        List("--refresh-backoff-period", "1 day"),
+        List("--default-branch", "1.x")
       ).flatten
     )
     val expected = Success(
@@ -50,7 +51,8 @@ class CliTest extends FunSuite {
         artifactMigrations = Some(File("/opt/scala-steward/extra-artifact-migrations.conf")),
         githubAppId = Some(12345678),
         githubAppKeyFile = Some(File("example_app_key")),
-        refreshBackoffPeriod = 1.day
+        refreshBackoffPeriod = 1.day,
+        defaultBranch = Some("1.x")
       )
     )
     assertEquals(obtained, expected)
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
index bae9fa3d..a51205b4 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/bitbucket/BitbucketApiAlgTest.scala
@@ -71,6 +71,15 @@ class BitbucketApiAlgTest extends FunSuite {
           }
         }"""
       )
+    case GET -> Root / "repositories" / "fthomas" / "base.g8" / "refs" / "branches" / "custom" =>
+      Ok(
+        json"""{
+          "name": "custom",
+          "target": {
+              "hash": "12ea4559063c74184861afece9eeff5ca9d33db3"
+          }
+        }"""
+      )
     case POST -> Root / "repositories" / "fthomas" / "base.g8" / "forks" =>
       Ok(
         json"""{
@@ -161,6 +170,7 @@ class BitbucketApiAlgTest extends FunSuite {
   private val prUrl = uri"https://bitbucket.org/fthomas/base.g8/pullrequests/2"
   private val repo = Repo("fthomas", "base.g8")
   private val master = Branch("master")
+  private val custom = Branch("custom")
   private val parent = RepoOut(
     "base.g8",
     UserOut("fthomas"),
@@ -169,6 +179,14 @@ class BitbucketApiAlgTest extends FunSuite {
     master
   )
 
+  private val parentWithCustomDefaultBranch = RepoOut(
+    "base.g8",
+    UserOut("fthomas"),
+    None,
+    uri"https://scala-steward@bitbucket.org/fthomas/base.g8.git",
+    custom
+  )
+
   private val fork = RepoOut(
     "base.g8",
     UserOut("scala-steward"),
@@ -177,11 +195,24 @@ class BitbucketApiAlgTest extends FunSuite {
     master
   )
 
+  private val forkWithCustomDefaultBranch = RepoOut(
+    "base.g8",
+    UserOut("scala-steward"),
+    Some(parentWithCustomDefaultBranch),
+    uri"https://scala-steward@bitbucket.org/scala-steward/base.g8.git",
+    custom
+  )
+
   private val defaultBranch = BranchOut(
     master,
     CommitOut(Sha1(HexString.unsafeFrom("07eb2a203e297c8340273950e98b2cab68b560c1")))
   )
 
+  private val defaultCustomBranch = BranchOut(
+    custom,
+    CommitOut(Sha1(HexString.unsafeFrom("12ea4559063c74184861afece9eeff5ca9d33db3")))
+  )
+
   private val pullRequest =
     PullRequestOut(prUrl, PullRequestState.Open, PullRequestNumber(2), "scala-steward-pr")
 
@@ -197,18 +228,40 @@ class BitbucketApiAlgTest extends FunSuite {
 
   test("createForkOrGetRepoWithDefaultBranch") {
     val (repoOut, branchOut) =
-      bitbucketApiAlg.createForkOrGetRepoWithDefaultBranch(repo, doNotFork = false).unsafeRunSync()
+      bitbucketApiAlg
+        .createForkOrGetRepoWithDefaultBranch(repo, doNotFork = false, defaultBranch = None)
+        .unsafeRunSync()
     assertEquals(repoOut, fork)
     assertEquals(branchOut, defaultBranch)
   }
 
+  test("createForkOrGetRepoWithDefaultBranch with custom default branch") {
+    val (repoOut, branchOut) =
+      bitbucketApiAlg
+        .createForkOrGetRepoWithDefaultBranch(repo, doNotFork = false, defaultBranch = Some(custom))
+        .unsafeRunSync()
+    assertEquals(repoOut, forkWithCustomDefaultBranch)
+    assertEquals(branchOut, defaultCustomBranch)
+  }
+
   test("createForkOrGetRepoWithDefaultBranch without forking") {
     val (repoOut, branchOut) =
-      bitbucketApiAlg.createForkOrGetRepoWithDefaultBranch(repo, doNotFork = true).unsafeRunSync()
+      bitbucketApiAlg
+        .createForkOrGetRepoWithDefaultBranch(repo, doNotFork = true, defaultBranch = None)
+        .unsafeRunSync()
     assertEquals(repoOut, parent)
     assertEquals(branchOut, defaultBranch)
   }
 
+  test("createForkOrGetRepoWithDefaultBranch without forking with custom default branch") {
+    val (repoOut, branchOut) =
+      bitbucketApiAlg
+        .createForkOrGetRepoWithDefaultBranch(repo, doNotFork = true, defaultBranch = Some(custom))
+        .unsafeRunSync()
+    assertEquals(repoOut, parentWithCustomDefaultBranch)
+    assertEquals(branchOut, defaultCustomBranch)
+  }
+
   test("createPullRequest") {
     val data = NewPullRequestData(
       "scala-steward-pr",
diff --git a/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala
index a338fcf8..8a6b5edc 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/vcs/github/GitHubApiAlgTest.scala
@@ -36,6 +36,14 @@ class GitHubApiAlgTest extends FunSuite {
           } """
         )
 
+      case GET -> Root / "repos" / "fthomas" / "base.g8" / "branches" / "custom" =>
+        Ok(
+          json""" {
+            "name": "custom",
+            "commit": { "sha": "12ea4559063c74184861afece9eeff5ca9d33db3" }
+          } """
+        )
+
       case GET -> Root / "repos" / "fthomas" / "base.g8" / "pulls" =>
         Ok(
           json"""[{
@@ -96,6 +104,14 @@ class GitHubApiAlgTest extends FunSuite {
     Branch("master")
   )
 
+  private val parentWithCustomDefaultBranch = RepoOut(
+    "base.g8",
+    UserOut("fthomas"),
+    None,
+    uri"https://github.com/fthomas/base.g8.git",
+    Branch("custom")
+  )
+
   private val fork = RepoOut(
     "base.g8-1",
     UserOut("scala-steward"),
@@ -104,11 +120,24 @@ class GitHubApiAlgTest extends FunSuite {
     Branch("master")
   )
 
+  private val forkWithCustomDefaultBranch = RepoOut(
+    "base.g8-1",
+    UserOut("scala-steward"),
+    Some(parentWithCustomDefaultBranch),
+    uri"https://github.com/scala-steward/base.g8-1.git",
+    Branch("custom")
+  )
+
   private val defaultBranch = BranchOut(
     Branch("master"),
     CommitOut(Sha1(HexString("07eb2a203e297c8340273950e98b2cab68b560c1")))
   )
 
+  private val defaultCustomBranch = BranchOut(
+    Branch("custom"),
+    CommitOut(Sha1(HexString("12ea4559063c74184861afece9eeff5ca9d33db3")))
+  )
+
   test("createForkOrGetRepo") {
     val repoOut = gitHubApiAlg.createForkOrGetRepo(repo, doNotFork = false).unsafeRunSync()
     assertEquals(repoOut, fork)
@@ -121,18 +150,48 @@ class GitHubApiAlgTest extends FunSuite {
 
   test("createForkOrGetRepoWithDefaultBranch") {
     val (repoOut, branchOut) =
-      gitHubApiAlg.createForkOrGetRepoWithDefaultBranch(repo, doNotFork = false).unsafeRunSync()
+      gitHubApiAlg
+        .createForkOrGetRepoWithDefaultBranch(repo, doNotFork = false, defaultBranch = None)
+        .unsafeRunSync()
     assertEquals(repoOut, fork)
     assertEquals(branchOut, defaultBranch)
   }
 
+  test("createForkOrGetRepoWithDefaultBranch") {
+    val (repoOut, branchOut) =
+      gitHubApiAlg
+        .createForkOrGetRepoWithDefaultBranch(
+          repo,
+          doNotFork = false,
+          defaultBranch = Some(Branch("custom"))
+        )
+        .unsafeRunSync()
+    assertEquals(repoOut, forkWithCustomDefaultBranch)
+    assertEquals(branchOut, defaultCustomBranch)
+  }
+
   test("createForkOrGetRepoWithDefaultBranch without forking") {
     val (repoOut, branchOut) =
-      gitHubApiAlg.createForkOrGetRepoWithDefaultBranch(repo, doNotFork = true).unsafeRunSync()
+      gitHubApiAlg
+        .createForkOrGetRepoWithDefaultBranch(repo, doNotFork = true, defaultBranch = None)
+        .unsafeRunSync()
     assertEquals(repoOut, parent)
     assertEquals(branchOut, defaultBranch)
   }
 
+  test("createForkOrGetRepoWithDefaultBranch without forking with custom default branch") {
+    val (repoOut, branchOut) =
+      gitHubApiAlg
+        .createForkOrGetRepoWithDefaultBranch(
+          repo,
+          doNotFork = true,
+          defaultBranch = Some(Branch("custom"))
+        )
+        .unsafeRunSync()
+    assertEquals(repoOut, parentWithCustomDefaultBranch)
+    assertEquals(branchOut, defaultCustomBranch)
+  }
+
   test("closePullRequest") {
     val prOut = gitHubApiAlg.closePullRequest(repo, PullRequestNumber(1347)).unsafeRunSync()
     assertEquals(prOut.state, PullRequestState.Closed)