diff --git a/project/CopyrightHeader.scala b/project/CopyrightHeader.scala index 6338c52287..6d98d1b009 100644 --- a/project/CopyrightHeader.scala +++ b/project/CopyrightHeader.scala @@ -4,7 +4,7 @@ package akka -import akka.ValidatePullRequest.{ValidatePR, additionalTasks} +import akka.AkkaValidatePullRequest.additionalTasks import de.heikoseeberger.sbtheader.HeaderPlugin.autoImport._ import de.heikoseeberger.sbtheader.{CommentCreator, HeaderPlugin} import com.typesafe.sbt.MultiJvmPlugin.MultiJvmKeys._ @@ -108,7 +108,7 @@ object CopyrightHeader extends CopyrightHeader object CopyrightHeaderInPr extends CopyrightHeader { override val additional = Def.settings( - additionalTasks in ValidatePR += headerCheck in Compile, - additionalTasks in ValidatePR += headerCheck in Test + additionalTasks += headerCheck in Compile, + additionalTasks += headerCheck in Test ) } diff --git a/project/GitHub.scala b/project/GitHub.scala index 2d7be2e59b..987c801f11 100644 --- a/project/GitHub.scala +++ b/project/GitHub.scala @@ -6,10 +6,14 @@ package akka object GitHub { - def envTokenOrThrow: String = - sys.env.getOrElse( - "PR_VALIDATOR_GH_TOKEN", - throw new Exception("No PR_VALIDATOR_GH_TOKEN env var provided, unable to reach github!")) + def envTokenOrThrow: Option[String] = + sys.env.get("PR_VALIDATOR_GH_TOKEN") orElse { + if (sys.env.contains("ghprbPullId")) { + throw new Exception("No PR_VALIDATOR_GH_TOKEN env var provided during GitHub Pull Request Builder build, unable to reach GitHub!") + } else { + None + } + } def url(v: String): String = { val branch = if (v.endsWith("SNAPSHOT")) "master" else "v" + v diff --git a/project/ValidatePullRequest.scala b/project/ValidatePullRequest.scala index 762565510e..6071929a1c 100644 --- a/project/ValidatePullRequest.scala +++ b/project/ValidatePullRequest.scala @@ -4,189 +4,43 @@ package akka +import com.hpe.sbt.ValidatePullRequest +import com.hpe.sbt.ValidatePullRequest.PathGlobFilter import com.lightbend.paradox.sbt.ParadoxPlugin import com.lightbend.paradox.sbt.ParadoxPlugin.autoImport.paradox import com.typesafe.tools.mima.plugin.MimaKeys.mimaReportBinaryIssues import com.typesafe.tools.mima.plugin.MimaPlugin -import net.virtualvoid.sbt.graph.backend.SbtUpdateReport -import net.virtualvoid.sbt.graph.DependencyGraphKeys._ -import net.virtualvoid.sbt.graph.ModuleGraph -import org.kohsuke.github._ import sbtunidoc.BaseUnidocPlugin.autoImport.unidoc import sbt.Keys._ import sbt._ -import scala.collection.immutable -import scala.sys.process._ -import scala.util.matching.Regex +object AkkaValidatePullRequest extends AutoPlugin { -object ValidatePullRequest extends AutoPlugin { + import ValidatePullRequest.autoImport._ override def trigger = allRequirements - override def requires = plugins.JvmPlugin - - sealed trait BuildMode { - def task: Option[TaskKey[_]] - def log(projectName: String, l: Logger): Unit - } - case object BuildSkip extends BuildMode { - override def task = None - def log(projectName: String, l: Logger) = - l.info(s"Skipping validation of [$projectName], as PR does NOT affect this project...") - } - case object BuildQuick extends BuildMode { - override def task = Some(test in ValidatePR) - def log(projectName: String, l: Logger) = - l.info(s"Building [$projectName] in quick mode, as it's dependencies were affected by PR.") - } - case object BuildProjectChangedQuick extends BuildMode { - override def task = Some(test in ValidatePR) - def log(projectName: String, l: Logger) = - l.info(s"Building [$projectName] as the root `project/` directory was affected by this PR.") - } - final case class BuildCommentForcedAll(phrase: String, c: GHIssueComment) extends BuildMode { - override def task = Some(test in Test) - def log(projectName: String, l: Logger) = - l.info(s"GitHub PR comment [ ${c.getUrl} ] contains [$phrase], forcing BUILD ALL mode!") - } + override def requires = ValidatePullRequest val ValidatePR = config("pr-validation") extend Test override lazy val projectConfigurations = Seq(ValidatePR) - /* - Assumptions: - Env variables set "by Jenkins" are assumed to come from this plugin: - https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin - */ + val additionalTasks = settingKey[Seq[TaskKey[_]]]("Additional tasks for pull request validation") - // settings - val PullIdEnvVarName = "ghprbPullId" // Set by "GitHub pull request builder plugin" - - val TargetBranchEnvVarName = "PR_TARGET_BRANCH" - val TargetBranchJenkinsEnvVarName = "ghprbTargetBranch" - - val SourceBranchEnvVarName = "PR_SOURCE_BRANCH" - val SourcePullIdJenkinsEnvVarName = "ghprbPullId" // used to obtain branch name in form of "pullreq/17397" - val sourceBranch = settingKey[String]("Branch containing the changes of this PR") - - val targetBranch = settingKey[String]("Target branch of this PR, defaults to `master`") - - // asking github comments if this PR should be PLS BUILD ALL - val githubEnforcedBuildAll = taskKey[Option[BuildMode]]("Checks via GitHub API if comments included the PLS BUILD ALL keyword") - val buildAllKeyword = taskKey[Regex]("Magic phrase to be used to trigger building of the entire project instead of analysing dependencies") - - // determining touched dirs and projects - val changedDirectories = taskKey[immutable.Set[String]]("List of touched modules in this PR branch") - val projectBuildMode = taskKey[BuildMode]("Determines what will run when this project is affected by the PR and should be rebuilt") - - // running validation - val validatePullRequest = taskKey[Unit]("Validate pull request") - val additionalTasks = taskKey[Seq[TaskKey[_]]]("Additional tasks for pull request validation") - - def changedDirectoryIsDependency(changedDirs: Set[String], - name: String, - graphsToTest: Seq[(Configuration, ModuleGraph)])(log: Logger): Boolean = { - graphsToTest exists { case (ivyScope, deps) => - log.debug(s"Analysing [$ivyScope] scoped dependencies...") - - deps.nodes.foreach { m ⇒ log.debug(" -> " + m.id) } - - // if this project depends on a modified module, we must test it - deps.nodes.exists { m => - // match just by name, we'd rather include too much than too little - val dependsOnModule = changedDirs.find(m.id.name contains _) - val depends = dependsOnModule.isDefined - if (depends) log.info(s"Project [$name] must be verified, because depends on [${dependsOnModule.get}]") - depends + override lazy val globalSettings = Seq( + credentials ++= { + // todo this should probably be supplied properly + GitHub.envTokenOrThrow.map { token => + Credentials("GitHub API", "api.github.com", "", token) } - } - } - - def localTargetBranch: Option[String] = sys.env.get("PR_TARGET_BRANCH") - def jenkinsTargetBranch: Option[String] = sys.env.get("ghprbTargetBranch") - def runningOnJenkins: Boolean = jenkinsTargetBranch.isDefined - def runningLocally: Boolean = !runningOnJenkins + }, + additionalTasks := Seq.empty + ) override lazy val buildSettings = Seq( - sourceBranch in Global in ValidatePR := { - sys.env.get(SourceBranchEnvVarName) orElse - sys.env.get(SourcePullIdJenkinsEnvVarName).map("pullreq/" + _) getOrElse // Set by "GitHub pull request builder plugin" - "HEAD" - }, - - targetBranch in Global in ValidatePR := { - (localTargetBranch, jenkinsTargetBranch) match { - case (Some(local), _) => local // local override - case (None, Some(branch)) => s"origin/$branch" // usually would be "master" or "release-2.3" etc - case (None, None) => "origin/master" // defaulting to diffing with "master" - } - }, - - buildAllKeyword in Global in ValidatePR := """PLS BUILD ALL""".r, - - githubEnforcedBuildAll in Global in ValidatePR := { - val log = streams.value.log - val buildAllMagicPhrase = (buildAllKeyword in ValidatePR).value - - sys.env.get(PullIdEnvVarName).map(_.toInt) flatMap { prId => - log.info("Checking GitHub comments for PR validation options...") - - try { - import scala.collection.JavaConverters._ - val gh = GitHubBuilder.fromEnvironment().withOAuthToken(GitHub.envTokenOrThrow).build() - val comments = gh.getRepository("akka/akka").getIssue(prId).getComments.asScala - - def triggersBuildAll(c: GHIssueComment): Boolean = buildAllMagicPhrase.findFirstIn(c.getBody).isDefined - comments collectFirst { case c if triggersBuildAll(c) => - BuildCommentForcedAll(buildAllMagicPhrase.toString(), c) - } - } catch { - case ex: Exception => - log.warn("Unable to reach GitHub! Exception was: " + ex.getMessage) - None - } - } - }, - - changedDirectories in Global in ValidatePR := { - val log = streams.value.log - - val prId = (sourceBranch in ValidatePR).value - - val target = (targetBranch in ValidatePR).value - - // TODO could use jgit - log.info(s"Diffing [$prId] to determine changed modules in PR...") - val diffOutput = s"git diff $target --name-only".!!.split("\n") - val diffedModuleNames = - diffOutput - .map(l => l.trim) - .filter(l => - l.startsWith("akka-") || - (l.startsWith("project") && l != "project/MiMa.scala") - ) - .map(l ⇒ l.takeWhile(_ != '/')) - .toSet - - val dirtyModuleNames: Set[String] = - if (runningOnJenkins) Set.empty - else { - val statusOutput = s"git status --short".!!.split("\n") - val dirtyDirectories = statusOutput - .map(l ⇒ l.trim.dropWhile(_ != ' ').drop(1)) - .map(_.takeWhile(_ != '/')) - .filter(dir => dir.startsWith("akka-") || dir == "project") - .toSet - log.info("Detected uncommitted changes in directories (including in dependency analysis): " + dirtyDirectories.mkString("[", ",", "]")) - dirtyDirectories - } - - - val allModuleNames = dirtyModuleNames ++ diffedModuleNames - log.info("Detected changes in directories: " + allModuleNames.mkString("[", ", ", "]")) - allModuleNames - } + validatePullRequest / includeFilter := PathGlobFilter("akka-*/**"), + validatePullRequestBuildAll / excludeFilter := PathGlobFilter("project/MiMa.scala"), + prValidatorGithubRepository := Some("akka/akka") ) override lazy val projectSettings = inConfig(ValidatePR)(Defaults.testTasks) ++ Seq( @@ -199,63 +53,8 @@ object ValidatePullRequest extends AutoPlugin { testGrouping in ValidatePR := (testGrouping in Test).value, javaOptions in ValidatePR := (javaOptions in Test).value, - projectBuildMode in ValidatePR := { - val log = streams.value.log - log.debug(s"Analysing project (for inclusion in PR validation): [${name.value}]") - val changedDirs = (changedDirectories in ValidatePR).value - val githubCommandEnforcedBuildAll = (githubEnforcedBuildAll in ValidatePR).value - - val thisProjectId = CrossVersion(scalaVersion.value, scalaBinaryVersion.value)(projectID.value) - - def graphFor(updateReport: UpdateReport, config: Configuration): (Configuration, ModuleGraph) = - config -> SbtUpdateReport.fromConfigurationReport(updateReport.configuration(config).get, thisProjectId) - - def isDependency: Boolean = - changedDirectoryIsDependency( - changedDirs, - name.value, - Seq( - graphFor((update in Compile).value, Compile), - graphFor((update in Test).value, Test), - graphFor((update in Runtime).value, Runtime), - graphFor((update in Provided).value, Provided), - graphFor((update in Optional).value, Optional)))(log) - - if (githubCommandEnforcedBuildAll.isDefined) - githubCommandEnforcedBuildAll.get - else if (changedDirs contains "project") - BuildProjectChangedQuick - else if (isDependency) - BuildQuick - else - BuildSkip - }, - - additionalTasks in ValidatePR := Seq.empty, - - validatePullRequest := Def.taskDyn { - val log = streams.value.log - val buildMode = (projectBuildMode in ValidatePR).value - - buildMode.log(name.value, log) - - val validationTasks = buildMode.task.toSeq ++ (buildMode match { - case BuildSkip => Seq.empty // do not run the additional task if project is skipped during pr validation - case _ => (additionalTasks in ValidatePR).value - }) - - // Create a task for every validation task key and - // then zip all of the tasks together discarding outputs. - // Task failures are propagated as normal. - val zero: Def.Initialize[Seq[Task[Any]]] = Def.setting { Seq(task(()))} - validationTasks.map(taskKey => Def.task { taskKey.value } ).foldLeft(zero) { (acc, current) => - acc.zipWith(current) { case (taskSeq, task) => - taskSeq :+ task.asInstanceOf[Task[Any]] - } - } apply { tasks: Seq[Task[Any]] => - tasks.join map { seq => () /* Ignore the sequence of unit returned */ } - } - }.value + prValidatorTasks := Seq(test in ValidatePR) ++ additionalTasks.value, + prValidatorEnforcedBuildAllTasks := Seq(test in Test) ++ additionalTasks.value ) } @@ -269,12 +68,13 @@ object ValidatePullRequest extends AutoPlugin { * autoplugin would trigger only on projects which have both of these plugins enabled. */ object MultiNodeWithPrValidation extends AutoPlugin { - import ValidatePullRequest._ + import AkkaValidatePullRequest._ import com.typesafe.sbt.MultiJvmPlugin.MultiJvmKeys.MultiJvm + override def trigger = allRequirements - override def requires = ValidatePullRequest && MultiNode + override def requires = AkkaValidatePullRequest && MultiNode override lazy val projectSettings = - if (MultiNode.multiNodeTestInTest) Seq(additionalTasks in ValidatePR += MultiNode.multiTest) + if (MultiNode.multiNodeTestInTest) Seq(additionalTasks += MultiNode.multiTest) else Seq.empty } @@ -283,12 +83,12 @@ object MultiNodeWithPrValidation extends AutoPlugin { * when a project has MimaPlugin autoplugin enabled. */ object MimaWithPrValidation extends AutoPlugin { - import ValidatePullRequest._ + import AkkaValidatePullRequest._ override def trigger = allRequirements - override def requires = ValidatePullRequest && MimaPlugin + override def requires = AkkaValidatePullRequest && MimaPlugin override lazy val projectSettings = Seq( - additionalTasks in ValidatePR += mimaReportBinaryIssues + additionalTasks += mimaReportBinaryIssues ) } @@ -297,20 +97,20 @@ object MimaWithPrValidation extends AutoPlugin { * when a project has ParadoxPlugin autoplugin enabled. */ object ParadoxWithPrValidation extends AutoPlugin { - import ValidatePullRequest._ + import AkkaValidatePullRequest._ override def trigger = allRequirements - override def requires = ValidatePullRequest && ParadoxPlugin + override def requires = AkkaValidatePullRequest && ParadoxPlugin override lazy val projectSettings = Seq( - additionalTasks in ValidatePR += paradox in Compile + additionalTasks += paradox in Compile ) } object UnidocWithPrValidation extends AutoPlugin { - import ValidatePullRequest._ + import AkkaValidatePullRequest._ override def trigger = noTrigger override lazy val projectSettings = Seq( - additionalTasks in ValidatePR += unidoc in Compile + additionalTasks += unidoc in Compile ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 3cddef4fa7..2ad675f5be 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,3 @@ -// need this to resolve http://jcenter.bintray.com/org/jenkins-ci/jenkins/1.26/ -// which is used by plugin "org.kohsuke" % "github-api" % "1.68" -resolvers += "Bintray Jcenter" at "https://jcenter.bintray.com/" -libraryDependencies += "org.kohsuke" % "github-api" % "1.95" - // these comment markers are for including code into the docs //#sbt-multi-jvm addSbtPlugin("com.typesafe.sbt" % "sbt-multi-jvm" % "0.4.0") @@ -23,7 +18,8 @@ addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.4") addSbtPlugin("com.lightbend.akka" % "sbt-paradox-akka" % "0.14") addSbtPlugin("com.lightbend" % "sbt-whitesource" % "0.1.13") addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") -addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.1") // for advanced PR validation features addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.0.0") // for maintenance of copyright file header +addSbtPlugin("com.hpe.sbt" % "sbt-pull-request-validator" % "1.0.0") + // used for @unidoc directive libraryDependencies += "io.github.classgraph" % "classgraph" % "4.4.12"