screwdriver-api 7.0.130 → 7.0.132
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/builds/index.js +174 -1137
- package/plugins/builds/triggers/and.js +100 -0
- package/plugins/builds/triggers/helpers.js +1069 -0
- package/plugins/builds/triggers/joinBase.js +115 -0
- package/plugins/builds/triggers/or.js +27 -0
- package/plugins/builds/triggers/orBase.js +80 -0
- package/plugins/builds/triggers/remoteJoin.js +70 -0
- package/plugins/builds/triggers/remoteTrigger.js +27 -0
package/plugins/builds/index.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const logger = require('screwdriver-logger');
|
|
4
4
|
const workflowParser = require('screwdriver-workflow-parser');
|
|
5
|
-
const merge = require('lodash.mergewith');
|
|
6
|
-
const schema = require('screwdriver-data-schema');
|
|
7
5
|
const hoek = require('@hapi/hoek');
|
|
8
6
|
const getRoute = require('./get');
|
|
9
7
|
const getBuildStatusesRoute = require('./getBuildStatuses');
|
|
@@ -18,59 +16,29 @@ const stepLogsRoute = require('./steps/logs');
|
|
|
18
16
|
const listSecretsRoute = require('./listSecrets');
|
|
19
17
|
const tokenRoute = require('./token');
|
|
20
18
|
const metricsRoute = require('./metrics');
|
|
21
|
-
const { EXTERNAL_TRIGGER_ALL } = schema.config.regex;
|
|
22
19
|
const locker = require('../lock');
|
|
23
|
-
const {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let externalJobName = name;
|
|
43
|
-
let externalPipelineId = pipelineId;
|
|
44
|
-
let isExternal = false;
|
|
45
|
-
|
|
46
|
-
if (isExternalTrigger(name)) {
|
|
47
|
-
isExternal = true;
|
|
48
|
-
[, externalPipelineId, externalJobName] = EXTERNAL_TRIGGER_ALL.exec(name);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return { externalPipelineId, externalJobName, isExternal };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Helper function to fetch external event from parentBuilds
|
|
56
|
-
* @param {Object} currentBuild Build for current completed job
|
|
57
|
-
* @param {String} pipelineId Pipeline ID for next job to be triggered.
|
|
58
|
-
* @param {Object} eventFactory Factory for querying event data store.
|
|
59
|
-
* @return {Object} External Event Event where the next job to be triggered belongs to.
|
|
60
|
-
*/
|
|
61
|
-
function getExternalEvent(currentBuild, pipelineId, eventFactory) {
|
|
62
|
-
if (!currentBuild.parentBuilds || !currentBuild.parentBuilds[pipelineId]) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const { eventId } = currentBuild.parentBuilds[pipelineId];
|
|
67
|
-
|
|
68
|
-
return eventFactory.get(eventId);
|
|
69
|
-
}
|
|
20
|
+
const { OrTrigger } = require('./triggers/or');
|
|
21
|
+
const { AndTrigger } = require('./triggers/and');
|
|
22
|
+
const { RemoteTrigger } = require('./triggers/remoteTrigger');
|
|
23
|
+
const { RemoteJoin } = require('./triggers/remoteJoin');
|
|
24
|
+
const {
|
|
25
|
+
strToInt,
|
|
26
|
+
createJoinObject,
|
|
27
|
+
createEvent,
|
|
28
|
+
parseJobInfo,
|
|
29
|
+
handleStageFailure,
|
|
30
|
+
getJobId,
|
|
31
|
+
isOrTrigger,
|
|
32
|
+
extractExternalJoinData,
|
|
33
|
+
extractCurrentPipelineJoinData,
|
|
34
|
+
createExternalEvent,
|
|
35
|
+
getBuildsForGroupEvent,
|
|
36
|
+
buildsToRestartFilter,
|
|
37
|
+
trimJobName
|
|
38
|
+
} = require('./triggers/helpers');
|
|
70
39
|
|
|
71
40
|
/**
|
|
72
41
|
* Delete a build
|
|
73
|
-
* @method delBuild
|
|
74
42
|
* @param {Object} buildConfig build object to delete
|
|
75
43
|
* @param {Object} buildFactory build factory
|
|
76
44
|
* @return {Promise}
|
|
@@ -86,722 +54,188 @@ async function deleteBuild(buildConfig, buildFactory) {
|
|
|
86
54
|
}
|
|
87
55
|
|
|
88
56
|
/**
|
|
89
|
-
*
|
|
90
|
-
* @
|
|
91
|
-
* @param {
|
|
92
|
-
* @
|
|
93
|
-
* @param {Factory} config.eventFactory Event Factory
|
|
94
|
-
* @param {Number} config.pipelineId Pipeline to be rebuilt
|
|
95
|
-
* @param {String} config.startFrom Job to be rebuilt
|
|
96
|
-
* @param {String} config.causeMessage Caused message, e.g. triggered by 1234(buildId)
|
|
97
|
-
* @param {String} config.parentBuildId ID of the build that triggers this event
|
|
98
|
-
* @param {Object} [config.parentBuilds] Builds that triggered this build
|
|
99
|
-
* @param {Number} [config.parentEventId] Parent event ID
|
|
100
|
-
* @param {Number} [config.groupEventId] Group parent event ID
|
|
101
|
-
* @return {Promise} Resolves to the newly created event
|
|
102
|
-
*/
|
|
103
|
-
async function createEvent(config) {
|
|
104
|
-
const {
|
|
105
|
-
pipelineFactory,
|
|
106
|
-
eventFactory,
|
|
107
|
-
pipelineId,
|
|
108
|
-
startFrom,
|
|
109
|
-
causeMessage,
|
|
110
|
-
parentBuildId,
|
|
111
|
-
parentBuilds,
|
|
112
|
-
parentEventId,
|
|
113
|
-
groupEventId
|
|
114
|
-
} = config;
|
|
115
|
-
const { scm } = eventFactory;
|
|
116
|
-
const payload = {
|
|
117
|
-
pipelineId,
|
|
118
|
-
startFrom,
|
|
119
|
-
type: 'pipeline',
|
|
120
|
-
causeMessage,
|
|
121
|
-
parentBuildId
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
if (parentEventId) {
|
|
125
|
-
payload.parentEventId = parentEventId;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// for backward compatibility, this field is optional
|
|
129
|
-
if (parentBuilds) {
|
|
130
|
-
payload.parentBuilds = parentBuilds;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (groupEventId) {
|
|
134
|
-
payload.groupEventId = groupEventId;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const pipeline = await pipelineFactory.get(pipelineId);
|
|
138
|
-
const realAdmin = await pipeline.admin;
|
|
139
|
-
const { scmContext, scmUri } = pipeline;
|
|
140
|
-
|
|
141
|
-
payload.scmContext = scmContext;
|
|
142
|
-
payload.username = realAdmin.username;
|
|
143
|
-
|
|
144
|
-
// get pipeline admin's token
|
|
145
|
-
const token = await realAdmin.unsealToken();
|
|
146
|
-
const scmConfig = {
|
|
147
|
-
scmContext,
|
|
148
|
-
scmUri,
|
|
149
|
-
token
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
// Get commit sha
|
|
153
|
-
const sha = await scm.getCommitSha(scmConfig);
|
|
154
|
-
|
|
155
|
-
payload.sha = sha;
|
|
156
|
-
|
|
157
|
-
// Set configPipelineSha for child pipeline
|
|
158
|
-
if (pipeline.configPipelineId) {
|
|
159
|
-
const configPipeline = await pipelineFactory.get(pipeline.configPipelineId);
|
|
160
|
-
const configAdmin = await configPipeline.admin;
|
|
161
|
-
const configToken = await configAdmin.unsealToken();
|
|
162
|
-
const configScmConfig = {
|
|
163
|
-
scmContext: configPipeline.scmContext,
|
|
164
|
-
scmUri: configPipeline.scmUri,
|
|
165
|
-
token: configToken
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
payload.configPipelineSha = await scm.getCommitSha(configScmConfig);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return eventFactory.create(payload);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Create external build (returns event with `builds` field)
|
|
176
|
-
* @method createExternalBuild
|
|
177
|
-
* @param {Object} config Configuration object
|
|
178
|
-
* @param {Factory} config.pipelineFactory Pipeline Factory
|
|
179
|
-
* @param {Factory} config.eventFactory Event Factory
|
|
180
|
-
* @param {Number} config.externalPipelineId External pipeline ID
|
|
181
|
-
* @param {String} config.startFrom External trigger to start from
|
|
182
|
-
* @param {Number} config.parentBuildId Parent Build ID
|
|
183
|
-
* @param {Object} config.parentBuilds Builds that triggered this build
|
|
184
|
-
* @param {String} config.causeMessage Cause message of this event
|
|
185
|
-
* @param {Number} [config.parentEventId] Parent event ID
|
|
186
|
-
* @param {Number} [config.groupEventId] Group parent event ID
|
|
187
|
-
* @return {Promise}
|
|
188
|
-
*/
|
|
189
|
-
async function createExternalBuild(config) {
|
|
190
|
-
const {
|
|
191
|
-
pipelineFactory,
|
|
192
|
-
eventFactory,
|
|
193
|
-
externalPipelineId,
|
|
194
|
-
startFrom,
|
|
195
|
-
parentBuildId,
|
|
196
|
-
parentBuilds,
|
|
197
|
-
causeMessage,
|
|
198
|
-
parentEventId,
|
|
199
|
-
groupEventId
|
|
200
|
-
} = config;
|
|
201
|
-
|
|
202
|
-
const createEventConfig = {
|
|
203
|
-
pipelineFactory,
|
|
204
|
-
eventFactory,
|
|
205
|
-
pipelineId: externalPipelineId,
|
|
206
|
-
startFrom,
|
|
207
|
-
parentBuildId, // current build
|
|
208
|
-
causeMessage,
|
|
209
|
-
parentBuilds
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
if (parentEventId) {
|
|
213
|
-
createEventConfig.parentEventId = parentEventId;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (groupEventId) {
|
|
217
|
-
createEventConfig.groupEventId = groupEventId;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return createEvent(createEventConfig);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Create internal build. If config.start is false or not passed in then do not start the job
|
|
225
|
-
* Need to pass in (jobName and pipelineId) or (jobId) to get job data
|
|
226
|
-
* @method createInternalBuild
|
|
227
|
-
* @param {Object} config Configuration object
|
|
228
|
-
* @param {Factory} config.jobFactory Job Factory
|
|
229
|
-
* @param {Factory} config.buildFactory Build Factory
|
|
230
|
-
* @param {Number} [config.pipelineId] Pipeline Id
|
|
231
|
-
* @param {String} [config.jobName] Job name
|
|
232
|
-
* @param {String} config.username Username of build
|
|
233
|
-
* @param {String} config.scmContext SCM context
|
|
234
|
-
* @param {Object} [config.parentBuilds] Builds that triggered this build
|
|
235
|
-
* @param {String} config.baseBranch Branch name
|
|
236
|
-
* @param {Number} [config.parentBuildId] Parent build ID
|
|
237
|
-
* @param {Boolean} [config.start] Whether to start the build or not
|
|
238
|
-
* @param {Number} [config.jobId] Job ID
|
|
239
|
-
* @param {Object} [config.event] Event build belongs to
|
|
240
|
-
* @return {Promise}
|
|
241
|
-
*/
|
|
242
|
-
async function createInternalBuild(config) {
|
|
243
|
-
const {
|
|
244
|
-
jobFactory,
|
|
245
|
-
buildFactory,
|
|
246
|
-
pipelineId,
|
|
247
|
-
jobName,
|
|
248
|
-
username,
|
|
249
|
-
scmContext,
|
|
250
|
-
event,
|
|
251
|
-
parentBuilds,
|
|
252
|
-
start,
|
|
253
|
-
baseBranch,
|
|
254
|
-
parentBuildId,
|
|
255
|
-
jobId
|
|
256
|
-
} = config;
|
|
257
|
-
const prRef = event.pr.ref ? event.pr.ref : '';
|
|
258
|
-
const prSource = event.pr.prSource || '';
|
|
259
|
-
const prInfo = event.pr.prBranchName
|
|
260
|
-
? {
|
|
261
|
-
url: event.pr.url || '',
|
|
262
|
-
prBranchName: event.pr.prBranchName || ''
|
|
263
|
-
}
|
|
264
|
-
: '';
|
|
265
|
-
|
|
266
|
-
let job = {};
|
|
267
|
-
|
|
268
|
-
if (!jobId) {
|
|
269
|
-
job = await jobFactory.get({
|
|
270
|
-
name: jobName,
|
|
271
|
-
pipelineId
|
|
272
|
-
});
|
|
273
|
-
} else {
|
|
274
|
-
job = await jobFactory.get(jobId);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const internalBuildConfig = {
|
|
278
|
-
jobId: job.id,
|
|
279
|
-
sha: event.sha,
|
|
280
|
-
parentBuildId,
|
|
281
|
-
parentBuilds: parentBuilds || {},
|
|
282
|
-
eventId: event.id,
|
|
283
|
-
username,
|
|
284
|
-
configPipelineSha: event.configPipelineSha,
|
|
285
|
-
scmContext,
|
|
286
|
-
prRef,
|
|
287
|
-
prSource,
|
|
288
|
-
prInfo,
|
|
289
|
-
start: start !== false,
|
|
290
|
-
baseBranch
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
let jobState = job.state;
|
|
294
|
-
|
|
295
|
-
if (prRef) {
|
|
296
|
-
// Whether a job is enabled is determined by the state of the original job.
|
|
297
|
-
// If the original job does not exist, it will be enabled.
|
|
298
|
-
const originalJobName = job.parsePRJobName('job');
|
|
299
|
-
const originalJob = await jobFactory.get({
|
|
300
|
-
name: originalJobName,
|
|
301
|
-
pipelineId
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
jobState = originalJob ? originalJob.state : 'ENABLED';
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (jobState === 'ENABLED') {
|
|
308
|
-
return buildFactory.create(internalBuildConfig);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Return PR job or not
|
|
316
|
-
* PR job name certainly has ":". e.g. "PR-1:jobName"
|
|
317
|
-
* @method isPR
|
|
318
|
-
* @param {String} destJobName
|
|
319
|
-
* @return {Boolean}
|
|
320
|
-
*/
|
|
321
|
-
function isPR(jobName) {
|
|
322
|
-
return jobName.startsWith('PR-');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* Trim Job name to follow data-schema
|
|
327
|
-
* @method trimJobName
|
|
328
|
-
* @param {String} jobName
|
|
329
|
-
* @return {String} trimmed jobName
|
|
330
|
-
*/
|
|
331
|
-
function trimJobName(jobName) {
|
|
332
|
-
if (isPR(jobName)) {
|
|
333
|
-
return jobName.split(':')[1];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return jobName;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Generates a parent builds object
|
|
341
|
-
* @param {Number} config.buildId Build ID
|
|
342
|
-
* @param {Number} config.eventId Event ID
|
|
343
|
-
* @param {Number} config.pipelineId Pipeline ID
|
|
344
|
-
* @param {String} config.jobName Job name
|
|
345
|
-
* @param {Array} [config.joinListNames] Job names in join list
|
|
346
|
-
* @return {Object} Returns parent builds object
|
|
57
|
+
* Trigger the next jobs of the current job
|
|
58
|
+
* @param { import('./types/index').ServerConfig } config Configuration object
|
|
59
|
+
* @param { import('./types/index').ServerApp } app Server app object
|
|
60
|
+
* @return {Promise<null>} Resolves to the newly created build or null
|
|
347
61
|
*/
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
eventId: null,
|
|
361
|
-
jobs: {}
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
joinParentBuilds[joinInfo.externalPipelineId].jobs[joinInfo.externalJobName] = null;
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
return joinParentBuilds;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return {
|
|
372
|
-
[pipelineId]: {
|
|
373
|
-
eventId,
|
|
374
|
-
jobs: { [jobName]: buildId }
|
|
375
|
-
}
|
|
62
|
+
async function triggerNextJobs(config, app) {
|
|
63
|
+
const currentPipeline = config.pipeline;
|
|
64
|
+
const currentJob = config.job;
|
|
65
|
+
const currentBuild = config.build;
|
|
66
|
+
const { jobFactory, buildFactory, eventFactory, pipelineFactory } = app;
|
|
67
|
+
|
|
68
|
+
/** @type {EventModel} */
|
|
69
|
+
const currentEvent = await eventFactory.get({ id: currentBuild.eventId });
|
|
70
|
+
const current = {
|
|
71
|
+
pipeline: currentPipeline,
|
|
72
|
+
build: currentBuild,
|
|
73
|
+
event: currentEvent
|
|
376
74
|
};
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
* - parentBuilds: parent build information
|
|
382
|
-
* - joinListNames: array of join jobs
|
|
383
|
-
* - joinParentBuilds: parent build information for join jobs
|
|
384
|
-
* @param {Object} joinObj Join object
|
|
385
|
-
* @param {Object} current Object holding current event, job & pipeline
|
|
386
|
-
* @param {String} nextJobName Next job's name
|
|
387
|
-
* @param {Number} nextPipelineId Next job's Pipeline Id
|
|
388
|
-
* @return {Object} With above information
|
|
389
|
-
*/
|
|
390
|
-
function parseJobInfo({ joinObj = {}, current, nextJobName, nextPipelineId }) {
|
|
391
|
-
const joinList = joinObj[nextJobName] ? joinObj[nextJobName].join : [];
|
|
392
|
-
const joinListNames = joinList.map(j => j.name);
|
|
393
|
-
|
|
394
|
-
/* CONSTRUCT AN OBJ LIKE {111: {eventId: 2, D:987}}
|
|
395
|
-
* FOR EASY LOOKUP OF BUILD STATUS */
|
|
396
|
-
// current job's parentBuilds
|
|
397
|
-
const currentJobParentBuilds = current.build.parentBuilds || {};
|
|
398
|
-
// join jobs, with eventId and buildId empty
|
|
399
|
-
const joinParentBuilds = createParentBuildsObj({
|
|
400
|
-
pipelineId: nextPipelineId || current.pipeline.id,
|
|
401
|
-
joinListNames
|
|
402
|
-
});
|
|
403
|
-
// override currentBuild in the joinParentBuilds
|
|
404
|
-
const currentBuildInfo = createParentBuildsObj({
|
|
405
|
-
buildId: current.build.id,
|
|
406
|
-
eventId: current.build.eventId,
|
|
407
|
-
pipelineId: current.pipeline.id,
|
|
408
|
-
jobName: current.job.name
|
|
75
|
+
/** @type Array<string> */
|
|
76
|
+
const nextJobsTrigger = workflowParser.getNextJobs(currentEvent.workflowGraph, {
|
|
77
|
+
trigger: currentJob.name,
|
|
78
|
+
chainPR: currentPipeline.chainPR
|
|
409
79
|
});
|
|
80
|
+
const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
|
|
81
|
+
const originalCurrentJobName = trimJobName(currentJob.name);
|
|
410
82
|
|
|
411
|
-
//
|
|
412
|
-
|
|
83
|
+
// Trigger OrTrigger and AndTrigger for current pipeline jobs.
|
|
84
|
+
// Helper function to handle triggering jobs in same pipeline
|
|
85
|
+
const orTrigger = new OrTrigger(app, config);
|
|
86
|
+
const andTrigger = new AndTrigger(app, config, currentEvent);
|
|
87
|
+
const currentPipelineNextJobs = extractCurrentPipelineJoinData(pipelineJoinData, currentPipeline.id);
|
|
413
88
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Get finished builds in all parent events
|
|
423
|
-
* @param {Event} event Current event
|
|
424
|
-
* @param {Number} [event.parentEventId] Parent event ID
|
|
425
|
-
* @param {Number} [event.groupEventId] Group parent event ID
|
|
426
|
-
* @param {Factory} buildFactory Build factory
|
|
427
|
-
* @return {Promise} All finished builds
|
|
428
|
-
*/
|
|
429
|
-
async function getFinishedBuilds(event, buildFactory) {
|
|
430
|
-
// FIXME: buildFactory.getLatestBuilds doesn't return build model
|
|
431
|
-
const builds = await buildFactory.getLatestBuilds({ groupEventId: event.groupEventId, readOnly: false });
|
|
89
|
+
for (const [nextJobName, nextJob] of Object.entries(currentPipelineNextJobs)) {
|
|
90
|
+
const nextJobId = nextJob.id || (await getJobId(nextJobName, currentPipeline.id, jobFactory));
|
|
91
|
+
const resource = `pipeline:${currentPipeline.id}:event:${currentEvent.id}`;
|
|
92
|
+
let lock;
|
|
432
93
|
|
|
433
|
-
builds.forEach(b => {
|
|
434
94
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Update parent builds info when next build already exists
|
|
446
|
-
* @param {Object} joinParentBuilds Parent builds object for join job
|
|
447
|
-
* @param {Build} nextBuild Next build
|
|
448
|
-
* @param {Build} build Build for current completed job
|
|
449
|
-
* @return {Promise} Updated next build
|
|
450
|
-
*/
|
|
451
|
-
async function updateParentBuilds({ joinParentBuilds, nextBuild, build }) {
|
|
452
|
-
// Override old parentBuilds info
|
|
453
|
-
const newParentBuilds = merge({}, joinParentBuilds, nextBuild.parentBuilds, (objVal, srcVal) =>
|
|
454
|
-
// passthrough objects, else mergeWith mutates source
|
|
455
|
-
srcVal && typeof srcVal === 'object' ? undefined : objVal || srcVal
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
nextBuild.parentBuilds = newParentBuilds;
|
|
459
|
-
// nextBuild.parentBuildId may be int or Array, so it needs to be flattened
|
|
460
|
-
nextBuild.parentBuildId = Array.from(new Set([build.id, nextBuild.parentBuildId || []].flat()));
|
|
461
|
-
|
|
462
|
-
// FIXME: Is this needed ? Why not update once in handleNewBuild()
|
|
463
|
-
return nextBuild.update();
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Check if all parent builds of the new build are done
|
|
468
|
-
* @param {Build} newBuild Updated build
|
|
469
|
-
* @param {Array} joinListNames Join list names
|
|
470
|
-
* @param {Number} pipelineId Pipeline ID
|
|
471
|
-
* @param {Factory} buildFactory Build factory
|
|
472
|
-
* @return {Promise} Object with done and hasFailure statuses
|
|
473
|
-
*/
|
|
474
|
-
async function getParentBuildStatus({ newBuild, joinListNames, pipelineId, buildFactory }) {
|
|
475
|
-
const upstream = newBuild.parentBuilds || {};
|
|
476
|
-
let done = true;
|
|
477
|
-
let hasFailure = false;
|
|
478
|
-
const promisesToAwait = [];
|
|
479
|
-
|
|
480
|
-
// Get buildId
|
|
481
|
-
for (let i = 0; i < joinListNames.length; i += 1) {
|
|
482
|
-
const name = joinListNames[i];
|
|
483
|
-
const joinInfo = getPipelineAndJob(name, pipelineId);
|
|
484
|
-
|
|
485
|
-
let bId;
|
|
486
|
-
|
|
487
|
-
if (
|
|
488
|
-
upstream[joinInfo.externalPipelineId] &&
|
|
489
|
-
upstream[joinInfo.externalPipelineId].jobs[joinInfo.externalJobName]
|
|
490
|
-
) {
|
|
491
|
-
bId = upstream[joinInfo.externalPipelineId].jobs[joinInfo.externalJobName];
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// If buildId is empty, the job hasn't executed yet and the join is not done
|
|
495
|
-
if (!bId) {
|
|
496
|
-
done = false;
|
|
497
|
-
// Otherwise, get the build to check the status
|
|
498
|
-
} else {
|
|
499
|
-
promisesToAwait.push(buildFactory.get(bId));
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Get the status of the builds
|
|
504
|
-
const joinedBuilds = await Promise.all(promisesToAwait);
|
|
95
|
+
lock = await locker.lock(resource);
|
|
96
|
+
const { parentBuilds, joinListNames } = parseJobInfo({
|
|
97
|
+
joinObj: currentPipelineNextJobs,
|
|
98
|
+
currentBuild,
|
|
99
|
+
currentPipeline,
|
|
100
|
+
currentJob,
|
|
101
|
+
nextJobName
|
|
102
|
+
});
|
|
505
103
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
104
|
+
// Handle no-join case. Sequential Workflow
|
|
105
|
+
// Note: current job can be "external" in nextJob's perspective
|
|
106
|
+
/* CREATE AND START NEXT BUILD IF ALL 2 SCENARIOS ARE TRUE
|
|
107
|
+
* 1. No join
|
|
108
|
+
* 2. ([~D,B,C]->A) currentJob=D, nextJob=A, joinList(A)=[B,C]
|
|
109
|
+
* joinList doesn't include D, so start A
|
|
110
|
+
*/
|
|
111
|
+
if (isOrTrigger(currentEvent.workflowGraph, originalCurrentJobName, trimJobName(nextJobName))) {
|
|
112
|
+
await orTrigger.execute(currentEvent, currentPipeline.id, nextJobName, nextJobId, parentBuilds);
|
|
113
|
+
} else {
|
|
114
|
+
await andTrigger.execute(nextJobName, nextJobId, parentBuilds, joinListNames);
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
logger.error(
|
|
118
|
+
`Error in triggerNextJobInSamePipeline:${nextJobName} from pipeline:${currentPipeline.id}-${currentJob.name}-event:${currentEvent.id} `,
|
|
119
|
+
err
|
|
120
|
+
);
|
|
514
121
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
return { hasFailure, done };
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Handle new build logic: update, start, or remove
|
|
522
|
-
* If the build is done, check if it has a failure:
|
|
523
|
-
* if failure, delete new build
|
|
524
|
-
* if no failure, start new build
|
|
525
|
-
* Otherwise, do nothing
|
|
526
|
-
* @param {Boolean} done If the build is done or not
|
|
527
|
-
* @param {Boolean} hasFailure If the build has a failure or not
|
|
528
|
-
* @param {Build} newBuild Next build
|
|
529
|
-
* @param {String} [jobName] Job name
|
|
530
|
-
* @param {String} [pipelineId] Pipeline ID
|
|
531
|
-
* @param {Object} [stage] Stage
|
|
532
|
-
* @return {Promise} The newly updated/created build
|
|
533
|
-
*/
|
|
534
|
-
async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId, stage }) {
|
|
535
|
-
if (!done) {
|
|
536
|
-
return null;
|
|
122
|
+
await locker.unlock(lock, resource);
|
|
537
123
|
}
|
|
538
|
-
if (!['CREATED', null, undefined].includes(newBuild.status)) {
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Delete new build since previous build failed
|
|
543
|
-
if (hasFailure) {
|
|
544
|
-
let stageTeardownName = '';
|
|
545
124
|
|
|
546
|
-
|
|
547
|
-
|
|
125
|
+
// Trigger RemoteJoin and RemoteTrigger for current and external pipeline jobs.
|
|
126
|
+
// Helper function to handle triggering jobs in external pipeline
|
|
127
|
+
const remoteTrigger = new RemoteTrigger(app, config);
|
|
128
|
+
const remoteJoin = new RemoteJoin(app, config, currentEvent);
|
|
129
|
+
const externalPipelineJoinData = extractExternalJoinData(pipelineJoinData, currentPipeline.id);
|
|
130
|
+
|
|
131
|
+
for (const [joinedPipelineId, joinedPipeline] of Object.entries(externalPipelineJoinData)) {
|
|
132
|
+
const isCurrentPipeline = strToInt(joinedPipelineId) === currentPipeline.id;
|
|
133
|
+
const remoteJoinName = `sd@${currentPipeline.id}:${originalCurrentJobName}`;
|
|
134
|
+
const remoteTriggerName = `~${remoteJoinName}`;
|
|
135
|
+
let lock;
|
|
136
|
+
let resource;
|
|
137
|
+
|
|
138
|
+
let externalEvent = joinedPipeline.event;
|
|
139
|
+
|
|
140
|
+
// This includes CREATED builds too
|
|
141
|
+
const groupEventBuilds =
|
|
142
|
+
externalEvent !== undefined ? await getBuildsForGroupEvent(externalEvent.groupEventId, buildFactory) : [];
|
|
143
|
+
const buildsToRestart = buildsToRestartFilter(joinedPipeline, groupEventBuilds, currentEvent, currentBuild);
|
|
144
|
+
const isRestart = buildsToRestart.length > 0;
|
|
145
|
+
|
|
146
|
+
// If user used external trigger syntax, the jobs are triggered as external
|
|
147
|
+
if (isCurrentPipeline || isRestart) {
|
|
148
|
+
externalEvent = null;
|
|
548
149
|
}
|
|
549
150
|
|
|
550
|
-
//
|
|
551
|
-
if (
|
|
552
|
-
|
|
553
|
-
`Failure occurred in upstream job, removing new build - build:${newBuild.id} pipeline:${pipelineId}-${jobName} event:${newBuild.eventId} `
|
|
554
|
-
);
|
|
555
|
-
await newBuild.remove();
|
|
151
|
+
// no need to lock if there is no external event
|
|
152
|
+
if (externalEvent) {
|
|
153
|
+
resource = `pipeline:${joinedPipelineId}:event:${externalEvent.id}`;
|
|
556
154
|
}
|
|
557
155
|
|
|
558
|
-
|
|
559
|
-
|
|
156
|
+
// Create a new external event
|
|
157
|
+
// First downstream trigger, restart case, same pipeline trigger as external
|
|
158
|
+
if (!externalEvent) {
|
|
159
|
+
const { parentBuilds } = parseJobInfo({
|
|
160
|
+
currentBuild,
|
|
161
|
+
currentPipeline,
|
|
162
|
+
currentJob
|
|
163
|
+
});
|
|
560
164
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
165
|
+
const externalEventConfig = {
|
|
166
|
+
pipelineFactory,
|
|
167
|
+
eventFactory,
|
|
168
|
+
externalPipelineId: joinedPipelineId,
|
|
169
|
+
parentBuildId: currentBuild.id,
|
|
170
|
+
parentBuilds,
|
|
171
|
+
causeMessage: `Triggered by ${remoteJoinName}`,
|
|
172
|
+
parentEventId: currentEvent.id,
|
|
173
|
+
startFrom: remoteTriggerName,
|
|
174
|
+
skipMessage: 'Skip bulk external builds creation', // Don't start builds in eventFactory.
|
|
175
|
+
groupEventId: null
|
|
176
|
+
};
|
|
565
177
|
|
|
566
|
-
|
|
567
|
-
|
|
178
|
+
// Restart case
|
|
179
|
+
if (isRestart) {
|
|
180
|
+
externalEventConfig.groupEventId = joinedPipeline.event.id;
|
|
181
|
+
externalEventConfig.parentBuilds = buildsToRestart[0].parentBuilds;
|
|
182
|
+
}
|
|
568
183
|
|
|
569
|
-
|
|
570
|
-
* Get all builds with same parent event id
|
|
571
|
-
* @param {Factory} eventFactory Event factory
|
|
572
|
-
* @param {Number} parentEventId Parent event ID
|
|
573
|
-
* @param {Number} pipelineId Pipeline ID
|
|
574
|
-
* @return {Promise} Array of builds with same parent event ID
|
|
575
|
-
*/
|
|
576
|
-
async function getParallelBuilds({ eventFactory, parentEventId, pipelineId }) {
|
|
577
|
-
let parallelEvents = await eventFactory.list({
|
|
578
|
-
params: {
|
|
579
|
-
parentEventId
|
|
184
|
+
externalEvent = await createExternalEvent(externalEventConfig);
|
|
580
185
|
}
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
// Remove previous events from same pipeline
|
|
584
|
-
parallelEvents = parallelEvents.filter(pe => pe.pipelineId !== pipelineId);
|
|
585
|
-
|
|
586
|
-
let parallelBuilds = [];
|
|
587
|
-
|
|
588
|
-
await Promise.all(
|
|
589
|
-
parallelEvents.map(async pe => {
|
|
590
|
-
const parallelBuild = await pe.getBuilds();
|
|
591
|
-
|
|
592
|
-
parallelBuilds = parallelBuilds.concat(parallelBuild);
|
|
593
|
-
})
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
return parallelBuilds;
|
|
597
|
-
}
|
|
598
186
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
* @param {Array} parentBuilds
|
|
602
|
-
* @param {Object} current Holds current build/event data
|
|
603
|
-
* @param {Array} builds Completed builds which is used to fill parentBuilds data
|
|
604
|
-
* @param {Object} [nextEvent] External event
|
|
605
|
-
*/
|
|
606
|
-
function fillParentBuilds(parentBuilds, current, builds, nextEvent) {
|
|
607
|
-
Object.keys(parentBuilds).forEach(pid => {
|
|
608
|
-
Object.keys(parentBuilds[pid].jobs).forEach(jName => {
|
|
609
|
-
let joinJob;
|
|
610
|
-
|
|
611
|
-
if (parentBuilds[pid].jobs[jName] === null) {
|
|
612
|
-
let workflowGraph;
|
|
613
|
-
let searchJob = trimJobName(jName);
|
|
187
|
+
for (const [nextJobName, nextJob] of Object.entries(joinedPipeline.jobs)) {
|
|
188
|
+
const nextJobId = nextJob.id || (await getJobId(nextJobName, currentPipeline.id, jobFactory));
|
|
614
189
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
workflowGraph = nextEvent.workflowGraph;
|
|
625
|
-
} else {
|
|
626
|
-
// parentBuild is remote triggered from current Event
|
|
627
|
-
searchJob = `sd@${pid}:${searchJob}`;
|
|
628
|
-
workflowGraph = current.event.workflowGraph;
|
|
629
|
-
}
|
|
630
|
-
joinJob = workflowGraph.nodes.find(node => node.name === searchJob);
|
|
190
|
+
const { parentBuilds } = parseJobInfo({
|
|
191
|
+
joinObj: joinedPipeline.jobs,
|
|
192
|
+
currentBuild,
|
|
193
|
+
currentPipeline,
|
|
194
|
+
currentJob,
|
|
195
|
+
nextJobName,
|
|
196
|
+
nextPipelineId: joinedPipelineId
|
|
197
|
+
});
|
|
631
198
|
|
|
632
|
-
|
|
633
|
-
|
|
199
|
+
try {
|
|
200
|
+
if (resource) lock = await locker.lock(resource);
|
|
201
|
+
|
|
202
|
+
if (isOrTrigger(externalEvent.workflowGraph, remoteTriggerName, nextJobName)) {
|
|
203
|
+
await remoteTrigger.execute(
|
|
204
|
+
externalEvent,
|
|
205
|
+
externalEvent.pipelineId,
|
|
206
|
+
nextJobName,
|
|
207
|
+
nextJobId,
|
|
208
|
+
parentBuilds
|
|
209
|
+
);
|
|
634
210
|
} else {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
211
|
+
// Re get join list when first time remote trigger since external event was empty and cannot get workflow graph then
|
|
212
|
+
const joinList =
|
|
213
|
+
nextJob.join.length > 0
|
|
214
|
+
? nextJob.join
|
|
215
|
+
: workflowParser.getSrcForJoin(externalEvent.workflowGraph, { jobName: nextJobName });
|
|
216
|
+
const joinListNames = joinList.map(j => j.name);
|
|
217
|
+
|
|
218
|
+
await remoteJoin.execute(
|
|
219
|
+
externalEvent,
|
|
220
|
+
nextJobName,
|
|
221
|
+
nextJobId,
|
|
222
|
+
parentBuilds,
|
|
223
|
+
groupEventBuilds,
|
|
224
|
+
joinListNames
|
|
225
|
+
);
|
|
643
226
|
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
logger.error(
|
|
229
|
+
`Error in triggerJobsInExternalPipeline:${joinedPipelineId} from pipeline:${currentPipeline.id}-${currentJob.name}-event:${currentEvent.id} `,
|
|
230
|
+
err
|
|
231
|
+
);
|
|
644
232
|
}
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Create joinObject for nextJobs to trigger
|
|
651
|
-
* For A & D in nextJobs for currentJobName B, create
|
|
652
|
-
* {A:[B,C], D:[B,F], X: []} where [B,C] join on A,
|
|
653
|
-
* [B,F] join on D and X has no join
|
|
654
|
-
* This can include external jobs
|
|
655
|
-
* @param {Array} nextJobs List of jobs to run next from workflow parser.
|
|
656
|
-
* @param {Object} current Object holding current job's build, event data
|
|
657
|
-
* @param {Object} eventFactory Object for querying DB for event data
|
|
658
|
-
* @return {Object} Object representing join data for next jobs grouped by pipeline id
|
|
659
|
-
* {"pipeineId" : {event: "externalEventId",
|
|
660
|
-
* jobs: {"nextJobName": {"id": "jobId", join: ["a", "b"]
|
|
661
|
-
* }
|
|
662
|
-
* }
|
|
663
|
-
*/
|
|
664
|
-
async function createJoinObject(nextJobs, current, eventFactory) {
|
|
665
|
-
const { build, event } = current;
|
|
666
|
-
|
|
667
|
-
const joinObj = {};
|
|
668
|
-
|
|
669
|
-
for (const jobName of nextJobs) {
|
|
670
|
-
const jobInfo = getPipelineAndJob(jobName, current.pipeline.id);
|
|
671
|
-
const { externalPipelineId: pid, externalJobName: jName, isExternal } = jobInfo;
|
|
672
|
-
|
|
673
|
-
const jId = event.workflowGraph.nodes.find(n => n.name === trimJobName(jobName)).id;
|
|
674
|
-
|
|
675
|
-
if (!joinObj[pid]) joinObj[pid] = {};
|
|
676
|
-
const pipelineObj = joinObj[pid];
|
|
677
|
-
let jobs;
|
|
678
|
-
|
|
679
|
-
if (pid !== current.pipeline.id) {
|
|
680
|
-
jobs = [];
|
|
681
233
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (externalEvent) {
|
|
685
|
-
pipelineObj.event = externalEvent;
|
|
686
|
-
jobs = workflowParser.getSrcForJoin(externalEvent.workflowGraph, { jobName: jName });
|
|
687
|
-
}
|
|
688
|
-
} else {
|
|
689
|
-
jobs = workflowParser.getSrcForJoin(event.workflowGraph, { jobName });
|
|
234
|
+
await locker.unlock(lock, resource);
|
|
690
235
|
}
|
|
691
|
-
|
|
692
|
-
if (!pipelineObj.jobs) pipelineObj.jobs = {};
|
|
693
|
-
pipelineObj.jobs[jName] = { id: jId, join: jobs, isExternal };
|
|
694
236
|
}
|
|
695
237
|
|
|
696
|
-
return
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Get parentBuildId from parentBuilds object
|
|
701
|
-
* @param {Object} parentBuilds Builds that triggered this build
|
|
702
|
-
* @param {Array} joinListNames Array of join job name
|
|
703
|
-
* @param {Number} pipelineId Pipeline ID
|
|
704
|
-
* @return {Array} Array of parentBuildId
|
|
705
|
-
*/
|
|
706
|
-
function getParentBuildIds({ currentBuildId, parentBuilds, joinListNames, pipelineId }) {
|
|
707
|
-
const parentBuildIds = [];
|
|
708
|
-
|
|
709
|
-
for (let i = 0; i < joinListNames.length; i += 1) {
|
|
710
|
-
const name = joinListNames[i];
|
|
711
|
-
const joinInfo = getPipelineAndJob(name, pipelineId);
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
parentBuilds[joinInfo.externalPipelineId] &&
|
|
715
|
-
parentBuilds[joinInfo.externalPipelineId].jobs[joinInfo.externalJobName]
|
|
716
|
-
) {
|
|
717
|
-
parentBuildIds.push(parentBuilds[joinInfo.externalPipelineId].jobs[joinInfo.externalJobName]);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return Array.from(new Set([currentBuildId, ...parentBuildIds]));
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Create stage teardown build if it doesn't already exist
|
|
726
|
-
* @param {Factory} jobFactory Job factory
|
|
727
|
-
* @param {Factory} buildFactory Build factory
|
|
728
|
-
* @param {Object} current Current object
|
|
729
|
-
* @param {String} stageTeardownName Stage teardown name
|
|
730
|
-
* @param {String} username Username
|
|
731
|
-
* @param {String} scmContext SCM context
|
|
732
|
-
*/
|
|
733
|
-
async function ensureStageTeardownBuildExists({
|
|
734
|
-
jobFactory,
|
|
735
|
-
buildFactory,
|
|
736
|
-
current,
|
|
737
|
-
stageTeardownName,
|
|
738
|
-
username,
|
|
739
|
-
scmContext
|
|
740
|
-
}) {
|
|
741
|
-
// Check if stage teardown build already exists
|
|
742
|
-
const stageTeardownJob = await jobFactory.get({
|
|
743
|
-
pipelineId: current.pipeline.id,
|
|
744
|
-
name: stageTeardownName
|
|
745
|
-
});
|
|
746
|
-
const existingStageTeardownBuild = await buildFactory.get({
|
|
747
|
-
eventId: current.event.id,
|
|
748
|
-
jobId: stageTeardownJob.id
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// Doesn't exist, create stage teardown job
|
|
752
|
-
if (!existingStageTeardownBuild) {
|
|
753
|
-
await createInternalBuild({
|
|
754
|
-
jobFactory,
|
|
755
|
-
buildFactory,
|
|
756
|
-
pipelineId: current.pipeline.id,
|
|
757
|
-
jobName: stageTeardownName,
|
|
758
|
-
username,
|
|
759
|
-
scmContext,
|
|
760
|
-
event: current.event, // this is the parentBuild for the next build
|
|
761
|
-
baseBranch: current.event.baseBranch || null,
|
|
762
|
-
start: false
|
|
763
|
-
});
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
/**
|
|
768
|
-
* Delete nextBuild, create teardown build if it doesn't exist, and return teardown build or return null
|
|
769
|
-
* @param {String} nextJobName Next job name
|
|
770
|
-
* @param {Object} current Object with stage, event, pipeline info
|
|
771
|
-
* @param {Object} buildConfig Build config
|
|
772
|
-
* @param {Factory} jobFactory Job factory
|
|
773
|
-
* @param {Factory} buildFactory Build factory
|
|
774
|
-
* @param {String} username Username
|
|
775
|
-
* @param {String} scmContext Scm context
|
|
776
|
-
* @return {Array} Array of promises
|
|
777
|
-
*/
|
|
778
|
-
async function handleStageFailure({
|
|
779
|
-
nextJobName,
|
|
780
|
-
current,
|
|
781
|
-
buildConfig,
|
|
782
|
-
jobFactory,
|
|
783
|
-
buildFactory,
|
|
784
|
-
username,
|
|
785
|
-
scmContext
|
|
786
|
-
}) {
|
|
787
|
-
const buildDeletePromises = [];
|
|
788
|
-
const stageTeardownName = getFullStageJobName({ stageName: current.stage.name, jobName: 'teardown' });
|
|
789
|
-
|
|
790
|
-
// Remove next build
|
|
791
|
-
if (buildConfig.eventId && nextJobName !== stageTeardownName) {
|
|
792
|
-
buildDeletePromises.push(deleteBuild(buildConfig, buildFactory));
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
await ensureStageTeardownBuildExists({
|
|
796
|
-
jobFactory,
|
|
797
|
-
buildFactory,
|
|
798
|
-
current,
|
|
799
|
-
stageTeardownName,
|
|
800
|
-
username,
|
|
801
|
-
scmContext
|
|
802
|
-
});
|
|
803
|
-
|
|
804
|
-
return buildDeletePromises;
|
|
238
|
+
return null;
|
|
805
239
|
}
|
|
806
240
|
|
|
807
241
|
/**
|
|
@@ -860,7 +294,7 @@ const buildsPlugin = {
|
|
|
860
294
|
|
|
861
295
|
// if nextBuild is stage teardown, just return nextBuild
|
|
862
296
|
if (current.stage) {
|
|
863
|
-
const buildDeletePromises =
|
|
297
|
+
const buildDeletePromises = handleStageFailure({
|
|
864
298
|
nextJobName,
|
|
865
299
|
current,
|
|
866
300
|
buildConfig,
|
|
@@ -906,405 +340,8 @@ const buildsPlugin = {
|
|
|
906
340
|
|
|
907
341
|
/**
|
|
908
342
|
* Trigger the next jobs of the current job
|
|
909
|
-
* @method triggerNextJobs
|
|
910
|
-
* @param {Object} config Configuration object
|
|
911
|
-
* @param {Pipeline} config.pipeline Current pipeline
|
|
912
|
-
* @param {Job} config.job Current job
|
|
913
|
-
* @param {Build} config.build Current build
|
|
914
|
-
* @param {String} config.username Username
|
|
915
|
-
* @param {String} config.scmContext Scm context
|
|
916
|
-
* @param {String} app Server app object
|
|
917
|
-
* @return {Promise} Resolves to the newly created build or null
|
|
918
343
|
*/
|
|
919
|
-
server.expose('triggerNextJobs',
|
|
920
|
-
const { pipeline, job, build, event, stage } = config;
|
|
921
|
-
const { eventFactory, pipelineFactory, buildFactory, jobFactory } = app;
|
|
922
|
-
const current = {
|
|
923
|
-
pipeline,
|
|
924
|
-
job,
|
|
925
|
-
build,
|
|
926
|
-
event,
|
|
927
|
-
stage
|
|
928
|
-
};
|
|
929
|
-
|
|
930
|
-
const nextJobsTrigger = workflowParser.getNextJobs(current.event.workflowGraph, {
|
|
931
|
-
trigger: current.job.name,
|
|
932
|
-
chainPR: pipeline.chainPR
|
|
933
|
-
});
|
|
934
|
-
const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
|
|
935
|
-
|
|
936
|
-
// Helper function to handle triggering jobs in same pipeline
|
|
937
|
-
const triggerNextJobInSamePipeline = async (nextJobName, joinObj) => {
|
|
938
|
-
const { username, scmContext } = config;
|
|
939
|
-
const { parentBuilds, joinListNames } = parseJobInfo({
|
|
940
|
-
joinObj,
|
|
941
|
-
current,
|
|
942
|
-
nextJobName
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
// Handle no-join case. Sequential Workflow
|
|
946
|
-
// Note: current job can be "external" in nextJob's perspective
|
|
947
|
-
/* CREATE AND START NEXT BUILD IF ALL 2 SCENARIOS ARE TRUE
|
|
948
|
-
* 1. No join
|
|
949
|
-
* 2. ([~D,B,C]->A) currentJob=D, nextJob=A, joinList(A)=[B,C]
|
|
950
|
-
* joinList doesn't include D, so start A
|
|
951
|
-
*/
|
|
952
|
-
const isORTrigger = !joinListNames.includes(current.job.name);
|
|
953
|
-
|
|
954
|
-
if (isORTrigger) {
|
|
955
|
-
const internalBuildConfig = {
|
|
956
|
-
jobFactory,
|
|
957
|
-
buildFactory,
|
|
958
|
-
pipelineId: current.pipeline.id,
|
|
959
|
-
jobName: nextJobName,
|
|
960
|
-
username,
|
|
961
|
-
scmContext,
|
|
962
|
-
event: current.event, // this is the parentBuild for the next build
|
|
963
|
-
baseBranch: current.event.baseBranch || null,
|
|
964
|
-
parentBuilds,
|
|
965
|
-
parentBuildId: current.build.id
|
|
966
|
-
};
|
|
967
|
-
|
|
968
|
-
const nextJob = await jobFactory.get({
|
|
969
|
-
name: nextJobName,
|
|
970
|
-
pipelineId: current.pipeline.id
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
const existNextBuild = await buildFactory.get({
|
|
974
|
-
eventId: current.event.id,
|
|
975
|
-
jobId: nextJob.id
|
|
976
|
-
});
|
|
977
|
-
|
|
978
|
-
if (existNextBuild === null) {
|
|
979
|
-
return createInternalBuild(internalBuildConfig);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (!['CREATED', null, undefined].includes(existNextBuild.status)) {
|
|
983
|
-
return existNextBuild;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Current build is not part of stage
|
|
987
|
-
existNextBuild.status = 'QUEUED';
|
|
988
|
-
await existNextBuild.update();
|
|
989
|
-
|
|
990
|
-
return existNextBuild.start();
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Handle join case. Fan-out/fan-in Workflow
|
|
994
|
-
logger.info(`Fetching finished builds for event ${event.id}`);
|
|
995
|
-
let finishedInternalBuilds = await getFinishedBuilds(current.event, buildFactory);
|
|
996
|
-
|
|
997
|
-
if (current.event.parentEventId) {
|
|
998
|
-
// FIXME: On restart cases parentEventId should be fetched
|
|
999
|
-
// from first event in the group
|
|
1000
|
-
const parallelBuilds = await getParallelBuilds({
|
|
1001
|
-
eventFactory,
|
|
1002
|
-
parentEventId: current.event.parentEventId,
|
|
1003
|
-
pipelineId: current.pipeline.id
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
finishedInternalBuilds = finishedInternalBuilds.concat(parallelBuilds);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const nextJobId = joinObj[nextJobName].id;
|
|
1010
|
-
|
|
1011
|
-
let nextBuild;
|
|
1012
|
-
|
|
1013
|
-
// If next build is internal, look at the finished builds for this event
|
|
1014
|
-
nextBuild = finishedInternalBuilds.find(b => b.jobId === nextJobId && b.eventId === current.event.id);
|
|
1015
|
-
|
|
1016
|
-
if (!nextBuild) {
|
|
1017
|
-
// If the build to join fails and it succeeds on restart, depending on the timing, the latest build will be that of a child event.
|
|
1018
|
-
// In that case, `nextBuild` will be null and will not be triggered even though there is a build that should be triggered.
|
|
1019
|
-
// Now we need to check for the existence of a build that should be triggered in its own event.
|
|
1020
|
-
nextBuild = await buildFactory.get({
|
|
1021
|
-
eventId: current.event.id,
|
|
1022
|
-
jobId: nextJobId
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
if (nextBuild) {
|
|
1026
|
-
finishedInternalBuilds = finishedInternalBuilds.concat(nextBuild);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
fillParentBuilds(parentBuilds, current, finishedInternalBuilds);
|
|
1031
|
-
|
|
1032
|
-
let newBuild;
|
|
1033
|
-
|
|
1034
|
-
// Create next build
|
|
1035
|
-
if (!nextBuild) {
|
|
1036
|
-
const internalBuildConfig = {
|
|
1037
|
-
jobFactory,
|
|
1038
|
-
buildFactory,
|
|
1039
|
-
pipelineId: current.pipeline.id,
|
|
1040
|
-
jobName: nextJobName,
|
|
1041
|
-
start: false,
|
|
1042
|
-
username,
|
|
1043
|
-
scmContext,
|
|
1044
|
-
event: current.event, // this is the parentBuild for the next build
|
|
1045
|
-
baseBranch: current.event.baseBranch || null,
|
|
1046
|
-
parentBuilds,
|
|
1047
|
-
parentBuildId: current.build.id
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
newBuild = await createInternalBuild(internalBuildConfig);
|
|
1051
|
-
} else {
|
|
1052
|
-
// nextBuild is not build model, so fetch proper build
|
|
1053
|
-
newBuild = await updateParentBuilds({
|
|
1054
|
-
joinParentBuilds: parentBuilds,
|
|
1055
|
-
nextBuild: await buildFactory.get(nextBuild.id),
|
|
1056
|
-
build: current.build
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
if (!newBuild) {
|
|
1061
|
-
logger.error(`No build found for ${current.pipeline.id}:${nextJobName}`);
|
|
1062
|
-
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
/* CHECK IF ALL PARENT BUILDS OF NEW BUILD ARE DONE */
|
|
1066
|
-
const { hasFailure, done } = await getParentBuildStatus({
|
|
1067
|
-
newBuild,
|
|
1068
|
-
joinListNames,
|
|
1069
|
-
pipelineId: current.pipeline.id,
|
|
1070
|
-
buildFactory
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
return handleNewBuild({
|
|
1074
|
-
done,
|
|
1075
|
-
hasFailure,
|
|
1076
|
-
newBuild,
|
|
1077
|
-
jobName: nextJobName,
|
|
1078
|
-
pipelineId: current.pipeline.id,
|
|
1079
|
-
stage: current.stage
|
|
1080
|
-
});
|
|
1081
|
-
};
|
|
1082
|
-
|
|
1083
|
-
// Helper function to handle triggering jobs in external pipeline
|
|
1084
|
-
const triggerJobsInExternalPipeline = async (externalPipelineId, joinObj) => {
|
|
1085
|
-
let externalEvent = joinObj.event;
|
|
1086
|
-
const nextJobs = joinObj.jobs;
|
|
1087
|
-
let nextJobNames = Object.keys(nextJobs);
|
|
1088
|
-
const triggerName = `sd@${current.pipeline.id}:${current.job.name}`;
|
|
1089
|
-
|
|
1090
|
-
if (externalEvent) {
|
|
1091
|
-
// Remote join case
|
|
1092
|
-
// fetch builds created due to restart
|
|
1093
|
-
const externalGroupBuilds = await getFinishedBuilds(externalEvent, buildFactory);
|
|
1094
|
-
|
|
1095
|
-
const buildsToRestart = nextJobNames
|
|
1096
|
-
.map(j => {
|
|
1097
|
-
const existingBuild = externalGroupBuilds.find(b => b.jobId === nextJobs[j].id);
|
|
1098
|
-
|
|
1099
|
-
return existingBuild &&
|
|
1100
|
-
existingBuild.status !== 'CREATED' &&
|
|
1101
|
-
!existingBuild.parentBuildId.includes(current.build.id) &&
|
|
1102
|
-
existingBuild.eventId !== current.event.parentEventId
|
|
1103
|
-
? existingBuild
|
|
1104
|
-
: null;
|
|
1105
|
-
})
|
|
1106
|
-
.filter(b => b !== null);
|
|
1107
|
-
|
|
1108
|
-
// fetch builds created due to trigger
|
|
1109
|
-
const parallelBuilds = await getParallelBuilds({
|
|
1110
|
-
eventFactory,
|
|
1111
|
-
parentEventId: externalEvent.id,
|
|
1112
|
-
pipelineId: externalEvent.pipelineId
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
externalGroupBuilds.push(...parallelBuilds);
|
|
1116
|
-
|
|
1117
|
-
if (buildsToRestart.length) {
|
|
1118
|
-
const { parentBuilds } = buildsToRestart[0];
|
|
1119
|
-
|
|
1120
|
-
// If restart handle like a fresh trigger
|
|
1121
|
-
// and start all jobs which are not join jobs
|
|
1122
|
-
const externalBuildConfig = {
|
|
1123
|
-
pipelineFactory,
|
|
1124
|
-
eventFactory,
|
|
1125
|
-
externalPipelineId,
|
|
1126
|
-
startFrom: `~${triggerName}`,
|
|
1127
|
-
parentBuildId: current.build.id,
|
|
1128
|
-
parentBuilds,
|
|
1129
|
-
causeMessage: `Triggered by ${triggerName}`,
|
|
1130
|
-
parentEventId: current.event.id,
|
|
1131
|
-
groupEventId: externalEvent.id
|
|
1132
|
-
};
|
|
1133
|
-
|
|
1134
|
-
// proceed with join jobs using new external event
|
|
1135
|
-
nextJobNames = nextJobNames.filter(j => nextJobs[j].join.length);
|
|
1136
|
-
|
|
1137
|
-
externalEvent = await createExternalBuild(externalBuildConfig);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// create/start build for each of nextJobs
|
|
1141
|
-
for (const nextJobName of nextJobNames) {
|
|
1142
|
-
const { username, scmContext } = config;
|
|
1143
|
-
const nextJob = nextJobs[nextJobName];
|
|
1144
|
-
// create new build if restart case.
|
|
1145
|
-
// externalGroupBuilds will contain previous externalEvent's builds
|
|
1146
|
-
const nextBuild = buildsToRestart.length
|
|
1147
|
-
? null
|
|
1148
|
-
: externalGroupBuilds.find(b => b.jobId === nextJob.id);
|
|
1149
|
-
let newBuild;
|
|
1150
|
-
|
|
1151
|
-
const { parentBuilds } = parseJobInfo({
|
|
1152
|
-
joinObj: nextJobs,
|
|
1153
|
-
current,
|
|
1154
|
-
nextJobName,
|
|
1155
|
-
nextPipelineId: externalPipelineId
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
fillParentBuilds(parentBuilds, current, externalGroupBuilds, externalEvent);
|
|
1159
|
-
|
|
1160
|
-
const joinList = nextJobs[nextJobName].join;
|
|
1161
|
-
const joinListNames = joinList.map(j => j.name);
|
|
1162
|
-
const isORTrigger = !joinListNames.includes(triggerName);
|
|
1163
|
-
|
|
1164
|
-
if (nextBuild) {
|
|
1165
|
-
// update current build info in parentBuilds
|
|
1166
|
-
// nextBuild is not build model, so fetch proper build
|
|
1167
|
-
newBuild = await updateParentBuilds({
|
|
1168
|
-
joinParentBuilds: parentBuilds,
|
|
1169
|
-
nextBuild: await buildFactory.get(nextBuild.id),
|
|
1170
|
-
build: current.build
|
|
1171
|
-
});
|
|
1172
|
-
} else {
|
|
1173
|
-
// no existing build, so first time processing this job
|
|
1174
|
-
// in the external pipeline's event
|
|
1175
|
-
const parentBuildId = getParentBuildIds({
|
|
1176
|
-
currentBuildId: current.build.id,
|
|
1177
|
-
parentBuilds,
|
|
1178
|
-
joinListNames,
|
|
1179
|
-
pipelineId: externalPipelineId
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
newBuild = await createInternalBuild({
|
|
1183
|
-
jobFactory,
|
|
1184
|
-
buildFactory,
|
|
1185
|
-
pipelineId: externalEvent.pipelineId,
|
|
1186
|
-
jobName: nextJob.name,
|
|
1187
|
-
jobId: nextJob.id,
|
|
1188
|
-
username,
|
|
1189
|
-
scmContext,
|
|
1190
|
-
event: externalEvent, // this is the parentBuild for the next build
|
|
1191
|
-
baseBranch: externalEvent.baseBranch || null,
|
|
1192
|
-
parentBuilds,
|
|
1193
|
-
parentBuildId,
|
|
1194
|
-
start: false
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
if (isORTrigger) {
|
|
1199
|
-
if (['CREATED', null, undefined].includes(newBuild.status)) {
|
|
1200
|
-
newBuild.status = 'QUEUED';
|
|
1201
|
-
await newBuild.update();
|
|
1202
|
-
await newBuild.start();
|
|
1203
|
-
}
|
|
1204
|
-
} else {
|
|
1205
|
-
const { hasFailure, done } = await getParentBuildStatus({
|
|
1206
|
-
newBuild,
|
|
1207
|
-
joinListNames,
|
|
1208
|
-
pipelineId: externalPipelineId,
|
|
1209
|
-
buildFactory
|
|
1210
|
-
});
|
|
1211
|
-
|
|
1212
|
-
// Check if external pipeline has Join
|
|
1213
|
-
// and join conditions are met
|
|
1214
|
-
await handleNewBuild({
|
|
1215
|
-
done,
|
|
1216
|
-
hasFailure,
|
|
1217
|
-
newBuild,
|
|
1218
|
-
jobName: nextJobName,
|
|
1219
|
-
pipelineId: externalPipelineId,
|
|
1220
|
-
stage: current.stage
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
return null;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
const { parentBuilds } = parseJobInfo({ current });
|
|
1229
|
-
|
|
1230
|
-
// Simply create an external event if external job is not join job.
|
|
1231
|
-
// Straight external trigger flow.
|
|
1232
|
-
const externalBuildConfig = {
|
|
1233
|
-
pipelineFactory,
|
|
1234
|
-
eventFactory,
|
|
1235
|
-
externalPipelineId,
|
|
1236
|
-
startFrom: `~${triggerName}`,
|
|
1237
|
-
parentBuildId: current.build.id,
|
|
1238
|
-
parentBuilds,
|
|
1239
|
-
causeMessage: `Triggered by ${triggerName}`,
|
|
1240
|
-
parentEventId: current.event.id,
|
|
1241
|
-
groupEventId: null
|
|
1242
|
-
};
|
|
1243
|
-
|
|
1244
|
-
return createExternalBuild(externalBuildConfig);
|
|
1245
|
-
};
|
|
1246
|
-
|
|
1247
|
-
for (const pid of Object.keys(pipelineJoinData)) {
|
|
1248
|
-
// typecast pid to number
|
|
1249
|
-
let triggerCurrentPipelineAsExternal = false;
|
|
1250
|
-
const isCurrentPipeline = +pid === current.pipeline.id;
|
|
1251
|
-
|
|
1252
|
-
if (isCurrentPipeline) {
|
|
1253
|
-
for (const nextJobName of Object.keys(pipelineJoinData[pid].jobs)) {
|
|
1254
|
-
const resource = `pipeline:${current.pipeline.id}:event:${current.event.id}`;
|
|
1255
|
-
let lock;
|
|
1256
|
-
|
|
1257
|
-
try {
|
|
1258
|
-
const { isExternal } = pipelineJoinData[pid].jobs[nextJobName];
|
|
1259
|
-
|
|
1260
|
-
triggerCurrentPipelineAsExternal = triggerCurrentPipelineAsExternal || isExternal;
|
|
1261
|
-
if (!isExternal) {
|
|
1262
|
-
lock = await locker.lock(resource);
|
|
1263
|
-
|
|
1264
|
-
await triggerNextJobInSamePipeline(nextJobName, pipelineJoinData[pid].jobs);
|
|
1265
|
-
}
|
|
1266
|
-
} catch (err) {
|
|
1267
|
-
logger.error(
|
|
1268
|
-
`Error in triggerNextJobInSamePipeline:${nextJobName} from pipeline:${current.pipeline.id}-${current.job.name}-event:${current.event.id} `,
|
|
1269
|
-
err
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
await locker.unlock(lock, resource);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
if (triggerCurrentPipelineAsExternal || !isCurrentPipeline) {
|
|
1278
|
-
let resource;
|
|
1279
|
-
let lock;
|
|
1280
|
-
|
|
1281
|
-
try {
|
|
1282
|
-
if (isCurrentPipeline) {
|
|
1283
|
-
// force external trigger for jobs in same pipeline if user used external trigger syntax
|
|
1284
|
-
delete pipelineJoinData[pid].event;
|
|
1285
|
-
}
|
|
1286
|
-
const extEvent = pipelineJoinData[pid].event;
|
|
1287
|
-
|
|
1288
|
-
// no need to lock if there is no external event
|
|
1289
|
-
if (extEvent) {
|
|
1290
|
-
resource = `pipeline:${pid}:event:${extEvent.id}`;
|
|
1291
|
-
lock = await locker.lock(resource);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
await triggerJobsInExternalPipeline(pid, pipelineJoinData[pid]);
|
|
1295
|
-
} catch (err) {
|
|
1296
|
-
logger.error(
|
|
1297
|
-
`Error in triggerJobsInExternalPipeline:${pid} from pipeline:${current.pipeline.id}-${current.job.name}-event:${current.event.id} `,
|
|
1298
|
-
err
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
|
|
1302
|
-
await locker.unlock(lock, resource);
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
return null;
|
|
1307
|
-
});
|
|
344
|
+
server.expose('triggerNextJobs', triggerNextJobs);
|
|
1308
345
|
|
|
1309
346
|
server.route([
|
|
1310
347
|
getRoute(),
|