screwdriver-api 4.1.181 → 4.1.185
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/bin/server +7 -0
- package/config/custom-environment-variables.yaml +5 -0
- package/config/default.yaml +4 -0
- package/lib/registerPlugins.js +2 -1
- package/lib/server.js +2 -1
- package/package.json +1 -1
- package/plugins/builds/index.js +6 -4
- package/plugins/events/README.md +3 -0
- package/plugins/events/create.js +1 -1
- package/plugins/events/listBuilds.js +26 -3
- package/plugins/jobs/README.md +2 -0
- package/plugins/jobs/listBuilds.js +25 -4
- package/plugins/processHooks/README.md +33 -0
- package/plugins/processHooks/index.js +47 -0
- package/plugins/webhooks/helper.js +1192 -0
- package/plugins/webhooks/index.js +18 -1158
|
@@ -0,0 +1,1192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const workflowParser = require('screwdriver-workflow-parser');
|
|
4
|
+
const schema = require('screwdriver-data-schema');
|
|
5
|
+
const logger = require('screwdriver-logger');
|
|
6
|
+
const { getReadOnlyInfo } = require('../helper');
|
|
7
|
+
|
|
8
|
+
const ANNOT_NS = 'screwdriver.cd';
|
|
9
|
+
const ANNOT_CHAIN_PR = `${ANNOT_NS}/chainPR`;
|
|
10
|
+
const ANNOT_RESTRICT_PR = `${ANNOT_NS}/restrictPR`;
|
|
11
|
+
const EXTRA_TRIGGERS = schema.config.regex.EXTRA_TRIGGER;
|
|
12
|
+
const CHECKOUT_URL_SCHEMA = schema.config.regex.CHECKOUT_URL;
|
|
13
|
+
const CHECKOUT_URL_SCHEMA_REGEXP = new RegExp(CHECKOUT_URL_SCHEMA);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if tag or release filtering is enabled or not
|
|
17
|
+
* @param {String} action SCM webhook action type
|
|
18
|
+
* @param {Array} workflowGraph pipeline workflowGraph
|
|
19
|
+
* @returns {Boolean} isFilteringEnabled
|
|
20
|
+
*/
|
|
21
|
+
function isReleaseOrTagFilteringEnabled(action, workflowGraph) {
|
|
22
|
+
let isFilteringEnabled = true;
|
|
23
|
+
|
|
24
|
+
workflowGraph.edges.forEach(edge => {
|
|
25
|
+
const releaseOrTagRegExp = action === 'release' ? new RegExp('^~(release)$') : new RegExp('^~(tag)$');
|
|
26
|
+
|
|
27
|
+
if (edge.src.match(releaseOrTagRegExp)) {
|
|
28
|
+
isFilteringEnabled = false;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return isFilteringEnabled;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Determine "startFrom" with type, action and branches
|
|
36
|
+
* @param {String} action SCM webhook action type
|
|
37
|
+
* @param {String} type Triggered SCM event type ('pr' or 'repo')
|
|
38
|
+
* @param {String} targetBranch The branch against which commit is pushed
|
|
39
|
+
* @param {String} pipelineBranch The pipeline branch
|
|
40
|
+
* @param {String} releaseName SCM webhook release name
|
|
41
|
+
* @param {String} tagName SCM webhook tag name
|
|
42
|
+
* @param {Boolean} isReleaseOrTagFiltering If the tag or release filtering is enabled
|
|
43
|
+
* @returns {String} startFrom
|
|
44
|
+
*/
|
|
45
|
+
function determineStartFrom(action, type, targetBranch, pipelineBranch, releaseName, tagName, isReleaseOrTagFiltering) {
|
|
46
|
+
let startFrom;
|
|
47
|
+
|
|
48
|
+
if (type && type === 'pr') {
|
|
49
|
+
startFrom = '~pr';
|
|
50
|
+
} else {
|
|
51
|
+
switch (action) {
|
|
52
|
+
case 'release':
|
|
53
|
+
return releaseName && isReleaseOrTagFiltering ? `~release:${releaseName}` : '~release';
|
|
54
|
+
case 'tag':
|
|
55
|
+
if (!tagName) {
|
|
56
|
+
logger.error('The ref of SCM Webhook is missing.');
|
|
57
|
+
|
|
58
|
+
return '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return isReleaseOrTagFiltering ? `~tag:${tagName}` : '~tag';
|
|
62
|
+
default:
|
|
63
|
+
startFrom = '~commit';
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return targetBranch !== pipelineBranch ? `${startFrom}:${targetBranch}` : startFrom;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Update admins array
|
|
73
|
+
* @param {UserFactory} userFactory UserFactory object
|
|
74
|
+
* @param {String} username Username of user
|
|
75
|
+
* @param {String} scmContext Scm which pipeline's repository exists in
|
|
76
|
+
* @param {Pipeline} pipeline Pipeline object
|
|
77
|
+
* @param {PipelineFactory}pipelineFactory PipelineFactory object
|
|
78
|
+
* @return {Promise} Updates the pipeline admins and throws an error if not an admin
|
|
79
|
+
*/
|
|
80
|
+
async function updateAdmins(userFactory, username, scmContext, pipeline, pipelineFactory) {
|
|
81
|
+
const { readOnlyEnabled } = getReadOnlyInfo({ scm: pipelineFactory.scm, scmContext });
|
|
82
|
+
|
|
83
|
+
// Skip update admins if read-only pipeline
|
|
84
|
+
if (readOnlyEnabled) {
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const user = await userFactory.get({ username, scmContext });
|
|
90
|
+
const userPermissions = await user.getPermissions(pipeline.scmUri);
|
|
91
|
+
const newAdmins = pipeline.admins;
|
|
92
|
+
|
|
93
|
+
// Delete user from admin list if bad permissions
|
|
94
|
+
if (!userPermissions.push) {
|
|
95
|
+
delete newAdmins[username];
|
|
96
|
+
// This is needed to make admins dirty and update db
|
|
97
|
+
pipeline.admins = newAdmins;
|
|
98
|
+
|
|
99
|
+
return pipeline.update();
|
|
100
|
+
}
|
|
101
|
+
// Add user as admin if permissions good and does not already exist
|
|
102
|
+
if (!pipeline.admins[username]) {
|
|
103
|
+
newAdmins[username] = true;
|
|
104
|
+
// This is needed to make admins dirty and update db
|
|
105
|
+
pipeline.admins = newAdmins;
|
|
106
|
+
|
|
107
|
+
return pipeline.update();
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.info(err.message);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Update admins for an array of pipelines
|
|
118
|
+
* @param {Object} config.userFactory UserFactory
|
|
119
|
+
* @param {Array} config.pipelines An array of pipelines
|
|
120
|
+
* @param {String} config.username Username
|
|
121
|
+
* @param {String} config.scmContext ScmContext
|
|
122
|
+
* @param {PipelineFactory} config.pipelineFactory PipelineFactory object
|
|
123
|
+
* @return {Promise}
|
|
124
|
+
*/
|
|
125
|
+
async function batchUpdateAdmins({ userFactory, pipelines, username, scmContext, pipelineFactory }) {
|
|
126
|
+
await Promise.all(
|
|
127
|
+
pipelines.map(pipeline => updateAdmins(userFactory, username, scmContext, pipeline, pipelineFactory))
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if the PR is being restricted or not
|
|
133
|
+
* @method isRestrictedPR
|
|
134
|
+
* @param {String} restriction Is the pipeline restricting PR based on origin
|
|
135
|
+
* @param {String} prSource Origin of the PR
|
|
136
|
+
* @return {Boolean} Should the build be restricted
|
|
137
|
+
*/
|
|
138
|
+
function isRestrictedPR(restriction, prSource) {
|
|
139
|
+
switch (restriction) {
|
|
140
|
+
case 'all':
|
|
141
|
+
return true;
|
|
142
|
+
case 'branch':
|
|
143
|
+
case 'fork':
|
|
144
|
+
return prSource === restriction;
|
|
145
|
+
case 'none':
|
|
146
|
+
default:
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Stop a job by stopping all the builds associated with it
|
|
153
|
+
* If the build is running, set state to ABORTED
|
|
154
|
+
* @method stopJob
|
|
155
|
+
* @param {Object} config
|
|
156
|
+
* @param {String} config.action Event action ('Closed' or 'Synchronized')
|
|
157
|
+
* @param {Job} config.job Job to stop
|
|
158
|
+
* @param {String} config.prNum Pull request number
|
|
159
|
+
* @return {Promise}
|
|
160
|
+
*/
|
|
161
|
+
function stopJob({ job, prNum, action }) {
|
|
162
|
+
const stopRunningBuild = build => {
|
|
163
|
+
if (build.isDone()) {
|
|
164
|
+
return Promise.resolve();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const statusMessage =
|
|
168
|
+
action === 'Closed'
|
|
169
|
+
? `Aborted because PR#${prNum} was closed`
|
|
170
|
+
: `Aborted because new commit was pushed to PR#${prNum}`;
|
|
171
|
+
|
|
172
|
+
build.status = 'ABORTED';
|
|
173
|
+
build.statusMessage = statusMessage;
|
|
174
|
+
|
|
175
|
+
return build.update();
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
job
|
|
180
|
+
.getRunningBuilds()
|
|
181
|
+
// Stop running builds
|
|
182
|
+
.then(builds => Promise.all(builds.map(stopRunningBuild)))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if the pipeline has a triggered job or not
|
|
188
|
+
* @method hasTriggeredJob
|
|
189
|
+
* @param {Pipeline} pipeline The pipeline to check
|
|
190
|
+
* @param {String} startFrom The trigger name
|
|
191
|
+
* @returns {Boolean} True if the pipeline contains the triggered job
|
|
192
|
+
*/
|
|
193
|
+
function hasTriggeredJob(pipeline, startFrom) {
|
|
194
|
+
try {
|
|
195
|
+
const nextJobs = workflowParser.getNextJobs(pipeline.workflowGraph, {
|
|
196
|
+
trigger: startFrom
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return nextJobs.length > 0;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
logger.error(`Error finding triggered jobs for ${pipeline.id}: ${err}`);
|
|
202
|
+
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Check if changedFiles are under rootDir. If no custom rootDir, return true.
|
|
209
|
+
* @param {Object} pipeline
|
|
210
|
+
* @param {Array} changedFiles
|
|
211
|
+
* @return {Boolean}
|
|
212
|
+
*/
|
|
213
|
+
function hasChangesUnderRootDir(pipeline, changedFiles) {
|
|
214
|
+
const splitUri = pipeline.scmUri.split(':');
|
|
215
|
+
const rootDir = splitUri.length > 3 ? splitUri[3] : '';
|
|
216
|
+
const changes = changedFiles || [];
|
|
217
|
+
|
|
218
|
+
// Only check if rootDir is set
|
|
219
|
+
if (rootDir) {
|
|
220
|
+
return changes.some(file => file.startsWith(rootDir));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Resolve ChainPR flag
|
|
228
|
+
* @method resolveChainPR
|
|
229
|
+
* @param {Boolean} chainPR Plugin Chain PR flag
|
|
230
|
+
* @param {Pipeline} pipeline Pipeline
|
|
231
|
+
* @param {Object} pipeline.annotations Pipeline-level annotations
|
|
232
|
+
* @return {Boolean}
|
|
233
|
+
*/
|
|
234
|
+
function resolveChainPR(chainPR, pipeline) {
|
|
235
|
+
const defaultChainPR = typeof chainPR === 'undefined' ? false : chainPR;
|
|
236
|
+
const annotChainPR = pipeline.annotations[ANNOT_CHAIN_PR];
|
|
237
|
+
|
|
238
|
+
return typeof annotChainPR === 'undefined' ? defaultChainPR : annotChainPR;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns an object with resolvedChainPR and skipMessage
|
|
243
|
+
* @param {Object} config.pipeline Pipeline
|
|
244
|
+
* @param {String} config.prSource The origin of this PR
|
|
245
|
+
* @param {String} config.restrictPR Restrict PR setting
|
|
246
|
+
* @param {Boolean} config.chainPR Chain PR flag
|
|
247
|
+
* @return {Object}
|
|
248
|
+
*/
|
|
249
|
+
function getSkipMessageAndChainPR({ pipeline, prSource, restrictPR, chainPR }) {
|
|
250
|
+
const defaultRestrictPR = restrictPR || 'none';
|
|
251
|
+
const restriction = pipeline.annotations[ANNOT_RESTRICT_PR] || defaultRestrictPR;
|
|
252
|
+
const result = {
|
|
253
|
+
resolvedChainPR: resolveChainPR(chainPR, pipeline)
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Check for restriction upfront
|
|
257
|
+
if (isRestrictedPR(restriction, prSource)) {
|
|
258
|
+
result.skipMessage = `Skipping build since pipeline is configured to restrict ${restriction} and PR is ${prSource}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Returns the uri keeping only the host and the repo ID
|
|
266
|
+
* @method uriTrimmer
|
|
267
|
+
* @param {String} uri The uri to be trimmed
|
|
268
|
+
* @return {String}
|
|
269
|
+
*/
|
|
270
|
+
const uriTrimmer = uri => {
|
|
271
|
+
const uriToArray = uri.split(':');
|
|
272
|
+
|
|
273
|
+
while (uriToArray.length > 2) uriToArray.pop();
|
|
274
|
+
|
|
275
|
+
return uriToArray.join(':');
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get all pipelines which has triggered job
|
|
280
|
+
* @method triggeredPipelines
|
|
281
|
+
* @param {PipelineFactory} pipelineFactory The pipeline factory to get the branch list from
|
|
282
|
+
* @param {Object} scmConfig Has the token and scmUri to get branches
|
|
283
|
+
* @param {String} branch The branch which is committed
|
|
284
|
+
* @param {String} type Triggered GitHub event type ('pr' or 'repo')
|
|
285
|
+
* @param {String} action Triggered GitHub event action
|
|
286
|
+
* @param {Array} changedFiles Changed files in this commit
|
|
287
|
+
* @param {String} releaseName SCM webhook release name
|
|
288
|
+
* @param {String} tagName SCM webhook tag name
|
|
289
|
+
* @returns {Promise} Promise that resolves into triggered pipelines
|
|
290
|
+
*/
|
|
291
|
+
async function triggeredPipelines(
|
|
292
|
+
pipelineFactory,
|
|
293
|
+
scmConfig,
|
|
294
|
+
branch,
|
|
295
|
+
type,
|
|
296
|
+
action,
|
|
297
|
+
changedFiles,
|
|
298
|
+
releaseName,
|
|
299
|
+
tagName
|
|
300
|
+
) {
|
|
301
|
+
const { scmUri } = scmConfig;
|
|
302
|
+
const splitUri = scmUri.split(':');
|
|
303
|
+
const scmBranch = `${splitUri[0]}:${splitUri[1]}:${splitUri[2]}`;
|
|
304
|
+
const scmRepoId = `${splitUri[0]}:${splitUri[1]}`;
|
|
305
|
+
const listConfig = { search: { field: 'scmUri', keyword: `${scmRepoId}:%` } };
|
|
306
|
+
const externalRepoSearchConfig = { search: { field: 'subscribedScmUrlsWithActions', keyword: `%${scmRepoId}:%` } };
|
|
307
|
+
|
|
308
|
+
const pipelines = await pipelineFactory.list(listConfig);
|
|
309
|
+
|
|
310
|
+
const pipelinesWithSubscribedRepos = await pipelineFactory.list(externalRepoSearchConfig);
|
|
311
|
+
|
|
312
|
+
let pipelinesOnCommitBranch = [];
|
|
313
|
+
let pipelinesOnOtherBranch = [];
|
|
314
|
+
|
|
315
|
+
pipelines.forEach(p => {
|
|
316
|
+
// This uri expects 'scmUriDomain:repoId:branchName:rootDir'. To Compare, rootDir is ignored.
|
|
317
|
+
const splitScmUri = p.scmUri.split(':');
|
|
318
|
+
const pipelineScmBranch = `${splitScmUri[0]}:${splitScmUri[1]}:${splitScmUri[2]}`;
|
|
319
|
+
|
|
320
|
+
if (pipelineScmBranch === scmBranch) {
|
|
321
|
+
pipelinesOnCommitBranch.push(p);
|
|
322
|
+
} else {
|
|
323
|
+
pipelinesOnOtherBranch.push(p);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Build runs regardless of changedFiles when release/tag trigger
|
|
328
|
+
pipelinesOnCommitBranch = pipelinesOnCommitBranch.filter(
|
|
329
|
+
p => ['release', 'tag'].includes(action) || hasChangesUnderRootDir(p, changedFiles)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
pipelinesOnOtherBranch = pipelinesOnOtherBranch.filter(p => {
|
|
333
|
+
let isReleaseOrTagFiltering = '';
|
|
334
|
+
|
|
335
|
+
if (action === 'release' || action === 'tag') {
|
|
336
|
+
isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, p.workflowGraph);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return hasTriggeredJob(
|
|
340
|
+
p,
|
|
341
|
+
determineStartFrom(action, type, branch, null, releaseName, tagName, isReleaseOrTagFiltering)
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const currentRepoPipelines = pipelinesOnCommitBranch.concat(pipelinesOnOtherBranch);
|
|
346
|
+
|
|
347
|
+
return currentRepoPipelines.concat(pipelinesWithSubscribedRepos);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Create events for each pipeline
|
|
352
|
+
* @async createPREvents
|
|
353
|
+
* @param {Object} options
|
|
354
|
+
* @param {String} options.username User who created the PR
|
|
355
|
+
* @param {String} options.scmConfig Has the token and scmUri to get branches
|
|
356
|
+
* @param {String} options.sha Specific SHA1 commit to start the build with
|
|
357
|
+
* @param {String} options.prRef Reference to pull request
|
|
358
|
+
* @param {String} options.prNum Pull request number
|
|
359
|
+
* @param {String} options.prTitle Pull request title
|
|
360
|
+
* @param {Array} options.changedFiles List of changed files
|
|
361
|
+
* @param {String} options.branch The branch against which pr is opened
|
|
362
|
+
* @param {String} options.action Event action
|
|
363
|
+
* @param {String} options.prSource The origin of this PR
|
|
364
|
+
* @param {String} options.restrictPR Restrict PR setting
|
|
365
|
+
* @param {Boolean} options.chainPR Chain PR flag
|
|
366
|
+
* @param {Hapi.request} request Request from user
|
|
367
|
+
* @return {Promise}
|
|
368
|
+
*/
|
|
369
|
+
async function createPREvents(options, request) {
|
|
370
|
+
const {
|
|
371
|
+
username,
|
|
372
|
+
scmConfig,
|
|
373
|
+
prRef,
|
|
374
|
+
prNum,
|
|
375
|
+
pipelines,
|
|
376
|
+
prTitle,
|
|
377
|
+
changedFiles,
|
|
378
|
+
branch,
|
|
379
|
+
action,
|
|
380
|
+
prSource,
|
|
381
|
+
restrictPR,
|
|
382
|
+
chainPR,
|
|
383
|
+
ref,
|
|
384
|
+
releaseName,
|
|
385
|
+
meta
|
|
386
|
+
} = options;
|
|
387
|
+
const { scm } = request.server.app.pipelineFactory;
|
|
388
|
+
const { eventFactory, pipelineFactory, userFactory } = request.server.app;
|
|
389
|
+
const scmDisplayName = scm.getDisplayName({ scmContext: scmConfig.scmContext });
|
|
390
|
+
const userDisplayName = `${scmDisplayName}:${username}`;
|
|
391
|
+
const events = [];
|
|
392
|
+
let { sha } = options;
|
|
393
|
+
|
|
394
|
+
scmConfig.prNum = prNum;
|
|
395
|
+
|
|
396
|
+
const eventConfigs = await Promise.all(
|
|
397
|
+
pipelines.map(async p => {
|
|
398
|
+
try {
|
|
399
|
+
const b = await p.branch;
|
|
400
|
+
// obtain pipeline's latest commit sha for branch specific job
|
|
401
|
+
let configPipelineSha = '';
|
|
402
|
+
let subscribedConfigSha = '';
|
|
403
|
+
let eventConfig = {};
|
|
404
|
+
|
|
405
|
+
// Check if the webhook event is from a subscribed repo and
|
|
406
|
+
// and fetch the source repo commit sha and save the subscribed sha
|
|
407
|
+
if (uriTrimmer(scmConfig.scmUri) !== uriTrimmer(p.scmUri)) {
|
|
408
|
+
subscribedConfigSha = sha;
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
sha = await pipelineFactory.scm.getCommitSha({
|
|
412
|
+
scmUri: p.scmUri,
|
|
413
|
+
scmContext: scmConfig.scmContext,
|
|
414
|
+
token: scmConfig.token
|
|
415
|
+
});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
if (err.status >= 500) {
|
|
418
|
+
throw err;
|
|
419
|
+
} else {
|
|
420
|
+
logger.info(`skip create event for branch: ${b}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
configPipelineSha = sha;
|
|
425
|
+
} else {
|
|
426
|
+
try {
|
|
427
|
+
configPipelineSha = await pipelineFactory.scm.getCommitSha(scmConfig);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
if (err.status >= 500) {
|
|
430
|
+
throw err;
|
|
431
|
+
} else {
|
|
432
|
+
logger.info(`skip create event for branch: ${b}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const { skipMessage, resolvedChainPR } = getSkipMessageAndChainPR({
|
|
438
|
+
// Workaround for pipelines which has NULL value in `pipeline.annotations`
|
|
439
|
+
pipeline: !p.annotations ? { annotations: {}, ...p } : p,
|
|
440
|
+
prSource,
|
|
441
|
+
restrictPR,
|
|
442
|
+
chainPR
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const prInfo = await eventFactory.scm.getPrInfo(scmConfig);
|
|
446
|
+
|
|
447
|
+
eventConfig = {
|
|
448
|
+
pipelineId: p.id,
|
|
449
|
+
type: 'pr',
|
|
450
|
+
webhooks: true,
|
|
451
|
+
username,
|
|
452
|
+
scmContext: scmConfig.scmContext,
|
|
453
|
+
sha,
|
|
454
|
+
configPipelineSha,
|
|
455
|
+
startFrom: `~pr:${branch}`,
|
|
456
|
+
changedFiles,
|
|
457
|
+
causeMessage: `${action} by ${userDisplayName}`,
|
|
458
|
+
chainPR: resolvedChainPR,
|
|
459
|
+
prRef,
|
|
460
|
+
prNum,
|
|
461
|
+
prTitle,
|
|
462
|
+
prInfo,
|
|
463
|
+
prSource,
|
|
464
|
+
baseBranch: branch
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
if (b === branch) {
|
|
468
|
+
eventConfig.startFrom = '~pr';
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Check if the webhook event is from a subscribed repo and
|
|
472
|
+
// set the jobs entrypoint from ~startFrom
|
|
473
|
+
// For subscribed PR event, it should be mimicked as a commit
|
|
474
|
+
// in order to function properly
|
|
475
|
+
if (uriTrimmer(scmConfig.scmUri) !== uriTrimmer(p.scmUri)) {
|
|
476
|
+
eventConfig = {
|
|
477
|
+
pipelineId: p.id,
|
|
478
|
+
type: 'pipeline',
|
|
479
|
+
webhooks: true,
|
|
480
|
+
username,
|
|
481
|
+
scmContext: scmConfig.scmContext,
|
|
482
|
+
startFrom: '~subscribe',
|
|
483
|
+
sha,
|
|
484
|
+
configPipelineSha,
|
|
485
|
+
changedFiles,
|
|
486
|
+
baseBranch: branch,
|
|
487
|
+
causeMessage: `Merged by ${username}`,
|
|
488
|
+
meta,
|
|
489
|
+
releaseName,
|
|
490
|
+
ref,
|
|
491
|
+
subscribedEvent: true,
|
|
492
|
+
subscribedConfigSha,
|
|
493
|
+
subscribedSourceUrl: prInfo.url
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
await updateAdmins(userFactory, username, scmConfig.scmContext, p.id, pipelineFactory);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (skipMessage) {
|
|
500
|
+
eventConfig.skipMessage = skipMessage;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return eventConfig;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
logger.warn(`pipeline:${p.id} error in starting event`, err);
|
|
506
|
+
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
})
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
eventConfigs.forEach(eventConfig => {
|
|
513
|
+
if (eventConfig && eventConfig.configPipelineSha) {
|
|
514
|
+
events.push(eventFactory.create(eventConfig));
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
return Promise.all(events);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Stop all the relevant PR jobs for an array of pipelines
|
|
523
|
+
* @async batchStopJobs
|
|
524
|
+
* @param {Array} config.pipelines An array of pipeline
|
|
525
|
+
* @param {Integer} config.prNum PR number
|
|
526
|
+
* @param {String} config.action Event action
|
|
527
|
+
* @param {String} config.name Prefix of the PR job name: PR-prNum
|
|
528
|
+
*/
|
|
529
|
+
async function batchStopJobs({ pipelines, prNum, action, name }) {
|
|
530
|
+
const prJobs = await Promise.all(
|
|
531
|
+
pipelines.map(p => p.getJobs({ type: 'pr' }).then(jobs => jobs.filter(j => j.name.includes(name))))
|
|
532
|
+
);
|
|
533
|
+
const flatPRJobs = prJobs.reduce((prev, curr) => prev.concat(curr));
|
|
534
|
+
|
|
535
|
+
await Promise.all(flatPRJobs.map(j => stopJob({ job: j, prNum, action })));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Create a new job and start the build for an opened pull-request
|
|
540
|
+
* @async pullRequestOpened
|
|
541
|
+
* @param {Object} options
|
|
542
|
+
* @param {String} options.hookId Unique ID for this scm event
|
|
543
|
+
* @param {String} options.prSource The origin of this PR
|
|
544
|
+
* @param {Pipeline} options.pipeline Pipeline model for the pr
|
|
545
|
+
* @param {String} options.restrictPR Restrict PR setting
|
|
546
|
+
* @param {Boolean} options.chainPR Chain PR flag
|
|
547
|
+
* @param {Hapi.request} request Request from user
|
|
548
|
+
* @param {Hapi.h} h Response toolkit
|
|
549
|
+
*/
|
|
550
|
+
async function pullRequestOpened(options, request, h) {
|
|
551
|
+
const { hookId } = options;
|
|
552
|
+
|
|
553
|
+
return createPREvents(options, request)
|
|
554
|
+
.then(events => {
|
|
555
|
+
events.forEach(e => {
|
|
556
|
+
request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
return h.response().code(201);
|
|
560
|
+
})
|
|
561
|
+
.catch(err => {
|
|
562
|
+
logger.error(
|
|
563
|
+
`Failed to pullRequestOpened: [${hookId}, pipeline:${options.pipeline && options.pipeline.id}]: ${err}`
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
throw err;
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Stop any running builds and disable the job for closed pull-request
|
|
572
|
+
* @async pullRequestClosed
|
|
573
|
+
* @param {Object} options
|
|
574
|
+
* @param {String} options.hookId Unique ID for this scm event
|
|
575
|
+
* @param {Pipeline} options.pipeline Pipeline model for the pr
|
|
576
|
+
* @param {String} options.name Name of the PR: PR-prNum
|
|
577
|
+
* @param {String} options.prNum Pull request number
|
|
578
|
+
* @param {String} options.action Event action
|
|
579
|
+
* @param {String} options.fullCheckoutUrl CheckoutUrl with branch name
|
|
580
|
+
* @param {Hapi.request} request Request from user
|
|
581
|
+
* @param {Hapi.reply} reply Reply to user
|
|
582
|
+
*/
|
|
583
|
+
async function pullRequestClosed(options, request, h) {
|
|
584
|
+
const { pipelines, hookId, name, prNum, action } = options;
|
|
585
|
+
const updatePRJobs = job =>
|
|
586
|
+
stopJob({ job, prNum, action })
|
|
587
|
+
.then(() => request.log(['webhook', hookId, job.id], `${job.name} stopped`))
|
|
588
|
+
.then(() => {
|
|
589
|
+
job.archived = true;
|
|
590
|
+
|
|
591
|
+
return job.update();
|
|
592
|
+
})
|
|
593
|
+
.then(() => request.log(['webhook', hookId, job.id], `${job.name} disabled and archived`));
|
|
594
|
+
|
|
595
|
+
return Promise.all(
|
|
596
|
+
pipelines.map(p =>
|
|
597
|
+
p.getJobs({ type: 'pr' }).then(jobs => {
|
|
598
|
+
const prJobs = jobs.filter(j => j.name.includes(name));
|
|
599
|
+
|
|
600
|
+
return Promise.all(prJobs.map(j => updatePRJobs(j)));
|
|
601
|
+
})
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
.then(() => h.response().code(200))
|
|
605
|
+
.catch(err => {
|
|
606
|
+
logger.error(
|
|
607
|
+
`Failed to pullRequestClosed: [${hookId}, pipeline:${options.pipeline && options.pipeline.id}]: ${err}`
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
throw err;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Stop any running builds and start the build for the synchronized pull-request
|
|
616
|
+
* @async pullRequestSync
|
|
617
|
+
* @param {Object} options
|
|
618
|
+
* @param {String} options.hookId Unique ID for this scm event
|
|
619
|
+
* @param {String} options.name Name of the new job (PR-1)
|
|
620
|
+
* @param {String} options.prSource The origin of this PR
|
|
621
|
+
* @param {String} options.restrictPR Restrict PR setting
|
|
622
|
+
* @param {Boolean} options.chainPR Chain PR flag
|
|
623
|
+
* @param {Pipeline} options.pipeline Pipeline model for the pr
|
|
624
|
+
* @param {Array} options.changedFiles List of files that were changed
|
|
625
|
+
* @param {String} options.prNum Pull request number
|
|
626
|
+
* @param {String} options.action Event action
|
|
627
|
+
* @param {Hapi.request} request Request from user
|
|
628
|
+
* @param {Hapi.reply} reply Reply to user
|
|
629
|
+
*/
|
|
630
|
+
async function pullRequestSync(options, request, h) {
|
|
631
|
+
const { pipelines, hookId, name, prNum, action } = options;
|
|
632
|
+
|
|
633
|
+
await batchStopJobs({ pipelines, name, prNum, action });
|
|
634
|
+
|
|
635
|
+
request.log(['webhook', hookId], `Job(s) for ${name} stopped`);
|
|
636
|
+
|
|
637
|
+
return createPREvents(options, request)
|
|
638
|
+
.then(events => {
|
|
639
|
+
events.forEach(e => {
|
|
640
|
+
request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
return h.response().code(201);
|
|
644
|
+
})
|
|
645
|
+
.catch(err => {
|
|
646
|
+
logger.error(
|
|
647
|
+
`Failed to pullRequestSync: [${hookId}, pipeline:${options.pipeline && options.pipeline.id}]: ${err}`
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
throw err;
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Obtains the SCM token for a given user.
|
|
656
|
+
* If a user does not have a valid SCM token registered with Screwdriver,
|
|
657
|
+
* it will use a generic user's token instead.
|
|
658
|
+
* If pipeline is in read-only SCM, use read-only token.
|
|
659
|
+
* Some SCM services have different thresholds between IP requests and token requests. This is
|
|
660
|
+
* to ensure we have a token to access the SCM service without being restricted by these quotas
|
|
661
|
+
* @method obtainScmToken
|
|
662
|
+
* @param {Object} pluginOptions
|
|
663
|
+
* @param {String} pluginOptions.username Generic scm username
|
|
664
|
+
* @param {UserFactory} userFactory UserFactory object
|
|
665
|
+
* @param {String} username Name of the user that the SCM token is associated with
|
|
666
|
+
* @param {String} scmContext Scm which pipeline's repository exists in
|
|
667
|
+
* @param {Object} scm Scm
|
|
668
|
+
* @return {Promise} Promise that resolves into a SCM token
|
|
669
|
+
*/
|
|
670
|
+
async function obtainScmToken({ pluginOptions, userFactory, username, scmContext, scm }) {
|
|
671
|
+
const { readOnlyEnabled, headlessAccessToken } = getReadOnlyInfo({ scm, scmContext });
|
|
672
|
+
|
|
673
|
+
// If pipeline is in read-only SCM, use read-only token
|
|
674
|
+
if (readOnlyEnabled && headlessAccessToken) {
|
|
675
|
+
return headlessAccessToken;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const user = await userFactory.get({ username, scmContext });
|
|
679
|
+
|
|
680
|
+
// Use generic username and token
|
|
681
|
+
if (!user) {
|
|
682
|
+
const genericUsername = pluginOptions.username;
|
|
683
|
+
const buildBotUser = await userFactory.get({ username: genericUsername, scmContext });
|
|
684
|
+
|
|
685
|
+
return buildBotUser.unsealToken();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return user.unsealToken();
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Create metadata by the parsed event
|
|
693
|
+
* @param {Object} parsed It has information to create metadata
|
|
694
|
+
* @returns {Object} Metadata
|
|
695
|
+
*/
|
|
696
|
+
function createMeta(parsed) {
|
|
697
|
+
const { action, ref, releaseId, releaseName, releaseAuthor } = parsed;
|
|
698
|
+
|
|
699
|
+
if (action === 'release') {
|
|
700
|
+
return {
|
|
701
|
+
sd: {
|
|
702
|
+
release: {
|
|
703
|
+
id: releaseId,
|
|
704
|
+
name: releaseName,
|
|
705
|
+
author: releaseAuthor
|
|
706
|
+
},
|
|
707
|
+
tag: {
|
|
708
|
+
name: ref
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
if (action === 'tag') {
|
|
714
|
+
return {
|
|
715
|
+
sd: {
|
|
716
|
+
tag: {
|
|
717
|
+
name: ref
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return {};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Act on a Pull Request change (create, sync, close)
|
|
728
|
+
* - Opening a PR should sync the pipeline (creating the job) and start the new PR job
|
|
729
|
+
* - Syncing a PR should stop the existing PR job and start a new one
|
|
730
|
+
* - Closing a PR should stop the PR job and sync the pipeline (disabling the job)
|
|
731
|
+
* @method pullRequestEvent
|
|
732
|
+
* @param {Object} pluginOptions
|
|
733
|
+
* @param {String} pluginOptions.username Generic scm username
|
|
734
|
+
* @param {String} pluginOptions.restrictPR Restrict PR setting
|
|
735
|
+
* @param {Boolean} pluginOptions.chainPR Chain PR flag
|
|
736
|
+
* @param {Hapi.request} request Request from user
|
|
737
|
+
* @param {Hapi.reply} reply Reply to user
|
|
738
|
+
* @param {String} token The token used to authenticate to the SCM
|
|
739
|
+
* @param {Object} parsed
|
|
740
|
+
*/
|
|
741
|
+
function pullRequestEvent(pluginOptions, request, h, parsed, token) {
|
|
742
|
+
const { pipelineFactory, userFactory } = request.server.app;
|
|
743
|
+
const {
|
|
744
|
+
hookId,
|
|
745
|
+
action,
|
|
746
|
+
checkoutUrl,
|
|
747
|
+
branch,
|
|
748
|
+
sha,
|
|
749
|
+
prNum,
|
|
750
|
+
prTitle,
|
|
751
|
+
prRef,
|
|
752
|
+
prSource,
|
|
753
|
+
username,
|
|
754
|
+
scmContext,
|
|
755
|
+
changedFiles,
|
|
756
|
+
type,
|
|
757
|
+
releaseName,
|
|
758
|
+
ref
|
|
759
|
+
} = parsed;
|
|
760
|
+
const fullCheckoutUrl = `${checkoutUrl}#${branch}`;
|
|
761
|
+
const scmConfig = {
|
|
762
|
+
scmUri: '',
|
|
763
|
+
token,
|
|
764
|
+
scmContext
|
|
765
|
+
};
|
|
766
|
+
const { restrictPR, chainPR } = pluginOptions;
|
|
767
|
+
const meta = createMeta(parsed);
|
|
768
|
+
|
|
769
|
+
request.log(['webhook', hookId], `PR #${prNum} ${action} for ${fullCheckoutUrl}`);
|
|
770
|
+
|
|
771
|
+
return pipelineFactory.scm
|
|
772
|
+
.parseUrl({
|
|
773
|
+
checkoutUrl: fullCheckoutUrl,
|
|
774
|
+
token,
|
|
775
|
+
scmContext
|
|
776
|
+
})
|
|
777
|
+
.then(scmUri => {
|
|
778
|
+
scmConfig.scmUri = scmUri;
|
|
779
|
+
|
|
780
|
+
return triggeredPipelines(pipelineFactory, scmConfig, branch, type, action, changedFiles, releaseName, ref);
|
|
781
|
+
})
|
|
782
|
+
.then(async pipelines => {
|
|
783
|
+
if (!pipelines || pipelines.length === 0) {
|
|
784
|
+
const message = `Skipping since Pipeline triggered by PRs against ${fullCheckoutUrl} does not exist`;
|
|
785
|
+
|
|
786
|
+
request.log(['webhook', hookId], message);
|
|
787
|
+
|
|
788
|
+
return h.response({ message }).code(204);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const options = {
|
|
792
|
+
name: `PR-${prNum}`,
|
|
793
|
+
hookId,
|
|
794
|
+
sha,
|
|
795
|
+
username,
|
|
796
|
+
scmConfig,
|
|
797
|
+
prRef,
|
|
798
|
+
prNum,
|
|
799
|
+
prTitle,
|
|
800
|
+
prSource,
|
|
801
|
+
changedFiles,
|
|
802
|
+
action: action.charAt(0).toUpperCase() + action.slice(1),
|
|
803
|
+
branch,
|
|
804
|
+
fullCheckoutUrl,
|
|
805
|
+
restrictPR,
|
|
806
|
+
chainPR,
|
|
807
|
+
pipelines,
|
|
808
|
+
ref,
|
|
809
|
+
releaseName,
|
|
810
|
+
meta
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
await batchUpdateAdmins({ userFactory, pipelines, username, scmContext, pipelineFactory });
|
|
814
|
+
|
|
815
|
+
switch (action) {
|
|
816
|
+
case 'opened':
|
|
817
|
+
case 'reopened':
|
|
818
|
+
return pullRequestOpened(options, request, h);
|
|
819
|
+
case 'synchronized':
|
|
820
|
+
return pullRequestSync(options, request, h);
|
|
821
|
+
case 'closed':
|
|
822
|
+
default:
|
|
823
|
+
return pullRequestClosed(options, request, h);
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
.catch(err => {
|
|
827
|
+
logger.error(`[${hookId}]: ${err}`);
|
|
828
|
+
|
|
829
|
+
throw err;
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Create events for each pipeline
|
|
835
|
+
* @async createEvents
|
|
836
|
+
* @param {EventFactory} eventFactory To create event
|
|
837
|
+
* @param {UserFactory} userFactory To get user permission
|
|
838
|
+
* @param {PipelineFactory} pipelineFactory To use scm module
|
|
839
|
+
* @param {Array} pipelines The pipelines to start events
|
|
840
|
+
* @param {Object} parsed It has information to create event
|
|
841
|
+
* @param {String} [skipMessage] Message to skip starting builds
|
|
842
|
+
* @returns {Promise} Promise that resolves into events
|
|
843
|
+
*/
|
|
844
|
+
async function createEvents(
|
|
845
|
+
eventFactory,
|
|
846
|
+
userFactory,
|
|
847
|
+
pipelineFactory,
|
|
848
|
+
pipelines,
|
|
849
|
+
parsed,
|
|
850
|
+
skipMessage,
|
|
851
|
+
scmConfigFromHook
|
|
852
|
+
) {
|
|
853
|
+
const { action, branch, sha, username, scmContext, changedFiles, type, releaseName, ref } = parsed;
|
|
854
|
+
const events = [];
|
|
855
|
+
const meta = createMeta(parsed);
|
|
856
|
+
|
|
857
|
+
const pipelineTuples = await Promise.all(
|
|
858
|
+
pipelines.map(async p => {
|
|
859
|
+
const resolvedBranch = await p.branch;
|
|
860
|
+
let isReleaseOrTagFiltering = '';
|
|
861
|
+
|
|
862
|
+
if (action === 'release' || action === 'tag') {
|
|
863
|
+
isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, p.workflowGraph);
|
|
864
|
+
}
|
|
865
|
+
const startFrom = determineStartFrom(
|
|
866
|
+
action,
|
|
867
|
+
type,
|
|
868
|
+
branch,
|
|
869
|
+
resolvedBranch,
|
|
870
|
+
releaseName,
|
|
871
|
+
ref,
|
|
872
|
+
isReleaseOrTagFiltering
|
|
873
|
+
);
|
|
874
|
+
const tuple = { branch: resolvedBranch, pipeline: p, startFrom };
|
|
875
|
+
|
|
876
|
+
return tuple;
|
|
877
|
+
})
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
const ignoreExtraTriggeredPipelines = pipelineTuples.filter(t => {
|
|
881
|
+
// empty event is not created when it is triggered by extra triggers (e.g. ~tag, ~release)
|
|
882
|
+
if (EXTRA_TRIGGERS.test(t.startFrom) && !hasTriggeredJob(t.pipeline, t.startFrom)) {
|
|
883
|
+
logger.warn(`Event not created: there are no jobs triggered by ${t.startFrom}`);
|
|
884
|
+
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return true;
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const eventConfigs = await Promise.all(
|
|
892
|
+
ignoreExtraTriggeredPipelines.map(async pTuple => {
|
|
893
|
+
try {
|
|
894
|
+
const pipelineBranch = pTuple.branch;
|
|
895
|
+
let isReleaseOrTagFiltering = '';
|
|
896
|
+
|
|
897
|
+
if (action === 'release' || action === 'tag') {
|
|
898
|
+
isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, pTuple.pipeline.workflowGraph);
|
|
899
|
+
}
|
|
900
|
+
const startFrom = determineStartFrom(
|
|
901
|
+
action,
|
|
902
|
+
type,
|
|
903
|
+
branch,
|
|
904
|
+
pipelineBranch,
|
|
905
|
+
releaseName,
|
|
906
|
+
ref,
|
|
907
|
+
isReleaseOrTagFiltering
|
|
908
|
+
);
|
|
909
|
+
const token = await pTuple.pipeline.token;
|
|
910
|
+
const scmConfig = {
|
|
911
|
+
scmUri: pTuple.pipeline.scmUri,
|
|
912
|
+
token,
|
|
913
|
+
scmContext
|
|
914
|
+
};
|
|
915
|
+
// obtain pipeline's latest commit sha for branch specific job
|
|
916
|
+
let configPipelineSha = '';
|
|
917
|
+
|
|
918
|
+
try {
|
|
919
|
+
configPipelineSha = await pipelineFactory.scm.getCommitSha(scmConfig);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
if (err.status >= 500) {
|
|
922
|
+
throw err;
|
|
923
|
+
} else {
|
|
924
|
+
logger.info(`skip create event for branch: ${pipelineBranch}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
const eventConfig = {
|
|
928
|
+
pipelineId: pTuple.pipeline.id,
|
|
929
|
+
type: 'pipeline',
|
|
930
|
+
webhooks: true,
|
|
931
|
+
username,
|
|
932
|
+
scmContext,
|
|
933
|
+
startFrom,
|
|
934
|
+
sha,
|
|
935
|
+
configPipelineSha,
|
|
936
|
+
changedFiles,
|
|
937
|
+
baseBranch: branch,
|
|
938
|
+
causeMessage: `Merged by ${username}`,
|
|
939
|
+
meta,
|
|
940
|
+
releaseName,
|
|
941
|
+
ref
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
// Check is the webhook event is from a subscribed repo and
|
|
945
|
+
// set the jobs entry point to ~subscribe
|
|
946
|
+
if (uriTrimmer(scmConfigFromHook.scmUri) !== uriTrimmer(pTuple.pipeline.scmUri)) {
|
|
947
|
+
eventConfig.subscribedEvent = true;
|
|
948
|
+
eventConfig.startFrom = '~subscribe';
|
|
949
|
+
eventConfig.subscribedConfigSha = eventConfig.sha;
|
|
950
|
+
|
|
951
|
+
try {
|
|
952
|
+
eventConfig.sha = await pipelineFactory.scm.getCommitSha(scmConfig);
|
|
953
|
+
} catch (err) {
|
|
954
|
+
if (err.status >= 500) {
|
|
955
|
+
throw err;
|
|
956
|
+
} else {
|
|
957
|
+
logger.info(`skip create event for this subscribed trigger`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
try {
|
|
962
|
+
const commitInfo = await pipelineFactory.scm.decorateCommit({
|
|
963
|
+
scmUri: scmConfigFromHook.scmUri,
|
|
964
|
+
scmContext,
|
|
965
|
+
sha: eventConfig.subscribedConfigSha,
|
|
966
|
+
token
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
eventConfig.subscribedSourceUrl = commitInfo.url;
|
|
970
|
+
} catch (err) {
|
|
971
|
+
if (err.status >= 500) {
|
|
972
|
+
throw err;
|
|
973
|
+
} else {
|
|
974
|
+
logger.info(`skip create event for this subscribed trigger`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (skipMessage) {
|
|
980
|
+
eventConfig.skipMessage = skipMessage;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
await updateAdmins(userFactory, username, scmContext, pTuple.pipeline, pipelineFactory);
|
|
984
|
+
|
|
985
|
+
return eventConfig;
|
|
986
|
+
} catch (err) {
|
|
987
|
+
logger.warn(`pipeline:${pTuple.pipeline.id} error in starting event`, err);
|
|
988
|
+
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
eventConfigs.forEach(eventConfig => {
|
|
995
|
+
if (eventConfig && eventConfig.configPipelineSha) {
|
|
996
|
+
events.push(eventFactory.create(eventConfig));
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
return Promise.all(events);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Act on a Push event
|
|
1005
|
+
* - Should start a new main job
|
|
1006
|
+
* @method pushEvent
|
|
1007
|
+
* @param {Hapi.request} request Request from user
|
|
1008
|
+
* @param {Hapi.h} h Response toolkit
|
|
1009
|
+
* @param {Object} parsed It has information to create event
|
|
1010
|
+
* @param {String} token The token used to authenticate to the SCM
|
|
1011
|
+
* @param {String} [skipMessage] Message to skip starting builds
|
|
1012
|
+
*/
|
|
1013
|
+
async function pushEvent(request, h, parsed, skipMessage, token) {
|
|
1014
|
+
const { eventFactory, pipelineFactory, userFactory } = request.server.app;
|
|
1015
|
+
const { hookId, checkoutUrl, branch, scmContext, type, action, changedFiles, releaseName, ref } = parsed;
|
|
1016
|
+
const fullCheckoutUrl = `${checkoutUrl}#${branch}`;
|
|
1017
|
+
const scmConfig = {
|
|
1018
|
+
scmUri: '',
|
|
1019
|
+
token: '',
|
|
1020
|
+
scmContext
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
request.log(['webhook', hookId], `Push for ${fullCheckoutUrl}`);
|
|
1024
|
+
|
|
1025
|
+
try {
|
|
1026
|
+
scmConfig.token = token;
|
|
1027
|
+
scmConfig.scmUri = await pipelineFactory.scm.parseUrl({
|
|
1028
|
+
checkoutUrl: fullCheckoutUrl,
|
|
1029
|
+
token,
|
|
1030
|
+
scmContext
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
const pipelines = await triggeredPipelines(
|
|
1034
|
+
pipelineFactory,
|
|
1035
|
+
scmConfig,
|
|
1036
|
+
branch,
|
|
1037
|
+
type,
|
|
1038
|
+
action,
|
|
1039
|
+
changedFiles,
|
|
1040
|
+
releaseName,
|
|
1041
|
+
ref
|
|
1042
|
+
);
|
|
1043
|
+
let events = [];
|
|
1044
|
+
|
|
1045
|
+
if (!pipelines || pipelines.length === 0) {
|
|
1046
|
+
request.log(['webhook', hookId], `Skipping since Pipeline ${fullCheckoutUrl} does not exist`);
|
|
1047
|
+
} else {
|
|
1048
|
+
events = await createEvents(
|
|
1049
|
+
eventFactory,
|
|
1050
|
+
userFactory,
|
|
1051
|
+
pipelineFactory,
|
|
1052
|
+
pipelines,
|
|
1053
|
+
parsed,
|
|
1054
|
+
skipMessage,
|
|
1055
|
+
scmConfig
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const hasBuildEvents = events.filter(e => e.builds !== null);
|
|
1060
|
+
|
|
1061
|
+
if (hasBuildEvents.length === 0) {
|
|
1062
|
+
return h.response({ message: 'No jobs to start' }).code(204);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
hasBuildEvents.forEach(e => {
|
|
1066
|
+
request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
return h.response().code(201);
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
logger.error(`[${hookId}]: ${err}`);
|
|
1072
|
+
|
|
1073
|
+
throw err;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** Execute scm.getCommitRefSha()
|
|
1078
|
+
* @method getCommitRefSha
|
|
1079
|
+
* @param {Object} scm
|
|
1080
|
+
* @param {String} token The token used to authenticate to the SCM
|
|
1081
|
+
* @param {String} ref The reference which we want
|
|
1082
|
+
* @param {String} checkoutUrl Scm checkout URL
|
|
1083
|
+
* @param {String} scmContext Scm which pipeline's repository exists in
|
|
1084
|
+
* @returns {Promise} Specific SHA1 commit to start the build with
|
|
1085
|
+
*/
|
|
1086
|
+
async function getCommitRefSha({ scm, token, ref, refType, checkoutUrl, scmContext }) {
|
|
1087
|
+
// For example, git@github.com:screwdriver-cd/data-schema.git => screwdriver-cd, data-schema
|
|
1088
|
+
const owner = CHECKOUT_URL_SCHEMA_REGEXP.exec(checkoutUrl)[2];
|
|
1089
|
+
const repo = CHECKOUT_URL_SCHEMA_REGEXP.exec(checkoutUrl)[3];
|
|
1090
|
+
|
|
1091
|
+
return scm.getCommitRefSha({
|
|
1092
|
+
token,
|
|
1093
|
+
owner,
|
|
1094
|
+
repo,
|
|
1095
|
+
ref,
|
|
1096
|
+
refType,
|
|
1097
|
+
scmContext
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Start pipeline events with scm webhook config
|
|
1103
|
+
* @method startHookEvent
|
|
1104
|
+
* @param {Hapi.request} request Request from user
|
|
1105
|
+
* @param {Object} h Response toolkit
|
|
1106
|
+
* @param {Object} webhookConfig Configuration required to start events
|
|
1107
|
+
* @return {Promise}
|
|
1108
|
+
*/
|
|
1109
|
+
async function startHookEvent(request, h, webhookConfig) {
|
|
1110
|
+
const { userFactory, pipelineFactory } = request.server.app;
|
|
1111
|
+
const { scm } = pipelineFactory;
|
|
1112
|
+
const ignoreUser = webhookConfig.pluginOptions.ignoreCommitsBy;
|
|
1113
|
+
let message = 'Unable to process this kind of event';
|
|
1114
|
+
let skipMessage;
|
|
1115
|
+
let parsedHookId = '';
|
|
1116
|
+
|
|
1117
|
+
const { type, hookId, username, scmContext, ref, checkoutUrl, action, prNum } = webhookConfig;
|
|
1118
|
+
|
|
1119
|
+
parsedHookId = hookId;
|
|
1120
|
+
|
|
1121
|
+
try {
|
|
1122
|
+
// skipping checks
|
|
1123
|
+
if (/\[(skip ci|ci skip)\]/.test(webhookConfig.lastCommitMessage)) {
|
|
1124
|
+
skipMessage = 'Skipping due to the commit message: [skip ci]';
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// if skip ci then don't return
|
|
1128
|
+
if (ignoreUser && ignoreUser.length !== 0 && !skipMessage) {
|
|
1129
|
+
const commitAuthors =
|
|
1130
|
+
Array.isArray(webhookConfig.commitAuthors) && webhookConfig.commitAuthors.length !== 0
|
|
1131
|
+
? webhookConfig.commitAuthors
|
|
1132
|
+
: [username];
|
|
1133
|
+
const validCommitAuthors = commitAuthors.filter(author => !ignoreUser.includes(author));
|
|
1134
|
+
|
|
1135
|
+
if (!validCommitAuthors.length) {
|
|
1136
|
+
message = `Skipping because user ${username} is ignored`;
|
|
1137
|
+
request.log(['webhook', hookId], message);
|
|
1138
|
+
|
|
1139
|
+
return h.response({ message }).code(204);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const token = await obtainScmToken({ pluginOptions: webhookConfig.pluginOptions, userFactory, username, scmContext, scm });
|
|
1144
|
+
|
|
1145
|
+
if (action !== 'release' && action !== 'tag') {
|
|
1146
|
+
let scmUri;
|
|
1147
|
+
|
|
1148
|
+
if (type === 'pr') {
|
|
1149
|
+
scmUri = await scm.parseUrl({ checkoutUrl, token, scmContext });
|
|
1150
|
+
}
|
|
1151
|
+
webhookConfig.changedFiles = await scm.getChangedFiles({
|
|
1152
|
+
webhookConfig,
|
|
1153
|
+
type,
|
|
1154
|
+
token,
|
|
1155
|
+
scmContext,
|
|
1156
|
+
scmUri,
|
|
1157
|
+
prNum
|
|
1158
|
+
});
|
|
1159
|
+
request.log(['webhook', hookId], `Changed files are ${webhookConfig.changedFiles}`);
|
|
1160
|
+
} else {
|
|
1161
|
+
// The payload has no sha when webhook event is tag or release, so we need to get it.
|
|
1162
|
+
try {
|
|
1163
|
+
webhookConfig.sha = await getCommitRefSha({
|
|
1164
|
+
scm,
|
|
1165
|
+
token,
|
|
1166
|
+
ref,
|
|
1167
|
+
refType: 'tags',
|
|
1168
|
+
checkoutUrl,
|
|
1169
|
+
scmContext
|
|
1170
|
+
});
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
request.log(['webhook', hookId, 'getCommitRefSha'], err);
|
|
1173
|
+
|
|
1174
|
+
// there is a possibility of scm.getCommitRefSha() is not implemented yet
|
|
1175
|
+
return h.response({ message }).code(204);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (type === 'pr') {
|
|
1180
|
+
// disregard skip ci for pull request events
|
|
1181
|
+
return pullRequestEvent(webhookConfig.pluginOptions, request, h, webhookConfig, token);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return pushEvent(request, h, webhookConfig, skipMessage, token);
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
logger.error(`[${parsedHookId}]: ${err}`);
|
|
1187
|
+
|
|
1188
|
+
throw err;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
module.exports = { startHookEvent };
|