From 75ebd2ce39eb3cc6ac2c4c16dd50cc470c2554ba Mon Sep 17 00:00:00 2001 From: "Frank S. Thomas" Date: Wed, 20 Dec 2023 21:38:36 +0100 Subject: [PATCH] Remove GitHub App code from core --- build.sbt | 18 +- .../scalasteward/core/application/Cli.scala | 17 - .../core/application/Config.scala | 2 - .../core/application/Context.scala | 4 - .../core/application/StewardAlg.scala | 49 +-- .../core/application/CliTest.scala | 4 - .../org/scalasteward/ghappfacade/Cli.scala | 391 ++++++++++++++++++ .../org/scalasteward/ghappfacade/Config.scala | 167 ++++++++ .../scalasteward/ghappfacade/Context.scala | 213 ++++++++++ .../scalasteward/ghappfacade}/GitHubApp.scala | 0 .../ghappfacade}/GitHubAppApiAlg.scala | 0 .../ghappfacade}/GitHubAuthAlg.scala | 0 .../ghappfacade}/InstallationOut.scala | 0 .../ghappfacade}/RepositoriesOut.scala | 0 .../scalasteward/ghappfacade/StewardAlg.scala | 105 +++++ .../scalasteward/ghappfacade}/TokenOut.scala | 0 .../ghappfacade}/GitHubAppApiAlgTest.scala | 0 .../ghappfacade}/GitHubAuthAlgTest.scala | 0 18 files changed, 901 insertions(+), 69 deletions(-) create mode 100644 modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Cli.scala create mode 100644 modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Config.scala create mode 100644 modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Context.scala rename modules/{core/src/main/scala/org/scalasteward/core/forge/github => gh-app-facade/src/main/scala/org/scalasteward/ghappfacade}/GitHubApp.scala (100%) rename modules/{core/src/main/scala/org/scalasteward/core/forge/github => gh-app-facade/src/main/scala/org/scalasteward/ghappfacade}/GitHubAppApiAlg.scala (100%) rename modules/{core/src/main/scala/org/scalasteward/core/forge/github => gh-app-facade/src/main/scala/org/scalasteward/ghappfacade}/GitHubAuthAlg.scala (100%) rename modules/{core/src/main/scala/org/scalasteward/core/forge/github => gh-app-facade/src/main/scala/org/scalasteward/ghappfacade}/InstallationOut.scala (100%) rename modules/{core/src/main/scala/org/scalasteward/core/forge/github => gh-app-facade/src/main/scala/org/scalasteward/ghappfacade}/RepositoriesOut.scala (100%) create mode 100644 modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/StewardAlg.scala rename modules/{core/src/main/scala/org/scalasteward/core/forge/github => gh-app-facade/src/main/scala/org/scalasteward/ghappfacade}/TokenOut.scala (100%) rename modules/{core/src/test/scala/org/scalasteward/core/forge/github => gh-app-facade/src/test/scala/org/scalasteward/ghappfacade}/GitHubAppApiAlgTest.scala (100%) rename modules/{core/src/test/scala/org/scalasteward/core/forge/github => gh-app-facade/src/test/scala/org/scalasteward/ghappfacade}/GitHubAuthAlgTest.scala (100%) diff --git a/build.sbt b/build.sbt index ce0a455ec4..b36244e8e7 100644 --- a/build.sbt +++ b/build.sbt @@ -18,7 +18,8 @@ val moduleCrossPlatformMatrix: Map[String, List[Platform]] = Map( "benchmark" -> List(JVMPlatform), "core" -> List(JVMPlatform), "docs" -> List(JVMPlatform), - "dummy" -> List(JVMPlatform) + "dummy" -> List(JVMPlatform), + "gh-app-facade" -> List(JVMPlatform) ) val Scala213 = "2.13.12" @@ -111,7 +112,6 @@ lazy val core = myCrossProject("core") .settings(dockerSettings) .settings( libraryDependencies ++= Seq( - Dependencies.bcprovJdk15to18, Dependencies.betterFiles, Dependencies.catsCore, Dependencies.catsEffect, @@ -132,9 +132,6 @@ lazy val core = myCrossProject("core") Dependencies.http4sClient, Dependencies.http4sCore, Dependencies.http4sJdkhttpClient, - Dependencies.jjwtApi, - Dependencies.jjwtImpl % Runtime, - Dependencies.jjwtJackson % Runtime, Dependencies.log4catsSlf4j, Dependencies.monocleCore, Dependencies.refined, @@ -276,6 +273,17 @@ lazy val dummy = myCrossProject("dummy") ) ) +lazy val ghAppFacade = myCrossProject("gh-app-facade") + .dependsOn(core) + .settings( + libraryDependencies ++= Seq( + Dependencies.bcprovJdk15to18, + Dependencies.jjwtApi, + Dependencies.jjwtImpl % Runtime, + Dependencies.jjwtJackson % Runtime + ) + ) + /// settings def myCrossProject(name: String): CrossProject = 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 8b53045a9b..5ded86da5a 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 @@ -27,7 +27,6 @@ import org.scalasteward.core.application.Config._ import org.scalasteward.core.data.Resolver import org.scalasteward.core.forge.ForgeType import org.scalasteward.core.forge.ForgeType.{AzureRepos, GitHub} -import org.scalasteward.core.forge.github.GitHubApp import org.scalasteward.core.git.Author import org.scalasteward.core.util.Nel import org.scalasteward.core.util.dateTime.renderFiniteDuration @@ -291,21 +290,6 @@ object Cli { GitLabCfg.apply ) - private val githubAppId: Opts[Long] = - option[Long]( - "github-app-id", - "GitHub application id. Repos accessible by this app are added to the repos in repos.md. git-ask-pass is still required." - ) - - private val githubAppKeyFile: Opts[File] = - option[File]( - "github-app-key-file", - "GitHub application key file. Repos accessible by this app are added to the repos in repos.md. git-ask-pass is still required." - ) - - private val gitHubApp: Opts[Option[GitHubApp]] = - (githubAppId, githubAppKeyFile).mapN(GitHubApp.apply).orNone - private val azureReposOrganization: Opts[Option[String]] = option[String]( "azure-repos-organization", @@ -352,7 +336,6 @@ object Cli { bitbucketServerCfg, gitLabCfg, azureReposCfg, - gitHubApp, urlCheckerTestUrls, defaultMavenRepo, refreshBackoffPeriod 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 208ed68562..3442bab5a2 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 @@ -26,7 +26,6 @@ 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 @@ -66,7 +65,6 @@ final case class Config( bitbucketServerCfg: BitbucketServerCfg, gitLabCfg: GitLabCfg, azureReposCfg: AzureReposCfg, - githubApp: Option[GitHubApp], urlCheckerTestUrls: Nel[Uri], defaultResolver: Resolver, refreshBackoffPeriod: FiniteDuration 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 7ba009548b..7cddf76e1d 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 @@ -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, ForgeRepoAlg, ForgeSelection} import org.scalasteward.core.git.{GenGitAlg, GitAlg} import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg} @@ -159,7 +158,6 @@ object Context { 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 hookExecutor: HookExecutor[F] = new HookExecutor[F] implicit val httpJsonClient: HttpJsonClient[F] = new HttpJsonClient[F] implicit val repoCacheRepository: RepoCacheRepository[F] = @@ -191,8 +189,6 @@ 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] } diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala index ac7c91c30d..6126a962ad 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/StewardAlg.scala @@ -18,9 +18,7 @@ package org.scalasteward.core.application 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.git.GitAlg import org.scalasteward.core.io.{FileAlg, WorkspaceAlg} import org.scalasteward.core.nurture.NurtureAlg @@ -30,14 +28,11 @@ 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], logger: Logger[F], nurtureAlg: NurtureAlg[F], pruningAlg: PruningAlg[F], @@ -47,25 +42,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) { @@ -87,19 +63,18 @@ final class StewardAlg[F[_]](config: Config)(implicit for { _ <- selfCheckAlg.checkAll _ <- workspaceAlg.removeAnyRunSpecificFiles - exitCode <- - (config.githubApp.map(getGitHubAppRepos).getOrElse(Stream.empty) ++ - reposFilesLoader.loadAll(config.reposFiles)) - .evalMap(repo => steward(repo).map(_.bimap(repo -> _, _ => repo))) - .compile - .toList - .flatMap { results => - val runResults = RunResults(results) - for { - summaryFile <- workspaceAlg.runSummaryFile - _ <- fileAlg.writeFile(summaryFile, runResults.markdownSummary) - } yield runResults.exitCode - } + exitCode <- reposFilesLoader + .loadAll(config.reposFiles) + .evalMap(repo => steward(repo).map(_.bimap(repo -> _, _ => repo))) + .compile + .toList + .flatMap { results => + val runResults = RunResults(results) + for { + summaryFile <- workspaceAlg.runSummaryFile + _ <- fileAlg.writeFile(summaryFile, runResults.markdownSummary) + } yield runResults.exitCode + } } yield exitCode } } 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 ffbe23d46f..db28828d5e 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 @@ -7,7 +7,6 @@ import org.http4s.syntax.literals._ import org.scalasteward.core.application.Cli.ParseResult._ import org.scalasteward.core.application.Cli.{EnvVar, Usage} import org.scalasteward.core.forge.ForgeType -import org.scalasteward.core.forge.github.GitHubApp import org.scalasteward.core.util.Nel import scala.concurrent.duration._ @@ -30,8 +29,6 @@ class CliTest extends FunSuite { List("--scalafix-migrations", "/opt/scala-steward/extra-scalafix-migrations.conf"), List("--artifact-migrations", "/opt/scala-steward/extra-artifact-migrations.conf"), List("--repo-config", "/opt/scala-steward/scala-steward.conf"), - List("--github-app-id", "12345678"), - List("--github-app-key-file", "example_app_key"), List("--refresh-backoff-period", "1 day"), List("--bitbucket-use-default-reviewers") ).flatten @@ -60,7 +57,6 @@ class CliTest extends FunSuite { obtained.artifactCfg.migrations, List(uri"/opt/scala-steward/extra-artifact-migrations.conf") ) - assertEquals(obtained.githubApp, Some(GitHubApp(12345678L, File("example_app_key")))) assertEquals(obtained.refreshBackoffPeriod, 1.day) assert(!obtained.gitLabCfg.mergeWhenPipelineSucceeds) assertEquals(obtained.gitLabCfg.requiredReviewers, None) diff --git a/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Cli.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Cli.scala new file mode 100644 index 0000000000..8b53045a9b --- /dev/null +++ b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Cli.scala @@ -0,0 +1,391 @@ +/* + * 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.application + +import better.files.File +import cats.data.Validated +import cats.syntax.all._ +import com.monovore.decline.Opts.{flag, option, options} +import com.monovore.decline._ +import org.http4s.Uri +import org.http4s.syntax.literals._ +import org.scalasteward.core.application.Config._ +import org.scalasteward.core.data.Resolver +import org.scalasteward.core.forge.ForgeType +import org.scalasteward.core.forge.ForgeType.{AzureRepos, GitHub} +import org.scalasteward.core.forge.github.GitHubApp +import org.scalasteward.core.git.Author +import org.scalasteward.core.util.Nel +import org.scalasteward.core.util.dateTime.renderFiniteDuration +import scala.concurrent.duration._ + +object Cli { + final case class EnvVar(name: String, value: String) + + object name { + val forgeApiHost = "forge-api-host" + val forgeLogin = "forge-login" + val forgeType = "forge-type" + val maxBufferSize = "max-buffer-size" + val processTimeout = "process-timeout" + } + + implicit val envVarArgument: Argument[EnvVar] = + Argument.from("name=value") { s => + s.trim.split('=').toList match { + case name :: (value @ _ :: _) => + Validated.valid(EnvVar(name.trim, value.mkString("=").trim)) + case _ => + val error = "The value is expected in the following format: NAME=VALUE." + Validated.invalidNel(error) + } + } + + implicit val fileArgument: Argument[File] = + Argument.from("file") { s => + Validated.catchNonFatal(File(s)).leftMap(_.getMessage).toValidatedNel + } + + implicit val uriArgument: Argument[Uri] = + Argument.from("uri") { s => + Validated.fromEither(Uri.fromString(s).leftMap(_.message)).toValidatedNel + } + + implicit val forgeTypeArgument: Argument[ForgeType] = + Argument.from(name.forgeType) { s => + Validated.fromEither(ForgeType.parse(s)).toValidatedNel + } + + private val multiple = "(can be used multiple times)" + + private val workspace: Opts[File] = + option[File]("workspace", "Location for cache and temporary files") + + private val reposFiles: Opts[Nel[Uri]] = + options[Uri]("repos-file", s"A markdown formatted file with a repository list $multiple") + + private val gitAuthorName: Opts[String] = { + val default = "Scala Steward" + option[String]("git-author-name", s"""Git "user.name"; default: $default""") + .withDefault(default) + } + + private val gitAuthorEmail: Opts[String] = + option[String]("git-author-email", """Git "user.email"""") + + private val gitAuthorSigningKey: Opts[Option[String]] = + option[String]("git-author-signing-key", """Git "user.signingKey"""").orNone + + private val gitAuthor: Opts[Author] = + (gitAuthorName, gitAuthorEmail, gitAuthorSigningKey).mapN(Author.apply) + + private val gitAskPass: Opts[File] = + option[File]("git-ask-pass", "An executable file that returns the git credentials") + + private val signCommits: Opts[Boolean] = + flag("sign-commits", "Whether to sign commits; default: false").orFalse + + private val gitCfg: Opts[GitCfg] = + (gitAuthor, gitAskPass, signCommits).mapN(GitCfg.apply) + + private val vcsType = + option[ForgeType]( + "vcs-type", + s"deprecated in favor of --${name.forgeType}", + visibility = Visibility.Partial + ) + + private val forgeType = { + val help = ForgeType.all.map(_.asString).mkString("One of ", ", ", "") + + s"; default: ${GitHub.asString}" + option[ForgeType](name.forgeType, help).orElse(vcsType).withDefault(GitHub) + } + + private val vcsApiHost = + option[Uri]( + "vcs-api-host", + s"deprecated in favor of --${name.forgeApiHost}", + visibility = Visibility.Partial + ) + + private val forgeApiHost: Opts[Uri] = + option[Uri](name.forgeApiHost, s"API URL of the forge; default: ${GitHub.publicApiBaseUrl}") + .orElse(vcsApiHost) + .withDefault(GitHub.publicApiBaseUrl) + + private val vcsLogin = + option[String]( + "vcs-login", + s"deprecated in favor of --${name.forgeLogin}", + visibility = Visibility.Partial + ) + + private val forgeLogin: Opts[String] = + option[String](name.forgeLogin, "The user name for the forge").orElse(vcsLogin) + + private val doNotFork: Opts[Boolean] = + flag("do-not-fork", "Whether to not push the update branches to a fork; default: false").orFalse + + private val addPrLabels: Opts[Boolean] = + flag( + "add-labels", + "Whether to add labels on pull or merge requests (if supported by the forge)" + ).orFalse + + private val forgeCfg: Opts[ForgeCfg] = + (forgeType, forgeApiHost, forgeLogin, doNotFork, addPrLabels) + .mapN(ForgeCfg.apply) + .validate( + s"${ForgeType.allNot(_.supportsForking)} do not support fork mode" + )(cfg => cfg.tpe.supportsForking || cfg.doNotFork) + .validate( + s"${ForgeType.allNot(_.supportsLabels)} do not support pull request labels" + )(cfg => cfg.tpe.supportsLabels || !cfg.addLabels) + + private val ignoreOptsFiles: Opts[Boolean] = + flag( + "ignore-opts-files", + """Whether to remove ".jvmopts" and ".sbtopts" files before invoking the build tool""" + ).orFalse + + private val envVar: Opts[List[EnvVar]] = { + val help = s"Assigns the value to the environment variable name $multiple" + options[EnvVar]("env-var", help).orEmpty + } + + private val processTimeout: Opts[FiniteDuration] = { + val default = 10.minutes + val help = + s"Timeout for external process invocations; default: ${renderFiniteDuration(default)}" + option[FiniteDuration](name.processTimeout, help).withDefault(default) + } + + private val whitelist: Opts[List[String]] = + options[String]( + "whitelist", + s"Directory white listed for the sandbox $multiple" + ).orEmpty + + private val readOnly: Opts[List[String]] = + options[String]( + "read-only", + s"Read only directory for the sandbox $multiple" + ).orEmpty + + private val enableSandbox: Opts[Boolean] = + flag("enable-sandbox", "Whether to use the sandbox") + .map(_ => true) + .orElse(flag("disable-sandbox", "Whether to not use the sandbox").map(_ => false)) + .orElse(Opts(false)) + + private val sandboxCfg: Opts[SandboxCfg] = + (whitelist, readOnly, enableSandbox).mapN(SandboxCfg.apply) + + private val maxBufferSize: Opts[Int] = { + val default = 16384 + val help = + s"Size of the buffer for the output of an external process in lines; default: $default" + option[Int](name.maxBufferSize, help).withDefault(default) + } + + private val processCfg: Opts[ProcessCfg] = + (envVar, processTimeout, sandboxCfg, maxBufferSize).mapN(ProcessCfg.apply) + + private val repoConfig: Opts[List[Uri]] = + options[Uri]("repo-config", s"Additional repo config file $multiple").orEmpty + + private val disableDefaultRepoConfig: Opts[Boolean] = + flag("disable-default-repo-config", "Whether to disable the default repo config file").orFalse + + private val repoConfigCfg: Opts[RepoConfigCfg] = + (repoConfig, disableDefaultRepoConfig).mapN(RepoConfigCfg.apply) + + private val scalafixMigrations: Opts[List[Uri]] = + options[Uri]( + "scalafix-migrations", + s"Additional Scalafix migrations configuration file $multiple" + ).orEmpty + + private val disableDefaultScalafixMigrations: Opts[Boolean] = + flag( + "disable-default-scalafix-migrations", + "Whether to disable the default Scalafix migration file; default: false" + ).orFalse + + private val scalafixCfg: Opts[ScalafixCfg] = + (scalafixMigrations, disableDefaultScalafixMigrations).mapN(ScalafixCfg.apply) + + private val artifactMigrations: Opts[List[Uri]] = + options[Uri]( + "artifact-migrations", + s"Additional artifact migration configuration file $multiple" + ).orEmpty + + private val disableDefaultArtifactMigrations: Opts[Boolean] = + flag( + "disable-default-artifact-migrations", + "Whether to disable the default artifact migration file" + ).orFalse + + private val artifactCfg: Opts[ArtifactCfg] = + (artifactMigrations, disableDefaultArtifactMigrations).mapN(ArtifactCfg.apply) + + private val cacheTtl: Opts[FiniteDuration] = { + val default = 2.hours + val help = s"TTL for the caches; default: ${renderFiniteDuration(default)}" + option[FiniteDuration]("cache-ttl", help).withDefault(default) + } + + private val bitbucketServerUseDefaultReviewers: Opts[Boolean] = + flag( + "bitbucket-server-use-default-reviewers", + "Whether to assign the default reviewers to a bitbucket server pull request; default: false" + ).orFalse + + private val bitbucketUseDefaultReviewers: Opts[Boolean] = + flag( + "bitbucket-use-default-reviewers", + "Whether to assign the default reviewers to a bitbucket pull request; default: false" + ).orFalse + + private val bitbucketServerCfg: Opts[BitbucketServerCfg] = + bitbucketServerUseDefaultReviewers.map(BitbucketServerCfg.apply) + + private val bitbucketCfg: Opts[BitbucketCfg] = + bitbucketUseDefaultReviewers.map(BitbucketCfg.apply) + + private val gitlabMergeWhenPipelineSucceeds: Opts[Boolean] = + flag( + "gitlab-merge-when-pipeline-succeeds", + "Whether to merge a gitlab merge request when the pipeline succeeds" + ).orFalse + + private val gitlabRequiredReviewers: Opts[Option[Int]] = + option[Int]( + "gitlab-required-reviewers", + "When set, the number of required reviewers for a merge request will be set to this number (non-negative integer). Is only used in the context of gitlab-merge-when-pipeline-succeeds being enabled, and requires that the configured access token have the appropriate privileges. Also requires a Gitlab Premium subscription." + ).validate("Required reviewers must be non-negative")(_ >= 0).orNone + + private val gitlabRemoveSourceBranch: Opts[Boolean] = + flag( + "gitlab-remove-source-branch", + "Flag indicating if a merge request should remove the source branch when merging." + ).orFalse + + private val gitLabCfg: Opts[GitLabCfg] = + (gitlabMergeWhenPipelineSucceeds, gitlabRequiredReviewers, gitlabRemoveSourceBranch).mapN( + GitLabCfg.apply + ) + + private val githubAppId: Opts[Long] = + option[Long]( + "github-app-id", + "GitHub application id. Repos accessible by this app are added to the repos in repos.md. git-ask-pass is still required." + ) + + private val githubAppKeyFile: Opts[File] = + option[File]( + "github-app-key-file", + "GitHub application key file. Repos accessible by this app are added to the repos in repos.md. git-ask-pass is still required." + ) + + private val gitHubApp: Opts[Option[GitHubApp]] = + (githubAppId, githubAppKeyFile).mapN(GitHubApp.apply).orNone + + private val azureReposOrganization: Opts[Option[String]] = + option[String]( + "azure-repos-organization", + s"The Azure organization (required when --${name.forgeType} is ${AzureRepos.asString})" + ).orNone + + private val azureReposCfg: Opts[AzureReposCfg] = + azureReposOrganization.map(AzureReposCfg.apply) + + private val refreshBackoffPeriod: Opts[FiniteDuration] = { + val default = 0.days + val help = "Period of time a failed build won't be triggered again" + + s"; default: ${renderFiniteDuration(default)}" + option[FiniteDuration]("refresh-backoff-period", help).withDefault(default) + } + + private val urlCheckerTestUrls: Opts[Nel[Uri]] = { + val default = uri"https://github.com" + options[Uri]( + "url-checker-test-url", + s"URL for testing the UrlChecker at start-up $multiple; default: $default" + ).withDefault(Nel.one(default)) + } + + private val defaultMavenRepo: Opts[Resolver] = { + val default = Resolver.mavenCentral + option[String]("default-maven-repo", s"default: ${default.location}") + .map(location => Resolver.MavenRepository("default", location, None, Nil)) + .withDefault(default) + } + + private val regular: Opts[Usage] = ( + workspace, + reposFiles, + gitCfg, + forgeCfg, + ignoreOptsFiles, + processCfg, + repoConfigCfg, + scalafixCfg, + artifactCfg, + cacheTtl, + bitbucketCfg, + bitbucketServerCfg, + gitLabCfg, + azureReposCfg, + gitHubApp, + urlCheckerTestUrls, + defaultMavenRepo, + refreshBackoffPeriod + ).mapN(Config.apply).map(Usage.Regular.apply) + + private val validateRepoConfig: Opts[Usage] = + Opts + .subcommand( + name = "validate-repo-config", + help = "Validate the repo config file and exit; report errors if any" + )(Opts.argument[File]()) + .map(Usage.ValidateRepoConfig.apply) + + val command: Command[Usage] = + Command("scala-steward", "")(regular.orElse(validateRepoConfig)) + + sealed trait ParseResult extends Product with Serializable + object ParseResult { + final case class Success(usage: Usage) extends ParseResult + final case class Help(help: String) extends ParseResult + final case class Error(error: String) extends ParseResult + } + + sealed trait Usage extends Product with Serializable + object Usage { + final case class Regular(config: Config) extends Usage + final case class ValidateRepoConfig(file: File) extends Usage + } + + def parseArgs(args: List[String]): ParseResult = + command.parse(args) match { + case Left(help) if help.errors.isEmpty => ParseResult.Help(help.toString) + case Left(help) => ParseResult.Error(help.toString) + case Right(usage) => ParseResult.Success(usage) + } +} diff --git a/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Config.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Config.scala new file mode 100644 index 0000000000..208ed68562 --- /dev/null +++ b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Config.scala @@ -0,0 +1,167 @@ +/* + * 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.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 + +/** Configuration for scala-steward. + * + * ==forgeCfg.apiHost== + * REST API v3 endpoints prefix + * + * For github.com this is "https://api.github.com", see [[https://developer.github.com/v3/]]. + * + * For GitHub Enterprise this is "http(s)://[hostname]/api/v3", see + * [[https://developer.github.com/enterprise/v3/]]. + * + * ==gitCfg.gitAskPass== + * Program that is invoked by scala-steward and git (via the `GIT_ASKPASS` environment variable) to + * request the password for the user forgeCfg.forgeLogin. + * + * This program could just be a simple shell script that echos the password. + * + * See also [[https://git-scm.com/docs/gitcredentials]]. + */ +final case class Config( + workspace: File, + reposFiles: Nel[Uri], + gitCfg: GitCfg, + forgeCfg: ForgeCfg, + ignoreOptsFiles: Boolean, + processCfg: ProcessCfg, + repoConfigCfg: RepoConfigCfg, + scalafixCfg: ScalafixCfg, + artifactCfg: ArtifactCfg, + cacheTtl: FiniteDuration, + bitbucketCfg: BitbucketCfg, + bitbucketServerCfg: BitbucketServerCfg, + gitLabCfg: GitLabCfg, + azureReposCfg: AzureReposCfg, + githubApp: Option[GitHubApp], + urlCheckerTestUrls: Nel[Uri], + 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 + case ForgeType.Bitbucket => bitbucketCfg + case ForgeType.BitbucketServer => bitbucketServerCfg + case ForgeType.GitHub => GitHubCfg() + case ForgeType.GitLab => gitLabCfg + case ForgeType.Gitea => GiteaCfg() + } +} + +object Config { + final case class GitCfg( + gitAuthor: Author, + gitAskPass: File, + signCommits: Boolean + ) + + final case class ForgeCfg( + tpe: ForgeType, + apiHost: Uri, + login: String, + doNotFork: Boolean, + addLabels: Boolean + ) + + final case class ProcessCfg( + envVars: List[EnvVar], + processTimeout: FiniteDuration, + sandboxCfg: SandboxCfg, + maxBufferSize: Int + ) + + final case class SandboxCfg( + whitelistedDirectories: List[String], + readOnlyDirectories: List[String], + enableSandbox: Boolean + ) + + final case class RepoConfigCfg( + repoConfigs: List[Uri], + disableDefault: Boolean + ) + + final case class ScalafixCfg( + migrations: List[Uri], + disableDefaults: Boolean + ) + + final case class ArtifactCfg( + migrations: List[Uri], + disableDefaults: Boolean + ) + + sealed trait ForgeSpecificCfg extends Product with Serializable + + final case class AzureReposCfg( + organization: Option[String] + ) extends ForgeSpecificCfg + + final case class BitbucketCfg( + useDefaultReviewers: Boolean + ) extends ForgeSpecificCfg + + final case class BitbucketServerCfg( + useDefaultReviewers: Boolean + ) extends ForgeSpecificCfg + + final case class GitHubCfg( + ) extends ForgeSpecificCfg + + final case class GitLabCfg( + mergeWhenPipelineSucceeds: Boolean, + requiredReviewers: Option[Int], + removeSourceBranch: Boolean + ) extends ForgeSpecificCfg + + final case class GiteaCfg( + ) extends ForgeSpecificCfg +} diff --git a/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Context.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Context.scala new file mode 100644 index 0000000000..7ba009548b --- /dev/null +++ b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/Context.scala @@ -0,0 +1,213 @@ +/* + * 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.application + +import cats.effect._ +import cats.effect.implicits._ +import cats.syntax.all._ +import eu.timepit.refined.auto._ +import org.http4s.Uri +import org.http4s.client.Client +import org.http4s.headers.`User-Agent` +import org.scalasteward.core.application.Config.ForgeCfg +import org.scalasteward.core.buildtool.BuildToolDispatcher +import org.scalasteward.core.buildtool.maven.MavenAlg +import org.scalasteward.core.buildtool.mill.MillAlg +import org.scalasteward.core.buildtool.sbt.SbtAlg +import org.scalasteward.core.buildtool.scalacli.ScalaCliAlg +import org.scalasteward.core.client.ClientConfiguration +import org.scalasteward.core.coursier.{CoursierAlg, VersionsCache} +import org.scalasteward.core.data.Repo +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, 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} +import org.scalasteward.core.persistence.{CachingKeyValueStore, JsonKeyValueStore} +import org.scalasteward.core.repocache._ +import org.scalasteward.core.repoconfig.{RepoConfigAlg, RepoConfigLoader} +import org.scalasteward.core.scalafmt.ScalafmtAlg +import org.scalasteward.core.update.artifact.{ArtifactMigrationsFinder, ArtifactMigrationsLoader} +import org.scalasteward.core.update.{FilterAlg, PruningAlg, UpdateAlg} +import org.scalasteward.core.util._ +import org.scalasteward.core.util.uri._ +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.slf4j.Slf4jLogger + +final class Context[F[_]](implicit + val artifactMigrationsLoader: ArtifactMigrationsLoader[F], + val buildToolDispatcher: BuildToolDispatcher[F], + val coursierAlg: CoursierAlg[F], + val dateTimeAlg: DateTimeAlg[F], + val editAlg: EditAlg[F], + val fileAlg: FileAlg[F], + val filterAlg: FilterAlg[F], + val forgeRepoAlg: ForgeRepoAlg[F], + val gitAlg: GitAlg[F], + val hookExecutor: HookExecutor[F], + val httpJsonClient: HttpJsonClient[F], + val logger: Logger[F], + val mavenAlg: MavenAlg[F], + val millAlg: MillAlg[F], + val nurtureAlg: NurtureAlg[F], + val pruningAlg: PruningAlg[F], + val pullRequestRepository: PullRequestRepository[F], + val refreshErrorAlg: RefreshErrorAlg[F], + val repoCacheAlg: RepoCacheAlg[F], + val repoConfigAlg: RepoConfigAlg[F], + val reposFilesLoader: ReposFilesLoader[F], + val sbtAlg: SbtAlg[F], + val scalaCliAlg: ScalaCliAlg[F], + val scalafixMigrationsFinder: ScalafixMigrationsFinder, + val scalafixMigrationsLoader: ScalafixMigrationsLoader[F], + val scalafmtAlg: ScalafmtAlg[F], + val stewardAlg: StewardAlg[F], + val updateAlg: UpdateAlg[F], + val updateInfoUrlFinder: UpdateInfoUrlFinder[F], + val urlChecker: UrlChecker[F], + val workspaceAlg: WorkspaceAlg[F] +) + +object Context { + def step0[F[_]](config: Config)(implicit F: Async[F]): Resource[F, Context[F]] = + for { + logger <- Resource.eval(Slf4jLogger.fromName[F]("org.scalasteward.core")) + _ <- Resource.eval(logger.info(banner)) + _ <- Resource.eval(F.delay(System.setProperty("http.agent", userAgentString))) + userAgent <- Resource.eval(F.fromEither(`User-Agent`.parse(1)(userAgentString))) + middleware = ClientConfiguration + .setUserAgent[F](userAgent) + .andThen(ClientConfiguration.retryAfter[F](maxAttempts = 5)) + defaultClient <- ClientConfiguration.build( + ClientConfiguration.BuilderMiddleware.default, + middleware + ) + urlCheckerClient <- ClientConfiguration.build( + ClientConfiguration.disableFollowRedirect, + middleware + ) + fileAlg = FileAlg.create(logger, F) + processAlg = ProcessAlg.create(config.processCfg)(logger, F) + workspaceAlg = WorkspaceAlg.create(config)(fileAlg, logger, F) + context <- Resource.eval { + step1(config)( + defaultClient, + UrlCheckerClient(urlCheckerClient), + fileAlg, + logger, + processAlg, + workspaceAlg, + F + ) + } + } yield context + + def step1[F[_]](config: Config)(implicit + client: Client[F], + urlCheckerClient: UrlCheckerClient[F], + fileAlg: FileAlg[F], + logger: Logger[F], + processAlg: ProcessAlg[F], + workspaceAlg: WorkspaceAlg[F], + F: Async[F] + ): F[Context[F]] = + for { + forgeUser <- config.forgeUser[F] + 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)) + kvsPrefix = Some(config.forgeCfg.tpe.asString) + pullRequestsStore <- JsonKeyValueStore + .create[F, Repo, Map[Uri, PullRequestRepository.Entry]]("pull_requests", "2", kvsPrefix) + .flatMap(CachingKeyValueStore.wrap(_)) + refreshErrorStore <- JsonKeyValueStore + .create[F, Repo, RefreshErrorAlg.Entry]("refresh_error", "1", kvsPrefix) + repoCacheStore <- JsonKeyValueStore + .create[F, Repo, RepoCache]("repo_cache", "1", kvsPrefix) + versionsStore <- JsonKeyValueStore + .create[F, VersionsCache.Key, VersionsCache.Value]("versions", "2") + } yield { + implicit val artifactMigrationsLoader: ArtifactMigrationsLoader[F] = artifactMigrationsLoader0 + implicit val artifactMigrationsFinder: ArtifactMigrationsFinder = artifactMigrationsFinder0 + implicit val scalafixMigrationsLoader: ScalafixMigrationsLoader[F] = scalafixMigrationsLoader0 + implicit val scalafixMigrationsFinder: ScalafixMigrationsFinder = scalafixMigrationsFinder0 + implicit val urlChecker: UrlChecker[F] = urlChecker0 + 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 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 forgeRepoAlg: ForgeRepoAlg[F] = new ForgeRepoAlg[F](config) + implicit val forgeCfg: ForgeCfg = config.forgeCfg + implicit val updateInfoUrlFinder: UpdateInfoUrlFinder[F] = new UpdateInfoUrlFinder[F] + implicit val pullRequestRepository: PullRequestRepository[F] = + new PullRequestRepository[F](pullRequestsStore) + implicit val scalafixCli: ScalafixCli[F] = new ScalafixCli[F] + implicit val scalafmtAlg: ScalafmtAlg[F] = new ScalafmtAlg[F](config.defaultResolver) + implicit val selfCheckAlg: SelfCheckAlg[F] = new SelfCheckAlg[F](config) + implicit val coursierAlg: CoursierAlg[F] = CoursierAlg.create[F] + implicit val versionsCache: VersionsCache[F] = + new VersionsCache[F](config.cacheTtl, versionsStore) + implicit val updateAlg: UpdateAlg[F] = new UpdateAlg[F] + implicit val mavenAlg: MavenAlg[F] = new MavenAlg[F](config) + implicit val sbtAlg: SbtAlg[F] = new SbtAlg[F](config) + implicit val scalaCliAlg: ScalaCliAlg[F] = new ScalaCliAlg[F] + implicit val millAlg: MillAlg[F] = new MillAlg[F](config.defaultResolver) + implicit val buildToolDispatcher: BuildToolDispatcher[F] = new BuildToolDispatcher[F] + implicit val refreshErrorAlg: RefreshErrorAlg[F] = + new RefreshErrorAlg[F](refreshErrorStore, config.refreshBackoffPeriod) + implicit val repoCacheAlg: RepoCacheAlg[F] = new RepoCacheAlg[F](config) + implicit val scannerAlg: ScannerAlg[F] = new ScannerAlg[F] + implicit val editAlg: EditAlg[F] = new EditAlg[F] + 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 = + """| ____ _ ____ _ _ + | / ___| ___ __ _| | __ _ / ___|| |_ _____ ____ _ _ __ __| | + | \___ \ / __/ _` | |/ _` | \___ \| __/ _ \ \ /\ / / _` | '__/ _` | + | ___) | (_| (_| | | (_| | ___) | || __/\ V V / (_| | | | (_| | + | |____/ \___\__,_|_|\__,_| |____/ \__\___| \_/\_/ \__,_|_| \__,_|""".stripMargin + List(" ", banner, s" v${org.scalasteward.core.BuildInfo.version}", " ") + .mkString(System.lineSeparator()) + } + + private val userAgentString: String = + s"Scala-Steward/${org.scalasteward.core.BuildInfo.version} (${org.scalasteward.core.BuildInfo.gitHubUrl})" +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApp.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/GitHubApp.scala similarity index 100% rename from modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubApp.scala rename to modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/GitHubApp.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAppApiAlg.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/GitHubAppApiAlg.scala similarity index 100% rename from modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAppApiAlg.scala rename to modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/GitHubAppApiAlg.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/GitHubAuthAlg.scala similarity index 100% rename from modules/core/src/main/scala/org/scalasteward/core/forge/github/GitHubAuthAlg.scala rename to modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/GitHubAuthAlg.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/InstallationOut.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/InstallationOut.scala similarity index 100% rename from modules/core/src/main/scala/org/scalasteward/core/forge/github/InstallationOut.scala rename to modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/InstallationOut.scala diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/RepositoriesOut.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/RepositoriesOut.scala similarity index 100% rename from modules/core/src/main/scala/org/scalasteward/core/forge/github/RepositoriesOut.scala rename to modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/RepositoriesOut.scala diff --git a/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/StewardAlg.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/StewardAlg.scala new file mode 100644 index 0000000000..ac7c91c30d --- /dev/null +++ b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/StewardAlg.scala @@ -0,0 +1,105 @@ +/* + * 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.application + +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.git.GitAlg +import org.scalasteward.core.io.{FileAlg, WorkspaceAlg} +import org.scalasteward.core.nurture.NurtureAlg +import org.scalasteward.core.repocache.RepoCacheAlg +import org.scalasteward.core.update.PruningAlg +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], + logger: Logger[F], + nurtureAlg: NurtureAlg[F], + pruningAlg: PruningAlg[F], + repoCacheAlg: RepoCacheAlg[F], + reposFilesLoader: ReposFilesLoader[F], + selfCheckAlg: SelfCheckAlg[F], + 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) { + logger.attemptError.label(util.string.lineLeftRight(label), Some(label)) { + F.guarantee( + repoCacheAlg.checkCache(repo).flatMap { case (data, fork) => + pruningAlg.needsAttention(data).flatMap { + _.traverse_(states => nurtureAlg.nurture(data, fork, states.map(_.update))) + } + }, + gitAlg.removeClone(repo) + ) + } + } + } + + def runF: F[ExitCode] = + logger.infoTotalTime("run") { + for { + _ <- selfCheckAlg.checkAll + _ <- workspaceAlg.removeAnyRunSpecificFiles + exitCode <- + (config.githubApp.map(getGitHubAppRepos).getOrElse(Stream.empty) ++ + reposFilesLoader.loadAll(config.reposFiles)) + .evalMap(repo => steward(repo).map(_.bimap(repo -> _, _ => repo))) + .compile + .toList + .flatMap { results => + val runResults = RunResults(results) + for { + summaryFile <- workspaceAlg.runSummaryFile + _ <- fileAlg.writeFile(summaryFile, runResults.markdownSummary) + } yield runResults.exitCode + } + } yield exitCode + } +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/forge/github/TokenOut.scala b/modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/TokenOut.scala similarity index 100% rename from modules/core/src/main/scala/org/scalasteward/core/forge/github/TokenOut.scala rename to modules/gh-app-facade/src/main/scala/org/scalasteward/ghappfacade/TokenOut.scala diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAppApiAlgTest.scala b/modules/gh-app-facade/src/test/scala/org/scalasteward/ghappfacade/GitHubAppApiAlgTest.scala similarity index 100% rename from modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAppApiAlgTest.scala rename to modules/gh-app-facade/src/test/scala/org/scalasteward/ghappfacade/GitHubAppApiAlgTest.scala diff --git a/modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAuthAlgTest.scala b/modules/gh-app-facade/src/test/scala/org/scalasteward/ghappfacade/GitHubAuthAlgTest.scala similarity index 100% rename from modules/core/src/test/scala/org/scalasteward/core/forge/github/GitHubAuthAlgTest.scala rename to modules/gh-app-facade/src/test/scala/org/scalasteward/ghappfacade/GitHubAuthAlgTest.scala