screwdriver-api 7.0.69 → 7.0.71

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": "7.0.69",
3
+ "version": "7.0.71",
4
4
  "description": "API server for the Screwdriver.cd service",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -114,7 +114,7 @@
114
114
  "screwdriver-executor-queue": "^4.0.0",
115
115
  "screwdriver-executor-router": "^3.0.0",
116
116
  "screwdriver-logger": "^2.0.0",
117
- "screwdriver-models": "^29.14.1",
117
+ "screwdriver-models": "^29.17.2",
118
118
  "screwdriver-notifications-email": "^3.0.0",
119
119
  "screwdriver-notifications-slack": "^5.0.0",
120
120
  "screwdriver-request": "^2.0.1",
@@ -20,6 +20,7 @@ const tokenRoute = require('./token');
20
20
  const metricsRoute = require('./metrics');
21
21
  const { EXTERNAL_TRIGGER_ALL } = schema.config.regex;
22
22
  const locker = require('../lock');
23
+ const { getFullStageJobName } = require('../helper');
23
24
 
24
25
  /**
25
26
  * Checks if job is external trigger
@@ -230,7 +231,7 @@ async function createExternalBuild(config) {
230
231
  * @param {String} [config.jobName] Job name
231
232
  * @param {String} config.username Username of build
232
233
  * @param {String} config.scmContext SCM context
233
- * @param {Object} config.parentBuilds Builds that triggered this build
234
+ * @param {Object} [config.parentBuilds] Builds that triggered this build
234
235
  * @param {String} config.baseBranch Branch name
235
236
  * @param {Number} [config.parentBuildId] Parent build ID
236
237
  * @param {Boolean} [config.start] Whether to start the build or not
@@ -272,6 +273,7 @@ async function createInternalBuild(config) {
272
273
  } else {
273
274
  job = await jobFactory.get(jobId);
274
275
  }
276
+
275
277
  const internalBuildConfig = {
276
278
  jobId: job.id,
277
279
  sha: event.sha,
@@ -526,9 +528,10 @@ async function getParentBuildStatus({ newBuild, joinListNames, pipelineId, build
526
528
  * @param {Build} newBuild Next build
527
529
  * @param {String} [jobName] Job name
528
530
  * @param {String} [pipelineId] Pipeline ID
531
+ * @param {Object} [stage] Stage
529
532
  * @return {Promise} The newly updated/created build
530
533
  */
531
- async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId }) {
534
+ async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId, stage }) {
532
535
  if (!done) {
533
536
  return null;
534
537
  }
@@ -538,10 +541,19 @@ async function handleNewBuild({ done, hasFailure, newBuild, jobName, pipelineId
538
541
 
539
542
  // Delete new build since previous build failed
540
543
  if (hasFailure) {
541
- logger.info(
542
- `Failure occurred in upstream job, removing new build - build:${newBuild.id} pipeline:${pipelineId}-${jobName} event:${newBuild.eventId} `
543
- );
544
- await newBuild.remove();
544
+ let stageTeardownName = '';
545
+
546
+ if (stage) {
547
+ stageTeardownName = getFullStageJobName({ stageName: stage.name, jobName: 'teardown' });
548
+ }
549
+
550
+ // New build is not stage teardown job
551
+ if (jobName !== stageTeardownName) {
552
+ logger.info(
553
+ `Failure occurred in upstream job, removing new build - build:${newBuild.id} pipeline:${pipelineId}-${jobName} event:${newBuild.eventId} `
554
+ );
555
+ await newBuild.remove();
556
+ }
545
557
 
546
558
  return null;
547
559
  }
@@ -709,6 +721,89 @@ function getParentBuildIds({ currentBuildId, parentBuilds, joinListNames, pipeli
709
721
  return Array.from(new Set([currentBuildId, ...parentBuildIds]));
710
722
  }
711
723
 
724
+ /**
725
+ * Create stage teardown build if it doesn't already exist
726
+ * @param {Factory} jobFactory Job factory
727
+ * @param {Factory} buildFactory Build factory
728
+ * @param {Object} current Current object
729
+ * @param {String} stageTeardownName Stage teardown name
730
+ * @param {String} username Username
731
+ * @param {String} scmContext SCM context
732
+ */
733
+ async function ensureStageTeardownBuildExists({
734
+ jobFactory,
735
+ buildFactory,
736
+ current,
737
+ stageTeardownName,
738
+ username,
739
+ scmContext
740
+ }) {
741
+ // Check if stage teardown build already exists
742
+ const stageTeardownJob = await jobFactory.get({
743
+ pipelineId: current.pipeline.id,
744
+ name: stageTeardownName
745
+ });
746
+ const existingStageTeardownBuild = await buildFactory.get({
747
+ eventId: current.event.id,
748
+ jobId: stageTeardownJob.id
749
+ });
750
+
751
+ // Doesn't exist, create stage teardown job
752
+ if (!existingStageTeardownBuild) {
753
+ await createInternalBuild({
754
+ jobFactory,
755
+ buildFactory,
756
+ pipelineId: current.pipeline.id,
757
+ jobName: stageTeardownName,
758
+ username,
759
+ scmContext,
760
+ event: current.event, // this is the parentBuild for the next build
761
+ baseBranch: current.event.baseBranch || null,
762
+ start: false
763
+ });
764
+ }
765
+ }
766
+
767
+ /**
768
+ * Delete nextBuild, create teardown build if it doesn't exist, and return teardown build or return null
769
+ * @param {String} nextJobName Next job name
770
+ * @param {Object} current Object with stage, event, pipeline info
771
+ * @param {Object} buildConfig Build config
772
+ * @param {Factory} jobFactory Job factory
773
+ * @param {Factory} buildFactory Build factory
774
+ * @param {String} username Username
775
+ * @param {String} scmContext Scm context
776
+ * @return {Array} Array of promises
777
+ */
778
+ async function handleStageFailure({
779
+ nextJobName,
780
+ current,
781
+ buildConfig,
782
+ jobFactory,
783
+ buildFactory,
784
+ username,
785
+ scmContext
786
+ }) {
787
+ const buildDeletePromises = [];
788
+ const stageTeardownName = getFullStageJobName({ stageName: current.stage.name, jobName: 'teardown' });
789
+
790
+ // Remove next build
791
+ if (buildConfig.eventId && nextJobName !== stageTeardownName) {
792
+ buildDeletePromises.push(deleteBuild(buildConfig, buildFactory));
793
+ }
794
+
795
+ await ensureStageTeardownBuildExists({
796
+ jobFactory,
797
+ buildFactory,
798
+ current,
799
+ stageTeardownName,
800
+ username,
801
+ scmContext
802
+ });
803
+
804
+ return buildDeletePromises;
805
+ }
806
+
712
807
  /**
713
808
  * Build API Plugin
714
809
  * @method register
@@ -732,21 +827,19 @@ const buildsPlugin = {
732
827
  * @return {Promise} Resolves to the removed build or null
733
828
  */
734
829
  server.expose('removeJoinBuilds', async (config, app) => {
735
- const { pipeline, job, build } = config;
736
- const { eventFactory, buildFactory } = app;
737
- const event = await eventFactory.get({ id: build.eventId });
830
+ const { pipeline, job, build, username, scmContext, event, stage } = config;
831
+ const { eventFactory, buildFactory, jobFactory } = app;
738
832
  const current = {
739
833
  pipeline,
740
834
  job,
741
835
  build,
742
- event
836
+ event,
837
+ stage
743
838
  };
744
-
745
839
  const nextJobsTrigger = workflowParser.getNextJobs(current.event.workflowGraph, {
746
840
  trigger: current.job.name,
747
841
  chainPR: pipeline.chainPR
748
842
  });
749
-
750
843
  const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
751
844
  const buildConfig = {};
752
845
  const deletePromises = [];
@@ -765,7 +858,20 @@ const buildsPlugin = {
765
858
  buildConfig.eventId = hoek.reach(pipelineJoinData[pid], 'event.id');
766
859
  }
767
860
 
768
- if (buildConfig.eventId) {
861
+ // if nextBuild is stage teardown, just return nextBuild
862
+ if (current.stage) {
863
+ const buildDeletePromises = await handleStageFailure({
864
+ nextJobName,
865
+ current,
866
+ buildConfig,
867
+ jobFactory,
868
+ buildFactory,
869
+ username,
870
+ scmContext
871
+ });
872
+
873
+ deletePromises.concat(buildDeletePromises);
874
+ } else if (buildConfig.eventId) {
769
875
  deletePromises.push(deleteBuild(buildConfig, buildFactory));
770
876
  }
771
877
  } catch (err) {
@@ -776,6 +882,7 @@ const buildsPlugin = {
776
882
  }
777
883
  }
778
884
  }
885
+
779
886
  await Promise.all(deletePromises);
780
887
  });
781
888
 
@@ -810,21 +917,20 @@ const buildsPlugin = {
810
917
  * @return {Promise} Resolves to the newly created build or null
811
918
  */
812
919
  server.expose('triggerNextJobs', async (config, app) => {
813
- const { pipeline, job, build } = config;
920
+ const { pipeline, job, build, event, stage } = config;
814
921
  const { eventFactory, pipelineFactory, buildFactory, jobFactory } = app;
815
- const event = await eventFactory.get({ id: build.eventId });
816
922
  const current = {
817
923
  pipeline,
818
924
  job,
819
925
  build,
820
- event
926
+ event,
927
+ stage
821
928
  };
822
929
 
823
930
  const nextJobsTrigger = workflowParser.getNextJobs(current.event.workflowGraph, {
824
931
  trigger: current.job.name,
825
932
  chainPR: pipeline.chainPR
826
933
  });
827
-
828
934
  const pipelineJoinData = await createJoinObject(nextJobsTrigger, current, eventFactory);
829
935
 
830
936
  // Helper function to handle triggering jobs in same pipeline
@@ -877,12 +983,14 @@ const buildsPlugin = {
877
983
  return existNextBuild;
878
984
  }
879
985
 
986
+ // Current build is not part of stage
880
987
  existNextBuild.status = 'QUEUED';
881
988
  await existNextBuild.update();
882
989
 
883
990
  return existNextBuild.start();
884
991
  }
885
992
 
993
+ // Handle join case. Fan-out/fan-in Workflow
886
994
  logger.info(`Fetching finished builds for event ${event.id}`);
887
995
  let finishedInternalBuilds = await getFinishedBuilds(current.event, buildFactory);
888
996
 
@@ -967,7 +1075,8 @@ const buildsPlugin = {
967
1075
  hasFailure,
968
1076
  newBuild,
969
1077
  jobName: nextJobName,
970
- pipelineId: current.pipeline.id
1078
+ pipelineId: current.pipeline.id,
1079
+ stage: current.stage
971
1080
  });
972
1081
  };
973
1082
 
@@ -1107,7 +1216,8 @@ const buildsPlugin = {
1107
1216
  hasFailure,
1108
1217
  newBuild,
1109
1218
  jobName: nextJobName,
1110
- pipelineId: externalPipelineId
1219
+ pipelineId: externalPipelineId,
1220
+ stage: current.stage
1111
1221
  });
1112
1222
  }
1113
1223
  }
@@ -1163,6 +1273,7 @@ const buildsPlugin = {
1163
1273
  await locker.unlock(lock, resource);
1164
1274
  }
1165
1275
  }
1276
+
1166
1277
  if (triggerCurrentPipelineAsExternal || !isCurrentPipeline) {
1167
1278
  let resource;
1168
1279
  let lock;
@@ -5,7 +5,10 @@ const hoek = require('@hapi/hoek');
5
5
  const schema = require('screwdriver-data-schema');
6
6
  const joi = require('joi');
7
7
  const idSchema = schema.models.build.base.extract('id');
8
- const { getScmUri, getUserPermissions } = require('../helper');
8
+ const { getScmUri, getUserPermissions, getFullStageJobName } = require('../helper');
9
+ const STAGE_TEARDOWN_PATTERN = /^stage@([\w-]+)(?::teardown)$/;
10
+ const TERMINAL_STATUSES = ['FAILURE', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
11
+ const FINISHED_STATUSES = ['FAILURE', 'SUCCESS', 'ABORTED', 'UNSTABLE', 'COLLAPSED'];
9
12
 
10
13
  /**
11
14
  * Identify whether this build resulted in a previously failed job to become successful.
@@ -124,7 +127,7 @@ async function validateUserPermission(build, request) {
124
127
  }
125
128
 
126
129
  /**
127
- *
130
+ * Set build status to desired status, set build statusMessage
128
131
  * @param {Object} build Build Model
129
132
  * @param {String} desiredStatus New Status
130
133
  * @param {String} statusMessage User passed status message
@@ -155,6 +158,51 @@ function updateBuildStatus(build, desiredStatus, statusMessage, username) {
155
158
  }
156
159
  }
157
160
 
161
+ /**
162
+ * Get stage for current node
163
+ * @param {StageFactory} stageFactory Stage factory
164
+ * @param {Object} workflowGraph Workflow graph
165
+ * @param {String} jobName Job name
166
+ * @param {Number} pipelineId Pipeline ID
167
+ * @return {Stage} Stage for node
168
+ */
169
+ async function getStage({ stageFactory, workflowGraph, jobName, pipelineId }) {
170
+ const currentNode = workflowGraph.nodes.find(node => node.name === jobName);
171
+ let stage = null;
172
+
173
+ if (currentNode && currentNode.stageName) {
174
+ stage = await stageFactory.get({
175
+ pipelineId,
176
+ name: currentNode.stageName
177
+ });
178
+ }
179
+
180
+ return Promise.resolve(stage);
181
+ }
182
+
183
+ /**
184
+ * Checks if all builds in stage are done running
185
+ * @param {Object} stage Stage
186
+ * @param {Object} event Event
187
+ * @return {Boolean} Flag if stage is done
188
+ */
189
+ async function isStageDone({ stage, event }) {
190
+ // Get all jobIds for jobs in the stage
191
+ const stageJobIds = stage.jobIds;
192
+
193
+ stageJobIds.push(stage.setup);
194
+
195
+ // Get all builds in a stage for this event
196
+ const stageJobBuilds = await event.getBuilds({ params: { jobId: stageJobIds } });
197
+ let stageIsDone = false;
198
+
199
+ if (stageJobBuilds && stageJobBuilds.length !== 0) {
200
+ stageIsDone = !stageJobBuilds.some(b => !FINISHED_STATUSES.includes(b.status));
201
+ }
202
+
203
+ return stageIsDone;
204
+ }
205
+
158
206
  module.exports = () => ({
159
207
  method: 'PUT',
160
208
  path: '/builds/{id}',
@@ -168,13 +216,14 @@ module.exports = () => ({
168
216
  },
169
217
 
170
218
  handler: async (request, h) => {
171
- const { buildFactory, eventFactory, jobFactory } = request.server.app;
219
+ const { buildFactory, eventFactory, jobFactory, stageFactory, stageBuildFactory } = request.server.app;
172
220
  const { id } = request.params;
173
221
  const { statusMessage, stats, status: desiredStatus } = request.payload;
174
222
  const { username, scmContext, scope } = request.auth.credentials;
175
223
  const isBuild = scope.includes('build') || scope.includes('temporal');
176
224
  const { triggerNextJobs, removeJoinBuilds } = request.server.plugins.builds;
177
225
 
226
+ // Check token permissions
178
227
  if (isBuild && username !== id) {
179
228
  return boom.forbidden(`Credential only valid for ${username}`);
180
229
  }
@@ -226,7 +275,6 @@ module.exports = () => ({
226
275
  }
227
276
 
228
277
  const [newBuild, newEvent] = await Promise.all([build.update(), event.update(), stopFrozen]);
229
-
230
278
  const job = await newBuild.job;
231
279
  const pipeline = await job.pipeline;
232
280
 
@@ -245,16 +293,74 @@ module.exports = () => ({
245
293
 
246
294
  const skipFurther = /\[(skip further)\]/.test(newEvent.causeMessage);
247
295
 
248
- // Guard against triggering non-successful or unstable builds
249
- // Don't further trigger pipeline if intented to skip further jobs
296
+ // Update stageBuild status if it has changed;
297
+ // if stageBuild status is currently terminal, do not update
298
+ const stage = await getStage({
299
+ stageFactory,
300
+ workflowGraph: newEvent.workflowGraph,
301
+ jobName: job.name,
302
+ pipelineId: pipeline.id
303
+ });
304
+ const isStageTeardown = STAGE_TEARDOWN_PATTERN.test(job.name);
305
+ let stageBuildHasFailure = false;
306
+
307
+ if (stage) {
308
+ const stageBuild = await stageBuildFactory.get({
309
+ stageId: stage.id,
310
+ eventId: newEvent.id
311
+ });
312
+
313
+ if (stageBuild.status !== newBuild.status) {
314
+ if (!TERMINAL_STATUSES.includes(stageBuild.status)) {
315
+ stageBuild.status = newBuild.status;
316
+ await stageBuild.update();
317
+ }
318
+ }
250
319
 
320
+ stageBuildHasFailure = TERMINAL_STATUSES.includes(stageBuild.status);
321
+ }
322
+
323
+ // Guard against triggering non-successful or unstable builds
324
+ // Don't further trigger pipeline if intend to skip further jobs
251
325
  if (newBuild.status !== 'SUCCESS' || skipFurther) {
252
326
  // Check for failed jobs and remove any child jobs in created state
253
327
  if (newBuild.status === 'FAILURE') {
254
- await removeJoinBuilds({ pipeline, job, build: newBuild }, request.server.app);
328
+ await removeJoinBuilds(
329
+ { pipeline, job, build: newBuild, username, scmContext, event: newEvent, stage },
330
+ request.server.app
331
+ );
255
332
  }
333
+ // Do not continue downstream is current job is stage teardown and statusBuild has failure
334
+ } else if (newBuild.status === 'SUCCESS' && isStageTeardown && stageBuildHasFailure) {
335
+ await removeJoinBuilds(
336
+ { pipeline, job, build: newBuild, username, scmContext, event: newEvent, stage },
337
+ request.server.app
338
+ );
256
339
  } else {
257
- await triggerNextJobs({ pipeline, job, build: newBuild, username, scmContext }, request.server.app);
340
+ await triggerNextJobs(
341
+ { pipeline, job, build: newBuild, username, scmContext, event: newEvent, stage },
342
+ request.server.app
343
+ );
344
+ }
345
+
346
+ // Determine if stage teardown build should start
347
+ // (if stage teardown build exists, and stageBuild.status is negative,
348
+ // and there are no active stage builds, and teardown build is not started)
349
+ if (stage && FINISHED_STATUSES.includes(newBuild.status)) {
350
+ const stageTeardownName = getFullStageJobName({ stageName: stage.name, jobName: 'teardown' });
351
+ const stageTeardownJob = await jobFactory.get({ pipelineId: pipeline.id, name: stageTeardownName });
352
+ const stageTeardownBuild = await buildFactory.get({ eventId: newEvent.id, jobId: stageTeardownJob.id });
353
+
354
+ // Start stage teardown build if stage is done
355
+ if (stageTeardownBuild && stageTeardownBuild.status === 'CREATED') {
356
+ const stageIsDone = await isStageDone({ stage, event: newEvent });
357
+
358
+ if (stageIsDone) {
359
+ stageTeardownBuild.status = 'QUEUED';
360
+ await stageTeardownBuild.update();
361
+ await stageTeardownBuild.start();
362
+ }
363
+ }
258
364
  }
259
365
 
260
366
  return h.response(await newBuild.toJsonWithSteps()).code(200);
package/plugins/helper.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  const boom = require('@hapi/boom');
4
4
  const dayjs = require('dayjs');
5
+ const STAGE_PREFIX = 'stage@';
5
6
 
6
7
  /**
7
8
  * Set default start time and end time
@@ -102,10 +103,21 @@ async function getScmUri({ pipeline, pipelineFactory }) {
102
103
  return scmUri;
103
104
  }
104
105
 
106
+ /**
107
+ * Returns full stage name with correct formatting and setup or teardown suffix (e.g. stage@deploy:setup)
108
+ * @param {String} stageName Stage name
109
+ * @param {String} type Type of stage job, either 'setup' or 'teardown'
110
+ * @return {String} Full stage name
111
+ */
112
+ function getFullStageJobName({ stageName, jobName }) {
113
+ return `${STAGE_PREFIX}${stageName}:${jobName}`;
114
+ }
115
+
105
116
  module.exports = {
106
117
  getReadOnlyInfo,
107
118
  getScmUri,
108
119
  getUserPermissions,
109
120
  setDefaultTimeRange,
110
- validTimeRange
121
+ validTimeRange,
122
+ getFullStageJobName
111
123
  };
@@ -122,8 +122,6 @@ Only PR events of specified PR number will be searched when `prNum` is set
122
122
 
123
123
  `GET /pipelines/{id}/stages`
124
124
 
125
- `GET /pipelines/{id}/stages?eventId={eventId}`
126
-
127
125
  #### Get all pipeline secrets
128
126
 
129
127
  `GET /pipelines/{id}/secrets`
@@ -18,8 +18,6 @@ module.exports = () => ({
18
18
  scope: ['user', 'build']
19
19
  },
20
20
  handler: async (request, h) => {
21
- console.log('request.params', request.params);
22
-
23
21
  const { namespace, name } = request.params;
24
22
  const { pipelineTemplateFactory } = request.server.app;
25
23