Skip to content

Commit

Permalink
Support GitHub Enterprise
Browse files Browse the repository at this point in the history
  • Loading branch information
joan38 authored and mzuehlke committed Nov 17, 2024
1 parent c1d4b6f commit c1e6eac
Show file tree
Hide file tree
Showing 35 changed files with 571 additions and 446 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import org.scalasteward.core.edit.EditAlg
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, ForgeAuthAlg, ForgeRepoAlg, ForgeSelection}
import org.scalasteward.core.git.{GenGitAlg, GitAlg}
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
Expand Down Expand Up @@ -128,18 +127,18 @@ object Context {
processAlg: ProcessAlg[F],
workspaceAlg: WorkspaceAlg[F],
F: Async[F]
): F[Context[F]] =
): F[Context[F]] = {
implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]()
implicit val forgeAuthAlg: ForgeAuthAlg[F] = ForgeAuthAlg.create[F](config)
for {
_ <- F.unit
forgeUser = new ForgeAuthAlg[F](config.gitCfg, config.forgeCfg).forgeUser
artifactMigrationsLoader0 = new ArtifactMigrationsLoader[F]
artifactMigrationsFinder0 <- artifactMigrationsLoader0.createFinder(config.artifactCfg)
scalafixMigrationsLoader0 = new ScalafixMigrationsLoader[F]
scalafixMigrationsFinder0 <- scalafixMigrationsLoader0.createFinder(config.scalafixCfg)
repoConfigLoader0 = new RepoConfigLoader[F]
maybeGlobalRepoConfig <- repoConfigLoader0.loadGlobalRepoConfig(config.repoConfigCfg)
urlChecker0 <- UrlChecker
.create[F](config, ForgeSelection.authenticateIfApiHost(config.forgeCfg, forgeUser))
urlChecker0 <- UrlChecker.create[F](config, forgeAuthAlg.authenticateApi)
kvsPrefix = Some(config.forgeCfg.tpe.asString)
pullRequestsStore <- JsonKeyValueStore
.create[F, Repo, Map[Uri, PullRequestRepository.Entry]]("pull_requests", "2", kvsPrefix)
Expand All @@ -159,14 +158,12 @@ object Context {
implicit val dateTimeAlg: DateTimeAlg[F] = DateTimeAlg.create[F]
implicit val repoConfigAlg: RepoConfigAlg[F] = new RepoConfigAlg[F](maybeGlobalRepoConfig)
implicit val filterAlg: FilterAlg[F] = new FilterAlg[F]
implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config.gitCfg)
implicit val gitHubAuthAlg: GitHubAuthAlg[F] = GitHubAuthAlg.create[F]
implicit val gitAlg: GitAlg[F] = GenGitAlg.create[F](config)
implicit val hookExecutor: HookExecutor[F] = new HookExecutor[F]
implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F]
implicit val repoCacheRepository: RepoCacheRepository[F] =
new RepoCacheRepository[F](repoCacheStore)
implicit val forgeApiAlg: ForgeApiAlg[F] =
ForgeSelection.forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeUser)
implicit val forgeApiAlg: ForgeApiAlg[F] = ForgeSelection
.forgeApiAlg[F](config.forgeCfg, config.forgeSpecificCfg, forgeAuthAlg.authenticateApi)
implicit val forgeRepoAlg: ForgeRepoAlg[F] = new ForgeRepoAlg[F](config)
implicit val forgeCfg: ForgeCfg = config.forgeCfg
implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F]
Expand All @@ -192,11 +189,10 @@ object Context {
implicit val nurtureAlg: NurtureAlg[F] = new NurtureAlg[F](config.forgeCfg)
implicit val pruningAlg: PruningAlg[F] = new PruningAlg[F]
implicit val reposFilesLoader: ReposFilesLoader[F] = new ReposFilesLoader[F]
implicit val gitHubAppApiAlg: GitHubAppApiAlg[F] =
new GitHubAppApiAlg[F](config.forgeCfg.apiHost)
implicit val stewardAlg: StewardAlg[F] = new StewardAlg[F](config)
new Context[F]
}
}

private val banner: String = {
val banner =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import cats.effect.{ExitCode, Sync}
import cats.syntax.all._
import fs2.Stream
import org.scalasteward.core.data.Repo
import org.scalasteward.core.forge.github.{GitHubApp, GitHubAppApiAlg, GitHubAuthAlg}
import org.scalasteward.core.forge.ForgeAuthAlg
import org.scalasteward.core.git.GitAlg
import org.scalasteward.core.io.{FileAlg, WorkspaceAlg}
import org.scalasteward.core.nurture.NurtureAlg
Expand All @@ -30,14 +30,12 @@ import org.scalasteward.core.util
import org.scalasteward.core.util.DateTimeAlg
import org.scalasteward.core.util.logger.LoggerOps
import org.typelevel.log4cats.Logger
import scala.concurrent.duration._

final class StewardAlg[F[_]](config: Config)(implicit
dateTimeAlg: DateTimeAlg[F],
fileAlg: FileAlg[F],
gitAlg: GitAlg[F],
githubAppApiAlg: GitHubAppApiAlg[F],
githubAuthAlg: GitHubAuthAlg[F],
forgeAuthAlg: ForgeAuthAlg[F],
logger: Logger[F],
nurtureAlg: NurtureAlg[F],
pruningAlg: PruningAlg[F],
Expand All @@ -47,25 +45,6 @@ final class StewardAlg[F[_]](config: Config)(implicit
workspaceAlg: WorkspaceAlg[F],
F: Sync[F]
) {
private def getGitHubAppRepos(githubApp: GitHubApp): Stream[F, Repo] =
Stream.evals[F, List, Repo] {
for {
jwt <- githubAuthAlg.createJWT(githubApp, 2.minutes)
installations <- githubAppApiAlg.installations(jwt)
repositories <- installations.traverse { installation =>
githubAppApiAlg
.accessToken(jwt, installation.id)
.flatMap(token => githubAppApiAlg.repositories(token.token))
}
repos <- repositories.flatMap(_.repositories).flatTraverse { repo =>
repo.full_name.split('/') match {
case Array(owner, name) => F.pure(List(Repo(owner, name)))
case _ => logger.error(s"invalid repo $repo").as(List.empty[Repo])
}
}
} yield repos
}

private def steward(repo: Repo): F[Either[Throwable, Unit]] = {
val label = s"Steward ${repo.show}"
logger.infoTotalTime(label) {
Expand All @@ -88,7 +67,7 @@ final class StewardAlg[F[_]](config: Config)(implicit
_ <- selfCheckAlg.checkAll
_ <- workspaceAlg.removeAnyRunSpecificFiles
exitCode <-
(config.githubApp.map(getGitHubAppRepos).getOrElse(Stream.empty) ++
(Stream.evals(forgeAuthAlg.accessibleRepos) ++
reposFilesLoader.loadAll(config.reposFiles))
.evalMap(repo => steward(repo).map(_.bimap(repo -> _, _ => repo)))
.compile
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 better.files.File
import cats.effect.Sync
import cats.syntax.all._
import org.http4s.Uri.UserInfo
import org.http4s.headers.Authorization
import org.http4s.{BasicCredentials, Request, Uri}
import org.scalasteward.core.data.Repo
import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
import org.scalasteward.core.util
import org.scalasteward.core.util.Nel

class BasicAuthAlg[F[_]](apiUri: Uri, login: String, gitAskPass: File)(implicit
F: Sync[F],
workspaceAlg: WorkspaceAlg[F],
processAlg: ProcessAlg[F]
) extends ForgeAuthAlg[F] {
protected lazy val userInfo: F[UserInfo] = for {
rootDir <- workspaceAlg.rootDir
userInfo = UserInfo(login, None)
urlWithUser = util.uri.withUserInfo.replace(userInfo)(apiUri).renderString
prompt = s"Password for '$urlWithUser': "
output <- processAlg.exec(Nel.of(gitAskPass.pathAsString, prompt), rootDir)
password = output.mkString.trim
} yield UserInfo(login, Some(password))

override def authenticateApi(req: Request[F]): F[Request[F]] =
userInfo.map {
case UserInfo(username, Some(password)) =>
req.putHeaders(Authorization(BasicCredentials(username, password)))
case _ => req
}

override def authenticateGit(uri: Uri): F[Uri] =
userInfo.map(user => util.uri.withUserInfo.replace(user)(uri))

override def accessibleRepos: F[List[Repo]] = F.pure(List.empty)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,52 @@

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 cats.effect.Sync
import org.http4s.{Request, Uri}
import org.scalasteward.core.application.Config
import org.scalasteward.core.data.Repo
import org.scalasteward.core.forge.ForgeType._
import org.scalasteward.core.forge.bitbucketserver.BitbucketServerAuthAlg
import org.scalasteward.core.forge.github.GitHubAuthAlg
import org.scalasteward.core.forge.gitlab.GitLabAuthAlg
import org.scalasteward.core.io.{ProcessAlg, WorkspaceAlg}
import org.scalasteward.core.util
import org.scalasteward.core.util.Nel
import org.scalasteward.core.util.HttpJsonClient
import org.typelevel.log4cats.Logger

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)
trait ForgeAuthAlg[F[_]] {
def authenticateApi(req: Request[F]): F[Request[F]]
def authenticateGit(uri: Uri): F[Uri]
def accessibleRepos: F[List[Repo]]
}

object ForgeAuthAlg {
def create[F[_]](config: Config)(implicit
F: Sync[F],
client: HttpJsonClient[F],
workspaceAlg: WorkspaceAlg[F],
processAlg: ProcessAlg[F],
logger: Logger[F]
): ForgeAuthAlg[F] =
config.forgeCfg.tpe match {
case AzureRepos =>
new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
case Bitbucket =>
new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
case BitbucketServer =>
new BitbucketServerAuthAlg(
config.forgeCfg.apiHost,
config.forgeCfg.login,
config.gitCfg.gitAskPass
)
case GitHub =>
val gitHub =
config.githubApp.getOrElse(
throw new IllegalArgumentException("GitHub app configuration is missing")
)
new GitHubAuthAlg(config.forgeCfg.apiHost, gitHub.id, gitHub.keyFile)
case GitLab =>
new GitLabAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
case Gitea =>
new BasicAuthAlg(config.forgeCfg.apiHost, config.forgeCfg.login, config.gitCfg.gitAskPass)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,29 @@ package org.scalasteward.core.forge

import cats.MonadThrow
import cats.syntax.all._
import org.http4s.Uri
import org.http4s.Uri.UserInfo
import org.scalasteward.core.application.Config
import org.scalasteward.core.data.Repo
import org.scalasteward.core.forge.ForgeType.GitHub
import org.scalasteward.core.forge.data.RepoOut
import org.scalasteward.core.git.{updateBranchPrefix, Branch, GitAlg}
import org.scalasteward.core.util
import org.scalasteward.core.util.logger._
import org.typelevel.log4cats.Logger

final class ForgeRepoAlg[F[_]](config: Config)(implicit
gitAlg: GitAlg[F],
forgeAuthAlg: ForgeAuthAlg[F],
logger: Logger[F],
F: MonadThrow[F]
) {
def cloneAndSync(repo: Repo, repoOut: RepoOut): F[Unit] =
clone(repo, repoOut) >> maybeCheckoutBranchOrSyncFork(repo, repoOut) >> initSubmodules(repo)

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

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

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

// We use "update" as prefix for our branches but Git doesn't allow branches named
// "update" and "update/..." in the same repo. We therefore delete the "update" branch
Expand All @@ -77,7 +78,4 @@ final class ForgeRepoAlg[F[_]](config: Config)(implicit
logger.attemptWarn.log_("Initializing and cloning submodules failed") {
gitAlg.initSubmodules(repo)
}

private val withLogin: Uri => Uri =
util.uri.withUserInfo.replace(UserInfo(config.forgeCfg.login, None))
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,29 @@
package org.scalasteward.core.forge

import cats.effect.Temporal
import cats.syntax.all._
import cats.{Applicative, Functor, Parallel}
import org.http4s.headers.Authorization
import org.http4s.{BasicCredentials, Header, Request}
import cats.Parallel
import org.http4s.Request
import org.scalasteward.core.application.Config
import org.scalasteward.core.application.Config.{ForgeCfg, ForgeSpecificCfg}
import org.scalasteward.core.forge.ForgeType._
import org.scalasteward.core.forge.azurerepos.AzureReposApiAlg
import org.scalasteward.core.forge.bitbucket.BitbucketApiAlg
import org.scalasteward.core.forge.bitbucketserver.BitbucketServerApiAlg
import org.scalasteward.core.forge.data.AuthenticatedUser
import org.scalasteward.core.forge.gitea.GiteaApiAlg
import org.scalasteward.core.forge.github.GitHubApiAlg
import org.scalasteward.core.forge.gitlab.GitLabApiAlg
import org.scalasteward.core.util.HttpJsonClient
import org.typelevel.ci._
import org.typelevel.log4cats.Logger

object ForgeSelection {
def forgeApiAlg[F[_]: Parallel](
forgeCfg: ForgeCfg,
forgeSpecificCfg: ForgeSpecificCfg,
user: F[AuthenticatedUser]
auth: Request[F] => F[Request[F]]
)(implicit
httpJsonClient: HttpJsonClient[F],
logger: Logger[F],
F: Temporal[F]
): ForgeApiAlg[F] = {
val auth = authenticate(forgeCfg.tpe, user)
): ForgeApiAlg[F] =
forgeSpecificCfg match {
case specificCfg: Config.AzureReposCfg =>
new AzureReposApiAlg(forgeCfg.apiHost, specificCfg, auth)
Expand All @@ -60,39 +54,4 @@ object ForgeSelection {
case _: Config.GiteaCfg =>
new GiteaApiAlg(forgeCfg, auth)
}
}

def authenticate[F[_]](
forgeType: ForgeType,
user: F[AuthenticatedUser]
)(implicit F: Functor[F]): Request[F] => F[Request[F]] =
forgeType match {
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: F[AuthenticatedUser]
)(implicit F: Applicative[F]): Request[F] => F[Request[F]] =
req => {
val sameScheme = req.uri.scheme === forgeCfg.apiHost.scheme
val sameHost = req.uri.host === forgeCfg.apiHost.host
if (sameScheme && sameHost) authenticate(forgeCfg.tpe, user)(F)(req)
else req.pure[F]
}
}
Loading

0 comments on commit c1e6eac

Please sign in to comment.