• 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

96.82
/plugins/builds/helper/updateBuild.js
1
'use strict';
2

3
const boom = require('@hapi/boom');
217✔
4
const hoek = require('@hapi/hoek');
217✔
5
const merge = require('lodash.mergewith');
217✔
6
const { PR_JOB_NAME, PR_STAGE_NAME, STAGE_TEARDOWN_PATTERN } = require('screwdriver-data-schema').config.regex;
217✔
7
const { getFullStageJobName } = require('../../helper');
217✔
8
const { updateVirtualBuildSuccess } = require('../triggers/helpers');
217✔
9
const TERMINAL_STATUSES = ['FAILURE', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
217✔
10
const FINISHED_STATUSES = ['FAILURE', 'SUCCESS', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
217✔
11

12
/**
13
 * @typedef {import('screwdriver-models/lib/build')} Build
14
 * @typedef {import('screwdriver-models/lib/event')} Event
15
 * @typedef {import('screwdriver-models/lib/step')} Step
16
 */
17

18
/**
19
 * Identify whether this build resulted in a previously failed job to become successful.
20
 *
21
 * @method isFixedBuild
22
 * @param  {Build}          build       Build Object
23
 * @param  {JobFactory}     jobFactory  Job Factory instance
24
 */
25
async function isFixedBuild(build, jobFactory) {
26
    if (build.status !== 'SUCCESS') {
75✔
27
        return false;
20✔
28
    }
29

30
    const job = await jobFactory.get(build.jobId);
55✔
31
    const failureBuild = await job.getLatestBuild({ status: 'FAILURE' });
55✔
32
    const successBuild = await job.getLatestBuild({ status: 'SUCCESS' });
55✔
33

34
    return !!((failureBuild && !successBuild) || failureBuild.id > successBuild.id);
55✔
35
}
36

37
/**
38
 * Stops a frozen build from executing
39
 *
40
 * @method stopFrozenBuild
41
 * @param  {Build}  build           Build Object
42
 * @param  {String} previousStatus  Previous build status
43
 */
44
async function stopFrozenBuild(build, previousStatus) {
45
    if (previousStatus !== 'FROZEN') {
75✔
46
        return Promise.resolve();
73✔
47
    }
48

49
    return build.stopFrozen(previousStatus);
2✔
50
}
51

52
/**
53
 * Updates execution details for init step
54
 *
55
 * @method  stopFrozenBuild
56
 * @param   {Build}         build   Build Object
57
 * @param   {Object}        app     Hapi app Object
58
 * @returns {Promise<Step>}         Updated step
59
 */
60
async function updateInitStep(build, app) {
61
    const step = await app.stepFactory.get({ buildId: build.id, name: 'sd-setup-init' });
5✔
62
    // If there is no init step, do nothing
63

64
    if (!step) {
5✔
65
        return null;
1✔
66
    }
67

68
    step.endTime = build.startTime || new Date().toISOString();
4✔
69
    step.code = 0;
4✔
70

71
    return step.update();
4✔
72
}
73

74
/**
75
 * Set build status to desired status, set build statusMessage
76
 *
77
 * @param {Build}   build               Build Model
78
 * @param {String}  desiredStatus       New Status
79
 * @param {String}  statusMessage       User passed status message
80
 * @param {String}  statusMessageType   User passed severity of the status message
81
 * @param {String}  username            User initiating status build update
82
 */
83
function updateBuildStatus(build, desiredStatus, statusMessage, statusMessageType, username) {
84
    const currentStatus = build.status;
80✔
85

86
    // UNSTABLE -> SUCCESS needs to update meta and endtime.
87
    // However, the status itself cannot be updated to SUCCESS
88
    if (currentStatus === 'UNSTABLE') {
80✔
89
        return;
1✔
90
    }
91

92
    if (desiredStatus !== undefined) {
79✔
93
        build.status = desiredStatus;
74✔
94
    }
95

96
    switch (build.status) {
79✔
97
        case 'ABORTED':
98
            build.statusMessage =
4✔
99
                currentStatus === 'FROZEN' ? `Frozen build aborted by ${username}` : `Aborted by ${username}`;
4✔
100
            break;
4✔
101
        case 'FAILURE':
102
        case 'SUCCESS':
103
            if (statusMessage) {
62✔
104
                build.statusMessage = statusMessage;
3✔
105
                build.statusMessageType = statusMessageType || null;
3✔
106
            }
107
            break;
62✔
108
        default:
109
            build.statusMessage = statusMessage || null;
13✔
110
            build.statusMessageType = statusMessageType || null;
13✔
111
            break;
13✔
112
    }
113
}
114

115
/**
116
 * Get stage for current node
117
 *
118
 * @param  {StageFactory}   stageFactory    Stage factory
119
 * @param  {Object}         workflowGraph   Workflow graph
120
 * @param  {String}         jobName         Job name
121
 * @param  {Number}         pipelineId      Pipeline ID
122
 * @return {Stage}                          Stage for node
123
 */
124
async function getStage({ stageFactory, workflowGraph, jobName, pipelineId }) {
125
    const prJobName = jobName.match(PR_JOB_NAME);
80✔
126
    const nodeName = prJobName ? prJobName[2] : jobName;
80✔
127

128
    const currentNode = workflowGraph.nodes.find(node => node.name === nodeName);
264✔
129
    let stage = null;
80✔
130

131
    if (currentNode && currentNode.stageName) {
80✔
132
        const stageName = prJobName ? `${prJobName[1]}:${currentNode.stageName}` : currentNode.stageName;
11!
133

134
        stage = await stageFactory.get({
11✔
135
            pipelineId,
136
            name: stageName
137
        });
138
    }
139

140
    return Promise.resolve(stage);
80✔
141
}
142

143
/**
144
 * Get all builds in stage
145
 *
146
 * @param  {Stage}             stage       Stage
147
 * @param  {Event}             event       Event
148
 * @param  {JobFactory}        jobFactory  Job Factory instance
149
 * @return {Promise<Build[]>}              Builds in stage
150
 */
151
async function getStageJobBuilds({ stage, event, jobFactory }) {
152
    const prStageName = stage.name.match(PR_STAGE_NAME);
3✔
153
    const stageName = prStageName ? prStageName[2] : stage.name;
3!
154

155
    // Get all jobIds for jobs in the stage
156
    const stageNodes = event.workflowGraph.nodes.filter(n => {
3✔
157
        const jobName = n.name.split(':')[1];
65✔
158

159
        return n.stageName === stageName && jobName !== 'teardown';
65✔
160
    });
161

162
    const stageJobIds = await Promise.all(
3✔
163
        stageNodes.map(async n => {
164
            if (n.id) {
14✔
165
                return n.id;
13✔
166
            }
167

168
            const jobName = prStageName ? `${prStageName[1]}:${n.name}` : n.name;
1!
169
            const job = await jobFactory.get({ pipelineId: event.pipelineId, name: jobName });
1✔
170

171
            return job ? job.id : null;
1!
172
        })
173
    );
174

175
    // Get all builds in a stage for this event
176
    return event.getBuilds({ params: { jobId: stageJobIds.filter(id => id !== null) } });
14✔
177
}
178

179
/**
180
 * Checks if all builds in stage are done running
181
 * @param {Build[]} stageJobBuilds Builds in stage
182
 * @returns {Boolean}              Flag if stage is done
183
 */
184
function isStageDone(stageJobBuilds) {
185
    let stageIsDone = false;
3✔
186

187
    if (stageJobBuilds && stageJobBuilds.length !== 0) {
3!
188
        stageIsDone = !stageJobBuilds.some(b => !FINISHED_STATUSES.includes(b.status));
9✔
189
    }
190

191
    return stageIsDone;
3✔
192
}
193

194
/**
195
 * Derives overall status of the event based on individual build statuses
196
 *
197
 * @param {Build[]} builds  Builds associated with the event
198
 * @returns {String}        new status for the event
199
 */
200
function deriveEventStatusFromBuildStatuses(builds) {
201
    let newEventStatus = null;
110✔
202

203
    const BUILD_STATUS_TO_EVENT_STATUS_MAPPING = {
110✔
204
        ABORTED: 'ABORTED',
205
        CREATED: null,
206
        FAILURE: 'FAILURE',
207
        QUEUED: 'IN_PROGRESS',
208
        RUNNING: 'IN_PROGRESS',
209
        SUCCESS: 'SUCCESS',
210
        BLOCKED: 'IN_PROGRESS',
211
        UNSTABLE: 'SUCCESS',
212
        COLLAPSED: null,
213
        FROZEN: 'IN_PROGRESS'
214
    };
215

216
    const eventStatusToBuildCount = {
110✔
217
        IN_PROGRESS: 0,
218
        ABORTED: 0,
219
        FAILURE: 0,
220
        SUCCESS: 0
221
    };
222

223
    for (const b of builds) {
110✔
224
        const eventStatus = BUILD_STATUS_TO_EVENT_STATUS_MAPPING[b.status];
197✔
225

226
        if (eventStatus) {
197✔
227
            eventStatusToBuildCount[eventStatus] += 1;
175✔
228
        }
229
    }
230

231
    if (eventStatusToBuildCount.IN_PROGRESS) {
110✔
232
        newEventStatus = 'IN_PROGRESS';
24✔
233
    } else if (eventStatusToBuildCount.ABORTED) {
86✔
234
        newEventStatus = 'ABORTED';
15✔
235
    } else if (eventStatusToBuildCount.FAILURE) {
71✔
236
        newEventStatus = 'FAILURE';
18✔
237
    } else if (eventStatusToBuildCount.SUCCESS) {
53✔
238
        newEventStatus = 'SUCCESS';
49✔
239
    }
240

241
    return newEventStatus;
110✔
242
}
243

244
/**
245
 * Updates the build and trigger its downstream jobs in the workflow
246
 *
247
 * @method updateBuildAndTriggerDownstreamJobs
248
 * @param   {Object}    config
249
 * @param   {Build}     build
250
 * @param   {Object}    server
251
 * @param   {String}    username
252
 * @param   {Object}    scmContext
253
 * @returns {Promise<Build>} Updated build
254
 */
255
async function updateBuildAndTriggerDownstreamJobs(config, build, server, username, scmContext) {
256
    const { buildFactory, eventFactory, jobFactory, stageFactory, stageBuildFactory } = server.app;
82✔
257
    const { statusMessage, statusMessageType, stats, status: desiredStatus, meta } = config;
82✔
258
    const { triggerNextJobs, removeJoinBuilds, createOrUpdateStageTeardownBuild } = server.plugins.builds;
82✔
259

260
    const currentStatus = build.status;
82✔
261

262
    const event = await eventFactory.get(build.eventId);
82✔
263

264
    if (stats) {
82✔
265
        // need to do this so the field is dirty
266
        build.stats = Object.assign(build.stats, stats);
3✔
267
    }
268

269
    // Short circuit for cases that don't need to update status
270
    if (!desiredStatus) {
82✔
271
        build.statusMessage = statusMessage || build.statusMessage;
5✔
272
        build.statusMessageType = statusMessageType || build.statusMessageType;
5✔
273
    } else if (['SUCCESS', 'FAILURE', 'ABORTED'].includes(desiredStatus)) {
77✔
274
        build.meta = meta || {};
67✔
275
        event.meta = merge({}, event.meta, build.meta);
67✔
276
        build.endTime = new Date().toISOString();
67✔
277
    } else if (desiredStatus === 'RUNNING') {
10✔
278
        build.startTime = new Date().toISOString();
3✔
279
    } else if (desiredStatus === 'BLOCKED' && !hoek.reach(build, 'stats.blockedStartTime')) {
7✔
280
        build.stats = Object.assign(build.stats, {
1✔
281
            blockedStartTime: new Date().toISOString()
282
        });
283
    } else if (desiredStatus === 'QUEUED' && currentStatus !== 'QUEUED') {
6✔
284
        throw boom.badRequest(`Cannot update builds to ${desiredStatus}`);
1✔
285
    } else if (desiredStatus === 'BLOCKED' && currentStatus === 'BLOCKED') {
5✔
286
        // Queue-Service can call BLOCKED status update multiple times
287
        throw boom.badRequest(`Cannot update builds to ${desiredStatus}`);
1✔
288
    }
289

290
    let isFixed = Promise.resolve(false);
80✔
291
    let stopFrozen = null;
80✔
292

293
    updateBuildStatus(build, desiredStatus, statusMessage, statusMessageType, username);
80✔
294

295
    // If status got updated to RUNNING or COLLAPSED, update init endTime and code
296
    if (['RUNNING', 'COLLAPSED', 'FROZEN'].includes(desiredStatus)) {
80✔
297
        await updateInitStep(build, server.app);
5✔
298
    } else {
299
        stopFrozen = stopFrozenBuild(build, currentStatus);
75✔
300
        isFixed = isFixedBuild(build, jobFactory);
75✔
301
    }
302

303
    const [newBuild, newEvent] = await Promise.all([build.update(), event.update(), stopFrozen]);
80✔
304
    const job = await newBuild.job;
80✔
305
    const pipeline = await job.pipeline;
80✔
306

307
    if (desiredStatus) {
80✔
308
        await server.events.emit('build_status', {
75✔
309
            settings: job.permutations[0].settings,
310
            status: newBuild.status,
311
            event: newEvent.toJson(),
312
            pipeline: pipeline.toJson(),
313
            jobName: job.name,
314
            build: newBuild.toJson(),
315
            buildLink: `${buildFactory.uiUri}/pipelines/${pipeline.id}/builds/${build.id}`,
316
            isFixed: await isFixed
317
        });
318
    }
319

320
    const skipFurther = /\[(skip further)\]/.test(newEvent.causeMessage);
80✔
321

322
    // Update stageBuild status if it has changed;
323
    // if stageBuild status is currently terminal, do not update
324
    const stage = await getStage({
80✔
325
        stageFactory,
326
        workflowGraph: newEvent.workflowGraph,
327
        jobName: job.name,
328
        pipelineId: pipeline.id
329
    });
330
    const isStageTeardown = STAGE_TEARDOWN_PATTERN.test(job.name);
80✔
331
    let stageBuildHasFailure = false;
80✔
332

333
    if (stage) {
80✔
334
        const stageBuild = await stageBuildFactory.get({
7✔
335
            stageId: stage.id,
336
            eventId: newEvent.id
337
        });
338

339
        if (stageBuild.status !== newBuild.status) {
7✔
340
            if (!TERMINAL_STATUSES.includes(stageBuild.status)) {
4✔
341
                stageBuild.status = newBuild.status;
2✔
342
                await stageBuild.update();
2✔
343
            }
344
        }
345

346
        stageBuildHasFailure = TERMINAL_STATUSES.includes(stageBuild.status);
7✔
347
    }
348

349
    // Guard against triggering non-successful or unstable builds
350
    // Don't further trigger pipeline if intend to skip further jobs
351
    if (newBuild.status !== 'SUCCESS' || skipFurther) {
80✔
352
        // Check for failed jobs and remove any child jobs in created state
353
        if (newBuild.status === 'FAILURE') {
25✔
354
            await removeJoinBuilds({ pipeline, job, build: newBuild, event: newEvent, stage }, server.app);
7✔
355
        }
356
        // Do not continue downstream is current job is stage teardown and statusBuild has failure
357
    } else if (newBuild.status === 'SUCCESS' && isStageTeardown && stageBuildHasFailure) {
55✔
358
        await removeJoinBuilds({ pipeline, job, build: newBuild, event: newEvent, stage }, server.app);
1✔
359
    } else {
360
        await triggerNextJobs({ pipeline, job, build: newBuild, username, scmContext, event: newEvent }, server.app);
54✔
361
    }
362

363
    // Determine if stage teardown build should start
364
    // (if stage teardown build exists, and stageBuild.status is negative,
365
    // and there are no active stage builds, and teardown build is not started)
366
    if (stage && FINISHED_STATUSES.includes(newBuild.status) && !isStageTeardown) {
78✔
367
        const stageTeardownName = getFullStageJobName({ stageName: stage.name, jobName: 'teardown' });
5✔
368
        const stageTeardownJob = await jobFactory.get({ pipelineId: pipeline.id, name: stageTeardownName });
5✔
369
        let stageTeardownBuild = await buildFactory.get({ eventId: newEvent.id, jobId: stageTeardownJob.id });
5✔
370

371
        // Start stage teardown build if stage is done
372
        if (!stageTeardownBuild || stageTeardownBuild.status === 'CREATED') {
5✔
373
            const stageJobBuilds = await getStageJobBuilds({ stage, event: newEvent, jobFactory });
3✔
374
            const stageIsDone = isStageDone(stageJobBuilds);
3✔
375

376
            if (stageIsDone) {
3!
377
                // Determine the actual job name in the graph by stripping PR prefix (e.g., "PR-123:")
378
                // if this is a PR build, since workflowGraph nodes do not include PR-specific prefixes.
379
                const prMatch = stage.name.match(PR_STAGE_NAME);
3✔
380
                const teardownOriginalJobName = prMatch
3!
381
                    ? stageTeardownName.replace(`${prMatch[1]}:`, '')
382
                    : stageTeardownName;
383

384
                const teardownNode = newEvent.workflowGraph.nodes.find(n => n.name === teardownOriginalJobName);
33✔
385

386
                // Update teardown build
387
                stageTeardownBuild = await createOrUpdateStageTeardownBuild(
3✔
388
                    { pipeline, job, build, username, scmContext, event, stage },
389
                    server.app
390
                );
391

392
                stageTeardownBuild.parentBuildId = stageJobBuilds.map(b => b.id);
9✔
393

394
                if (teardownNode && teardownNode.virtual) {
3!
UNCOV
395
                    await updateVirtualBuildSuccess(stageTeardownBuild);
×
396
                } else {
397
                    stageTeardownBuild.status = 'QUEUED';
3✔
398

399
                    await stageTeardownBuild.update();
3✔
400
                    await stageTeardownBuild.start();
3✔
401
                }
402
            }
403
        }
404
    }
405

406
    // update event status
407
    const latestBuilds = await newEvent.getBuilds();
78✔
408
    const newEventStatus = deriveEventStatusFromBuildStatuses(latestBuilds);
78✔
409

410
    if (newEventStatus && newEvent.status !== newEventStatus) {
78✔
411
        newEvent.status = newEventStatus;
76✔
412
        await newEvent.update();
76✔
413
    }
414

415
    return newBuild;
78✔
416
}
417

418
module.exports = {
217✔
419
    updateBuildAndTriggerDownstreamJobs,
420
    deriveEventStatusFromBuildStatuses
421
};
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