Skip to content

Commit c1e6eac

Browse files
joan38mzuehlke
authored andcommitted
Support GitHub Enterprise
1 parent c1d4b6f commit c1e6eac

35 files changed

+571
-446
lines changed

modules/core/src/main/scala/org/scalasteward/core/application/Context.scala

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import org.scalasteward.core.edit.EditAlg
3636
import org.scalasteward.core.edit.hooks.HookExecutor
3737
import org.scalasteward.core.edit.scalafix._
3838
import org.scalasteward.core.edit.update.ScannerAlg
39-
import org.scalasteward.core.forge.github.{GitHubAppApiAlg, GitHubAuthAlg}
4039
import org.scalasteward.core.forge.{ForgeApiAlg, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection}
4140
import org.scalasteward.core.git.{GenGitAlg, GitAlg}
4241
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
@@ -128,18 +127,18 @@ object Context {
128127
processAlg: ProcessAlg[F],
129128
workspaceAlg: WorkspaceAlg[F],
130129
F: Async[F]
131-
): F[Context[F]] =
130+
): F[Context[F]] = {
131+
implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]()
132+
implicit val forgeAuthAlg: ForgeAuthAlg[F] = ForgeAuthAlg.create[F](config)
132133
for {
133134
_ <- F.unit
134-
forgeUser = new ForgeAuthAlg[F](config.gitCfg, config.forgeCfg).forgeUser
135135
artifactMigrationsLoader0 = new ArtifactMigrationsLoader[F]
136136
artifactMigrationsFinder0 <- artifactMigrationsLoader0.createFinder(config.artifactCfg)
137137
scalafixMigrationsLoader0 = new ScalafixMigrationsLoader[F]
138138
scalafixMigrationsFinder0 <- scalafixMigrationsLoader0.createFinder(config.scalafixCfg)
139139
repoConfigLoader0 = new RepoConfigLoader[F]
140140
maybeGlobalRepoConfig <- repoConfigLoader0.loadGlobalRepoConfig(config.repoConfigCfg)
141-
urlChecker0 <- UrlChecker
142-
.create[F](config, ForgeSelection.authenticateIfApiHost(config.forgeCfg, forgeUser))
141+
urlChecker0 <- UrlChecker.create[F](config, forgeAuthAlg.authenticateApi)
143142
kvsPrefix = Some(config.forgeCfg.tpe.asString)
144143
pullRequestsStore <- JsonKeyValueStore
145144
.create[F, Repo, Map[Uri, PullRequestRepository.Entry]]("pull_requests", "2", kvsPrefix)
@@ -159,14 +158,12 @@ object Context {
159158
implicit val dateTimeAlg: DateTimeAlg[F] = DateTimeAlg.create[F]
160159
implicit val repoConfigAlg: RepoConfigAlg[F] = new RepoConfigAlg[F](maybeGlobalRepoConfig)
161160
implicit val filterAlg: FilterAlg[F] = new FilterAlg[F]
162-
implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config.gitCfg)
163-
implicit val gitHubAuthAlg: GitHubAuthAlg[F] = GitHubAuthAlg.create[F]
161+
implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config)
164162
implicit val hookExecutor: HookExecutor[F] = new HookExecutor[F]
165-
implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]
166163
implicit val repoCacheRepository: RepoCacheRepository[F] =
167164
new RepoCacheRepository[F](repoCacheStore)
168-
implicit val forgeApiAlg: ForgeApiAlg[F] =
169-
ForgeSelection.forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeUser)
165+
implicit val forgeApiAlg: ForgeApiAlg[F] = ForgeSelection
166+
.forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeAuthAlg.authenticateApi)
170167
implicit val forgeRepoAlg: ForgeRepoAlg[F] = new ForgeRepoAlg[F](config)
171168
implicit val forgeCfg: ForgeCfg = config.forgeCfg
172169
implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F]
@@ -192,11 +189,10 @@ object Context {
192189
implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config.forgeCfg)
193190
implicit val pruningAlg: PruningAlg[F] = new PruningAlg[F]
194191
implicit val reposFilesLoader: ReposFilesLoader[F] = new ReposFilesLoader[F]
195-
implicit val gitHubAppApiAlg: GitHubAppApiAlg[F] =
196-
new GitHubAppApiAlg[F](config.forgeCfg.apiHost)
197192
implicit val stewardAlg: StewardAlg[F] = new StewardAlg[F](config)
198193
new Context[F]
199194
}
195+
}
200196

201197
private val banner: String = {
202198
val banner =

modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import cats.effect.{ExitCode, Sync}
2020
import cats.syntax.all._
2121
import fs2.Stream
2222
import org.scalasteward.core.data.Repo
23-
import org.scalasteward.core.forge.github.{GitHubApp, GitHubAppApiAlg, GitHubAuthAlg}
23+
import org.scalasteward.core.forge.ForgeAuthAlg
2424
import org.scalasteward.core.git.GitAlg
2525
import org.scalasteward.core.io.{FileAlg, WorkspaceAlg}
2626
import org.scalasteward.core.nurture.NurtureAlg
@@ -30,14 +30,12 @@ import org.scalasteward.core.util
3030
import org.scalasteward.core.util.DateTimeAlg
3131
import org.scalasteward.core.util.logger.LoggerOps
3232
import org.typelevel.log4cats.Logger
33-
import scala.concurrent.duration._
3433

3534
final class StewardAlg[F[_]](config: Config)(implicit
3635
dateTimeAlg: DateTimeAlg[F],
3736
fileAlg: FileAlg[F],
3837
gitAlg: GitAlg[F],
39-
githubAppApiAlg: GitHubAppApiAlg[F],
40-
githubAuthAlg: GitHubAuthAlg[F],
38+
forgeAuthAlg: ForgeAuthAlg[F],
4139
logger: Logger[F],
4240
nurtureAlg: NurtureAlg[F],
4341
pruningAlg: PruningAlg[F],
@@ -47,25 +45,6 @@ final class StewardAlg[F[_]](config: Config)(implicit
4745
workspaceAlg: WorkspaceAlg[F],
4846
F: Sync[F]
4947
) {
50-
private def getGitHubAppRepos(githubApp: GitHubApp): Stream[F, Repo] =
51-
Stream.evals[F, List, Repo] {
52-
for {
53-
jwt <- githubAuthAlg.createJWT(githubApp, 2.minutes)
54-
installations <- githubAppApiAlg.installations(jwt)
55-
repositories <- installations.traverse { installation =>
56-
githubAppApiAlg
57-
.accessToken(jwt, installation.id)
58-
.flatMap(token => githubAppApiAlg.repositories(token.token))
59-
}
60-
repos <- repositories.flatMap(_.repositories).flatTraverse { repo =>
61-
repo.full_name.split('/') match {
62-
case Array(owner, name) => F.pure(List(Repo(owner, name)))
63-
case _ => logger.error(s"invalid repo $repo").as(List.empty[Repo])
64-
}
65-
}
66-
} yield repos
67-
}
68-
6948
private def steward(repo: Repo): F[Either[Throwable, Unit]] = {
7049
val label = s"Steward ${repo.show}"
7150
logger.infoTotalTime(label) {
@@ -88,7 +67,7 @@ final class StewardAlg[F[_]](config: Config)(implicit
8867
_ <- selfCheckAlg.checkAll
8968
_ <- workspaceAlg.removeAnyRunSpecificFiles
9069
exitCode <-
91-
(config.githubApp.map(getGitHubAppRepos).getOrElse(Stream.empty) ++
70+
(Stream.evals(forgeAuthAlg.accessibleRepos) ++
9271
reposFilesLoader.loadAll(config.reposFiles))
9372
.evalMap(repo => steward(repo).map(_.bimap(repo -> _, _ => repo)))
9473
.compile
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2018-2023 Scala Steward contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.scalasteward.core.forge
18+
19+
import better.files.File
20+
import cats.effect.Sync
21+
import cats.syntax.all._
22+
import org.http4s.Uri.UserInfo
23+
import org.http4s.headers.Authorization
24+
import org.http4s.{BasicCredentials, Request, Uri}
25+
import org.scalasteward.core.data.Repo
26+
import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
27+
import org.scalasteward.core.util
28+
import org.scalasteward.core.util.Nel
29+
30+
class BasicAuthAlg[F[_]](apiUri: Uri, login: String, gitAskPass: File)(implicit
31+
F: Sync[F],
32+
workspaceAlg: WorkspaceAlg[F],
33+
processAlg: ProcessAlg[F]
34+
) extends ForgeAuthAlg[F] {
35+
protected lazy val userInfo: F[UserInfo] = for {
36+
rootDir <- workspaceAlg.rootDir
37+
userInfo = UserInfo(login, None)
38+
urlWithUser = util.uri.withUserInfo.replace(userInfo)(apiUri).renderString
39+
prompt = s"Password for '$urlWithUser': "
40+
output <- processAlg.exec(Nel.of(gitAskPass.pathAsString, prompt), rootDir)
41+
password = output.mkString.trim
42+
} yield UserInfo(login, Some(password))
43+
44+
override def authenticateApi(req: Request[F]): F[Request[F]] =
45+
userInfo.map {
46+
case UserInfo(username, Some(password)) =>
47+
req.putHeaders(Authorization(BasicCredentials(username, password)))
48+
case _ => req
49+
}
50+
51+
override def authenticateGit(uri: Uri): F[Uri] =
52+
userInfo.map(user => util.uri.withUserInfo.replace(user)(uri))
53+
54+
override def accessibleRepos: F[List[Repo]] = F.pure(List.empty)
55+
}

modules/core/src/main/scala/org/scalasteward/core/forge/ForgeAuthAlg.scala

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,27 +16,52 @@
1616

1717
package org.scalasteward.core.forge
1818

19-
import cats.Monad
20-
import cats.syntax.all._
21-
import org.http4s.Uri.UserInfo
22-
import org.scalasteward.core.application.Config.{ForgeCfg, GitCfg}
23-
import org.scalasteward.core.forge.data.AuthenticatedUser
19+
import cats.effect.Sync
20+
import org.http4s.{Request, Uri}
21+
import org.scalasteward.core.application.Config
22+
import org.scalasteward.core.data.Repo
23+
import org.scalasteward.core.forge.ForgeType._
24+
import org.scalasteward.core.forge.bitbucketserver.BitbucketServerAuthAlg
25+
import org.scalasteward.core.forge.github.GitHubAuthAlg
26+
import org.scalasteward.core.forge.gitlab.GitLabAuthAlg
2427
import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
25-
import org.scalasteward.core.util
26-
import org.scalasteward.core.util.Nel
28+
import org.scalasteward.core.util.HttpJsonClient
29+
import org.typelevel.log4cats.Logger
2730

28-
final class ForgeAuthAlg[F[_]](gitCfg: GitCfg, forgeCfg: ForgeCfg)(implicit
29-
processAlg: ProcessAlg[F],
30-
workspaceAlg: WorkspaceAlg[F],
31-
F: Monad[F]
32-
) {
33-
def forgeUser: F[AuthenticatedUser] =
34-
for {
35-
rootDir <- workspaceAlg.rootDir
36-
userInfo = UserInfo(forgeCfg.login, None)
37-
urlWithUser = util.uri.withUserInfo.replace(userInfo)(forgeCfg.apiHost).renderString
38-
prompt = s"Password for '$urlWithUser': "
39-
output <- processAlg.exec(Nel.of(gitCfg.gitAskPass.pathAsString, prompt), rootDir)
40-
password = output.mkString.trim
41-
} yield AuthenticatedUser(forgeCfg.login, password)
31+
trait ForgeAuthAlg[F[_]] {
32+
def authenticateApi(req: Request[F]): F[Request[F]]
33+
def authenticateGit(uri: Uri): F[Uri]
34+
def accessibleRepos: F[List[Repo]]
35+
}
36+
37+
object ForgeAuthAlg {
38+
def create[F[_]](config: Config)(implicit
39+
F: Sync[F],
40+
client: HttpJsonClient[F],
41+
workspaceAlg: WorkspaceAlg[F],
42+
processAlg: ProcessAlg[F],
43+
logger: Logger[F]
44+
): ForgeAuthAlg[F] =
45+
config.forgeCfg.tpe match {
46+
case AzureRepos =>
47+
new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
48+
case Bitbucket =>
49+
new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
50+
case BitbucketServer =>
51+
new BitbucketServerAuthAlg(
52+
config.forgeCfg.apiHost,
53+
config.forgeCfg.login,
54+
config.gitCfg.gitAskPass
55+
)
56+
case GitHub =>
57+
val gitHub =
58+
config.githubApp.getOrElse(
59+
throw new IllegalArgumentException("GitHub app configuration is missing")
60+
)
61+
new GitHubAuthAlg(config.forgeCfg.apiHost, gitHub.id, gitHub.keyFile)
62+
case GitLab =>
63+
new GitLabAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
64+
case Gitea =>
65+
new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
66+
}
4267
}

modules/core/src/main/scala/org/scalasteward/core/forge/ForgeRepoAlg.scala

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,29 @@ package org.scalasteward.core.forge
1818

1919
import cats.MonadThrow
2020
import cats.syntax.all._
21-
import org.http4s.Uri
22-
import org.http4s.Uri.UserInfo
2321
import org.scalasteward.core.application.Config
2422
import org.scalasteward.core.data.Repo
2523
import org.scalasteward.core.forge.ForgeType.GitHub
2624
import org.scalasteward.core.forge.data.RepoOut
2725
import org.scalasteward.core.git.{updateBranchPrefix, Branch, GitAlg}
28-
import org.scalasteward.core.util
2926
import org.scalasteward.core.util.logger._
3027
import org.typelevel.log4cats.Logger
3128

3229
final class ForgeRepoAlg[F[_]](config: Config)(implicit
3330
gitAlg: GitAlg[F],
31+
forgeAuthAlg: ForgeAuthAlg[F],
3432
logger: Logger[F],
3533
F: MonadThrow[F]
3634
) {
3735
def cloneAndSync(repo: Repo, repoOut: RepoOut): F[Unit] =
3836
clone(repo, repoOut) >> maybeCheckoutBranchOrSyncFork(repo, repoOut) >> initSubmodules(repo)
3937

40-
private def clone(repo: Repo, repoOut: RepoOut): F[Unit] =
41-
logger.info(s"Clone ${repoOut.repo.show}") >>
42-
gitAlg.clone(repo, withLogin(repoOut.clone_url)).adaptErr(adaptCloneError) >>
43-
gitAlg.setAuthor(repo, config.gitCfg.gitAuthor)
38+
private def clone(repo: Repo, repoOut: RepoOut): F[Unit] = for {
39+
_ <- logger.info(s"Clone ${repoOut.repo.show}")
40+
uri <- forgeAuthAlg.authenticateGit(repoOut.clone_url)
41+
_ <- gitAlg.clone(repo, uri).adaptErr(adaptCloneError)
42+
_ <- gitAlg.setAuthor(repo, config.gitCfg.gitAuthor)
43+
} yield ()
4444

4545
private val adaptCloneError: PartialFunction[Throwable, Throwable] = {
4646
case throwable if config.forgeCfg.tpe === GitHub && !config.forgeCfg.doNotFork =>
@@ -56,12 +56,13 @@ final class ForgeRepoAlg[F[_]](config: Config)(implicit
5656
if (config.forgeCfg.doNotFork) repo.branch.fold(F.unit)(gitAlg.checkoutBranch(repo, _))
5757
else syncFork(repo, repoOut)
5858

59-
private def syncFork(repo: Repo, repoOut: RepoOut): F[Unit] =
60-
repoOut.parentOrRaise[F].flatMap { parent =>
61-
logger.info(s"Synchronize with ${parent.repo.show}") >>
62-
gitAlg.syncFork(repo, withLogin(parent.clone_url), parent.default_branch) >>
63-
deleteUpdateBranch(repo)
64-
}
59+
private def syncFork(repo: Repo, repoOut: RepoOut): F[Unit] = for {
60+
parent <- repoOut.parentOrRaise[F]
61+
_ <- logger.info(s"Synchronize with ${parent.repo.show}")
62+
uri <- forgeAuthAlg.authenticateGit(parent.clone_url)
63+
_ <- gitAlg.syncFork(repo, uri, parent.default_branch)
64+
_ <- deleteUpdateBranch(repo)
65+
} yield ()
6566

6667
// We use "update" as prefix for our branches but Git doesn't allow branches named
6768
// "update" and "update/..." in the same repo. We therefore delete the "update" branch
@@ -77,7 +78,4 @@ final class ForgeRepoAlg[F[_]](config: Config)(implicit
7778
logger.attemptWarn.log_("Initializing and cloning submodules failed") {
7879
gitAlg.initSubmodules(repo)
7980
}
80-
81-
private val withLogin: Uri => Uri =
82-
util.uri.withUserInfo.replace(UserInfo(config.forgeCfg.login, None))
8381
}

modules/core/src/main/scala/org/scalasteward/core/forge/ForgeSelection.scala

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,29 @@
1717
package org.scalasteward.core.forge
1818

1919
import cats.effect.Temporal
20-
import cats.syntax.all._
21-
import cats.{Applicative, Functor, Parallel}
22-
import org.http4s.headers.Authorization
23-
import org.http4s.{BasicCredentials, Header, Request}
20+
import cats.Parallel
21+
import org.http4s.Request
2422
import org.scalasteward.core.application.Config
2523
import org.scalasteward.core.application.Config.{ForgeCfg, ForgeSpecificCfg}
26-
import org.scalasteward.core.forge.ForgeType._
2724
import org.scalasteward.core.forge.azurerepos.AzureReposApiAlg
2825
import org.scalasteward.core.forge.bitbucket.BitbucketApiAlg
2926
import org.scalasteward.core.forge.bitbucketserver.BitbucketServerApiAlg
30-
import org.scalasteward.core.forge.data.AuthenticatedUser
3127
import org.scalasteward.core.forge.gitea.GiteaApiAlg
3228
import org.scalasteward.core.forge.github.GitHubApiAlg
3329
import org.scalasteward.core.forge.gitlab.GitLabApiAlg
3430
import org.scalasteward.core.util.HttpJsonClient
35-
import org.typelevel.ci._
3631
import org.typelevel.log4cats.Logger
3732

3833
object ForgeSelection {
3934
def forgeApiAlg[F[_]: Parallel](
4035
forgeCfg: ForgeCfg,
4136
forgeSpecificCfg: ForgeSpecificCfg,
42-
user: F[AuthenticatedUser]
37+
auth: Request[F] => F[Request[F]]
4338
)(implicit
4439
httpJsonClient: HttpJsonClient[F],
4540
logger: Logger[F],
4641
F: Temporal[F]
47-
): ForgeApiAlg[F] = {
48-
val auth = authenticate(forgeCfg.tpe, user)
42+
): ForgeApiAlg[F] =
4943
forgeSpecificCfg match {
5044
case specificCfg: Config.AzureReposCfg =>
5145
new AzureReposApiAlg(forgeCfg.apiHost, specificCfg, auth)
@@ -60,39 +54,4 @@ object ForgeSelection {
6054
case _: Config.GiteaCfg =>
6155
new GiteaApiAlg(forgeCfg, auth)
6256
}
63-
}
64-
65-
def authenticate[F[_]](
66-
forgeType: ForgeType,
67-
user: F[AuthenticatedUser]
68-
)(implicit F: Functor[F]): Request[F] => F[Request[F]] =
69-
forgeType match {
70-
case AzureRepos => req => user.map(u => req.putHeaders(basicAuth(u)))
71-
case Bitbucket => req => user.map(u => req.putHeaders(basicAuth(u)))
72-
case BitbucketServer => req => user.map(u => req.putHeaders(basicAuth(u), xAtlassianToken))
73-
case GitHub => req => user.map(u => req.putHeaders(basicAuth(u)))
74-
case GitLab => req => user.map(u => req.putHeaders(privateToken(u)))
75-
case Gitea => req => user.map(u => req.putHeaders(basicAuth(u)))
76-
}
77-
78-
private def basicAuth(user: AuthenticatedUser): Authorization =
79-
Authorization(BasicCredentials(user.login, user.accessToken))
80-
81-
private def privateToken(user: AuthenticatedUser): Header.Raw =
82-
Header.Raw(ci"Private-Token", user.accessToken)
83-
84-
// Bypass the server-side XSRF check, see
85-
// https://github.com/scala-steward-org/scala-steward/pull/1863#issuecomment-754538364
86-
private val xAtlassianToken = Header.Raw(ci"X-Atlassian-Token", "no-check")
87-
88-
def authenticateIfApiHost[F[_]](
89-
forgeCfg: ForgeCfg,
90-
user: F[AuthenticatedUser]
91-
)(implicit F: Applicative[F]): Request[F] => F[Request[F]] =
92-
req => {
93-
val sameScheme = req.uri.scheme === forgeCfg.apiHost.scheme
94-
val sameHost = req.uri.host === forgeCfg.apiHost.host
95-
if (sameScheme && sameHost) authenticate(forgeCfg.tpe, user)(F)(req)
96-
else req.pure[F]
97-
}
9857
}

0 commit comments

Comments
 (0)