screwdriver-api 8.0.65 → 8.0.67

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screwdriver-api",
3
- "version": "8.0.65",
3
+ "version": "8.0.67",
4
4
  "description": "API server for the Screwdriver.cd service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -7,7 +7,7 @@ const { PR_JOB_NAME, PR_STAGE_NAME, STAGE_TEARDOWN_PATTERN } = require('screwdri
7
7
  const { getFullStageJobName } = require('../../helper');
8
8
  const { updateVirtualBuildSuccess } = require('../triggers/helpers');
9
9
  const TERMINAL_STATUSES = ['FAILURE', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
10
- const FINISHED_STATUSES = ['FAILURE', 'SUCCESS', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
10
+ const FINISHED_STATUSES = ['SUCCESS', ...TERMINAL_STATUSES];
11
11
 
12
12
  /**
13
13
  * @typedef {import('screwdriver-models/lib/build')} Build
@@ -140,6 +140,35 @@ async function getStage({ stageFactory, workflowGraph, jobName, pipelineId }) {
140
140
  return Promise.resolve(stage);
141
141
  }
142
142
 
143
+ /**
144
+ * Get stage build for current event
145
+ * @param {StageFactory} stageFactory Stage factory
146
+ * @param {StageBuildFactory} stageBuildFactory stage build factory
147
+ * @param {Object} workflowGraph workflow graph
148
+ * @param {String} jobName job name
149
+ * @param {Number} pipelineId pipeline id
150
+ * @param {Number} eventId event id
151
+ *
152
+ * @returns {StageBuild} stage build for current event
153
+ */
154
+ async function getStageBuild({ stageFactory, stageBuildFactory, workflowGraph, jobName, pipelineId, eventId }) {
155
+ const stage = await getStage({
156
+ stageFactory,
157
+ workflowGraph,
158
+ jobName,
159
+ pipelineId
160
+ });
161
+
162
+ if (stage) {
163
+ return stageBuildFactory.get({
164
+ stageId: stage.id,
165
+ eventId
166
+ });
167
+ }
168
+
169
+ return null;
170
+ }
171
+
143
172
  /**
144
173
  * Get all builds in stage
145
174
  *
@@ -241,6 +270,50 @@ function deriveEventStatusFromBuildStatuses(builds) {
241
270
  return newEventStatus;
242
271
  }
243
272
 
273
+ /**
274
+ * Update stage build's status
275
+ * Stage build's status can transition as below
276
+ * CREATED -> RUNNING -when all builds are succeeded-> SUCCESS
277
+ * -when some builds are terminated-> FAILURE, ABORTED, UNSTABLE, COLLAPSED
278
+ *
279
+ * @param {Object} param
280
+ * @param {StageBuild} param.stageBuild
281
+ * @param {String} param.newStatus
282
+ * @param {Job} param.job
283
+ * @returns {StageBuild} new stage build's status
284
+ */
285
+ async function updateStageBuildStatus({ stageBuild, newStatus, job }) {
286
+ if (stageBuild.status === newStatus) {
287
+ return stageBuild;
288
+ }
289
+
290
+ // Not update when the stage build status is terminal
291
+ if (TERMINAL_STATUSES.includes(stageBuild.status)) {
292
+ return stageBuild;
293
+ }
294
+
295
+ const isStageTeardown = STAGE_TEARDOWN_PATTERN.test(job.name);
296
+ const intermediateStatuses = ['RUNNING', ...TERMINAL_STATUSES];
297
+ const teardownStatuses = ['RUNNING', ...FINISHED_STATUSES];
298
+
299
+ // It means all builds are succeeded if stage build status is not terminal when teardown build is done.
300
+ if (isStageTeardown && teardownStatuses.includes(newStatus)) {
301
+ stageBuild.status = newStatus;
302
+
303
+ return stageBuild.update();
304
+ }
305
+
306
+ // Stage build status can transition to terminated status in the intermediate builds.
307
+ // But not update to SUCCESS
308
+ if (!isStageTeardown && intermediateStatuses.includes(newStatus)) {
309
+ stageBuild.status = newStatus;
310
+
311
+ return stageBuild.update();
312
+ }
313
+
314
+ return stageBuild;
315
+ }
316
+
244
317
  /**
245
318
  * Updates the build and trigger its downstream jobs in the workflow
246
319
  *
@@ -336,14 +409,9 @@ async function updateBuildAndTriggerDownstreamJobs(config, build, server, userna
336
409
  eventId: newEvent.id
337
410
  });
338
411
 
339
- if (stageBuild.status !== newBuild.status) {
340
- if (!TERMINAL_STATUSES.includes(stageBuild.status)) {
341
- stageBuild.status = newBuild.status;
342
- await stageBuild.update();
343
- }
344
- }
412
+ const newStageBuild = await updateStageBuildStatus({ stageBuild, newStatus: newBuild.status, job });
345
413
 
346
- stageBuildHasFailure = TERMINAL_STATUSES.includes(stageBuild.status);
414
+ stageBuildHasFailure = TERMINAL_STATUSES.includes(newStageBuild.status);
347
415
  }
348
416
 
349
417
  // Guard against triggering non-successful or unstable builds
@@ -417,5 +485,7 @@ async function updateBuildAndTriggerDownstreamJobs(config, build, server, userna
417
485
 
418
486
  module.exports = {
419
487
  updateBuildAndTriggerDownstreamJobs,
420
- deriveEventStatusFromBuildStatuses
488
+ deriveEventStatusFromBuildStatuses,
489
+ getStageBuild,
490
+ updateStageBuildStatus
421
491
  };
@@ -44,6 +44,7 @@ const {
44
44
  getNextJobStageName
45
45
  } = require('./triggers/helpers');
46
46
  const { getFullStageJobName } = require('../helper');
47
+ const { updateStageBuildStatus, getStageBuild } = require('./helper/updateBuild');
47
48
 
48
49
  /**
49
50
  * Delete a build
@@ -71,7 +72,7 @@ async function triggerNextJobs(config, app) {
71
72
  const currentPipeline = config.pipeline;
72
73
  const currentJob = config.job;
73
74
  const currentBuild = config.build;
74
- const { jobFactory, buildFactory, eventFactory, pipelineFactory } = app;
75
+ const { jobFactory, buildFactory, eventFactory, pipelineFactory, stageFactory, stageBuildFactory } = app;
75
76
 
76
77
  /** @type {EventModel} */
77
78
  const currentEvent = await eventFactory.get({ id: currentBuild.eventId });
@@ -100,8 +101,8 @@ async function triggerNextJobs(config, app) {
100
101
  for (const [nextJobName] of Object.entries(currentPipelineNextJobs)) {
101
102
  const nextJob = await getJob(nextJobName, currentPipeline.id, jobFactory);
102
103
  const node = currentEvent.workflowGraph.nodes.find(n => n.name === trimJobName(nextJobName));
103
- const isNextJobVirtual = node.virtual || false;
104
- const nextJobStageName = getNextJobStageName({ stageName: node.stageName, nextJobName });
104
+ const isNextJobVirtual = node && node.virtual === true;
105
+ const nextJobStageName = node ? getNextJobStageName({ stageName: node.stageName, nextJobName }) : null;
105
106
  const resource = `pipeline:${currentPipeline.id}:groupEvent:${currentEvent.groupEventId}`;
106
107
  let lock;
107
108
  let nextBuild;
@@ -144,15 +145,31 @@ async function triggerNextJobs(config, app) {
144
145
  );
145
146
  }
146
147
 
147
- if (isNextJobVirtual && nextBuild && nextBuild.status === Status.SUCCESS) {
148
- downstreamOfNextJobsToBeProcessed.push({
149
- build: nextBuild,
150
- event: currentEvent,
151
- job: nextJob,
152
- pipeline: currentPipeline,
153
- scmContext: config.scmContext,
154
- username: config.username
148
+ if (isNextJobVirtual) {
149
+ const stageBuild = await getStageBuild({
150
+ stageFactory,
151
+ stageBuildFactory,
152
+ workflowGraph: currentEvent.workflowGraph,
153
+ jobName: nextJobName,
154
+ pipelineId: currentPipeline.id,
155
+ eventId: currentEvent.id
155
156
  });
157
+
158
+ if (stageBuild) {
159
+ await updateStageBuildStatus({ stageBuild, newStatus: nextBuild.status, job: nextJob });
160
+ }
161
+
162
+ // Trigger downstream jobs
163
+ if (nextBuild && nextBuild.status === Status.SUCCESS) {
164
+ downstreamOfNextJobsToBeProcessed.push({
165
+ build: nextBuild,
166
+ event: currentEvent,
167
+ job: nextJob,
168
+ pipeline: currentPipeline,
169
+ scmContext: config.scmContext,
170
+ username: config.username
171
+ });
172
+ }
156
173
  }
157
174
  } catch (err) {
158
175
  logger.error(
@@ -290,8 +307,8 @@ async function triggerNextJobs(config, app) {
290
307
  for (const [nextJobName, nextJobInfo] of Object.entries(joinedPipeline.jobs)) {
291
308
  const nextJob = await getJob(nextJobName, joinedPipelineId, jobFactory);
292
309
  const node = externalEvent.workflowGraph.nodes.find(n => n.name === trimJobName(nextJobName));
293
- const isNextJobVirtual = node.virtual || false;
294
- const nextJobStageName = getNextJobStageName({ stageName: node.stageName, nextJobName });
310
+ const isNextJobVirtual = node && node.virtual === true;
311
+ const nextJobStageName = node ? getNextJobStageName({ stageName: node.stageName, nextJobName }) : null;
295
312
 
296
313
  const { parentBuilds } = parseJobInfo({
297
314
  joinObj: joinedPipeline.jobs,
@@ -334,15 +351,30 @@ async function triggerNextJobs(config, app) {
334
351
  );
335
352
  }
336
353
 
337
- if (isNextJobVirtual && nextBuild && nextBuild.status === Status.SUCCESS) {
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
354
+ if (isNextJobVirtual) {
355
+ const stageBuild = await getStageBuild({
356
+ stageFactory,
357
+ stageBuildFactory,
358
+ workflowGraph: currentEvent.workflowGraph,
359
+ jobName: nextJob.name,
360
+ pipelineId: currentPipeline.id,
361
+ eventId: currentEvent.id
345
362
  });
363
+
364
+ if (stageBuild) {
365
+ await updateStageBuildStatus({ stageBuild, newStatus: nextBuild.status, job: nextJob });
366
+ }
367
+
368
+ if (nextBuild && nextBuild.status === Status.SUCCESS) {
369
+ downstreamOfNextJobsToBeProcessed.push({
370
+ build: nextBuild,
371
+ event: currentEvent,
372
+ job: nextJob,
373
+ pipeline: await nextJob.pipeline,
374
+ scmContext: config.scmContext,
375
+ username: config.username
376
+ });
377
+ }
346
378
  }
347
379
  } catch (err) {
348
380
  logger.error(
@@ -8,6 +8,7 @@ const schema = require('screwdriver-data-schema');
8
8
  const getSchema = schema.models.event.get;
9
9
  const idSchema = schema.models.event.base.extract('id');
10
10
  const { deriveEventStatusFromBuildStatuses } = require('../builds/helper/updateBuild');
11
+ const nonTerminatedStatus = ['CREATED', 'RUNNING', 'QUEUED', 'BLOCKED', 'FROZEN'];
11
12
 
12
13
  module.exports = () => ({
13
14
  method: 'PUT',
@@ -78,25 +79,25 @@ module.exports = () => ({
78
79
 
79
80
  // User has good permissions, get event builds
80
81
  const builds = await event.getBuilds();
81
- const toUpdate = [];
82
+ const toUpdateBuilds = [];
82
83
  const updatedBuilds = [];
83
84
 
84
85
  // Update endtime and stop running builds
85
86
  // Note: COLLAPSED builds will never run
86
87
  builds.forEach(b => {
87
- if (['CREATED', 'RUNNING', 'QUEUED', 'BLOCKED', 'FROZEN'].includes(b.status)) {
88
+ if (nonTerminatedStatus.includes(b.status)) {
88
89
  if (b.status === 'RUNNING') {
89
90
  b.endTime = new Date().toISOString();
90
91
  }
91
92
  b.status = 'ABORTED';
92
93
  b.statusMessage = `Aborted by ${username}`;
93
94
 
94
- toUpdate.push(b.update());
95
+ toUpdateBuilds.push(b.update());
95
96
  } else {
96
97
  updatedBuilds.push(b);
97
98
  }
98
99
  });
99
- updatedBuilds.push(...(await Promise.all(toUpdate)));
100
+ updatedBuilds.push(...(await Promise.all(toUpdateBuilds)));
100
101
 
101
102
  const newEventStatus = deriveEventStatusFromBuildStatuses(updatedBuilds);
102
103
 
@@ -105,6 +106,19 @@ module.exports = () => ({
105
106
  await event.update();
106
107
  }
107
108
 
109
+ // Update stageBuild status to ABORTED
110
+ const stageBuilds = await event.getStageBuilds();
111
+ const toUpdateStageBuilds = [];
112
+
113
+ stageBuilds.forEach(sb => {
114
+ if (nonTerminatedStatus.includes(sb.status)) {
115
+ sb.status = 'ABORTED';
116
+ toUpdateStageBuilds.push(sb.update());
117
+ }
118
+ });
119
+
120
+ await Promise.all(toUpdateStageBuilds);
121
+
108
122
  // everything succeeded, inform the user
109
123
  const location = urlLib.format({
110
124
  host: request.headers.host,
@@ -20,7 +20,7 @@ module.exports = () => ({
20
20
  },
21
21
 
22
22
  handler: async (request, h) => {
23
- const pipelineFactory = request.server.app.pipelineFactory;
23
+ const { pipelineFactory } = request.server.app;
24
24
  const { scope } = request.auth.credentials;
25
25
  const { scmContext, includeUserToken } = request.query;
26
26