-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathValidatePullRequest.scala
More file actions
351 lines (300 loc) · 13.5 KB
/
ValidatePullRequest.scala
File metadata and controls
351 lines (300 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
/*
* Sbt Pull Request Validator
* Copyright Lightbend and Hewlett Packard Enterprise
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package com.github.sbt.pullrequestvalidator
import java.util.regex.Pattern
import org.kohsuke.github._
import sbt.Keys._
import sbt._
import scala.collection.immutable
import scala.sys.process._
import scala.util.matching.Regex
object ValidatePullRequest extends AutoPlugin {
final case class Changes(allBuildMatched: Boolean, projectRefs: immutable.Set[ProjectRef])
/**
* Like GlobFilter, but matches on the full path, and ** matches /, while * doesn't.
*/
object PathGlobFilter {
def apply(glob: String): FileFilter = {
val pattern = Pattern.compile(
// First, split by **
glob
.split("\\*\\*", -1)
.map {
case "" => ""
case part =>
// Now, handle *
part
.split("\\*", -1)
.map {
case "" => ""
case literal => Pattern.quote(literal)
}
.mkString("[^/]*")
}
.mkString(".*")
)
(pathname: File) => pattern.matcher(pathname.getPath).matches()
}
}
object autoImport {
// git configuration
lazy val prValidatorSourceBranch = settingKey[String]("Branch containing the changes of this PR")
lazy val prValidatorTargetBranch = settingKey[String]("Target branch of this PR, defaults to `main`")
// Configuration for what tasks to run in which scenarios
lazy val prValidatorTasks =
settingKey[Seq[TaskKey[?]]]("The tasks that should be run on a project when that project has changed")
lazy val prValidatorBuildAllTasks = settingKey[Seq[TaskKey[?]]](
"The tasks that should be run when one of the files that have changed matches the build all filters"
)
lazy val prValidatorEnforcedBuildAllTasks =
settingKey[Seq[TaskKey[?]]]("The tasks that should be run when build all is explicitly requested")
// enforced build all configuration
lazy val prValidatorBuildAllKeyword = settingKey[Regex](
"Magic phrase to be used to trigger building of the entire project instead of analysing dependencies"
)
lazy val prValidatorGithubEndpoint = settingKey[URI](
"URI for the GitHub API, defaults to https://api.github.com. Override for GitHub enterprise, note that the URI for GitHub enterprise should end in /api/v3."
)
lazy val prValidatorGithubRepository = settingKey[Option[String]](
"Optional name of the repository where Pull Requests are created. Necessary for explicit 'enforced build all'"
)
@transient
lazy val prValidatorGithubEnforcedBuildAll =
taskKey[Boolean]("Checks via GitHub API if comments included the PLS BUILD ALL keyword.")
@transient
lazy val prValidatorTravisNonPrEnforcedBuildAll =
taskKey[Boolean]("Checks whether this is a non PR build on Travis.")
@transient
lazy val prValidatorEnforcedBuildAll = taskKey[Boolean]("Whether an enforced build all is done.")
// determining touched dirs and projects
@transient
lazy val prValidatorChangedProjects = taskKey[Changes]("List of touched projects in this PR branch")
@transient
lazy val prValidatorProjectBuildTasks =
taskKey[Seq[TaskKey[?]]]("The tasks that should be run, according to what has changed")
// running validation
@transient
lazy val validatePullRequest = taskKey[Unit]("Validate pull request")
@transient
lazy val validatePullRequestBuildAll = taskKey[Unit]("Validate pull request, building all projects")
}
import autoImport._
override lazy val trigger = allRequirements
override lazy val requires = plugins.JvmPlugin
/*
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
private val JenkinsPullIdEnvVarName = "ghprbPullId" // Set by "GitHub pull request builder plugin"
private val TravisPullIdEnvVarName = "TRAVIS_PULL_REQUEST"
private val TravisRepoName = "TRAVIS_REPO_SLUG"
private val TargetBranchEnvVarName = "PR_TARGET_BRANCH"
private val TargetBranchTravisEnvVarName = "TRAVIS_BRANCH"
private val TargetBranchJenkinsEnvVarName = "ghprbTargetBranch"
private val SourceBranchEnvVarName = "PR_SOURCE_BRANCH"
private lazy val localTargetBranch = sys.env.get(TargetBranchEnvVarName)
private lazy val jenkinsTargetBranch = sys.env.get(TargetBranchJenkinsEnvVarName).map("origin/" + _)
private lazy val travisTargetBranch = sys.env.get(TargetBranchTravisEnvVarName).map("origin/" + _)
private lazy val localSourceBranch = sys.env.get(SourceBranchEnvVarName)
private lazy val jenkinsSourceBranch = sys.env.get(JenkinsPullIdEnvVarName).map("pullreq/" + _)
private lazy val jenkinsPullRequestId = sys.env.get(JenkinsPullIdEnvVarName).map(_.toInt)
private lazy val travisPullRequestId = sys.env.get(TravisPullIdEnvVarName).filterNot(_ == "false").map(_.toInt)
private lazy val pullRequestId = jenkinsPullRequestId orElse travisPullRequestId
override lazy val globalSettings = Seq(
validatePullRequest := (),
validatePullRequestBuildAll := (),
prValidatorSourceBranch := {
localSourceBranch orElse jenkinsSourceBranch getOrElse "HEAD"
},
prValidatorTargetBranch := {
localTargetBranch orElse jenkinsTargetBranch orElse travisTargetBranch getOrElse "origin/main"
},
prValidatorTasks := Nil,
prValidatorBuildAllTasks := Nil,
prValidatorEnforcedBuildAllTasks := Nil,
prValidatorGithubEndpoint := uri("https://api.github.com"),
prValidatorGithubRepository := sys.env.get(TravisRepoName),
prValidatorBuildAllKeyword := """PLS BUILD ALL""".r,
prValidatorGithubEnforcedBuildAll := false,
prValidatorTravisNonPrEnforcedBuildAll := {
sys.env.get(TravisPullIdEnvVarName).contains("false")
},
prValidatorEnforcedBuildAll := false,
prValidatorChangedProjects := Changes(allBuildMatched = false, Set.empty),
prValidatorProjectBuildTasks := Nil
)
override lazy val buildSettings = Seq(
validatePullRequest / includeFilter := "*",
validatePullRequest / excludeFilter := "README.*",
validatePullRequestBuildAll / includeFilter := PathGlobFilter("project/**") || PathGlobFilter("*.sbt"),
validatePullRequestBuildAll / excludeFilter := NothingFilter,
prValidatorGithubEnforcedBuildAll := {
val log = streams.value.log
val buildAllMagicPhrase = prValidatorBuildAllKeyword.value
val githubRepository = prValidatorGithubRepository.value
val githubEndpoint = prValidatorGithubEndpoint.value
val githubCredentials = ValidatePullRequestCompat.credentialsForHost(credentials.value, githubEndpoint.getHost)
pullRequestId.exists { prId =>
log.info("Checking GitHub comments for PR validation options...")
try {
import scala.collection.JavaConverters._
val gh = {
val builder = GitHubBuilder.fromEnvironment().withEndpoint(githubEndpoint.toString)
githubCredentials match {
case Some(creds) => builder.withOAuthToken(creds.passwd).build()
case _ => builder.build()
}
}
val commentsOpt =
githubRepository.map(repository => gh.getRepository(repository).getIssue(prId).getComments.asScala)
def triggersBuildAll(c: GHIssueComment): Boolean = buildAllMagicPhrase.findFirstIn(c.getBody).isDefined
val shouldBuildAll = commentsOpt.exists(comments => comments.exists(c => triggersBuildAll(c)))
if (shouldBuildAll)
log.info(s"Building all projects because of 'build all' Github comment")
shouldBuildAll
} catch {
case ex: Exception =>
log.warn("Unable to reach GitHub! Exception was: " + ex.getMessage)
false
}
}
},
prValidatorEnforcedBuildAll := prValidatorTravisNonPrEnforcedBuildAll.value || prValidatorGithubEnforcedBuildAll.value,
prValidatorChangedProjects := {
val log = streams.value.log
val prId = prValidatorSourceBranch.value
val target = prValidatorTargetBranch.value
val state = Keys.state.value
val extracted = Project.extract(state)
val rootBaseDir = (ThisBuild / baseDirectory).value
// All projects in reverse order of path, this ensures when we search through them, we get the most specific
// first
val projects = extracted.structure.allProjects
.flatMap(project =>
project.base
.relativeTo(rootBaseDir)
.map { relativePath =>
val projRef = ProjectRef(extracted.structure.root, project.id)
if (relativePath.getPath == "")
"" -> projRef
else
relativePath.getPath + "/" -> projRef
}
)
.sortBy(_._1)
.reverse
val filter = (validatePullRequest / includeFilter).value -- (validatePullRequest / excludeFilter).value
val allBuildFilter =
(validatePullRequestBuildAll / includeFilter).value -- (validatePullRequestBuildAll / excludeFilter).value
log.info(s"Diffing [$prId] to determine changed modules in PR...")
val diffOutput = s"git diff $target --name-only".!!.split("\n")
val diffedFiles = diffOutput
.map(l => file(l.trim))
.toSeq
val statusOutput = s"git status --short".!!.split("\n")
val dirtyFiles = statusOutput
.filterNot(_.isEmpty)
// Need to drop the leading status characters, followed by a space
.map(l => file(l.trim.dropWhile(_ != ' ').drop(1)))
if (dirtyFiles.nonEmpty)
log.info("Detected uncommitted changes: " + dirtyFiles.take(5).mkString("[", ",", "]"))
val allChangedFiles = diffedFiles ++ dirtyFiles
val allBuildMatched = allChangedFiles.exists(allBuildFilter.accept)
val changedProjects = allChangedFiles
.filter(filter.accept)
.flatMap { file =>
projects.collectFirst {
case (path, project) if file.getPath.startsWith(path) => project
}
}
if (allBuildMatched)
log.info("Building all modules because the all build filter was matched")
Changes(allBuildMatched, changedProjects.toSet)
}
)
override lazy val projectSettings = Seq(
prValidatorProjectBuildTasks := {
val log = streams.value.log
val proj = name.value
log.debug(s"Analysing project (for inclusion in PR validation): [$proj]")
val changedProjects = prValidatorChangedProjects.value
val changedProjectTasks = prValidatorBuildAllTasks.value
val enforcedBuildAll = prValidatorEnforcedBuildAll.value
val enforcedBuildAllTasks = prValidatorEnforcedBuildAllTasks.value
val projectDependencies = Keys.buildDependencies.value.classpathRefs(thisProjectRef.value)
def isDependency: Boolean =
projectDependencies.exists(changedProjects.projectRefs) || changedProjects.projectRefs(thisProjectRef.value)
val dependencyChangedTasks = prValidatorTasks.value
if (enforcedBuildAll) {
log.debug(s"Building [$proj] because this is an enforced all build project")
enforcedBuildAllTasks
} else if (changedProjects.allBuildMatched) {
log.debug(s"Building [$proj] because the all build filter was matched")
changedProjectTasks
} else if (isDependency) {
log.info(s"Building [$proj] because it or a dependency has changed")
dependencyChangedTasks
} else {
log.debug(s"Skipping build of [$proj] because it, and none of its dependencies are changed")
Seq()
}
},
prValidatorTasks := Seq(Test / ValidatePullRequestCompat.testFull),
prValidatorBuildAllTasks := prValidatorTasks.value,
prValidatorEnforcedBuildAllTasks := prValidatorBuildAllTasks.value,
validatePullRequest := Def.taskDyn {
val validationTasks = prValidatorProjectBuildTasks.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 { _ => () /* Ignore the sequence of unit returned */ }
}
}.value,
validatePullRequestBuildAll := Def.taskDyn {
val validationTasks = prValidatorBuildAllTasks.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 { _ => () /* Ignore the sequence of unit returned */ }
}
}
)
}