Repository: scala-steward-org/scala-steward
Pull request #3250 (2023-12-22 07:51:52)

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.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scala-steward-org/scala-steward/pull/3250?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`a3a2507`)](https://app.codecov.io/gh/scala-steward-org/scala-steward/commit/a3a25078a9578371a4c5f20c70b3a38039f96fef?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) 91.17% compared to head [(`ad7a673`)](https://app.codecov.io/gh/scala-steward-org/scala-steward/pull/3250?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org) 91.18%.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #3250      +/-   ##
==========================================
+ Coverage   91.17%   91.18%   +0.01%     
==========================================
  Files         166      167       +1     
  Lines        3400     3404       +4     
  Branches      310      315       +5     
==========================================
+ Hits         3100     3104       +4     
  Misses        300      300              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scala-steward-org/scala-steward/pull/3250?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scala-steward-org).


Patch:
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 208ed685..3385af72 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 7ba00954..cb183378 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 00000000..65debaa0
--- /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 e53fe350..ad14d84b 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,27 +64,30 @@ object ForgeSelection {
 
   def authenticate[F[_]](
       forgeType: ForgeType,
-      user: AuthenticatedUser
-  )(implicit F: Applicative[F]): Request[F] => F[Request[F]] =
+      user: F[AuthenticatedUser]
+  )(implicit F: Functor[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]
+      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")
 
   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


Test patch:
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 70b11e54..c30d67fd 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 be66fa5d..4c06bc68 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 5b33d287..a8a6bf69 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 80f066bd..dcfa7498 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 e6f15da4..0c48e4a3 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 b6a22445..20c9fa12 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(