-
Notifications
You must be signed in to change notification settings - Fork 42
/
Copy pathgate.groovy
360 lines (312 loc) · 12.9 KB
/
gate.groovy
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
352
353
354
355
356
357
358
359
360
import org.jenkinsci.plugins.workflow.job.WorkflowJob
import org.jenkinsci.plugins.workflow.job.WorkflowRun
import groovy.transform.Field
import com.rackspace.exceptions.REException
void testPullRequest(String repoName, Boolean testWithAntecedents, String githubStatusContext){
try {
if (testWithAntecedents) {
testPullRequestWithAntecedents(repoName, githubStatusContext)
} else {
testPullRequestOnly(repoName, githubStatusContext)
}
}catch (REException e){
currentBuild.result="FAILURE"
}
}
void testPullRequestOnly(String repoName, String statusContext){
List allWorkflowJobs = Hudson.instance.getAllItems(WorkflowJob)
List filteredComponentGateJobs = filterGateJobs(repoName, allWorkflowJobs)
def parallelBuilds = [:]
// Cannot do for (job in jobNames), see:
// https://jenkins.io/doc/pipeline/examples/#parallel-multiple-nodes
for (j in filteredComponentGateJobs) {
WorkflowJob job = j
String jobName = job.displayName
parallelBuilds[jobName] = {
build(
job: jobName,
wait: true,
parameters: [
[
$class: "StringParameterValue",
name: "RPC_GATING_BRANCH",
value: RPC_GATING_BRANCH,
],
[
$class: "StringParameterValue",
name: "BRANCH",
value: sha1,
]
]
)
} // parallelBuilds
} // for
parallel parallelBuilds
String description = "Gate tests passed, merging..."
updateStatusAndMerge(description, statusContext)
}
@Field WorkflowRun triggerBuild = currentBuild.rawBuild
@Field Result SUCCESS = Result.fromString("SUCCESS")
/**
* Global variable for tracking the status of downstream gate builds
* that are managed by testPullRequestWithAntecedents.
*/
@Field Map gateBuilds = [:]
/**
* Test pull request with antecedent builds.
*
* For any build, an antecedent build is defined as being one:
* - of the same job type
* - triggered by a pull request targeted at the same base branch
* - with a lower build number
* - that is not finished
*
* If an antecedent build is no longer running, either it was successful
* and the code has already merged or it failed and its changes can be
* ignored.
*
* If multiple gate trigger builds are running, to ensure that each
* pull request is tested against the HEAD of the branch that will exist
* when it merges instead of the one that exists when the builds starts,
* this function finds all the active antecedent builds and performs
* tests on the assumption that the antecedent builds are all successful
* and merge in the order they were started. If an antecedent fails, the
* tests are restarted.
*/
void testPullRequestWithAntecedents(String repoName, String statusContext){
List allWorkflowJobs = Hudson.instance.getAllItems(WorkflowJob)
List filteredComponentGateJobs = filterGateJobs(repoName, allWorkflowJobs)
triggerJobName = triggerBuild.getParent().displayName
triggerJob = allWorkflowJobs.find {it.displayName == triggerJobName}
antecedentTriggerBuilds = []
pullRequestIDs = []
triggerBuildTargetBranch = triggerBuild.getAction(ParametersAction).getParameter("ghprbTargetBranch").getValue()
while (true) {
updateGateBuilds()
currentAntecedentTriggerBuilds = triggerJob.getBuilds().findAll {
(
it.isBuilding() == true
&& it.getNumber() < triggerBuild.getNumber()
&& it.getAction(ParametersAction).getParameter("ghprbTargetBranch").getValue() == triggerBuildTargetBranch
)
}.reverse()
if (antecedentTriggerBuilds.size() != currentAntecedentTriggerBuilds.size()) {
failedAntecedents = antecedentTriggerBuilds.findAll() {it.getResult() && it.getResult().isWorseThan(SUCCESS)}
if (failedAntecedents) {
println("Antecedent builds that have completed with failure:\n${failedAntecedents.join("\n")}")
println("Terminating existing builds that depend on failed antecedents.")
killGateBuilds()
}else if (antecedentTriggerBuilds){
completed = antecedentTriggerBuilds.findAll() {!(it in currentAntecedentTriggerBuilds)}
println("Antecedent builds that have completed with success:\n${completed.join("\n")}")
}
antecedentTriggerBuilds = currentAntecedentTriggerBuilds
} else{
failedAntecedents = []
}
if (gateBuilds.isEmpty() || failedAntecedents){
if (antecedentTriggerBuilds){
println("Active antecedent builds:\n${antecedentTriggerBuilds.join("\n")}")
}else {
println("No active antecedent builds.")
}
println("Base branch being tested against: ${ghprbTargetBranch}.")
pullRequestIDs = antecedentTriggerBuilds.collect(){getPullRequestID(it)}
pullRequestIDs.add(getPullRequestID(triggerBuild))
println("Pull requests to merge on top of base branch in tests: ${pullRequestIDs}.")
}
// A failed gate build only causes the trigger build to fail if all antecedents succeed
if (antecedentTriggerBuilds.empty) {
failedGateBuilds = getFailedGateBuilds()
if (failedGateBuilds) {
errMsg = "One or more builds failed to reach success:\n${failedGateBuilds.collect() {it.getAbsoluteUrl()}.join("\n")}"
println(errMsg)
throw new REException(errMsg)
}
}
pullRequestIDsParam = common.dumpCSV(pullRequestIDs)
def parallelBuilds = [:]
for (j in filteredComponentGateJobs) {
WorkflowJob job = j
if (! (job in gateBuilds)) {
gateBuilds[job] = [
"build": null,
"nextNumber": job.getNextBuildNumber(),
]
String jobName = job.displayName
parallelBuilds[jobName] = {
build(
job: jobName,
wait: false,
parameters: [
[
$class: "StringParameterValue",
name: "RPC_GATING_BRANCH",
value: RPC_GATING_BRANCH,
],
[
$class: "StringParameterValue",
name: "BRANCH",
value: ghprbTargetBranch,
],
[
$class: "StringParameterValue",
name: "pullRequestChain",
value: pullRequestIDsParam,
],
]
)
} // parallelBuilds
} // if
} // for
if (parallelBuilds){
parallel parallelBuilds
}
if (antecedentTriggerBuilds.empty && isGateBuildsSuccess()){
break
}else {
sleep(time: 120, unit: "SECONDS")
}
}
String description = "Gate tests passed, merging..."
updateStatusAndMerge(description, statusContext)
}
List filterGateJobs(String repoName, List allWorkflowJobs){
List componentGateJobs = (allWorkflowJobs.findAll {it.displayName =~ /GATE_${repoName}-${ghprbTargetBranch}/}).sort(false)
println("Discovered the following pull request gate jobs for repo ${repoName}:")
println(componentGateJobs.collect() {it.displayName}.join("\n"))
List filteredComponentGateJobs = componentGateJobs.findAll {
def job_skip_pattern = it.getProperty(hudson.model.ParametersDefinitionProperty)
.getParameterDefinition("skip_pattern")
.getDefaultValue()
! common.isSkippable(job_skip_pattern, "")
}
println("Remaining pull request gate jobs for repo ${repoName} after filtering out skip_pattern:")
println(filteredComponentGateJobs.collect() {it.displayName}.join("\n"))
return filteredComponentGateJobs
}
Integer getPullRequestID(build){
build.getAction(ParametersAction).getParameter("ghprbPullId").getValue()
}
Boolean isGateBuildsSuccess(){
gateBuilds.every {
build = getBuild(it)
build && ! build.hasntStartedYet() && ! build.isBuilding() && build.getResult() && build.getResult().equals(SUCCESS)
}
}
List getFailedGateBuilds(){
gateBuilds.findAll() {
build = getBuild(it)
(
build
&& build.getResult()
&& build.getResult().isWorseThan(SUCCESS)
)
}.collect {getBuild(it)}
}
WorkflowRun getBuild(build){
build.value["build"]
}
Integer getNextNumber(build){
build.value["nextNumber"]
}
WorkflowJob getJob(build){
build.key
}
WorkflowRun findBuild(WorkflowJob job, Integer oldestNumber){
build = null
potentialBuild = job.getLastBuild()
while(potentialBuild.getNumber() >= oldestNumber){
potentialBuildCause = potentialBuild.getCauses()[0]
if (potentialBuildCause instanceof Cause.UpstreamCause){
if (potentialBuildCause.getUpstreamBuild() == triggerBuild.getNumber()){
build = potentialBuild
break
}
}
potentialBuild = potentialBuild.getPreviousBuild()
}
return build
}
void updateGateBuilds(){
buildCount = gateBuilds.findAll {getBuild(it)}.size()
missingBuilds = false
gateBuilds.clone().each { gateBuild ->
build = getBuild(gateBuild)
if (! build){
job = getJob(gateBuild)
if (findQueueItem(job)){
return
}
startedBuild = findBuild(job, getNextNumber(gateBuild))
if (startedBuild){
gateBuilds[job]["build"] = startedBuild
println("New gate test build: ${startedBuild} ${startedBuild.getAbsoluteUrl()}")
} else {
println("Requested build of job ${job.displayName} cannot be found, preparing for new request.")
gateBuilds.remove(job)
missingBuilds = true
}
}
}
updatedBuildCount = gateBuilds.findAll {getBuild(it)}.size()
if ((! missingBuilds) && gateBuilds.size() == updatedBuildCount && updatedBuildCount > buildCount){
buildURLs = gateBuilds.findAll {getBuild(it)}.collect {getBuild(it).getAbsoluteUrl()}
println("All gate tests started:\n${buildURLs.join("\n")}")
}
}
Queue.WaitingItem findQueueItem(WorkflowJob job){
item = null
queue = Queue.getInstance()
for (queueItem in queue.getItems()){
if (queueItem instanceof Queue.WaitingItem){
qica = queueItem.getAction(CauseAction)
qiuc = (qica.findCause(Cause.UpstreamCause))
if (qiuc.getUpstreamBuild() == triggerBuild.getNumber() && queueItem.task == job){
item = queueItem
break
}
}
}
return item
}
void killGateBuilds(){
queue = Queue.getInstance()
gateBuilds.each { gateBuild ->
build = getBuild(gateBuild)
if (! build){
job = getJob(gateBuild)
queueItem = findQueueItem(job)
if (queueItem){
println("Terminating queued downstream test ${queueItem}")
queue.cancel(queueItem)
}
build = findBuild(job, getNextNumber(gateBuild))
}
if (build){
println("Terminating downstream test ${build}")
listener = build.asFlowExecutionOwner().getListener()
listener.hyperlink(
triggerBuild.getAbsoluteUrl(),
"Terminated by upstream build due to failure in antecedent test.\n"
)
build.doKill()
}
}
gateBuilds = [:]
}
void updateStatusAndMerge(String description, String statusContext){
def (prRepoOrg, prRepoName) = ghprbGhRepository.split("/")
/* When a check is required by GitHub, it prevents the pull request being merged if
it is not marked as `"success"`. Normally GHPRB would be soley responsible for
updating the status context however this would necessitate a separate job to
perform the merge. The following section updates the pull request's status
context on GitHub if all tests were successful to enable the same build to merge
the pull request. GHPRB will the report success again assuming all subsequent
steps succeed.
*/
println("Updating pull request status context.")
github.create_status(prRepoOrg, prRepoName, ghprbActualCommit, "success", triggerBuild.getAbsoluteUrl(), description, statusContext)
github.merge_pr(prRepoOrg, prRepoName, ghprbPullId, ghprbActualCommit)
}
return this;