• 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

90.56
/plugins/builds/index.js
1
'use strict';
2

3
const logger = require('screwdriver-logger');
216✔
4
const workflowParser = require('screwdriver-workflow-parser');
216✔
5
const { STAGE_TEARDOWN_PATTERN } = require('screwdriver-data-schema').config.regex;
216✔
6
const hoek = require('@hapi/hoek');
216✔
7
const getRoute = require('./get');
216✔
8
const getBuildStatusesRoute = require('./getBuildStatuses');
216✔
9
const updateRoute = require('./update');
216✔
10
const createRoute = require('./create');
216✔
11
const stepGetRoute = require('./steps/get');
216✔
12
const listStepsRoute = require('./steps/list');
216✔
13
const artifactGetRoute = require('./artifacts/get');
216✔
14
const artifactGetAllRoute = require('./artifacts/getAll');
216✔
15
const artifactUnzipRoute = require('./artifacts/unzip');
216✔
16
const stepUpdateRoute = require('./steps/update');
216✔
17
const stepLogsRoute = require('./steps/logs');
216✔
18
const listSecretsRoute = require('./listSecrets');
216✔
19
const tokenRoute = require('./token');
216✔
20
const metricsRoute = require('./metrics');
216✔
21
const locker = require('../lock');
216✔
22
const { OrTrigger } = require('./triggers/or');
216✔
23
const { AndTrigger } = require('./triggers/and');
216✔
24
const { RemoteTrigger } = require('./triggers/remoteTrigger');
216✔
25
const { RemoteJoin } = require('./triggers/remoteJoin');
216✔
26
const {
27
    strToInt,
28
    createJoinObject,
29
    createEvent,
30
    parseJobInfo,
31
    ensureStageTeardownBuildExists,
32
    getJob,
33
    isOrTrigger,
34
    extractExternalJoinData,
35
    extractCurrentPipelineJoinData,
36
    createExternalEvent,
37
    getBuildsForGroupEvent,
38
    buildsToRestartFilter,
39
    trimJobName,
40
    getParallelBuilds,
41
    isStartFromMiddleOfCurrentStage,
42
    Status,
43
    getSameParentEvents,
44
    getNextJobStageName
45
} = require('./triggers/helpers');
216✔
46
const { getFullStageJobName } = require('../helper');
216✔
47

48
/**
49
 * Delete a build
50
 * @param  {Object}  buildConfig  build object to delete
51
 * @param  {Object}  buildFactory build factory
52
 * @return {Promise}
53
 * */
54
async function deleteBuild(buildConfig, buildFactory) {
55
    const buildToDelete = await buildFactory.get(buildConfig);
10✔
56

57
    if (buildToDelete && buildToDelete.status === 'CREATED') {
10✔
58
        return buildToDelete.remove();
2✔
59
    }
60

61
    return null;
8✔
62
}
63

64
/**
65
 * Trigger the next jobs of the current job
66
 * @param { import('./types/index').ServerConfig }  config  Configuration object
67
 * @param { import('./types/index').ServerApp }     app     Server app object
68
 * @return {Promise<null>}                                  Resolves to the newly created build or null
69
 */
70
async function triggerNextJobs(config, app) {
71
    const currentPipeline = config.pipeline;
56✔
72
    const currentJob = config.job;
56✔
73
    const currentBuild = config.build;
56✔
74
    const { jobFactory, buildFactory, eventFactory, pipelineFactory } = app;
56✔
75

76
    /** @type {EventModel} */
77
    const currentEvent = await eventFactory.get({ id: currentBuild.eventId });
56✔
78
    const current = {
56✔
79
        pipeline: currentPipeline,
80
        build: currentBuild,
81
        event: currentEvent
82
    };
83
    /** @type Array<string> */
84
    const nextJobsTrigger = workflowParser.getNextJobs(currentEvent.workflowGraph, {
56✔
85
        trigger: currentJob.name,
86
        chainPR: currentPipeline.chainPR,
87
        startFrom: currentEvent.startFrom
88
    });
89
    const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
56✔
90
    const originalCurrentJobName = trimJobName(currentJob.name);
56✔
91

92
    // Trigger OrTrigger and AndTrigger for current pipeline jobs.
93
    // Helper function to handle triggering jobs in same pipeline
94
    const orTrigger = new OrTrigger(app, config);
56✔
95
    const andTrigger = new AndTrigger(app, config, currentEvent);
56✔
96
    const currentPipelineNextJobs = extractCurrentPipelineJoinData(pipelineJoinData, currentPipeline.id);
56✔
97

98
    const downstreamOfNextJobsToBeProcessed = [];
56✔
99

100
    for (const [nextJobName] of Object.entries(currentPipelineNextJobs)) {
56✔
101
        const nextJob = await getJob(nextJobName, currentPipeline.id, jobFactory);
51✔
102
        const node = currentEvent.workflowGraph.nodes.find(n => n.name === trimJobName(nextJobName));
236✔
103
        const isNextJobVirtual = node.virtual || false;
51✔
104
        const nextJobStageName = getNextJobStageName({ stageName: node.stageName, nextJobName });
51✔
105
        const resource = `pipeline:${currentPipeline.id}:groupEvent:${currentEvent.groupEventId}`;
51✔
106
        let lock;
107
        let nextBuild;
108

109
        try {
51✔
110
            lock = await locker.lock(resource);
51✔
111
            const { parentBuilds, joinListNames } = parseJobInfo({
49✔
112
                joinObj: currentPipelineNextJobs,
113
                currentBuild,
114
                currentPipeline,
115
                currentJob,
116
                nextJobName
117
            });
118

119
            // Handle no-join case. Sequential Workflow
120
            // Note: current job can be "external" in nextJob's perspective
121
            /* CREATE AND START NEXT BUILD IF ALL 2 SCENARIOS ARE TRUE
122
             * 1. No join
123
             * 2. ([~D,B,C]->A) currentJob=D, nextJob=A, joinList(A)=[B,C]
124
             *    joinList doesn't include D, so start A
125
             */
126
            if (
49✔
127
                isOrTrigger(currentEvent.workflowGraph, originalCurrentJobName, trimJobName(nextJobName)) ||
67✔
128
                isStartFromMiddleOfCurrentStage(currentJob.name, currentEvent.startFrom, currentEvent.workflowGraph)
129
            ) {
130
                nextBuild = await orTrigger.execute(
33✔
131
                    currentEvent,
132
                    currentPipeline.id,
133
                    nextJob,
134
                    parentBuilds,
135
                    isNextJobVirtual
136
                );
137
            } else {
138
                nextBuild = await andTrigger.execute(
16✔
139
                    nextJob,
140
                    parentBuilds,
141
                    joinListNames,
142
                    isNextJobVirtual,
143
                    nextJobStageName
144
                );
145
            }
146

147
            if (isNextJobVirtual && nextBuild && nextBuild.status === Status.SUCCESS) {
49✔
148
                downstreamOfNextJobsToBeProcessed.push({
2✔
149
                    build: nextBuild,
150
                    event: currentEvent,
151
                    job: nextJob,
152
                    pipeline: currentPipeline,
153
                    scmContext: config.scmContext,
154
                    username: config.username
155
                });
156
            }
157
        } catch (err) {
158
            logger.error(
2✔
159
                `Error in triggerNextJobInSamePipeline:${nextJobName} from pipeline:${currentPipeline.id}-${currentJob.name}-event:${currentEvent.id} `,
160
                err
161
            );
162
        }
163
        await locker.unlock(lock, resource);
51✔
164
    }
165

166
    // Trigger RemoteJoin and RemoteTrigger for current and external pipeline jobs.
167
    // Helper function to handle triggering jobs in external pipeline
168
    const remoteTrigger = new RemoteTrigger(app, config);
56✔
169
    const remoteJoin = new RemoteJoin(app, config, currentEvent);
56✔
170
    const externalPipelineJoinData = extractExternalJoinData(pipelineJoinData, currentPipeline.id);
56✔
171

172
    for (const [joinedPipelineId, joinedPipeline] of Object.entries(externalPipelineJoinData)) {
56✔
173
        const isCurrentPipeline = strToInt(joinedPipelineId) === currentPipeline.id;
14✔
174
        const remoteJoinName = `sd@${currentPipeline.id}:${originalCurrentJobName}`;
14✔
175
        const remoteTriggerName = `~${remoteJoinName}`;
14✔
176
        let lock;
177
        let resource;
178

179
        let externalEvent = joinedPipeline.event;
14✔
180

181
        // This includes CREATED builds too
182
        const groupEventBuilds =
183
            externalEvent !== undefined ? await getBuildsForGroupEvent(externalEvent.groupEventId, buildFactory) : [];
14✔
184

185
        // fetch builds created due to trigger
186
        if (externalEvent) {
14✔
187
            const parallelBuilds = await getParallelBuilds({
6✔
188
                eventFactory,
189
                parentEventId: externalEvent.id,
190
                pipelineId: externalEvent.pipelineId
191
            });
192

193
            groupEventBuilds.push(...parallelBuilds);
6✔
194
        } else {
195
            const sameParentEvents = await getSameParentEvents({
8✔
196
                eventFactory,
197
                parentEventId: currentEvent.id,
198
                pipelineId: strToInt(joinedPipelineId)
199
            });
200

201
            if (sameParentEvents.length > 0) {
8!
UNCOV
202
                externalEvent = sameParentEvents[0];
×
203
            }
204
        }
205

206
        let isRestartPipeline = false;
14✔
207

208
        if (currentEvent.parentEventId) {
14✔
209
            const parentEvent = await eventFactory.get({ id: currentEvent.parentEventId });
1✔
210

211
            isRestartPipeline = parentEvent && strToInt(currentEvent.pipelineId) === strToInt(parentEvent.pipelineId);
1✔
212
        }
213

214
        // If user used external trigger syntax, the jobs are triggered as external
215
        if (isCurrentPipeline) {
14✔
216
            externalEvent = null;
1✔
217
        } else if (isRestartPipeline) {
13✔
218
            // If parentEvent and currentEvent have the same pipelineId, then currentEvent is the event that started the restart
219
            // If restarted from the downstream pipeline, the remote trigger must create a new event in the upstream pipeline
220
            const sameParentEvents = await getSameParentEvents({
1✔
221
                eventFactory,
222
                parentEventId: currentEvent.id,
223
                pipelineId: strToInt(joinedPipelineId)
224
            });
225

226
            externalEvent = sameParentEvents.length > 0 ? sameParentEvents[0] : null;
1!
227
        }
228

229
        // no need to lock if there is no external event
230
        if (externalEvent) {
14✔
231
            resource = `pipeline:${joinedPipelineId}:event:${externalEvent.id}`;
6✔
232
        }
233

234
        // Create a new external event
235
        // First downstream trigger, restart case, same pipeline trigger as external
236
        if (!externalEvent) {
14✔
237
            const { parentBuilds } = parseJobInfo({
8✔
238
                currentBuild,
239
                currentPipeline,
240
                currentJob
241
            });
242

243
            const externalEventConfig = {
8✔
244
                pipelineFactory,
245
                eventFactory,
246
                externalPipelineId: joinedPipelineId,
247
                parentBuildId: currentBuild.id,
248
                parentBuilds,
249
                causeMessage: `Triggered by ${remoteJoinName}`,
250
                parentEventId: currentEvent.id,
251
                startFrom: remoteTriggerName,
252
                skipMessage: 'Skip bulk external builds creation', // Don't start builds in eventFactory.
253
                groupEventId: null
254
            };
255

256
            const buildsToRestart = buildsToRestartFilter(joinedPipeline, groupEventBuilds, currentEvent, currentBuild);
8✔
257
            const isRestart = buildsToRestart.length > 0;
8✔
258

259
            // Restart case
260
            if (isRestart) {
8!
261
                // 'joinedPipeline.event.id' is restart event, not group event.
UNCOV
262
                const groupEvent = await eventFactory.get({ id: joinedPipeline.event.id });
×
263

UNCOV
264
                externalEventConfig.groupEventId = groupEvent.groupEventId;
×
UNCOV
265
                externalEventConfig.parentBuilds = buildsToRestart[0].parentBuilds;
×
266
            } else {
267
                const sameParentEvents = await getSameParentEvents({
8✔
268
                    eventFactory,
269
                    parentEventId: currentEvent.groupEventId,
270
                    pipelineId: strToInt(joinedPipelineId)
271
                });
272

273
                externalEventConfig.groupEventId =
8✔
274
                    sameParentEvents.length > 0 ? sameParentEvents[0].groupEventId : currentEvent.groupEventId;
8!
275
            }
276

277
            try {
8✔
278
                externalEvent = await createExternalEvent(externalEventConfig);
8✔
279
            } catch (err) {
280
                // The case of triggered external pipeline which is already deleted from DB, etc
281
                logger.error(
1✔
282
                    `Error in createExternalEvent:${joinedPipelineId} from pipeline:${currentPipeline.id}-${currentJob.name}-event:${currentEvent.id}`,
283
                    err
284
                );
285
            }
286
        }
287

288
        // Skip trigger process if createExternalEvent fails
289
        if (externalEvent) {
14✔
290
            for (const [nextJobName, nextJobInfo] of Object.entries(joinedPipeline.jobs)) {
13✔
291
                const nextJob = await getJob(nextJobName, joinedPipelineId, jobFactory);
15✔
292
                const node = externalEvent.workflowGraph.nodes.find(n => n.name === trimJobName(nextJobName));
61✔
293
                const isNextJobVirtual = node.virtual || false;
15✔
294
                const nextJobStageName = getNextJobStageName({ stageName: node.stageName, nextJobName });
13✔
295

296
                const { parentBuilds } = parseJobInfo({
13✔
297
                    joinObj: joinedPipeline.jobs,
298
                    currentBuild,
299
                    currentPipeline,
300
                    currentJob,
301
                    nextJobName,
302
                    nextPipelineId: joinedPipelineId
303
                });
304

305
                let nextBuild;
306

307
                try {
13✔
308
                    if (resource) lock = await locker.lock(resource);
13✔
309

310
                    if (isOrTrigger(externalEvent.workflowGraph, remoteTriggerName, nextJobName)) {
13✔
311
                        nextBuild = await remoteTrigger.execute(
7✔
312
                            externalEvent,
313
                            externalEvent.pipelineId,
314
                            nextJob,
315
                            parentBuilds,
316
                            isNextJobVirtual
317
                        );
318
                    } else {
319
                        // Re get join list when first time remote trigger since external event was empty and cannot get workflow graph then
320
                        const joinList =
321
                            nextJobInfo.join.length > 0
6✔
322
                                ? nextJobInfo.join
323
                                : workflowParser.getSrcForJoin(externalEvent.workflowGraph, { jobName: nextJobName });
324
                        const joinListNames = joinList.map(j => j.name);
6✔
325

326
                        nextBuild = await remoteJoin.execute(
6✔
327
                            externalEvent,
328
                            nextJob,
329
                            parentBuilds,
330
                            groupEventBuilds,
331
                            joinListNames,
332
                            isNextJobVirtual,
333
                            nextJobStageName
334
                        );
335
                    }
336

337
                    if (isNextJobVirtual && nextBuild && nextBuild.status === Status.SUCCESS) {
13!
UNCOV
338
                        downstreamOfNextJobsToBeProcessed.push({
×
339
                            build: nextBuild,
340
                            event: currentEvent,
341
                            job: nextJob,
342
                            pipeline: await nextJob.pipeline,
343
                            scmContext: config.scmContext,
344
                            username: config.username
345
                        });
346
                    }
347
                } catch (err) {
348
                    logger.error(
×
349
                        `Error in triggerJobsInExternalPipeline:${joinedPipelineId} from pipeline:${currentPipeline.id}-${currentJob.name}-event:${currentEvent.id} `,
350
                        err
351
                    );
352
                }
353

354
                await locker.unlock(lock, resource);
13✔
355
            }
356
        }
357
    }
358

359
    for (const nextConfig of downstreamOfNextJobsToBeProcessed) {
54✔
360
        await triggerNextJobs(nextConfig, app);
2✔
361
    }
362

363
    return null;
54✔
364
}
365

366
/**
367
 * Create or update stage teardown build
368
 * @method createOrUpdateStageTeardownBuild
369
 * @param {Object}      config              Configuration object
370
 * @param {Pipeline}    config.pipeline     Current pipeline
371
 * @param {Job}         config.job          Current job
372
 * @param {Build}       config.build        Current build
373
 * @param {Build}       config.event        Current event
374
 * @param {Build}       config.stage        Current stage
375
 * @param {String}      config.username     Username
376
 * @param {String}      config.scmContext   SCM context
377
 * @param {String}      app                 Server app object
378
 * @return {Promise}                        Create a new build or update an existing build
379
 */
380
async function createOrUpdateStageTeardownBuild(config, app) {
381
    const { pipeline, job, build, username, scmContext, event, stage } = config;
3✔
382
    const { buildFactory, jobFactory, eventFactory } = app;
3✔
383
    const current = {
3✔
384
        pipeline,
385
        job,
386
        build,
387
        event,
388
        stage
389
    };
390

391
    const stageTeardownName = getFullStageJobName({ stageName: current.stage.name, jobName: 'teardown' });
3✔
392

393
    const nextJobsTrigger = [stageTeardownName];
3✔
394
    const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
3✔
395

396
    const resource = `pipeline:${pipeline.id}:groupEvent:${event.groupEventId}`;
3✔
397
    let lock;
398
    let teardownBuild;
399

400
    try {
3✔
401
        lock = await locker.lock(resource);
3✔
402
        const { parentBuilds } = parseJobInfo({
3✔
403
            joinObj: pipelineJoinData,
404
            currentBuild: build,
405
            currentPipeline: pipeline,
406
            currentJob: job,
407
            nextJobName: stageTeardownName
408
        });
409

410
        teardownBuild = await ensureStageTeardownBuildExists({
3✔
411
            jobFactory,
412
            buildFactory,
413
            current,
414
            parentBuilds,
415
            stageTeardownName,
416
            username,
417
            scmContext
418
        });
419
    } catch (err) {
420
        logger.error(
×
421
            `Error in createOrUpdateStageTeardownBuild:${stageTeardownName} from pipeline:${pipeline.id}-event:${event.id} `,
422
            err
423
        );
424
    }
425
    await locker.unlock(lock, resource);
3✔
426

427
    return teardownBuild;
3✔
428
}
429

430
/**
431
 * Build API Plugin
432
 * @method register
433
 * @param  {Hapi}     server                Hapi Server
434
 * @param  {Object}   options               Configuration
435
 * @param  {String}   options.logBaseUrl    Log service's base URL
436
 * @param  {Function} next                  Function to call when done
437
 */
438
const buildsPlugin = {
216✔
439
    name: 'builds',
440
    async register(server, options) {
441
        /**
442
         * Remove builds for downstream jobs of current job
443
         * @method removeJoinBuilds
444
         * @param {Object}      config              Configuration object
445
         * @param {Pipeline}    config.pipeline     Current pipeline
446
         * @param {Job}         config.job          Current job
447
         * @param {Build}       config.build        Current build
448
         * @param {String}  app                      Server app object
449
         * @return {Promise}                        Resolves to the removed build or null
450
         */
451
        server.expose('removeJoinBuilds', async (config, app) => {
313✔
452
            const { pipeline, job, build, event, stage } = config;
8✔
453
            const { eventFactory, buildFactory } = app;
8✔
454
            const current = {
8✔
455
                pipeline,
456
                job,
457
                build,
458
                event,
459
                stage
460
            };
461
            const nextJobsTrigger = workflowParser.getNextJobs(current.event.workflowGraph, {
8✔
462
                trigger: current.job.name,
463
                chainPR: pipeline.chainPR
464
            });
465
            const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
8✔
466
            const buildConfig = {};
8✔
467
            const deletePromises = [];
8✔
468

469
            for (const pid of Object.keys(pipelineJoinData)) {
8✔
470
                const isExternal = +pid !== current.pipeline.id;
6✔
471

472
                for (const nextJobName of Object.keys(pipelineJoinData[pid].jobs)) {
6✔
473
                    try {
7✔
474
                        const isNextJobStageTeardown = STAGE_TEARDOWN_PATTERN.test(nextJobName);
7✔
475

476
                        if (!isNextJobStageTeardown) {
7✔
477
                            const nextJob = pipelineJoinData[pid].jobs[nextJobName];
6✔
478

479
                            buildConfig.jobId = nextJob.id;
6✔
480
                            if (!isExternal) {
6!
481
                                buildConfig.eventId = event.id;
6✔
482
                            } else {
UNCOV
483
                                buildConfig.eventId = hoek.reach(pipelineJoinData[pid], 'event.id');
×
484
                            }
485

486
                            if (buildConfig.eventId) {
6!
487
                                if (current.stage) {
6✔
488
                                    const stageTeardownName = getFullStageJobName({
4✔
489
                                        stageName: current.stage.name,
490
                                        jobName: 'teardown'
491
                                    });
492

493
                                    // Do not remove stage teardown builds as they need to be executed on stage failure as well.
494
                                    if (nextJobName !== stageTeardownName) {
4!
495
                                        deletePromises.push(deleteBuild(buildConfig, buildFactory));
4✔
496
                                    }
497
                                }
498

499
                                deletePromises.push(deleteBuild(buildConfig, buildFactory));
6✔
500
                            }
501
                        }
502
                    } catch (err) {
503
                        logger.error(
×
504
                            `Error in removeJoinBuilds:${nextJobName} from pipeline:${current.pipeline.id}-${current.job.name}-event:${current.event.id} `,
505
                            err
506
                        );
507
                    }
508
                }
509
            }
510

511
            await Promise.all(deletePromises);
8✔
512
        });
513

514
        /**
515
         * Create event for downstream pipeline that need to be rebuilt
516
         * @method triggerEvent
517
         * @param {Object}  config               Configuration object
518
         * @param {String}  config.pipelineId    Pipeline to be rebuilt
519
         * @param {String}  config.startFrom     Job to be rebuilt
520
         * @param {String}  config.causeMessage  Caused message, e.g. triggered by 1234(buildId)
521
         * @param {String}  config.parentBuildId ID of the build that triggers this event
522
         * @param {String}  app                  Server app object
523
         * @return {Promise}                     Resolves to the newly created event
524
         */
525
        server.expose('triggerEvent', (config, app) => {
313✔
526
            config.eventFactory = app.eventFactory;
×
527
            config.pipelineFactory = app.pipelineFactory;
×
528

529
            return createEvent(config);
×
530
        });
531

532
        /**
533
         * Trigger the next jobs of the current job
534
         */
535
        server.expose('triggerNextJobs', triggerNextJobs);
313✔
536

537
        /**
538
         * Create or Update stage teardown build on stage failure
539
         */
540
        server.expose('createOrUpdateStageTeardownBuild', createOrUpdateStageTeardownBuild);
313✔
541

542
        server.route([
313✔
543
            getRoute(),
544
            getBuildStatusesRoute(),
545
            updateRoute(options),
546
            createRoute(),
547
            // Steps
548
            stepGetRoute(),
549
            stepUpdateRoute(),
550
            stepLogsRoute(options),
551
            listStepsRoute(),
552
            // Secrets
553
            listSecretsRoute(),
554
            tokenRoute(),
555
            metricsRoute(),
556
            artifactGetRoute(options),
557
            artifactGetAllRoute(options),
558
            artifactUnzipRoute()
559
        ]);
560
    }
561
};
562

563
module.exports = buildsPlugin;
216✔
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