• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

screwdriver-cd / screwdriver / #3202

25 Jul 2025 04:52PM UTC coverage: 67.669% (-27.3%) from 94.935%
#3202

push

screwdriver

web-flow
feat(3363): Update the existing endpoint to get admin for a pipeline from the specified SCM context (#3370)

1284 of 2114 branches covered (60.74%)

Branch coverage included in aggregate %.

1 of 11 new or added lines in 1 file covered. (9.09%)

1235 existing lines in 49 files now uncovered.

3417 of 4833 relevant lines covered (70.7%)

50.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.03
/plugins/webhooks/helper.js
1
'use strict';
2

3
const workflowParser = require('screwdriver-workflow-parser');
1✔
4
const schema = require('screwdriver-data-schema');
1✔
5
const logger = require('screwdriver-logger');
1✔
6
const { getReadOnlyInfo } = require('../helper');
1✔
7
const { createEvent } = require('../events/helper/createEvent');
1✔
8

9
const ANNOT_NS = 'screwdriver.cd';
1✔
10
const ANNOT_CHAIN_PR = `${ANNOT_NS}/chainPR`;
1✔
11
const ANNOT_RESTRICT_PR = `${ANNOT_NS}/restrictPR`;
1✔
12
const EXTRA_TRIGGERS = schema.config.regex.EXTRA_TRIGGER;
1✔
13
const CHECKOUT_URL_SCHEMA = schema.config.regex.CHECKOUT_URL;
1✔
14
const CHECKOUT_URL_SCHEMA_REGEXP = new RegExp(CHECKOUT_URL_SCHEMA);
1✔
15

16
/**
17
 * Check if tag or release filtering is enabled or not
18
 * @param {String}    action          SCM webhook action type
19
 * @param {Array}     workflowGraph   pipeline workflowGraph
20
 * @returns {Boolean} isFilteringEnabled
21
 */
22
function isReleaseOrTagFilteringEnabled(action, workflowGraph) {
UNCOV
23
    let isFilteringEnabled = true;
×
24

UNCOV
25
    workflowGraph.edges.forEach(edge => {
×
UNCOV
26
        const releaseOrTagRegExp = action === 'release' ? /^~(release)$/ : /^~(tag)$/;
×
27

UNCOV
28
        if (edge.src.match(releaseOrTagRegExp)) {
×
UNCOV
29
            isFilteringEnabled = false;
×
30
        }
31
    });
32

UNCOV
33
    return isFilteringEnabled;
×
34
}
35
/**
36
 * Determine "startFrom" with type, action and branches
37
 * @param {String}   action                    SCM webhook action type
38
 * @param {String}   type                      Triggered SCM event type ('pr' or 'repo')
39
 * @param {String}   targetBranch              The branch against which commit is pushed
40
 * @param {String}   pipelineBranch            The pipeline branch
41
 * @param {String}   releaseName               SCM webhook release name
42
 * @param {String}   tagName                   SCM webhook tag name
43
 * @param {Boolean}  isReleaseOrTagFiltering   If the tag or release filtering is enabled
44
 * @returns {String} startFrom
45
 */
46
function determineStartFrom(action, type, targetBranch, pipelineBranch, releaseName, tagName, isReleaseOrTagFiltering) {
47
    let startFrom;
48

UNCOV
49
    if (type && type === 'pr') {
×
UNCOV
50
        if (action && action === 'closed') {
×
UNCOV
51
            startFrom = '~pr-closed';
×
52
        } else {
UNCOV
53
            startFrom = '~pr';
×
54
        }
55
    } else {
UNCOV
56
        switch (action) {
×
57
            case 'release':
UNCOV
58
                return releaseName && isReleaseOrTagFiltering ? `~release:${releaseName}` : '~release';
×
59
            case 'tag':
UNCOV
60
                if (!tagName) {
×
61
                    logger.error('The ref of SCM Webhook is missing.');
×
62

63
                    return '';
×
64
                }
65

UNCOV
66
                return isReleaseOrTagFiltering ? `~tag:${tagName}` : '~tag';
×
67
            default:
UNCOV
68
                startFrom = '~commit';
×
UNCOV
69
                break;
×
70
        }
71
    }
72

UNCOV
73
    return targetBranch !== pipelineBranch ? `${startFrom}:${targetBranch}` : startFrom;
×
74
}
75

76
/**
77
 * Update admins array
78
 * @param  {UserFactory}    userFactory     UserFactory object
79
 * @param  {String}         username        Username of user
80
 * @param  {String}         scmContext      Scm which pipeline's repository exists in
81
 * @param  {Pipeline}       pipeline        Pipeline object
82
 * @param  {PipelineFactory}pipelineFactory PipelineFactory object
83
 * @return {Promise}                        Updates the pipeline admins and throws an error if not an admin
84
 */
85
async function updateAdmins(userFactory, username, scmContext, pipeline, pipelineFactory) {
UNCOV
86
    const { readOnlyEnabled } = getReadOnlyInfo({ scm: pipelineFactory.scm, scmContext });
×
87

88
    // Skip update admins if read-only pipeline
UNCOV
89
    if (readOnlyEnabled) {
×
UNCOV
90
        return Promise.resolve();
×
91
    }
92

UNCOV
93
    try {
×
UNCOV
94
        const user = await userFactory.get({ username, scmContext });
×
UNCOV
95
        const userPermissions = await user.getPermissions(pipeline.scmUri, user.scmContext, pipeline.scmRepo);
×
96

97
        // for mysql backward compatibility
UNCOV
98
        if (!pipeline.adminUserIds) {
×
UNCOV
99
            pipeline.adminUserIds = [];
×
100
        }
101
        // Delete user from admin list if bad permissions
UNCOV
102
        if (!userPermissions.push) {
×
UNCOV
103
            const newAdmins = pipeline.admins;
×
104

UNCOV
105
            delete newAdmins[username];
×
UNCOV
106
            const newAdminUserIds = pipeline.adminUserIds.filter(adminUserId => adminUserId !== user.id);
×
107

108
            // This is needed to make admins dirty and update db
UNCOV
109
            pipeline.admins = newAdmins;
×
UNCOV
110
            pipeline.adminUserIds = newAdminUserIds;
×
111

UNCOV
112
            return pipeline.update();
×
113
        }
114

115
        // Put current user at the head of admins to use its SCM token after this
116
        // SCM token is got from the first pipeline admin
UNCOV
117
        const newAdminNames = [username, ...Object.keys(pipeline.admins)];
×
UNCOV
118
        const newAdmins = {};
×
119

UNCOV
120
        newAdminNames.forEach(name => {
×
UNCOV
121
            newAdmins[name] = true;
×
122
        });
123

UNCOV
124
        const newAdminUserIds = [user.id];
×
125

UNCOV
126
        pipeline.adminUserIds.forEach(adminUserId => {
×
UNCOV
127
            if (adminUserId !== user.id) {
×
UNCOV
128
                newAdminUserIds.push(adminUserId);
×
129
            }
130
        });
131

UNCOV
132
        pipeline.admins = newAdmins;
×
UNCOV
133
        pipeline.adminUserIds = newAdminUserIds;
×
134

UNCOV
135
        return pipeline.update();
×
136
    } catch (err) {
UNCOV
137
        logger.info(err.message);
×
138
    }
139

UNCOV
140
    return Promise.resolve();
×
141
}
142

143
/**
144
 * Update admins for an array of pipelines
145
 * @param  {Object}             config.userFactory      UserFactory
146
 * @param  {Array}              config.pipelines        An array of pipelines
147
 * @param  {String}             config.username         Username
148
 * @param  {String}             config.scmContext       ScmContext
149
 * @param  {PipelineFactory}    config.pipelineFactory  PipelineFactory object
150
 * @return {Promise}
151
 */
152
async function batchUpdateAdmins({ userFactory, pipelines, username, scmContext, pipelineFactory }) {
UNCOV
153
    await Promise.all(
×
UNCOV
154
        pipelines.map(pipeline => updateAdmins(userFactory, username, scmContext, pipeline, pipelineFactory))
×
155
    );
156
}
157

158
/**
159
 * Check if the PR is being restricted or not
160
 * @method isRestrictedPR
161
 * @param  {String}       restriction Is the pipeline restricting PR based on origin
162
 * @param  {String}       prSource    Origin of the PR
163
 * @return {Boolean}                  Should the build be restricted
164
 */
165
function isRestrictedPR(restriction, prSource) {
UNCOV
166
    switch (restriction) {
×
167
        case 'all':
168
        case 'all-admin':
UNCOV
169
            return true;
×
170
        case 'branch':
171
        case 'branch-admin':
UNCOV
172
            return prSource === 'branch';
×
173
        case 'fork':
174
        case 'fork-admin':
UNCOV
175
            return prSource === 'fork';
×
176
        case 'none':
177
        case 'none-admin':
178
        default:
UNCOV
179
            return false;
×
180
    }
181
}
182

183
/**
184
 * Stop a job by stopping all the builds associated with it
185
 * If the build is running, set state to ABORTED
186
 * @method stopJob
187
 * @param  {Object} config
188
 * @param  {String} config.action  Event action ('Closed' or 'Synchronized')
189
 * @param  {Job}    config.job     Job to stop
190
 * @param  {String} config.prNum   Pull request number
191
 * @return {Promise}
192
 */
193
function stopJob({ job, prNum, action }) {
UNCOV
194
    const stopRunningBuild = build => {
×
UNCOV
195
        if (build.isDone()) {
×
UNCOV
196
            return Promise.resolve();
×
197
        }
198

199
        const statusMessage =
UNCOV
200
            action === 'Closed'
×
201
                ? `Aborted because PR#${prNum} was closed`
202
                : `Aborted because new commit was pushed to PR#${prNum}`;
203

UNCOV
204
        build.status = 'ABORTED';
×
UNCOV
205
        build.statusMessage = statusMessage;
×
206

UNCOV
207
        return build.update();
×
208
    };
209

UNCOV
210
    return (
×
211
        job
212
            .getRunningBuilds()
213
            // Stop running builds
UNCOV
214
            .then(builds => Promise.all(builds.map(stopRunningBuild)))
×
215
    );
216
}
217

218
/**
219
 * Check if the pipeline has a triggered job or not
220
 * @method  hasTriggeredJob
221
 * @param   {Pipeline}  pipeline    The pipeline to check
222
 * @param   {String}    startFrom   The trigger name
223
 * @returns {Boolean}               True if the pipeline contains the triggered job
224
 */
225
function hasTriggeredJob(pipeline, startFrom) {
UNCOV
226
    try {
×
UNCOV
227
        const nextJobs = workflowParser.getNextJobs(pipeline.workflowGraph, {
×
228
            trigger: startFrom
229
        });
230

UNCOV
231
        return nextJobs.length > 0;
×
232
    } catch (err) {
UNCOV
233
        logger.error(`Error finding triggered jobs for ${pipeline.id}: ${err}`);
×
234

UNCOV
235
        return false;
×
236
    }
237
}
238

239
/**
240
 * Check if changedFiles are under rootDir. If no custom rootDir, return true.
241
 * @param  {Object}  pipeline
242
 * @param  {Array}  changedFiles
243
 * @return {Boolean}
244
 */
245
function hasChangesUnderRootDir(pipeline, changedFiles) {
UNCOV
246
    const splitUri = pipeline.scmUri.split(':');
×
UNCOV
247
    const rootDir = splitUri.length > 3 ? splitUri[3] : '';
×
UNCOV
248
    const changes = changedFiles || [];
×
249

250
    // Only check if rootDir is set
UNCOV
251
    if (rootDir) {
×
UNCOV
252
        return changes.some(file => file.startsWith(`${rootDir}/`));
×
253
    }
254

UNCOV
255
    return true;
×
256
}
257

258
/**
259
 * Resolve ChainPR flag
260
 * @method resolveChainPR
261
 * @param  {Boolean}  chainPR              Plugin Chain PR flag
262
 * @param  {Pipeline} pipeline             Pipeline
263
 * @param  {Object}   pipeline.annotations Pipeline-level annotations
264
 * @return {Boolean}
265
 */
266
function resolveChainPR(chainPR, pipeline) {
UNCOV
267
    const defaultChainPR = typeof chainPR === 'undefined' ? false : chainPR;
×
UNCOV
268
    const annotChainPR = pipeline.annotations[ANNOT_CHAIN_PR];
×
269

UNCOV
270
    return typeof annotChainPR === 'undefined' ? defaultChainPR : annotChainPR;
×
271
}
272

273
/**
274
 * Returns an object with resolvedChainPR and skipMessage
275
 * @param  {Object}       config.pipeline       Pipeline
276
 * @param  {String}       config.prSource       The origin of this PR
277
 * @param  {String}       config.restrictPR     Restrict PR setting
278
 * @param  {Boolean}      config.chainPR        Chain PR flag
279
 * @return {Object}
280
 */
281
function getSkipMessageAndChainPR({ pipeline, prSource, restrictPR, chainPR }) {
UNCOV
282
    const defaultRestrictPR = restrictPR || 'none';
×
UNCOV
283
    const result = {
×
284
        resolvedChainPR: resolveChainPR(chainPR, pipeline)
285
    };
286
    let restriction;
287

UNCOV
288
    if (['all-admin', 'none-admin', 'branch-admin', 'fork-admin'].includes(defaultRestrictPR)) {
×
UNCOV
289
        restriction = defaultRestrictPR;
×
290
    } else {
UNCOV
291
        restriction = pipeline.annotations[ANNOT_RESTRICT_PR] || defaultRestrictPR;
×
292
    }
293

294
    // Check for restriction upfront
UNCOV
295
    if (isRestrictedPR(restriction, prSource)) {
×
UNCOV
296
        result.skipMessage = `Skipping build since pipeline is configured to restrict ${restriction} and PR is ${prSource}`;
×
297
    }
298

UNCOV
299
    return result;
×
300
}
301

302
/**
303
 * Returns the uri keeping only the host and the repo ID
304
 * @method  uriTrimmer
305
 * @param  {String}       uri       The uri to be trimmed
306
 * @return {String}
307
 */
308
const uriTrimmer = uri => {
1✔
UNCOV
309
    const uriToArray = uri.split(':');
×
310

UNCOV
311
    while (uriToArray.length > 2) uriToArray.pop();
×
312

UNCOV
313
    return uriToArray.join(':');
×
314
};
315

316
/**
317
 * Create metadata by the parsed event
318
 * @param   {Object}   parsed   It has information to create metadata
319
 * @returns {Object}            Metadata
320
 */
321
function createMeta(parsed) {
UNCOV
322
    const { action, ref, releaseId, releaseName, releaseAuthor, prMerged, prNum, prRef } = parsed;
×
323

UNCOV
324
    if (action === 'release') {
×
UNCOV
325
        return {
×
326
            sd: {
327
                release: {
328
                    id: releaseId,
329
                    name: releaseName,
330
                    author: releaseAuthor
331
                },
332
                tag: {
333
                    name: ref
334
                }
335
            }
336
        };
337
    }
UNCOV
338
    if (action === 'tag') {
×
UNCOV
339
        return {
×
340
            sd: {
341
                tag: {
342
                    name: ref
343
                }
344
            }
345
        };
346
    }
UNCOV
347
    if (action.toLowerCase() === 'closed') {
×
UNCOV
348
        return {
×
349
            sd: {
350
                pr: {
351
                    name: prRef,
352
                    merged: prMerged,
353
                    number: prNum
354
                }
355
            }
356
        };
357
    }
358

UNCOV
359
    return {};
×
360
}
361

362
/**
363
 * Get all pipelines which has triggered job
364
 * @method  triggeredPipelines
365
 * @param   {PipelineFactory}   pipelineFactory The pipeline factory to get the branch list from
366
 * @param   {Object}            scmConfig       Has the token and scmUri to get branches
367
 * @param   {String}            branch          The branch which is committed
368
 * @param   {String}            type            Triggered GitHub event type ('pr' or 'repo')
369
 * @param   {String}            action          Triggered GitHub event action
370
 * @param   {Array}            changedFiles     Changed files in this commit
371
 * @param   {String}            releaseName     SCM webhook release name
372
 * @param   {String}            tagName         SCM webhook tag name
373
 * @returns {Promise}                           Promise that resolves into triggered pipelines
374
 */
375
async function triggeredPipelines(
376
    pipelineFactory,
377
    scmConfig,
378
    branch,
379
    type,
380
    action,
381
    changedFiles,
382
    releaseName,
383
    tagName
384
) {
UNCOV
385
    const { scmUri } = scmConfig;
×
UNCOV
386
    const splitUri = scmUri.split(':');
×
UNCOV
387
    const scmBranch = `${splitUri[0]}:${splitUri[1]}:${splitUri[2]}`;
×
UNCOV
388
    const scmRepoId = `${splitUri[0]}:${splitUri[1]}`;
×
UNCOV
389
    const listConfig = {
×
390
        search: { field: 'scmUri', keyword: `${scmRepoId}:%` },
391
        params: {
392
            state: 'ACTIVE'
393
        }
394
    };
UNCOV
395
    const externalRepoSearchConfig = {
×
396
        search: { field: 'subscribedScmUrlsWithActions', keyword: `%${scmRepoId}:%` },
397
        params: {
398
            state: 'ACTIVE'
399
        }
400
    };
401

UNCOV
402
    const pipelines = await pipelineFactory.list(listConfig);
×
UNCOV
403
    const pipelinesWithSubscribedRepos = await pipelineFactory.list(externalRepoSearchConfig);
×
404

UNCOV
405
    let pipelinesOnCommitBranch = [];
×
UNCOV
406
    let pipelinesOnOtherBranch = [];
×
407

UNCOV
408
    pipelines.forEach(p => {
×
409
        // This uri expects 'scmUriDomain:repoId:branchName:rootDir'. To Compare, rootDir is ignored.
UNCOV
410
        const splitScmUri = p.scmUri.split(':');
×
UNCOV
411
        const pipelineScmBranch = `${splitScmUri[0]}:${splitScmUri[1]}:${splitScmUri[2]}`;
×
412

UNCOV
413
        if (pipelineScmBranch === scmBranch) {
×
UNCOV
414
            pipelinesOnCommitBranch.push(p);
×
415
        } else {
UNCOV
416
            pipelinesOnOtherBranch.push(p);
×
417
        }
418
    });
419

420
    // Build runs regardless of changedFiles when release/tag trigger
UNCOV
421
    pipelinesOnCommitBranch = pipelinesOnCommitBranch.filter(
×
UNCOV
422
        p => ['release', 'tag'].includes(action) || hasChangesUnderRootDir(p, changedFiles)
×
423
    );
424

UNCOV
425
    pipelinesOnOtherBranch = pipelinesOnOtherBranch.filter(p => {
×
UNCOV
426
        let isReleaseOrTagFiltering = '';
×
427

UNCOV
428
        if (action === 'release' || action === 'tag') {
×
429
            isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, p.workflowGraph);
×
430
        }
431

UNCOV
432
        return hasTriggeredJob(
×
433
            p,
434
            determineStartFrom(action, type, branch, null, releaseName, tagName, isReleaseOrTagFiltering)
435
        );
436
    });
437

UNCOV
438
    const currentRepoPipelines = pipelinesOnCommitBranch.concat(pipelinesOnOtherBranch);
×
439

UNCOV
440
    if (pipelinesOnCommitBranch.length === 0) {
×
UNCOV
441
        return currentRepoPipelines;
×
442
    }
443

444
    // process the pipelinesWithSubscribedRepos only when the pipelinesOnCommitBranch is not empty
445
    // pipelinesOnCommitBranch has the information to determine the triggering event of downstream subscribing repo
UNCOV
446
    pipelinesWithSubscribedRepos.forEach(p => {
×
UNCOV
447
        if (!Array.isArray(p.subscribedScmUrlsWithActions)) {
×
UNCOV
448
            return;
×
449
        }
UNCOV
450
        p.subscribedScmUrlsWithActions.forEach(subscribedScmUriWithAction => {
×
UNCOV
451
            const { scmUri: subscribedScmUri, actions: subscribedActions } = subscribedScmUriWithAction;
×
452

UNCOV
453
            if (pipelinesOnCommitBranch[0].scmUri === subscribedScmUri) {
×
UNCOV
454
                const pipeline = pipelinesOnCommitBranch[0];
×
UNCOV
455
                const isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, pipeline.workflowGraph);
×
UNCOV
456
                const startFrom = determineStartFrom(
×
457
                    action,
458
                    type,
459
                    branch,
460
                    null,
461
                    releaseName,
462
                    tagName,
463
                    isReleaseOrTagFiltering
464
                );
465

UNCOV
466
                for (const subscribedAction of subscribedActions) {
×
UNCOV
467
                    if (new RegExp(subscribedAction).test(startFrom)) {
×
UNCOV
468
                        currentRepoPipelines.push(p);
×
UNCOV
469
                        break;
×
470
                    }
471
                }
472
            }
473
        });
474
    });
475

UNCOV
476
    return currentRepoPipelines;
×
477
}
478

479
/**
480
 * Start Events
481
 * @async  startEvents
482
 * @param  {Array}  eventConfigs Array of event config objects
483
 * @param  {Object} server Server
484
 * @return {Promise<Array>}  Array of created events
485
 */
486
async function startEvents(eventConfigs, server) {
UNCOV
487
    const events = [];
×
UNCOV
488
    let errorCount = 0;
×
UNCOV
489
    let eventsCount = 0;
×
490

UNCOV
491
    const results = await Promise.allSettled(
×
492
        eventConfigs.map(eventConfig => {
UNCOV
493
            if (eventConfig && eventConfig.configPipelineSha) {
×
UNCOV
494
                eventsCount += 1;
×
495

UNCOV
496
                return createEvent(eventConfig, server);
×
497
            }
498

UNCOV
499
            return Promise.resolve(null);
×
500
        })
501
    );
502

UNCOV
503
    const errors = [];
×
504

UNCOV
505
    results.forEach((result, i) => {
×
UNCOV
506
        if (result.status === 'fulfilled') {
×
UNCOV
507
            if (result.value) events.push(result.value);
×
508
        } else {
UNCOV
509
            errorCount += 1;
×
UNCOV
510
            errors.push(result.reason);
×
UNCOV
511
            logger.error(`pipeline:${eventConfigs[i].pipelineId} error in starting event`, result.reason);
×
512
        }
513
    });
514

UNCOV
515
    if (errorCount > 0 && errorCount === eventsCount) {
×
516
        // preserve current behavior of returning 500 on error
UNCOV
517
        const errorMessages = new Set();
×
UNCOV
518
        let statusCode = 500;
×
519

UNCOV
520
        errors.forEach(err => {
×
UNCOV
521
            errorMessages.add(`"${err.message}"`);
×
UNCOV
522
            if (err.statusCode !== undefined) {
×
UNCOV
523
                statusCode = err.statusCode;
×
524
            }
525
        });
526

UNCOV
527
        const errorMessage = [...errorMessages].join(', ');
×
528
        const error =
UNCOV
529
            errorCount === 1
×
530
                ? new Error(`Failed to start a event caused by ${errorMessage}`)
531
                : new Error(`Failed to start some events caused by ${errorMessage}`);
532

UNCOV
533
        error.statusCode = statusCode;
×
UNCOV
534
        throw error;
×
535
    }
536

UNCOV
537
    return events;
×
538
}
539

540
/**
541
 * Create events for each pipeline
542
 * @async  createPREvents
543
 * @param  {Object}       options
544
 * @param  {String}       options.username        User who created the PR
545
 * @param  {String}       options.scmConfig       Has the token and scmUri to get branches
546
 * @param  {String}       options.sha             Specific SHA1 commit to start the build with
547
 * @param  {String}       options.prRef           Reference to pull request
548
 * @param  {String}       options.prNum           Pull request number
549
 * @param  {String}       options.prTitle         Pull request title
550
 * @param  {Array}        options.changedFiles    List of changed files
551
 * @param  {String}       options.branch          The branch against which pr is opened
552
 * @param  {String}       options.action          Event action
553
 * @param  {String}       options.prSource      The origin of this PR
554
 * @param  {String}       options.restrictPR    Restrict PR setting
555
 * @param  {Boolean}      options.chainPR       Chain PR flag
556
 * @param  {Hapi.request} request                 Request from user
557
 * @return {Promise}
558
 */
559
async function createPREvents(options, request) {
560
    const {
561
        username,
562
        scmConfig,
563
        prRef,
564
        prNum,
565
        pipelines,
566
        prTitle,
567
        changedFiles,
568
        branch,
569
        action,
570
        prSource,
571
        restrictPR,
572
        chainPR,
573
        ref,
574
        releaseName
UNCOV
575
    } = options;
×
576

UNCOV
577
    const { scm } = request.server.app.pipelineFactory;
×
UNCOV
578
    const { eventFactory, pipelineFactory, userFactory } = request.server.app;
×
UNCOV
579
    const scmDisplayName = scm.getDisplayName({ scmContext: scmConfig.scmContext });
×
UNCOV
580
    const userDisplayName = `${scmDisplayName}:${username}`;
×
UNCOV
581
    const { sha } = options;
×
582

UNCOV
583
    scmConfig.prNum = prNum;
×
584

UNCOV
585
    const eventConfigs = await Promise.all(
×
586
        pipelines.map(async p => {
UNCOV
587
            try {
×
UNCOV
588
                const b = await p.branch;
×
589
                // obtain pipeline's latest commit sha for branch specific job
UNCOV
590
                let configPipelineSha = '';
×
UNCOV
591
                let subscribedConfigSha = '';
×
UNCOV
592
                let eventConfig = {};
×
593

UNCOV
594
                if (uriTrimmer(scmConfig.scmUri) !== uriTrimmer(p.scmUri)) {
×
UNCOV
595
                    subscribedConfigSha = sha;
×
596

UNCOV
597
                    try {
×
UNCOV
598
                        configPipelineSha = await pipelineFactory.scm.getCommitSha({
×
599
                            scmUri: p.scmUri,
600
                            scmContext: scmConfig.scmContext,
601
                            token: scmConfig.token
602
                        });
603
                    } catch (err) {
604
                        if (err.status >= 500) {
×
605
                            throw err;
×
606
                        } else {
607
                            logger.info(`skip create event for branch: ${b}`);
×
608
                        }
609
                    }
610
                } else {
UNCOV
611
                    try {
×
UNCOV
612
                        configPipelineSha = await pipelineFactory.scm.getCommitSha(scmConfig);
×
613
                    } catch (err) {
UNCOV
614
                        if (err.status >= 500) {
×
615
                            throw err;
×
616
                        } else {
UNCOV
617
                            logger.info(`skip create event for branch: ${b}`);
×
618
                        }
619
                    }
620
                }
621

UNCOV
622
                const { skipMessage, resolvedChainPR } = getSkipMessageAndChainPR({
×
623
                    // Workaround for pipelines which has NULL value in `pipeline.annotations`
624
                    pipeline: !p.annotations ? { annotations: {}, ...p } : p,
×
625
                    prSource,
626
                    restrictPR,
627
                    chainPR
628
                });
629

UNCOV
630
                const prInfo = await eventFactory.scm.getPrInfo(scmConfig);
×
631

UNCOV
632
                eventConfig = {
×
633
                    pipelineId: p.id,
634
                    type: 'pr',
635
                    webhooks: true,
636
                    username,
637
                    scmContext: scmConfig.scmContext,
638
                    sha,
639
                    configPipelineSha,
640
                    startFrom: `~pr:${branch}`,
641
                    changedFiles,
642
                    causeMessage: `${action} by ${userDisplayName}`,
643
                    chainPR: resolvedChainPR,
644
                    prRef,
645
                    prNum,
646
                    prTitle,
647
                    prInfo,
648
                    prSource,
649
                    baseBranch: branch
650
                };
651

UNCOV
652
                if (b === branch) {
×
UNCOV
653
                    eventConfig.startFrom = '~pr';
×
654
                }
655

656
                // Check if the webhook event is from a subscribed repo and
657
                // set the jobs entrypoint from ~startFrom
658
                // For subscribed PR event, it should be mimicked as a commit
659
                // in order to function properly
UNCOV
660
                if (uriTrimmer(scmConfig.scmUri) !== uriTrimmer(p.scmUri)) {
×
UNCOV
661
                    eventConfig = {
×
662
                        pipelineId: p.id,
663
                        type: 'pipeline',
664
                        webhooks: true,
665
                        username,
666
                        scmContext: scmConfig.scmContext,
667
                        startFrom: '~subscribe',
668
                        sha: configPipelineSha,
669
                        configPipelineSha,
670
                        changedFiles,
671
                        baseBranch: branch,
672
                        causeMessage: `Merged by ${username}`,
673
                        releaseName,
674
                        ref,
675
                        subscribedEvent: true,
676
                        subscribedConfigSha,
677
                        subscribedSourceUrl: prInfo.url
678
                    };
679

UNCOV
680
                    await updateAdmins(userFactory, username, scmConfig.scmContext, p.id, pipelineFactory);
×
681
                }
682

UNCOV
683
                if (skipMessage) {
×
UNCOV
684
                    eventConfig.skipMessage = skipMessage;
×
685
                }
686

UNCOV
687
                return eventConfig;
×
688
            } catch (err) {
689
                logger.warn(`pipeline:${p.id} error in starting event`, err);
×
690

691
                return null;
×
692
            }
693
        })
694
    );
695

UNCOV
696
    const events = await startEvents(eventConfigs, request.server);
×
697

UNCOV
698
    return events;
×
699
}
700

701
/**
702
 * Stop all the relevant PR jobs for an array of pipelines
703
 * @async  batchStopJobs
704
 * @param  {Array}      config.pipelines    An array of pipeline
705
 * @param  {Integer}    config.prNum        PR number
706
 * @param  {String}     config.action       Event action
707
 * @param  {String}     config.name         Prefix of the PR job name: PR-prNum
708
 */
709
async function batchStopJobs({ pipelines, prNum, action, name }) {
UNCOV
710
    const prJobs = await Promise.all(
×
UNCOV
711
        pipelines.map(p => p.getJobs({ type: 'pr' }).then(jobs => jobs.filter(j => j.name.includes(name))))
×
712
    );
UNCOV
713
    const flatPRJobs = prJobs.reduce((prev, curr) => prev.concat(curr));
×
714

UNCOV
715
    await Promise.all(flatPRJobs.map(j => stopJob({ job: j, prNum, action })));
×
716
}
717

718
/**
719
 * Create a new job and start the build for an opened pull-request
720
 * @async  pullRequestOpened
721
 * @param  {Object}       options
722
 * @param  {String}       options.hookId        Unique ID for this scm event
723
 * @param  {String}       options.prSource      The origin of this PR
724
 * @param  {Pipeline}     options.pipeline      Pipeline model for the pr
725
 * @param  {String}       options.restrictPR    Restrict PR setting
726
 * @param  {Boolean}      options.chainPR       Chain PR flag
727
 * @param  {Hapi.request} request               Request from user
728
 * @param  {Hapi.h}       h                     Response toolkit
729
 */
730
async function pullRequestOpened(options, request, h) {
UNCOV
731
    const { hookId } = options;
×
732

UNCOV
733
    return createPREvents(options, request)
×
734
        .then(events => {
UNCOV
735
            events.forEach(e => {
×
UNCOV
736
                request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
×
737
            });
738

UNCOV
739
            return h.response().code(201);
×
740
        })
741
        .catch(err => {
UNCOV
742
            logger.error(
×
743
                `Failed to pullRequestOpened: [${hookId}, pipeline:${options.pipeline && options.pipeline.id}]: ${err}`
×
744
            );
745

UNCOV
746
            throw err;
×
747
        });
748
}
749

750
/**
751
 * Create events for each pipeline
752
 * @async  createPREvents
753
 * @param  {Object}       options
754
 * @param  {String}       options.username        User who created the PR
755
 * @param  {String}       options.scmConfig       Has the token and scmUri to get branches
756
 * @param  {String}       options.sha             Specific SHA1 commit to start the build with
757
 * @param  {String}       options.prNum           Pull request number
758
 * @param  {Array}        options.changedFiles    List of changed files
759
 * @param  {String}       options.branch          The branch against which pr is opened
760
 * @param  {String}       options.action          Event action
761
 * @param  {String}       options.prSource      The origin of this PR
762
 * @param  {String}       options.restrictPR    Restrict PR setting
763
 * @param  {Boolean}      options.chainPR       Chain PR flag
764
 * @param  {Boolean}      options.ref      Chain PR flag
765
 * @param  {Hapi.request} request                 Request from user
766
 * @return {Promise}
767
 */
768
async function createPrClosedEvent(options, request) {
769
    const { username, scmConfig, prNum, pipelines, changedFiles, branch, action, ref, prSource, restrictPR, chainPR } =
UNCOV
770
        options;
×
771

UNCOV
772
    const { scm } = request.server.app.pipelineFactory;
×
UNCOV
773
    const { pipelineFactory } = request.server.app;
×
UNCOV
774
    const scmDisplayName = scm.getDisplayName({ scmContext: scmConfig.scmContext });
×
UNCOV
775
    const userDisplayName = `${scmDisplayName}:${username}`;
×
UNCOV
776
    const { sha } = options;
×
777

UNCOV
778
    scmConfig.prNum = prNum;
×
779

UNCOV
780
    const eventConfigs = await Promise.all(
×
781
        pipelines.map(async p => {
UNCOV
782
            try {
×
UNCOV
783
                const b = await p.branch;
×
UNCOV
784
                let eventConfig = {};
×
UNCOV
785
                const token = await p.token;
×
UNCOV
786
                const pScmConfig = {
×
787
                    scmUri: p.scmUri,
788
                    token,
789
                    scmRepo: p.scmRepo,
790
                    scmContext: scmConfig.scmContext
791
                };
UNCOV
792
                let latestPipelineSha = '';
×
793

UNCOV
794
                try {
×
UNCOV
795
                    latestPipelineSha = await pipelineFactory.scm.getCommitSha(pScmConfig);
×
796
                } catch (err) {
797
                    if (err.status >= 500) {
×
798
                        throw err;
×
799
                    } else {
800
                        logger.info(`skip create event for branch: ${p.branch}`);
×
801
                    }
802
                }
803

UNCOV
804
                const { skipMessage, resolvedChainPR } = getSkipMessageAndChainPR({
×
805
                    pipeline: !p.annotations ? { annotations: {}, ...p } : p,
×
806
                    prSource,
807
                    restrictPR,
808
                    chainPR
809
                });
810

UNCOV
811
                const startFrom = `~pr-closed`;
×
UNCOV
812
                const causeMessage = `PR-${prNum} ${action.toLowerCase()} by ${userDisplayName}`;
×
UNCOV
813
                const isPipelineBranch = b === branch;
×
814

UNCOV
815
                eventConfig = {
×
816
                    pipelineId: p.id,
817
                    type: 'pipeline',
818
                    webhooks: true,
819
                    username,
820
                    scmContext: scmConfig.scmContext,
821
                    sha: latestPipelineSha || sha,
×
822
                    startFrom: isPipelineBranch ? startFrom : `${startFrom}:${branch}`,
×
823
                    changedFiles,
824
                    causeMessage: isPipelineBranch ? causeMessage : `${causeMessage} on branch ${branch}`,
×
825
                    ref,
826
                    baseBranch: branch,
827
                    meta: createMeta(options),
828
                    configPipelineSha: latestPipelineSha,
829
                    prNum,
830
                    chainPR: resolvedChainPR
831
                };
832

UNCOV
833
                if (skipMessage) {
×
UNCOV
834
                    eventConfig.skipMessage = skipMessage;
×
835
                }
836

UNCOV
837
                return eventConfig;
×
838
            } catch (err) {
839
                logger.warn(`pipeline:${p.id} error in starting event`, err);
×
840

841
                return null;
×
842
            }
843
        })
844
    );
845

UNCOV
846
    const events = await startEvents(eventConfigs, request.server);
×
847

UNCOV
848
    return events;
×
849
}
850

851
/**
852
 * Stop any running builds and disable the job for closed pull-request
853
 * @async  pullRequestClosed
854
 * @param  {Object}       options
855
 * @param  {String}       options.hookId            Unique ID for this scm event
856
 * @param  {Pipeline}     options.pipeline          Pipeline model for the pr
857
 * @param  {String}       options.name              Name of the PR: PR-prNum
858
 * @param  {String}       options.prNum             Pull request number
859
 * @param  {String}       options.action            Event action
860
 * @param  {String}       options.fullCheckoutUrl   CheckoutUrl with branch name
861
 * @param  {Hapi.request} request                   Request from user
862
 * @param  {Hapi.reply}   reply                     Reply to user
863
 */
864
async function pullRequestClosed(options, request, h) {
UNCOV
865
    const { pipelines, hookId, name, prNum, action } = options;
×
UNCOV
866
    const updatePRJobs = job =>
×
UNCOV
867
        stopJob({ job, prNum, action })
×
UNCOV
868
            .then(() => request.log(['webhook', hookId, job.id], `${job.name} stopped`))
×
869
            .then(() => {
UNCOV
870
                job.archived = true;
×
871

UNCOV
872
                return job.update();
×
873
            })
UNCOV
874
            .then(() => request.log(['webhook', hookId, job.id], `${job.name} disabled and archived`));
×
875

UNCOV
876
    try {
×
UNCOV
877
        await Promise.all(
×
878
            pipelines.map(p =>
UNCOV
879
                p.getJobs({ type: 'pr' }).then(jobs => {
×
UNCOV
880
                    const prJobs = jobs.filter(j => j.name.includes(name));
×
881

UNCOV
882
                    return Promise.all(prJobs.map(j => updatePRJobs(j)));
×
883
                })
884
            )
885
        );
886

UNCOV
887
        const prClosedJobs = [];
×
888

UNCOV
889
        for (const p of pipelines) {
×
UNCOV
890
            const jobs = await p.getJobs({ type: 'pipeline' });
×
UNCOV
891
            const filteredJobs = jobs.filter(
×
892
                j =>
UNCOV
893
                    j.permutations &&
×
894
                    j.permutations.length > 0 &&
895
                    j.permutations[0] &&
896
                    j.permutations[0].requires &&
897
                    j.permutations[0].requires.some(
UNCOV
898
                        require => require === '~pr-closed' || require.startsWith('~pr-closed:')
×
899
                    )
900
            );
901

UNCOV
902
            prClosedJobs.push(...filteredJobs);
×
903
        }
904

UNCOV
905
        if (prClosedJobs.length === 0) {
×
UNCOV
906
            return h.response().code(200);
×
907
        }
908

UNCOV
909
        const events = await createPrClosedEvent(options, request);
×
910

UNCOV
911
        events.forEach(e => {
×
UNCOV
912
            request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
×
913
        });
914

UNCOV
915
        return h.response().code(201);
×
916
    } catch (err) {
UNCOV
917
        logger.error(
×
918
            `Failed to pullRequestClosed: [${hookId}, pipeline:${options.pipeline && options.pipeline.id}]: ${err}`
×
919
        );
920

UNCOV
921
        throw err;
×
922
    }
923
}
924

925
/**
926
 * Stop any running builds and start the build for the synchronized pull-request
927
 * @async  pullRequestSync
928
 * @param  {Object}       options
929
 * @param  {String}       options.hookId        Unique ID for this scm event
930
 * @param  {String}       options.name          Name of the new job (PR-1)
931
 * @param  {String}       options.prSource      The origin of this PR
932
 * @param  {String}       options.restrictPR    Restrict PR setting
933
 * @param  {Boolean}      options.chainPR       Chain PR flag
934
 * @param  {Pipeline}     options.pipeline      Pipeline model for the pr
935
 * @param  {Array}        options.changedFiles  List of files that were changed
936
 * @param  {String}       options.prNum         Pull request number
937
 * @param  {String}       options.action        Event action
938
 * @param  {Hapi.request} request               Request from user
939
 * @param  {Hapi.reply}   reply                 Reply to user
940
 */
941
async function pullRequestSync(options, request, h) {
UNCOV
942
    const { pipelines, hookId, name, prNum, action } = options;
×
943

UNCOV
944
    await batchStopJobs({ pipelines, name, prNum, action });
×
945

UNCOV
946
    request.log(['webhook', hookId], `Job(s) for ${name} stopped`);
×
947

UNCOV
948
    return createPREvents(options, request)
×
949
        .then(events => {
UNCOV
950
            events.forEach(e => {
×
UNCOV
951
                request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
×
952
            });
953

UNCOV
954
            return h.response().code(201);
×
955
        })
956
        .catch(err => {
UNCOV
957
            logger.error(
×
958
                `Failed to pullRequestSync: [${hookId}, pipeline:${options.pipeline && options.pipeline.id}]: ${err}`
×
959
            );
960

UNCOV
961
            throw err;
×
962
        });
963
}
964

965
/**
966
 * Obtains the SCM token for a given user.
967
 * If a user does not have a valid SCM token registered with Screwdriver,
968
 * it will use a generic user's token instead.
969
 * If pipeline is in read-only SCM, use read-only token.
970
 * Some SCM services have different thresholds between IP requests and token requests. This is
971
 * to ensure we have a token to access the SCM service without being restricted by these quotas
972
 * @method obtainScmToken
973
 * @param  {Object}         pluginOptions
974
 * @param  {String}         pluginOptions.username  Generic scm username
975
 * @param  {UserFactory}    userFactory             UserFactory object
976
 * @param  {String}         username                Name of the user that the SCM token is associated with
977
 * @param  {String}         scmContext              Scm which pipeline's repository exists in
978
 * @param  {Object}         scm                     Scm
979
 * @return {Promise}                                Promise that resolves into a SCM token
980
 */
981
async function obtainScmToken({ pluginOptions, userFactory, username, scmContext, scm }) {
UNCOV
982
    const { readOnlyEnabled, headlessAccessToken } = getReadOnlyInfo({ scm, scmContext });
×
983

984
    // If pipeline is in read-only SCM, use read-only token
UNCOV
985
    if (readOnlyEnabled && headlessAccessToken) {
×
UNCOV
986
        return headlessAccessToken;
×
987
    }
988

UNCOV
989
    const user = await userFactory.get({ username, scmContext });
×
990

991
    // Use generic username and token
UNCOV
992
    if (!user) {
×
UNCOV
993
        const genericUsername = pluginOptions.username;
×
UNCOV
994
        const buildBotUser = await userFactory.get({ username: genericUsername, scmContext });
×
995

UNCOV
996
        return buildBotUser.unsealToken();
×
997
    }
998

UNCOV
999
    return user.unsealToken();
×
1000
}
1001

1002
/**
1003
 * Act on a Pull Request change (create, sync, close)
1004
 *  - Opening a PR should sync the pipeline (creating the job) and start the new PR job
1005
 *  - Syncing a PR should stop the existing PR job and start a new one
1006
 *  - Closing a PR should stop the PR job and sync the pipeline (disabling the job)
1007
 * @method pullRequestEvent
1008
 * @param  {Object}             pluginOptions
1009
 * @param  {String}             pluginOptions.username    Generic scm username
1010
 * @param  {String}             pluginOptions.restrictPR  Restrict PR setting
1011
 * @param  {Boolean}            pluginOptions.chainPR     Chain PR flag
1012
 * @param  {Hapi.request}       request                   Request from user
1013
 * @param  {Hapi.reply}         reply                     Reply to user
1014
 * @param  {String}             token                     The token used to authenticate to the SCM
1015
 * @param  {Object}             parsed
1016
 */
1017
function pullRequestEvent(pluginOptions, request, h, parsed, token) {
UNCOV
1018
    const { pipelineFactory, userFactory } = request.server.app;
×
1019
    const {
1020
        hookId,
1021
        action,
1022
        checkoutUrl,
1023
        branch,
1024
        sha,
1025
        prNum,
1026
        prTitle,
1027
        prRef,
1028
        prSource,
1029
        username,
1030
        scmContext,
1031
        changedFiles,
1032
        type,
1033
        releaseName,
1034
        ref,
1035
        prMerged
UNCOV
1036
    } = parsed;
×
UNCOV
1037
    const fullCheckoutUrl = `${checkoutUrl}#${branch}`;
×
UNCOV
1038
    const scmConfig = {
×
1039
        scmUri: '',
1040
        token,
1041
        scmContext
1042
    };
UNCOV
1043
    const { restrictPR, chainPR } = pluginOptions;
×
1044

UNCOV
1045
    request.log(['webhook', hookId], `PR #${prNum} ${action} for ${fullCheckoutUrl}`);
×
1046

UNCOV
1047
    return pipelineFactory.scm
×
1048
        .parseUrl({
1049
            checkoutUrl: fullCheckoutUrl,
1050
            token,
1051
            scmContext
1052
        })
1053
        .then(scmUri => {
UNCOV
1054
            scmConfig.scmUri = scmUri;
×
1055

UNCOV
1056
            return triggeredPipelines(pipelineFactory, scmConfig, branch, type, action, changedFiles, releaseName, ref);
×
1057
        })
1058
        .then(async pipelines => {
UNCOV
1059
            if (!pipelines || pipelines.length === 0) {
×
UNCOV
1060
                const message = `Skipping since Pipeline triggered by PRs against ${fullCheckoutUrl} does not exist`;
×
1061

UNCOV
1062
                request.log(['webhook', hookId], message);
×
1063

UNCOV
1064
                return h.response({ message }).code(204);
×
1065
            }
1066

UNCOV
1067
            const options = {
×
1068
                name: `PR-${prNum}`,
1069
                hookId,
1070
                sha,
1071
                username,
1072
                scmConfig,
1073
                prRef,
1074
                prNum,
1075
                prTitle,
1076
                prSource,
1077
                changedFiles,
1078
                action: action.charAt(0).toUpperCase() + action.slice(1),
1079
                branch,
1080
                fullCheckoutUrl,
1081
                restrictPR,
1082
                chainPR,
1083
                pipelines,
1084
                ref,
1085
                releaseName,
1086
                prMerged
1087
            };
1088

UNCOV
1089
            await batchUpdateAdmins({ userFactory, pipelines, username, scmContext, pipelineFactory });
×
1090

UNCOV
1091
            switch (action) {
×
1092
                case 'opened':
1093
                case 'reopened':
UNCOV
1094
                    return pullRequestOpened(options, request, h);
×
1095
                case 'synchronized':
UNCOV
1096
                    return pullRequestSync(options, request, h);
×
1097
                case 'closed':
1098
                default:
UNCOV
1099
                    return pullRequestClosed(options, request, h);
×
1100
            }
1101
        })
1102
        .catch(err => {
UNCOV
1103
            logger.error(`[${hookId}]: ${err}`);
×
1104

UNCOV
1105
            throw err;
×
1106
        });
1107
}
1108

1109
/**
1110
 * Create events for each pipeline
1111
 * @async   createEvents
1112
 * @param   {Object}            server
1113
 * @param   {UserFactory}       userFactory         To get user permission
1114
 * @param   {PipelineFactory}   pipelineFactory     To use scm module
1115
 * @param   {Array}             pipelines           The pipelines to start events
1116
 * @param   {Object}            parsed              It has information to create event
1117
 * @param   {String}            [skipMessage]       Message to skip starting builds
1118
 * @returns {Promise}                               Promise that resolves into events
1119
 */
1120
async function createEvents(server, userFactory, pipelineFactory, pipelines, parsed, skipMessage, scmConfigFromHook) {
UNCOV
1121
    const { action, branch, sha, username, scmContext, changedFiles, type, releaseName, ref } = parsed;
×
1122

UNCOV
1123
    const pipelineTuples = await Promise.all(
×
1124
        pipelines.map(async p => {
UNCOV
1125
            const resolvedBranch = await p.branch;
×
UNCOV
1126
            let isReleaseOrTagFiltering = '';
×
1127

UNCOV
1128
            if (action === 'release' || action === 'tag') {
×
UNCOV
1129
                isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, p.workflowGraph);
×
1130
            }
UNCOV
1131
            const startFrom = determineStartFrom(
×
1132
                action,
1133
                type,
1134
                branch,
1135
                resolvedBranch,
1136
                releaseName,
1137
                ref,
1138
                isReleaseOrTagFiltering
1139
            );
UNCOV
1140
            const tuple = { branch: resolvedBranch, pipeline: p, startFrom };
×
1141

UNCOV
1142
            return tuple;
×
1143
        })
1144
    );
1145

UNCOV
1146
    const ignoreExtraTriggeredPipelines = pipelineTuples.filter(t => {
×
1147
        // empty event is not created when it is triggered by extra triggers (e.g. ~tag, ~release)
UNCOV
1148
        if (EXTRA_TRIGGERS.test(t.startFrom) && !hasTriggeredJob(t.pipeline, t.startFrom)) {
×
UNCOV
1149
            logger.warn(`Event not created: there are no jobs triggered by ${t.startFrom}`);
×
1150

UNCOV
1151
            return false;
×
1152
        }
1153

UNCOV
1154
        return true;
×
1155
    });
1156

UNCOV
1157
    const eventConfigs = await Promise.all(
×
1158
        ignoreExtraTriggeredPipelines.map(async pTuple => {
UNCOV
1159
            try {
×
UNCOV
1160
                const pipelineBranch = pTuple.branch;
×
UNCOV
1161
                let isReleaseOrTagFiltering = '';
×
1162

UNCOV
1163
                if (action === 'release' || action === 'tag') {
×
UNCOV
1164
                    isReleaseOrTagFiltering = isReleaseOrTagFilteringEnabled(action, pTuple.pipeline.workflowGraph);
×
1165
                }
UNCOV
1166
                const startFrom = determineStartFrom(
×
1167
                    action,
1168
                    type,
1169
                    branch,
1170
                    pipelineBranch,
1171
                    releaseName,
1172
                    ref,
1173
                    isReleaseOrTagFiltering
1174
                );
1175

UNCOV
1176
                await updateAdmins(userFactory, username, scmContext, pTuple.pipeline, pipelineFactory);
×
1177

UNCOV
1178
                const token = await pTuple.pipeline.token;
×
UNCOV
1179
                const scmConfig = {
×
1180
                    scmUri: pTuple.pipeline.scmUri,
1181
                    token,
1182
                    scmRepo: pTuple.pipeline.scmRepo,
1183
                    scmContext
1184
                };
1185
                // obtain pipeline's latest commit sha for branch specific job
UNCOV
1186
                let configPipelineSha = '';
×
1187

UNCOV
1188
                try {
×
UNCOV
1189
                    configPipelineSha = await pipelineFactory.scm.getCommitSha(scmConfig);
×
1190
                } catch (err) {
UNCOV
1191
                    if (err.status >= 500) {
×
UNCOV
1192
                        throw err;
×
1193
                    } else {
UNCOV
1194
                        logger.info(`skip create event for branch: ${pipelineBranch}`);
×
1195
                    }
1196
                }
UNCOV
1197
                const eventConfig = {
×
1198
                    pipelineId: pTuple.pipeline.id,
1199
                    type: 'pipeline',
1200
                    webhooks: true,
1201
                    username,
1202
                    scmContext,
1203
                    startFrom,
1204
                    sha,
1205
                    configPipelineSha,
1206
                    changedFiles,
1207
                    baseBranch: branch,
1208
                    causeMessage: `Merged by ${username}`,
1209
                    meta: createMeta(parsed),
1210
                    releaseName,
1211
                    ref
1212
                };
1213

1214
                // Check is the webhook event is from a subscribed repo and
1215
                // set the jobs entry point to ~subscribe
UNCOV
1216
                if (uriTrimmer(scmConfigFromHook.scmUri) !== uriTrimmer(pTuple.pipeline.scmUri)) {
×
UNCOV
1217
                    eventConfig.subscribedEvent = true;
×
UNCOV
1218
                    eventConfig.startFrom = '~subscribe';
×
UNCOV
1219
                    eventConfig.subscribedConfigSha = eventConfig.sha;
×
1220

UNCOV
1221
                    try {
×
UNCOV
1222
                        eventConfig.sha = await pipelineFactory.scm.getCommitSha(scmConfig);
×
1223
                    } catch (err) {
1224
                        if (err.status >= 500) {
×
1225
                            throw err;
×
1226
                        } else {
1227
                            logger.info(`skip create event for this subscribed trigger`);
×
1228
                        }
1229
                    }
1230

UNCOV
1231
                    try {
×
UNCOV
1232
                        const commitInfo = await pipelineFactory.scm.decorateCommit({
×
1233
                            scmUri: scmConfigFromHook.scmUri,
1234
                            scmContext,
1235
                            sha: eventConfig.subscribedConfigSha,
1236
                            token
1237
                        });
1238

1239
                        eventConfig.subscribedSourceUrl = commitInfo.url;
×
1240
                    } catch (err) {
UNCOV
1241
                        if (err.status >= 500) {
×
1242
                            throw err;
×
1243
                        } else {
UNCOV
1244
                            logger.info(`skip create event for this subscribed trigger`);
×
1245
                        }
1246
                    }
1247
                }
1248

UNCOV
1249
                if (skipMessage) {
×
UNCOV
1250
                    eventConfig.skipMessage = skipMessage;
×
1251
                }
1252

UNCOV
1253
                return eventConfig;
×
1254
            } catch (err) {
UNCOV
1255
                logger.warn(`pipeline:${pTuple.pipeline.id} error in starting event`, err);
×
1256

UNCOV
1257
                return null;
×
1258
            }
1259
        })
1260
    );
1261

UNCOV
1262
    const events = await startEvents(eventConfigs, server);
×
1263

UNCOV
1264
    return events;
×
1265
}
1266

1267
/**
1268
 * Act on a Push event
1269
 *  - Should start a new main job
1270
 * @method pushEvent
1271
 * @param  {Hapi.request}       request                Request from user
1272
 * @param  {Hapi.h}             h                      Response toolkit
1273
 * @param  {Object}             parsed                 It has information to create event
1274
 * @param  {String}             token                  The token used to authenticate to the SCM
1275
 * @param  {String}             [skipMessage]          Message to skip starting builds
1276
 */
1277
async function pushEvent(request, h, parsed, skipMessage, token) {
UNCOV
1278
    const { pipelineFactory, userFactory } = request.server.app;
×
UNCOV
1279
    const { hookId, checkoutUrl, branch, scmContext, type, action, changedFiles, releaseName, ref } = parsed;
×
1280

UNCOV
1281
    const fullCheckoutUrl = `${checkoutUrl}#${branch}`;
×
UNCOV
1282
    const scmConfig = {
×
1283
        scmUri: '',
1284
        token: '',
1285
        scmContext
1286
    };
1287

UNCOV
1288
    request.log(['webhook', hookId], `Push for ${fullCheckoutUrl}`);
×
1289

UNCOV
1290
    try {
×
UNCOV
1291
        scmConfig.token = token;
×
UNCOV
1292
        scmConfig.scmUri = await pipelineFactory.scm.parseUrl({
×
1293
            checkoutUrl: fullCheckoutUrl,
1294
            token,
1295
            scmContext
1296
        });
1297

UNCOV
1298
        const pipelines = await triggeredPipelines(
×
1299
            pipelineFactory,
1300
            scmConfig,
1301
            branch,
1302
            type,
1303
            action,
1304
            changedFiles,
1305
            releaseName,
1306
            ref
1307
        );
1308

UNCOV
1309
        let events = [];
×
1310

UNCOV
1311
        if (!pipelines || pipelines.length === 0) {
×
UNCOV
1312
            request.log(['webhook', hookId], `Skipping since Pipeline ${fullCheckoutUrl} does not exist`);
×
1313
        } else {
UNCOV
1314
            events = await createEvents(
×
1315
                request.server,
1316
                userFactory,
1317
                pipelineFactory,
1318
                pipelines,
1319
                parsed,
1320
                skipMessage,
1321
                scmConfig
1322
            );
1323
        }
1324

UNCOV
1325
        const hasBuildEvents = events.filter(e => e.builds !== null);
×
1326

UNCOV
1327
        if (hasBuildEvents.length === 0) {
×
UNCOV
1328
            return h.response({ message: 'No jobs to start' }).code(204);
×
1329
        }
1330

UNCOV
1331
        hasBuildEvents.forEach(e => {
×
UNCOV
1332
            request.log(['webhook', hookId, e.id], `Event ${e.id} started`);
×
1333
        });
1334

UNCOV
1335
        return h.response().code(201);
×
1336
    } catch (err) {
UNCOV
1337
        logger.error(`[${hookId}]: ${err}`);
×
1338

UNCOV
1339
        throw err;
×
1340
    }
1341
}
1342

1343
/** Execute scm.getCommitRefSha()
1344
 * @method getCommitRefSha
1345
 * @param    {Object}     scm
1346
 * @param    {String}     token            The token used to authenticate to the SCM
1347
 * @param    {String}     ref              The reference which we want
1348
 * @param    {String}     checkoutUrl      Scm checkout URL
1349
 * @param    {String}     scmContext       Scm which pipeline's repository exists in
1350
 * @returns  {Promise}                     Specific SHA1 commit to start the build with
1351
 */
1352
async function getCommitRefSha({ scm, token, ref, refType, checkoutUrl, scmContext }) {
1353
    // For example, git@github.com:screwdriver-cd/data-schema.git => screwdriver-cd, data-schema
UNCOV
1354
    const owner = CHECKOUT_URL_SCHEMA_REGEXP.exec(checkoutUrl)[2];
×
UNCOV
1355
    const repo = CHECKOUT_URL_SCHEMA_REGEXP.exec(checkoutUrl)[3];
×
1356

UNCOV
1357
    return scm.getCommitRefSha({
×
1358
        token,
1359
        owner,
1360
        repo,
1361
        ref,
1362
        refType,
1363
        scmContext
1364
    });
1365
}
1366

1367
/**
1368
 * Start pipeline events with scm webhook config
1369
 * @method startHookEvent
1370
 * @param  {Hapi.request} request   Request from user
1371
 * @param  {Object} h               Response toolkit
1372
 * @param  {Object} webhookConfig   Configuration required to start events
1373
 * @return {Promise}
1374
 */
1375
async function startHookEvent(request, h, webhookConfig) {
UNCOV
1376
    const { userFactory, pipelineFactory } = request.server.app;
×
UNCOV
1377
    const { scm } = pipelineFactory;
×
UNCOV
1378
    const ignoreUser = webhookConfig.pluginOptions.ignoreCommitsBy;
×
UNCOV
1379
    let message = 'Unable to process this kind of event';
×
1380
    let skipMessage;
UNCOV
1381
    let parsedHookId = '';
×
1382

UNCOV
1383
    const { type, hookId, username, scmContext, ref, checkoutUrl, action, prNum } = webhookConfig;
×
1384

UNCOV
1385
    parsedHookId = hookId;
×
1386

UNCOV
1387
    try {
×
1388
        // skipping checks
UNCOV
1389
        if (/\[(skip ci|ci skip)\]/.test(webhookConfig.lastCommitMessage)) {
×
UNCOV
1390
            skipMessage = 'Skipping due to the commit message: [skip ci]';
×
1391
        }
1392

1393
        // if skip ci then don't return
UNCOV
1394
        if (ignoreUser && ignoreUser.length !== 0 && !skipMessage) {
×
1395
            const commitAuthors =
UNCOV
1396
                Array.isArray(webhookConfig.commitAuthors) && webhookConfig.commitAuthors.length !== 0
×
1397
                    ? webhookConfig.commitAuthors
1398
                    : [username];
UNCOV
1399
            const validCommitAuthors = commitAuthors.filter(author => !ignoreUser.includes(author));
×
1400

UNCOV
1401
            if (!validCommitAuthors.length) {
×
UNCOV
1402
                message = `Skipping because user ${username} is ignored`;
×
UNCOV
1403
                request.log(['webhook', hookId], message);
×
1404

UNCOV
1405
                return h.response({ message }).code(204);
×
1406
            }
1407
        }
1408

UNCOV
1409
        const token = await obtainScmToken({
×
1410
            pluginOptions: webhookConfig.pluginOptions,
1411
            userFactory,
1412
            username,
1413
            scmContext,
1414
            scm
1415
        });
1416

UNCOV
1417
        if (action !== 'release' && action !== 'tag') {
×
1418
            let scmUri;
1419

UNCOV
1420
            if (type === 'pr') {
×
UNCOV
1421
                scmUri = await scm.parseUrl({ checkoutUrl, token, scmContext });
×
1422
            }
UNCOV
1423
            webhookConfig.changedFiles = await scm.getChangedFiles({
×
1424
                webhookConfig,
1425
                type,
1426
                token,
1427
                scmContext,
1428
                scmUri,
1429
                prNum
1430
            });
UNCOV
1431
            request.log(['webhook', hookId], `Changed files are ${webhookConfig.changedFiles}`);
×
1432
        } else {
1433
            // The payload has no sha when webhook event is tag or release, so we need to get it.
UNCOV
1434
            try {
×
UNCOV
1435
                webhookConfig.sha = await getCommitRefSha({
×
1436
                    scm,
1437
                    token,
1438
                    ref,
1439
                    refType: 'tags',
1440
                    checkoutUrl,
1441
                    scmContext
1442
                });
1443
            } catch (err) {
UNCOV
1444
                request.log(['webhook', hookId, 'getCommitRefSha'], err);
×
1445

1446
                // there is a possibility of scm.getCommitRefSha() is not implemented yet
UNCOV
1447
                return h.response({ message }).code(204);
×
1448
            }
1449
        }
1450

UNCOV
1451
        if (type === 'pr') {
×
1452
            // disregard skip ci for pull request events
UNCOV
1453
            return pullRequestEvent(webhookConfig.pluginOptions, request, h, webhookConfig, token);
×
1454
        }
1455

UNCOV
1456
        return pushEvent(request, h, webhookConfig, skipMessage, token);
×
1457
    } catch (err) {
1458
        logger.error(`[${parsedHookId}]: ${err}`);
×
1459

1460
        throw err;
×
1461
    }
1462
}
1463

1464
module.exports = { startHookEvent };
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc