screwdriver-api 4.1.178 → 4.1.182

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