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

screwdriver-cd / screwdriver / #3404

02 May 2026 12:54PM UTC coverage: 94.633% (-0.8%) from 95.401%
#3404

Pull #3492

screwdriver

yakanechi
fix lint
Pull Request #3492: fix(3491): Make it possible to inherit parentEvent branch [3]

2208 of 2424 branches covered (91.09%)

Branch coverage included in aggregate %.

2 of 13 new or added lines in 1 file covered. (15.38%)

18 existing lines in 1 file now uncovered.

5391 of 5606 relevant lines covered (96.16%)

110.8 hits per line

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

70.0
/plugins/events/create.js
1
'use strict';
2

3
const urlLib = require('url');
1✔
4
const boom = require('@hapi/boom');
1✔
5
const validationSchema = require('screwdriver-data-schema');
1✔
6
const ANNOT_RESTRICT_PR = 'screwdriver.cd/restrictPR';
1✔
7
const { getScmUri, isStageTeardown } = require('../helper');
1✔
8
const { createEvent } = require('./helper/createEvent');
1✔
9

10
module.exports = () => ({
107✔
11
    method: 'POST',
12
    path: '/events',
13
    options: {
14
        description: 'Create and start a event',
15
        notes: 'Create and start a specific event',
16
        tags: ['api', 'events'],
17
        auth: {
18
            strategies: ['token'],
19
            scope: ['user', '!guest', 'pipeline']
20
        },
21

22
        handler: async (request, h) => {
23
            const { buildFactory, jobFactory, eventFactory, pipelineFactory, userFactory } = request.server.app;
32✔
24
            const { buildId, causeMessage, creator, sha, startAction } = request.payload;
32✔
25
            const { scmContext, username, scope } = request.auth.credentials;
32✔
26
            const { scm } = eventFactory;
32✔
27
            const { isValidToken } = request.server.plugins.pipelines;
32✔
28
            const { updateAdmins } = request.server.plugins.events;
32✔
29

30
            let { pipelineId, startFrom, parentBuildId, parentBuilds, groupEventId, parentEventId, prNum } =
31
                request.payload;
32✔
32

33
            // Validation: Prevent event creation if startFrom is a stage teardown and parentEventID does not exist (start case)
34
            if (isStageTeardown(startFrom) && !parentEventId) {
32✔
35
                throw boom.badRequest('Event cannot be started from a stage teardown');
1✔
36
            }
37

38
            // restart case
39
            if (buildId) {
31!
UNCOV
40
                const b = await buildFactory.get(buildId);
×
UNCOV
41
                const j = await jobFactory.get(b.jobId);
×
42

UNCOV
43
                ({ pipelineId, name: startFrom } = j);
×
UNCOV
44
                ({ parentBuildId, eventId: parentEventId } = b);
×
45

UNCOV
46
                if (b.parentBuilds) {
×
UNCOV
47
                    parentBuilds = b.parentBuilds;
×
48
                }
49

UNCOV
50
                if (b.eventId && !groupEventId) {
×
UNCOV
51
                    const parentEvent = await eventFactory.get(b.eventId);
×
52

UNCOV
53
                    groupEventId = parentEvent.groupEventId || b.eventId;
×
54
                }
55
            }
56

57
            const payload = {
31✔
58
                pipelineId,
59
                scmContext,
60
                startFrom,
61
                type: 'pipeline',
62
                username,
63
                meta: request.payload.meta // always exists because default is {}
64
            };
65

66
            if (sha) {
31✔
67
                payload.sha = sha;
1✔
68
            }
69

70
            // If you specify a parentEventId in the payload, the metadata will be inherited.
71
            // Therefore, you should not specify a parentEventId in the payload except when using RESTART.
72
            // Prevents metadata from transferring when Start from a specific event
73
            if (parentEventId) {
31!
74
                // eslint-disable-next-line default-case
NEW
75
                switch (startAction) {
×
76
                    case 'RESTART_FROM_EVENT':
77
                    case 'RESTART_FROM_BUILD': {
NEW
78
                        payload.parentEventId = parentEventId;
×
79
                    }
80
                }
81
            }
82

83
            if (parentBuildId) {
31✔
84
                payload.parentBuildId = parentBuildId;
25✔
85
            }
86

87
            if (groupEventId) {
31!
UNCOV
88
                payload.groupEventId = groupEventId;
×
89
            }
90

91
            if (parentBuilds) {
31!
UNCOV
92
                payload.parentBuilds = parentBuilds;
×
93
            }
94

95
            if (causeMessage) {
31✔
96
                payload.causeMessage = causeMessage;
2✔
97
            }
98

99
            if (creator) {
31✔
100
                payload.creator = creator;
2✔
101
                if (creator.username !== 'sd:scheduler') {
2✔
102
                    payload.creator.username = username;
1✔
103
                }
104
            } else if (scope.includes('pipeline')) {
29✔
105
                payload.creator = {
2✔
106
                    name: 'Pipeline Access Token', // Display name
107
                    username
108
                };
109
            }
110

111
            // Check for startFrom
112
            if (!startFrom) {
31✔
113
                throw boom.badRequest('Missing "startFrom" field');
1✔
114
            }
115

116
            // Trigger "~pr" needs to have PR number given
117
            // Note: To kick start builds for all jobs under a PR,
118
            // you need both the prNum and the trigger "~pr" as startFrom
119
            if (startFrom.match(validationSchema.config.regex.PR_TRIGGER) && !prNum) {
30✔
120
                throw boom.badRequest('Trigger "~pr" must be accompanied by a PR number');
1✔
121
            }
122

123
            if (!prNum) {
29✔
124
                // If PR number isn't given, induce it from "startFrom"
125
                // Match PR-prNum, then extract prNum
126
                // e.g. if startFrom is "PR-1:main", prNumFullName will be "PR-1"; prNum will be "1"
127
                const prNumFullName = startFrom.match(validationSchema.config.regex.PR_JOB_NAME);
28✔
128

129
                prNum = prNumFullName ? prNumFullName[1].split('-')[1] : null;
28✔
130
            }
131

132
            // Fetch the pipeline and user models
133
            const [pipeline, user] = await Promise.all([
29✔
134
                pipelineFactory.get(pipelineId),
135
                userFactory.get({ username, scmContext })
136
            ]);
137

138
            if (!pipeline) {
29✔
139
                throw boom.notFound();
2✔
140
            } else if (pipeline.state !== 'ACTIVE') {
27✔
141
                // INACTIVE, DELETING, or DISABLED pipeline
142
                throw boom.badRequest(`Cannot create an event for a(n) ${pipeline.state} pipeline`);
2✔
143
            }
144

145
            payload.scmContext = pipeline.scmContext;
25✔
146

147
            // In pipeline scope, check if the token is allowed to the pipeline
148
            if (!isValidToken(pipeline.id, request.auth.credentials)) {
25✔
149
                throw boom.unauthorized('Token does not have permission to this pipeline');
1✔
150
            }
151

152
            // Use parent's scmUri if pipeline is child pipeline and using read-only SCM
153
            const scmUri = await getScmUri({ pipeline, pipelineFactory });
24✔
154

155
            // Check the user's permission
156
            let permissions;
157

158
            try {
24✔
159
                permissions = await user.getPermissions(scmUri, pipeline.scmContext, pipeline.scmRepo);
24✔
160
            } catch (err) {
161
                if (err.statusCode === 403 && pipeline.scmRepo && pipeline.scmRepo.private) {
3✔
162
                    throw boom.notFound();
1✔
163
                }
164
                throw boom.boomify(err, { statusCode: err.statusCode });
2✔
165
            }
166

167
            // Update admins
168
            if (!prNum) {
21✔
169
                try {
13✔
170
                    await updateAdmins({ permissions, pipeline, user });
13✔
171
                } catch (err) {
172
                    throw boom.boomify(err, { statusCode: err.statusCode });
1✔
173
                }
174
            }
175

176
            // Get scmConfig
177
            const token = await user.unsealToken();
20✔
178
            const scmConfig = {
20✔
179
                prNum,
180
                scmContext: pipeline.scmContext,
181
                scmUri: pipeline.scmUri,
182
                scmRepo: pipeline.scmRepo,
183
                token
184
            };
185

186
            // Get and set PR data; update admins
187
            if (prNum) {
20✔
188
                payload.prNum = String(prNum);
8✔
189
                payload.type = 'pr';
8✔
190

191
                let files;
192
                let prInfo;
193

194
                try {
8✔
195
                    [files, prInfo] = await Promise.all([
8✔
196
                        scm.getChangedFiles({
197
                            webhookConfig: null,
198
                            type: 'pr',
199
                            ...scmConfig
200
                        }),
201
                        scm.getPrInfo(scmConfig)
202
                    ]);
203
                } catch (err) {
204
                    throw boom.boomify(err, { statusCode: err.statusCode });
1✔
205
                }
206

207
                if (files && files.length) {
7✔
208
                    payload.changedFiles = files;
6✔
209
                }
210

211
                payload.prInfo = prInfo;
7✔
212
                payload.prRef = prInfo.ref;
7✔
213
                payload.prSource = prInfo.prSource;
7✔
214
                payload.chainPR = pipeline.chainPR;
7✔
215
                let restrictPR = 'none';
7✔
216

217
                if (pipeline.annotations && pipeline.annotations[ANNOT_RESTRICT_PR]) {
7!
218
                    restrictPR = pipeline.annotations[ANNOT_RESTRICT_PR];
7✔
219
                }
220

221
                if (restrictPR !== 'none' || prInfo.username !== username) {
7✔
222
                    // Remove user from admins
223
                    try {
2✔
224
                        await updateAdmins({
2✔
225
                            permissions,
226
                            pipeline,
227
                            user
228
                        });
229
                    } catch (err) {
230
                        throw boom.boomify(err, { statusCode: err.statusCode });
2✔
231
                    }
232
                }
233
            }
234

235
            let latestCommitSha;
236

237
            try {
17✔
238
                // User has good permissions, create an event
239
                latestCommitSha = await scm.getCommitSha(scmConfig);
17✔
240
            } catch (err) {
241
                if (err.statusCode) {
1!
242
                    throw boom.boomify(err, { statusCode: err.statusCode });
1✔
243
                }
244
            }
245

246
            if (!payload.sha || prNum) {
16!
247
                payload.sha = latestCommitSha;
16✔
248
            }
249

250
            // If there is parentEvent, pass workflowGraph, meta and sha to payload
251
            // Skip PR, for PR builds, we should always start from latest commit
252
            if (parentEventId) {
16!
UNCOV
253
                const parentEvent = await eventFactory.get(parentEventId);
×
254

UNCOV
255
                payload.baseBranch = parentEvent.baseBranch || null;
×
256

UNCOV
257
                if (!prNum) {
×
UNCOV
258
                    payload.workflowGraph = parentEvent.workflowGraph;
×
UNCOV
259
                    payload.sha = parentEvent.sha;
×
260

UNCOV
261
                    if (parentEvent.configPipelineSha) {
×
UNCOV
262
                        payload.configPipelineSha = parentEvent.configPipelineSha;
×
263
                    }
264
                }
265

266
                // eslint-disable-next-line default-case
NEW
267
                switch (startAction) {
×
268
                    case 'RESTART_FROM_EVENT':
269
                    case 'RESTART_FROM_BUILD': {
NEW
270
                        let mergedParameters = payload.meta.parameters || {};
×
271

272
                        // Merge parameters if they exist in the parent event and not in the payload
NEW
273
                        if (!payload.meta.parameters && parentEvent.meta && parentEvent.meta.parameters) {
×
NEW
274
                            mergedParameters = parentEvent.meta.parameters;
×
275
                        }
NEW
276
                        delete payload.meta.parameters;
×
277

278
                        // Copy meta from parent event if payload.meta is empty except for the parameters
NEW
279
                        if (Object.keys(payload.meta).length === 0) {
×
NEW
280
                            payload.meta = { ...parentEvent.meta };
×
281
                        }
282

NEW
283
                        if (Object.keys(mergedParameters).length > 0) {
×
NEW
284
                            payload.meta.parameters = mergedParameters;
×
285
                        }
286
                    }
287
                }
288
            }
289

290
            const event = await createEvent(payload, request.server);
16✔
291

292
            if (event.builds === null) {
15✔
293
                return boom.notFound('No jobs to start.');
1✔
294
            }
295

296
            // everything succeeded, inform the user
297
            const location = urlLib.format({
14✔
298
                host: request.headers.host,
299
                port: request.headers.port,
300
                protocol: request.server.info.protocol,
301
                pathname: `${request.path}/${event.id}`
302
            });
303

304
            return h.response(event.toJson()).header('Location', location).code(201);
14✔
305
        },
306
        validate: {
307
            payload: validationSchema.models.event.create
308
        }
309
    }
310
});
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