pekko/project/ValidatePullRequest.scala

212 lines
8.7 KiB
Scala

/**
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
*/
package akka
import com.typesafe.tools.mima.plugin.MimaKeys.reportBinaryIssues
import net.virtualvoid.sbt.graph.IvyGraphMLDependencies
import net.virtualvoid.sbt.graph.IvyGraphMLDependencies.ModuleId
import org.kohsuke.github._
import sbt.Keys._
import sbt._
import scala.collection.immutable
import scala.util.matching.Regex
object ValidatePullRequest extends AutoPlugin {
override def trigger = allRequirements
override def requires = plugins.JvmPlugin
sealed trait BuildMode {
val Zero = Def.task { () } // when you stare into the void, the void stares back at you
def task: Def.Initialize[Task[Unit]]
def log(projectName: String, l: Logger): Unit
}
case object BuildSkip extends BuildMode {
override def task = Zero
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 = Zero.dependsOn(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 = Zero.dependsOn(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 = Zero.dependsOn(test in Test)
def log(projectName: String, l: Logger) =
l.info(s"GitHub PR comment [ ${c.getUrl} ] contains [$phrase], forcing BUILD ALL mode!")
}
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
*/
// settings
val PullIdEnvVarName = "ghprbPullId" // Set by "GitHub pull request builder plugin"
val TargetBranchEnvVarName = "PR_TARGET_BRANCH"
val TargetBranchJenkinsEnvVarName = "ghprbTargetBranch"
val targetBranch = settingKey[String]("Branch with which the PR changes should be diffed against")
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")
// 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]("True if this project is affected by the PR and should be rebuilt")
// running validation
val validatePullRequest = taskKey[Unit]("Additional tasks for pull request validation")
def changedDirectoryIsDependency(changedDirs: Set[String],
target: File,
scalaBinaryVersion: String, version: String,
organization: String, name: String)(log: Logger): Boolean = {
changedDirs.exists { modifiedProject
Set(Compile, Test, Runtime, Provided, Optional) exists { ivyScope: Configuration
log.debug(s"Analysing [$ivyScope] scoped dependencies...")
def moduleId(artifactName: String) = ModuleId("com.typesafe.akka", artifactName + "_" + scalaBinaryVersion, version)
val modifiedModuleIds = Set(moduleId(modifiedProject), moduleId(modifiedProject + "-experimental"))
def resolutionFilename(includeScalaVersion: Boolean) =
s"%s-%s-%s.xml".format(
organization,
name + (if (includeScalaVersion) "_" + scalaBinaryVersion else ""),
ivyScope.toString())
def resolutionFile(includeScalaVersion: Boolean) =
target / "resolution-cache" / "reports" / resolutionFilename(includeScalaVersion)
val ivyReportFile = {
val f1 = resolutionFile(includeScalaVersion = true)
val f2 = resolutionFile(includeScalaVersion = false)
if (f1.exists()) f1 else f2
}
val deps = IvyGraphMLDependencies.graph(ivyReportFile.getAbsolutePath)
deps.nodes.foreach { m log.debug(" -> " + m.id) }
// if this project depends on a modified module, we must test it
deps.nodes.exists { m =>
val depends = modifiedModuleIds exists {
_.name == m.id.name
} // match just by name, we'd rather include too much than too little
if (depends) log.info(s"Project [$name] must be verified, because depends on [${modifiedModuleIds.find(_ == m.id).get}]")
depends
}
}
}
}
override lazy val projectSettings = inConfig(ValidatePR)(Defaults.testTasks) ++ Seq(
testOptions in ValidatePR += Tests.Argument(TestFrameworks.ScalaTest, "-l", "performance"),
testOptions in ValidatePR += Tests.Argument(TestFrameworks.ScalaTest, "-l", "long-running"),
testOptions in ValidatePR += Tests.Argument(TestFrameworks.ScalaTest, "-l", "timing"),
targetBranch in ValidatePR := {
sys.env.get(TargetBranchEnvVarName) orElse
sys.env.get(TargetBranchJenkinsEnvVarName) getOrElse // Set by "GitHub pull request builder plugin"
"master"
},
sourceBranch in ValidatePR := {
sys.env.get(SourceBranchEnvVarName) orElse
sys.env.get(SourcePullIdJenkinsEnvVarName).map("pullreq/" + _) getOrElse // Set by "GitHub pull request builder plugin"
"HEAD"
},
changedDirectories in ValidatePR := {
val log = streams.value.log
val targetId = (targetBranch in ValidatePR).value
val prId = (sourceBranch in ValidatePR).value
// TODO could use jgit
log.info(s"Comparing [$targetId] with [$prId] to determine changed modules in PR...")
val gitOutput = "git diff %s..%s --name-only".format(targetId, prId).!!.split("\n")
val moduleNames =
gitOutput
.map(l l.trim.takeWhile(_ != '/'))
.filter(dir => dir.startsWith("akka-") || dir == "project")
.toSet
log.info("Detected changes in directories: " + moduleNames.mkString("[", ", ", "]"))
moduleNames
},
buildAllKeyword in ValidatePR := """PLS BUILD ALL""".r,
githubEnforcedBuildAll in ValidatePR := {
sys.env.get(PullIdEnvVarName).map(_.toInt) flatMap { prId =>
val log = streams.value.log
val buildAllMagicPhrase = (buildAllKeyword in ValidatePR).value
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
}
}
},
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
if (githubCommandEnforcedBuildAll.isDefined)
githubCommandEnforcedBuildAll.get
else if (changedDirs contains "project")
BuildProjectChangedQuick
else if (changedDirectoryIsDependency(changedDirs, target.value, scalaBinaryVersion.value, version.value,
organization.value, name.value)(log))
BuildQuick
else
BuildSkip
},
validatePullRequest := Def.taskDyn {
val log = streams.value.log
val buildMode = (projectBuildMode in ValidatePR).value
buildMode.log(name.value, log)
buildMode.task
}.value,
// add reportBinaryIssues to validatePullRequest on minor version maintenance branch
validatePullRequest <<= validatePullRequest.dependsOn(reportBinaryIssues)
)
}