screwdriver-api 7.0.235 → 7.0.237
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/plugins/builds/helper/updateBuild.js +310 -0
- package/plugins/builds/update.js +14 -277
- package/plugins/events/create.js +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screwdriver-api",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.237",
|
|
4
4
|
"description": "API server for the Screwdriver.cd service",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"screwdriver-executor-queue": "^5.0.0",
|
|
119
119
|
"screwdriver-executor-router": "^4.0.0",
|
|
120
120
|
"screwdriver-logger": "^2.0.0",
|
|
121
|
-
"screwdriver-models": "^
|
|
121
|
+
"screwdriver-models": "^31.0.0",
|
|
122
122
|
"screwdriver-notifications-email": "^4.0.0",
|
|
123
123
|
"screwdriver-notifications-slack": "^6.0.0",
|
|
124
124
|
"screwdriver-request": "^2.0.1",
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const boom = require('@hapi/boom');
|
|
4
|
+
const hoek = require('@hapi/hoek');
|
|
5
|
+
const merge = require('lodash.mergewith');
|
|
6
|
+
const { getFullStageJobName } = require('../../helper');
|
|
7
|
+
const STAGE_TEARDOWN_PATTERN = /^stage@([\w-]+)(?::teardown)$/;
|
|
8
|
+
const TERMINAL_STATUSES = ['FAILURE', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
|
|
9
|
+
const FINISHED_STATUSES = ['FAILURE', 'SUCCESS', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('screwdriver-models/lib/build')} Build
|
|
13
|
+
* @typedef {import('screwdriver-models/lib/event')} Event
|
|
14
|
+
* @typedef {import('screwdriver-models/lib/step')} Step
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Identify whether this build resulted in a previously failed job to become successful.
|
|
19
|
+
*
|
|
20
|
+
* @method isFixedBuild
|
|
21
|
+
* @param {Build} build Build Object
|
|
22
|
+
* @param {JobFactory} jobFactory Job Factory instance
|
|
23
|
+
*/
|
|
24
|
+
async function isFixedBuild(build, jobFactory) {
|
|
25
|
+
if (build.status !== 'SUCCESS') {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const job = await jobFactory.get(build.jobId);
|
|
30
|
+
const failureBuild = await job.getLatestBuild({ status: 'FAILURE' });
|
|
31
|
+
const successBuild = await job.getLatestBuild({ status: 'SUCCESS' });
|
|
32
|
+
|
|
33
|
+
return !!((failureBuild && !successBuild) || failureBuild.id > successBuild.id);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stops a frozen build from executing
|
|
38
|
+
*
|
|
39
|
+
* @method stopFrozenBuild
|
|
40
|
+
* @param {Build} build Build Object
|
|
41
|
+
* @param {String} previousStatus Previous build status
|
|
42
|
+
*/
|
|
43
|
+
async function stopFrozenBuild(build, previousStatus) {
|
|
44
|
+
if (previousStatus !== 'FROZEN') {
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return build.stopFrozen(previousStatus);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Updates execution details for init step
|
|
53
|
+
*
|
|
54
|
+
* @method stopFrozenBuild
|
|
55
|
+
* @param {Build} build Build Object
|
|
56
|
+
* @param {Object} app Hapi app Object
|
|
57
|
+
* @returns {Promise<Step>} Updated step
|
|
58
|
+
*/
|
|
59
|
+
async function updateInitStep(build, app) {
|
|
60
|
+
const step = await app.stepFactory.get({ buildId: build.id, name: 'sd-setup-init' });
|
|
61
|
+
// If there is no init step, do nothing
|
|
62
|
+
|
|
63
|
+
if (!step) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
step.endTime = build.startTime || new Date().toISOString();
|
|
68
|
+
step.code = 0;
|
|
69
|
+
|
|
70
|
+
return step.update();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set build status to desired status, set build statusMessage
|
|
75
|
+
*
|
|
76
|
+
* @param {Build} build Build Model
|
|
77
|
+
* @param {String} desiredStatus New Status
|
|
78
|
+
* @param {String} statusMessage User passed status message
|
|
79
|
+
* @param {String} statusMessageType User passed severity of the status message
|
|
80
|
+
* @param {String} username User initiating status build update
|
|
81
|
+
*/
|
|
82
|
+
function updateBuildStatus(build, desiredStatus, statusMessage, statusMessageType, username) {
|
|
83
|
+
const currentStatus = build.status;
|
|
84
|
+
|
|
85
|
+
// UNSTABLE -> SUCCESS needs to update meta and endtime.
|
|
86
|
+
// However, the status itself cannot be updated to SUCCESS
|
|
87
|
+
if (currentStatus === 'UNSTABLE') {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (desiredStatus !== undefined) {
|
|
92
|
+
build.status = desiredStatus;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
switch (build.status) {
|
|
96
|
+
case 'ABORTED':
|
|
97
|
+
build.statusMessage =
|
|
98
|
+
currentStatus === 'FROZEN' ? `Frozen build aborted by ${username}` : `Aborted by ${username}`;
|
|
99
|
+
break;
|
|
100
|
+
case 'FAILURE':
|
|
101
|
+
case 'SUCCESS':
|
|
102
|
+
if (statusMessage) {
|
|
103
|
+
build.statusMessage = statusMessage;
|
|
104
|
+
build.statusMessageType = statusMessageType || null;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
build.statusMessage = statusMessage || null;
|
|
109
|
+
build.statusMessageType = statusMessageType || null;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get stage for current node
|
|
116
|
+
*
|
|
117
|
+
* @param {StageFactory} stageFactory Stage factory
|
|
118
|
+
* @param {Object} workflowGraph Workflow graph
|
|
119
|
+
* @param {String} jobName Job name
|
|
120
|
+
* @param {Number} pipelineId Pipeline ID
|
|
121
|
+
* @return {Stage} Stage for node
|
|
122
|
+
*/
|
|
123
|
+
async function getStage({ stageFactory, workflowGraph, jobName, pipelineId }) {
|
|
124
|
+
const currentNode = workflowGraph.nodes.find(node => node.name === jobName);
|
|
125
|
+
let stage = null;
|
|
126
|
+
|
|
127
|
+
if (currentNode && currentNode.stageName) {
|
|
128
|
+
stage = await stageFactory.get({
|
|
129
|
+
pipelineId,
|
|
130
|
+
name: currentNode.stageName
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return Promise.resolve(stage);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Checks if all builds in stage are done running
|
|
139
|
+
*
|
|
140
|
+
* @param {Stage} stage Stage
|
|
141
|
+
* @param {Event} event Event
|
|
142
|
+
* @return {Boolean} Flag if stage is done
|
|
143
|
+
*/
|
|
144
|
+
async function isStageDone({ stage, event }) {
|
|
145
|
+
// Get all jobIds for jobs in the stage
|
|
146
|
+
const stageJobIds = [...stage.jobIds, stage.setup];
|
|
147
|
+
|
|
148
|
+
// Get all builds in a stage for this event
|
|
149
|
+
const stageJobBuilds = await event.getBuilds({ params: { jobId: stageJobIds } });
|
|
150
|
+
let stageIsDone = false;
|
|
151
|
+
|
|
152
|
+
if (stageJobBuilds && stageJobBuilds.length !== 0) {
|
|
153
|
+
stageIsDone = !stageJobBuilds.some(b => !FINISHED_STATUSES.includes(b.status));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return stageIsDone;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Updates the build and trigger its downstream jobs in the workflow
|
|
161
|
+
*
|
|
162
|
+
* @method updateBuildAndTriggerDownstreamJobs
|
|
163
|
+
* @param {Object} config
|
|
164
|
+
* @param {Build} build
|
|
165
|
+
* @param {Object} server
|
|
166
|
+
* @param {String} username
|
|
167
|
+
* @param {Object} scmContext
|
|
168
|
+
* @returns {Promise<Build>} Updated build
|
|
169
|
+
*/
|
|
170
|
+
async function updateBuildAndTriggerDownstreamJobs(config, build, server, username, scmContext) {
|
|
171
|
+
const { buildFactory, eventFactory, jobFactory, stageFactory, stageBuildFactory } = server.app;
|
|
172
|
+
const { statusMessage, statusMessageType, stats, status: desiredStatus, meta } = config;
|
|
173
|
+
const { triggerNextJobs, removeJoinBuilds, createOrUpdateStageTeardownBuild } = server.plugins.builds;
|
|
174
|
+
|
|
175
|
+
const currentStatus = build.status;
|
|
176
|
+
|
|
177
|
+
const event = await eventFactory.get(build.eventId);
|
|
178
|
+
|
|
179
|
+
if (stats) {
|
|
180
|
+
// need to do this so the field is dirty
|
|
181
|
+
build.stats = Object.assign(build.stats, stats);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Short circuit for cases that don't need to update status
|
|
185
|
+
if (!desiredStatus) {
|
|
186
|
+
build.statusMessage = statusMessage || build.statusMessage;
|
|
187
|
+
build.statusMessageType = statusMessageType || build.statusMessageType;
|
|
188
|
+
} else if (['SUCCESS', 'FAILURE', 'ABORTED'].includes(desiredStatus)) {
|
|
189
|
+
build.meta = meta || {};
|
|
190
|
+
event.meta = merge({}, event.meta, build.meta);
|
|
191
|
+
build.endTime = new Date().toISOString();
|
|
192
|
+
} else if (desiredStatus === 'RUNNING') {
|
|
193
|
+
build.startTime = new Date().toISOString();
|
|
194
|
+
} else if (desiredStatus === 'BLOCKED' && !hoek.reach(build, 'stats.blockedStartTime')) {
|
|
195
|
+
build.stats = Object.assign(build.stats, {
|
|
196
|
+
blockedStartTime: new Date().toISOString()
|
|
197
|
+
});
|
|
198
|
+
} else if (desiredStatus === 'QUEUED' && currentStatus !== 'QUEUED') {
|
|
199
|
+
throw boom.badRequest(`Cannot update builds to ${desiredStatus}`);
|
|
200
|
+
} else if (desiredStatus === 'BLOCKED' && currentStatus === 'BLOCKED') {
|
|
201
|
+
// Queue-Service can call BLOCKED status update multiple times
|
|
202
|
+
throw boom.badRequest(`Cannot update builds to ${desiredStatus}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let isFixed = Promise.resolve(false);
|
|
206
|
+
let stopFrozen = null;
|
|
207
|
+
|
|
208
|
+
updateBuildStatus(build, desiredStatus, statusMessage, statusMessageType, username);
|
|
209
|
+
|
|
210
|
+
// If status got updated to RUNNING or COLLAPSED, update init endTime and code
|
|
211
|
+
if (['RUNNING', 'COLLAPSED', 'FROZEN'].includes(desiredStatus)) {
|
|
212
|
+
await updateInitStep(build, server.app);
|
|
213
|
+
} else {
|
|
214
|
+
stopFrozen = stopFrozenBuild(build, currentStatus);
|
|
215
|
+
isFixed = isFixedBuild(build, jobFactory);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const [newBuild, newEvent] = await Promise.all([build.update(), event.update(), stopFrozen]);
|
|
219
|
+
const job = await newBuild.job;
|
|
220
|
+
const pipeline = await job.pipeline;
|
|
221
|
+
|
|
222
|
+
if (desiredStatus) {
|
|
223
|
+
await server.events.emit('build_status', {
|
|
224
|
+
settings: job.permutations[0].settings,
|
|
225
|
+
status: newBuild.status,
|
|
226
|
+
event: newEvent.toJson(),
|
|
227
|
+
pipeline: pipeline.toJson(),
|
|
228
|
+
jobName: job.name,
|
|
229
|
+
build: newBuild.toJson(),
|
|
230
|
+
buildLink: `${buildFactory.uiUri}/pipelines/${pipeline.id}/builds/${build.id}`,
|
|
231
|
+
isFixed: await isFixed
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const skipFurther = /\[(skip further)\]/.test(newEvent.causeMessage);
|
|
236
|
+
|
|
237
|
+
// Update stageBuild status if it has changed;
|
|
238
|
+
// if stageBuild status is currently terminal, do not update
|
|
239
|
+
const stage = await getStage({
|
|
240
|
+
stageFactory,
|
|
241
|
+
workflowGraph: newEvent.workflowGraph,
|
|
242
|
+
jobName: job.name,
|
|
243
|
+
pipelineId: pipeline.id
|
|
244
|
+
});
|
|
245
|
+
const isStageTeardown = STAGE_TEARDOWN_PATTERN.test(job.name);
|
|
246
|
+
let stageBuildHasFailure = false;
|
|
247
|
+
|
|
248
|
+
if (stage) {
|
|
249
|
+
const stageBuild = await stageBuildFactory.get({
|
|
250
|
+
stageId: stage.id,
|
|
251
|
+
eventId: newEvent.id
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (stageBuild.status !== newBuild.status) {
|
|
255
|
+
if (!TERMINAL_STATUSES.includes(stageBuild.status)) {
|
|
256
|
+
stageBuild.status = newBuild.status;
|
|
257
|
+
await stageBuild.update();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
stageBuildHasFailure = TERMINAL_STATUSES.includes(stageBuild.status);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Guard against triggering non-successful or unstable builds
|
|
265
|
+
// Don't further trigger pipeline if intend to skip further jobs
|
|
266
|
+
if (newBuild.status !== 'SUCCESS' || skipFurther) {
|
|
267
|
+
// Check for failed jobs and remove any child jobs in created state
|
|
268
|
+
if (newBuild.status === 'FAILURE') {
|
|
269
|
+
await removeJoinBuilds({ pipeline, job, build: newBuild, event: newEvent, stage }, server.app);
|
|
270
|
+
|
|
271
|
+
if (stage && !isStageTeardown) {
|
|
272
|
+
await createOrUpdateStageTeardownBuild(
|
|
273
|
+
{ pipeline, job, build, username, scmContext, event, stage },
|
|
274
|
+
server.app
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Do not continue downstream is current job is stage teardown and statusBuild has failure
|
|
279
|
+
} else if (newBuild.status === 'SUCCESS' && isStageTeardown && stageBuildHasFailure) {
|
|
280
|
+
await removeJoinBuilds({ pipeline, job, build: newBuild, event: newEvent, stage }, server.app);
|
|
281
|
+
} else {
|
|
282
|
+
await triggerNextJobs({ pipeline, job, build: newBuild, username, scmContext, event: newEvent }, server.app);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Determine if stage teardown build should start
|
|
286
|
+
// (if stage teardown build exists, and stageBuild.status is negative,
|
|
287
|
+
// and there are no active stage builds, and teardown build is not started)
|
|
288
|
+
if (stage && FINISHED_STATUSES.includes(newBuild.status)) {
|
|
289
|
+
const stageTeardownName = getFullStageJobName({ stageName: stage.name, jobName: 'teardown' });
|
|
290
|
+
const stageTeardownJob = await jobFactory.get({ pipelineId: pipeline.id, name: stageTeardownName });
|
|
291
|
+
const stageTeardownBuild = await buildFactory.get({ eventId: newEvent.id, jobId: stageTeardownJob.id });
|
|
292
|
+
|
|
293
|
+
// Start stage teardown build if stage is done
|
|
294
|
+
if (stageTeardownBuild && stageTeardownBuild.status === 'CREATED') {
|
|
295
|
+
const stageIsDone = await isStageDone({ stage, event: newEvent });
|
|
296
|
+
|
|
297
|
+
if (stageIsDone) {
|
|
298
|
+
stageTeardownBuild.status = 'QUEUED';
|
|
299
|
+
await stageTeardownBuild.update();
|
|
300
|
+
await stageTeardownBuild.start();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return newBuild;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
updateBuildAndTriggerDownstreamJobs
|
|
310
|
+
};
|
package/plugins/builds/update.js
CHANGED
|
@@ -1,72 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const boom = require('@hapi/boom');
|
|
4
|
-
const hoek = require('@hapi/hoek');
|
|
5
4
|
const schema = require('screwdriver-data-schema');
|
|
6
5
|
const joi = require('joi');
|
|
7
6
|
const idSchema = schema.models.build.base.extract('id');
|
|
8
|
-
const
|
|
9
|
-
const {
|
|
10
|
-
const STAGE_TEARDOWN_PATTERN = /^stage@([\w-]+)(?::teardown)$/;
|
|
11
|
-
const TERMINAL_STATUSES = ['FAILURE', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
|
|
12
|
-
const FINISHED_STATUSES = ['FAILURE', 'SUCCESS', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Identify whether this build resulted in a previously failed job to become successful.
|
|
16
|
-
*
|
|
17
|
-
* @method isFixedBuild
|
|
18
|
-
* @param build Build Object
|
|
19
|
-
* @param jobFactory Job Factory instance
|
|
20
|
-
*/
|
|
21
|
-
async function isFixedBuild(build, jobFactory) {
|
|
22
|
-
if (build.status !== 'SUCCESS') {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const job = await jobFactory.get(build.jobId);
|
|
27
|
-
const failureBuild = await job.getLatestBuild({ status: 'FAILURE' });
|
|
28
|
-
const successBuild = await job.getLatestBuild({ status: 'SUCCESS' });
|
|
29
|
-
|
|
30
|
-
if ((failureBuild && !successBuild) || failureBuild.id > successBuild.id) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Stops a frozen build from executing
|
|
39
|
-
* @method stopFrozenBuild
|
|
40
|
-
* @param {Object} build Build Object
|
|
41
|
-
* @param {String} previousStatus Prevous build status
|
|
42
|
-
*/
|
|
43
|
-
async function stopFrozenBuild(build, previousStatus) {
|
|
44
|
-
if (previousStatus !== 'FROZEN') {
|
|
45
|
-
return Promise.resolve();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return build.stopFrozen(previousStatus);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Updates execution details for init step
|
|
53
|
-
* @method stopFrozenBuild
|
|
54
|
-
* @param {Object} build Build Object
|
|
55
|
-
* @param {Object} app Hapi app Object
|
|
56
|
-
*/
|
|
57
|
-
async function updateInitStep(build, app) {
|
|
58
|
-
const step = await app.stepFactory.get({ buildId: build.id, name: 'sd-setup-init' });
|
|
59
|
-
// If there is no init step, do nothing
|
|
60
|
-
|
|
61
|
-
if (!step) {
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
step.endTime = build.startTime || new Date().toISOString();
|
|
66
|
-
step.code = 0;
|
|
67
|
-
|
|
68
|
-
return step.update();
|
|
69
|
-
}
|
|
7
|
+
const { getScmUri, getUserPermissions } = require('../helper');
|
|
8
|
+
const { updateBuildAndTriggerDownstreamJobs } = require('./helper/updateBuild');
|
|
70
9
|
|
|
71
10
|
/**
|
|
72
11
|
* Validate if build status can be updated
|
|
@@ -127,86 +66,6 @@ async function validateUserPermission(build, request) {
|
|
|
127
66
|
await getUserPermissions({ user, scmUri, level: 'push', isAdmin: adminDetails.isAdmin });
|
|
128
67
|
}
|
|
129
68
|
|
|
130
|
-
/**
|
|
131
|
-
* Set build status to desired status, set build statusMessage
|
|
132
|
-
* @param {Object} build Build Model
|
|
133
|
-
* @param {String} desiredStatus New Status
|
|
134
|
-
* @param {String} statusMessage User passed status message
|
|
135
|
-
* @param {String} statusMessageType User passed severity of the status message
|
|
136
|
-
* @param {String} username User initiating status build update
|
|
137
|
-
*/
|
|
138
|
-
function updateBuildStatus(build, desiredStatus, statusMessage, statusMessageType, username) {
|
|
139
|
-
// UNSTABLE -> SUCCESS needs to update meta and endtime.
|
|
140
|
-
// However, the status itself cannot be updated to SUCCESS
|
|
141
|
-
const currentStatus = build.status;
|
|
142
|
-
|
|
143
|
-
if (currentStatus !== 'UNSTABLE') {
|
|
144
|
-
if (desiredStatus !== undefined) {
|
|
145
|
-
build.status = desiredStatus;
|
|
146
|
-
}
|
|
147
|
-
if (build.status === 'ABORTED') {
|
|
148
|
-
if (currentStatus === 'FROZEN') {
|
|
149
|
-
build.statusMessage = `Frozen build aborted by ${username}`;
|
|
150
|
-
} else {
|
|
151
|
-
build.statusMessage = `Aborted by ${username}`;
|
|
152
|
-
}
|
|
153
|
-
} else if (build.status === 'FAILURE' || build.status === 'SUCCESS') {
|
|
154
|
-
if (statusMessage) {
|
|
155
|
-
build.statusMessage = statusMessage;
|
|
156
|
-
build.statusMessageType = statusMessageType || null;
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
build.statusMessage = statusMessage || null;
|
|
160
|
-
build.statusMessageType = statusMessageType || null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get stage for current node
|
|
167
|
-
* @param {StageFactory} stageFactory Stage factory
|
|
168
|
-
* @param {Object} workflowGraph Workflow graph
|
|
169
|
-
* @param {String} jobName Job name
|
|
170
|
-
* @param {Number} pipelineId Pipeline ID
|
|
171
|
-
* @return {Stage} Stage for node
|
|
172
|
-
*/
|
|
173
|
-
async function getStage({ stageFactory, workflowGraph, jobName, pipelineId }) {
|
|
174
|
-
const currentNode = workflowGraph.nodes.find(node => node.name === jobName);
|
|
175
|
-
let stage = null;
|
|
176
|
-
|
|
177
|
-
if (currentNode && currentNode.stageName) {
|
|
178
|
-
stage = await stageFactory.get({
|
|
179
|
-
pipelineId,
|
|
180
|
-
name: currentNode.stageName
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return Promise.resolve(stage);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Checks if all builds in stage are done running
|
|
189
|
-
* @param {Object} stage Stage
|
|
190
|
-
* @param {Object} event Event
|
|
191
|
-
* @return {Boolean} Flag if stage is done
|
|
192
|
-
*/
|
|
193
|
-
async function isStageDone({ stage, event }) {
|
|
194
|
-
// Get all jobIds for jobs in the stage
|
|
195
|
-
const stageJobIds = stage.jobIds;
|
|
196
|
-
|
|
197
|
-
stageJobIds.push(stage.setup);
|
|
198
|
-
|
|
199
|
-
// Get all builds in a stage for this event
|
|
200
|
-
const stageJobBuilds = await event.getBuilds({ params: { jobId: stageJobIds } });
|
|
201
|
-
let stageIsDone = false;
|
|
202
|
-
|
|
203
|
-
if (stageJobBuilds && stageJobBuilds.length !== 0) {
|
|
204
|
-
stageIsDone = !stageJobBuilds.some(b => !FINISHED_STATUSES.includes(b.status));
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
return stageIsDone;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
69
|
module.exports = () => ({
|
|
211
70
|
method: 'PUT',
|
|
212
71
|
path: '/builds/{id}',
|
|
@@ -220,13 +79,10 @@ module.exports = () => ({
|
|
|
220
79
|
},
|
|
221
80
|
|
|
222
81
|
handler: async (request, h) => {
|
|
223
|
-
const { buildFactory
|
|
82
|
+
const { buildFactory } = request.server.app;
|
|
224
83
|
const { id } = request.params;
|
|
225
|
-
const { statusMessage, statusMessageType, stats, status: desiredStatus } = request.payload;
|
|
226
84
|
const { username, scmContext, scope } = request.auth.credentials;
|
|
227
85
|
const isBuild = scope.includes('build') || scope.includes('temporal');
|
|
228
|
-
const { triggerNextJobs, removeJoinBuilds, createOrUpdateStageTeardownBuild } =
|
|
229
|
-
request.server.plugins.builds;
|
|
230
86
|
|
|
231
87
|
// Check token permissions
|
|
232
88
|
if (isBuild && username !== id) {
|
|
@@ -234,144 +90,25 @@ module.exports = () => ({
|
|
|
234
90
|
}
|
|
235
91
|
|
|
236
92
|
const build = await getBuildToUpdate(id, buildFactory);
|
|
237
|
-
const currentStatus = build.status;
|
|
238
93
|
|
|
239
94
|
if (!isBuild) {
|
|
240
95
|
await validateUserPermission(build, request);
|
|
241
96
|
}
|
|
242
|
-
const event = await eventFactory.get(build.eventId);
|
|
243
|
-
|
|
244
|
-
if (stats) {
|
|
245
|
-
// need to do this so the field is dirty
|
|
246
|
-
build.stats = Object.assign(build.stats, stats);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Short circuit for cases that don't need to update status
|
|
250
|
-
if (!desiredStatus) {
|
|
251
|
-
build.statusMessage = statusMessage || build.statusMessage;
|
|
252
|
-
build.statusMessageType = statusMessageType || build.statusMessageType;
|
|
253
|
-
} else if (['SUCCESS', 'FAILURE', 'ABORTED'].includes(desiredStatus)) {
|
|
254
|
-
build.meta = request.payload.meta || {};
|
|
255
|
-
event.meta = merge({}, event.meta, build.meta);
|
|
256
|
-
build.endTime = new Date().toISOString();
|
|
257
|
-
} else if (desiredStatus === 'RUNNING') {
|
|
258
|
-
build.startTime = new Date().toISOString();
|
|
259
|
-
} else if (desiredStatus === 'BLOCKED' && !hoek.reach(build, 'stats.blockedStartTime')) {
|
|
260
|
-
build.stats = Object.assign(build.stats, {
|
|
261
|
-
blockedStartTime: new Date().toISOString()
|
|
262
|
-
});
|
|
263
|
-
} else if (desiredStatus === 'QUEUED' && currentStatus !== 'QUEUED') {
|
|
264
|
-
throw boom.badRequest(`Cannot update builds to ${desiredStatus}`);
|
|
265
|
-
} else if (desiredStatus === 'BLOCKED' && currentStatus === 'BLOCKED') {
|
|
266
|
-
// Queue-Service can call BLOCKED status update multiple times
|
|
267
|
-
throw boom.badRequest(`Cannot update builds to ${desiredStatus}`);
|
|
268
|
-
}
|
|
269
97
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
// If status got updated to RUNNING or COLLAPSED, update init endTime and code
|
|
276
|
-
if (['RUNNING', 'COLLAPSED', 'FROZEN'].includes(desiredStatus)) {
|
|
277
|
-
await updateInitStep(build, request.server.app);
|
|
278
|
-
} else {
|
|
279
|
-
stopFrozen = stopFrozenBuild(build, currentStatus);
|
|
280
|
-
isFixed = isFixedBuild(build, jobFactory);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const [newBuild, newEvent] = await Promise.all([build.update(), event.update(), stopFrozen]);
|
|
284
|
-
const job = await newBuild.job;
|
|
285
|
-
const pipeline = await job.pipeline;
|
|
286
|
-
|
|
287
|
-
if (desiredStatus) {
|
|
288
|
-
await request.server.events.emit('build_status', {
|
|
289
|
-
settings: job.permutations[0].settings,
|
|
290
|
-
status: newBuild.status,
|
|
291
|
-
event: newEvent.toJson(),
|
|
292
|
-
pipeline: pipeline.toJson(),
|
|
293
|
-
jobName: job.name,
|
|
294
|
-
build: newBuild.toJson(),
|
|
295
|
-
buildLink: `${buildFactory.uiUri}/pipelines/${pipeline.id}/builds/${id}`,
|
|
296
|
-
isFixed: await isFixed
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const skipFurther = /\[(skip further)\]/.test(newEvent.causeMessage);
|
|
301
|
-
|
|
302
|
-
// Update stageBuild status if it has changed;
|
|
303
|
-
// if stageBuild status is currently terminal, do not update
|
|
304
|
-
const stage = await getStage({
|
|
305
|
-
stageFactory,
|
|
306
|
-
workflowGraph: newEvent.workflowGraph,
|
|
307
|
-
jobName: job.name,
|
|
308
|
-
pipelineId: pipeline.id
|
|
309
|
-
});
|
|
310
|
-
const isStageTeardown = STAGE_TEARDOWN_PATTERN.test(job.name);
|
|
311
|
-
let stageBuildHasFailure = false;
|
|
312
|
-
|
|
313
|
-
if (stage) {
|
|
314
|
-
const stageBuild = await stageBuildFactory.get({
|
|
315
|
-
stageId: stage.id,
|
|
316
|
-
eventId: newEvent.id
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
if (stageBuild.status !== newBuild.status) {
|
|
320
|
-
if (!TERMINAL_STATUSES.includes(stageBuild.status)) {
|
|
321
|
-
stageBuild.status = newBuild.status;
|
|
322
|
-
await stageBuild.update();
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
stageBuildHasFailure = TERMINAL_STATUSES.includes(stageBuild.status);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Guard against triggering non-successful or unstable builds
|
|
330
|
-
// Don't further trigger pipeline if intend to skip further jobs
|
|
331
|
-
if (newBuild.status !== 'SUCCESS' || skipFurther) {
|
|
332
|
-
// Check for failed jobs and remove any child jobs in created state
|
|
333
|
-
if (newBuild.status === 'FAILURE') {
|
|
334
|
-
await removeJoinBuilds(
|
|
335
|
-
{ pipeline, job, build: newBuild, event: newEvent, stage },
|
|
336
|
-
request.server.app
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
if (stage && !isStageTeardown) {
|
|
340
|
-
await createOrUpdateStageTeardownBuild(
|
|
341
|
-
{ pipeline, job, build, username, scmContext, event, stage },
|
|
342
|
-
request.server.app
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Do not continue downstream is current job is stage teardown and statusBuild has failure
|
|
347
|
-
} else if (newBuild.status === 'SUCCESS' && isStageTeardown && stageBuildHasFailure) {
|
|
348
|
-
await removeJoinBuilds({ pipeline, job, build: newBuild, event: newEvent, stage }, request.server.app);
|
|
349
|
-
} else {
|
|
350
|
-
await triggerNextJobs(
|
|
351
|
-
{ pipeline, job, build: newBuild, username, scmContext, event: newEvent },
|
|
352
|
-
request.server.app
|
|
98
|
+
if (request.payload.status && request.payload.status === 'FAILURE') {
|
|
99
|
+
request.log(
|
|
100
|
+
['PUT', 'builds', id],
|
|
101
|
+
`Build failed. Received payload: ${JSON.stringify(request.payload)}`
|
|
353
102
|
);
|
|
354
103
|
}
|
|
355
104
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
// Start stage teardown build if stage is done
|
|
365
|
-
if (stageTeardownBuild && stageTeardownBuild.status === 'CREATED') {
|
|
366
|
-
const stageIsDone = await isStageDone({ stage, event: newEvent });
|
|
367
|
-
|
|
368
|
-
if (stageIsDone) {
|
|
369
|
-
stageTeardownBuild.status = 'QUEUED';
|
|
370
|
-
await stageTeardownBuild.update();
|
|
371
|
-
await stageTeardownBuild.start();
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
}
|
|
105
|
+
const newBuild = await updateBuildAndTriggerDownstreamJobs(
|
|
106
|
+
request.payload,
|
|
107
|
+
build,
|
|
108
|
+
request.server,
|
|
109
|
+
username,
|
|
110
|
+
scmContext
|
|
111
|
+
);
|
|
375
112
|
|
|
376
113
|
return h.response(await newBuild.toJsonWithSteps()).code(200);
|
|
377
114
|
},
|
package/plugins/events/create.js
CHANGED
|
@@ -5,6 +5,8 @@ const boom = require('@hapi/boom');
|
|
|
5
5
|
const validationSchema = require('screwdriver-data-schema');
|
|
6
6
|
const ANNOT_RESTRICT_PR = 'screwdriver.cd/restrictPR';
|
|
7
7
|
const { getScmUri, isStageTeardown } = require('../helper');
|
|
8
|
+
const { updateBuildAndTriggerDownstreamJobs } = require('../builds/helper/updateBuild');
|
|
9
|
+
const { Status, BUILD_STATUS_MESSAGES } = require('../builds/triggers/helpers');
|
|
8
10
|
|
|
9
11
|
module.exports = () => ({
|
|
10
12
|
method: 'POST',
|
|
@@ -244,6 +246,23 @@ module.exports = () => ({
|
|
|
244
246
|
if (event.builds === null) {
|
|
245
247
|
return boom.notFound('No jobs to start.');
|
|
246
248
|
}
|
|
249
|
+
|
|
250
|
+
const virtualJobBuilds = event.builds.filter(b => b.status === 'CREATED');
|
|
251
|
+
|
|
252
|
+
for (const build of virtualJobBuilds) {
|
|
253
|
+
await updateBuildAndTriggerDownstreamJobs(
|
|
254
|
+
{
|
|
255
|
+
status: Status.SUCCESS,
|
|
256
|
+
statusMessage: BUILD_STATUS_MESSAGES.SKIP_VIRTUAL_JOB.statusMessage,
|
|
257
|
+
statusMessageType: BUILD_STATUS_MESSAGES.SKIP_VIRTUAL_JOB.statusMessageType
|
|
258
|
+
},
|
|
259
|
+
build,
|
|
260
|
+
request.server,
|
|
261
|
+
username,
|
|
262
|
+
scmContext
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
// everything succeeded, inform the user
|
|
248
267
|
const location = urlLib.format({
|
|
249
268
|
host: request.headers.host,
|