@backstage/plugin-scaffolder-backend 1.12.0-next.2 → 1.12.1-next.0

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/dist/index.cjs.js CHANGED
@@ -11,6 +11,7 @@ var backendCommon = require('@backstage/backend-common');
11
11
  var zod = require('zod');
12
12
  var integration = require('@backstage/integration');
13
13
  var path = require('path');
14
+ var luxon = require('luxon');
14
15
  var globby = require('globby');
15
16
  var isbinaryfile = require('isbinaryfile');
16
17
  var vm2 = require('vm2');
@@ -26,7 +27,6 @@ var fs$1 = require('fs');
26
27
  var limiterFactory = require('p-limit');
27
28
  var node = require('@gitbeaker/node');
28
29
  var uuid = require('uuid');
29
- var luxon = require('luxon');
30
30
  var ObservableImpl = require('zen-observable');
31
31
  var PQueue = require('p-queue');
32
32
  var winston = require('winston');
@@ -39,7 +39,7 @@ var express = require('express');
39
39
  var Router = require('express-promise-router');
40
40
  var url = require('url');
41
41
  var os = require('os');
42
- var pluginCatalogBackend = require('@backstage/plugin-catalog-backend');
42
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
43
43
 
44
44
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
45
45
 
@@ -77,14 +77,14 @@ var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
77
77
  var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
78
78
  var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
79
79
 
80
- const id$3 = "catalog:register";
81
- const examples$3 = [
80
+ const id$4 = "catalog:register";
81
+ const examples$4 = [
82
82
  {
83
83
  description: "Register with the catalog",
84
84
  example: yaml__default["default"].stringify({
85
85
  steps: [
86
86
  {
87
- action: id$3,
87
+ action: id$4,
88
88
  id: "register-with-catalog",
89
89
  name: "Register with the catalog",
90
90
  input: {
@@ -98,9 +98,9 @@ const examples$3 = [
98
98
  function createCatalogRegisterAction(options) {
99
99
  const { catalogClient, integrations } = options;
100
100
  return pluginScaffolderNode.createTemplateAction({
101
- id: id$3,
101
+ id: id$4,
102
102
  description: "Registers entities from a catalog descriptor file in the workspace into the software catalog.",
103
- examples: examples$3,
103
+ examples: examples$4,
104
104
  schema: {
105
105
  input: {
106
106
  oneOf: [
@@ -218,14 +218,14 @@ function createCatalogRegisterAction(options) {
218
218
  });
219
219
  }
220
220
 
221
- const id$2 = "catalog:write";
222
- const examples$2 = [
221
+ const id$3 = "catalog:write";
222
+ const examples$3 = [
223
223
  {
224
224
  description: "Write a catalog yaml file",
225
225
  example: yaml__namespace.stringify({
226
226
  steps: [
227
227
  {
228
- action: id$2,
228
+ action: id$3,
229
229
  id: "create-catalog-info-file",
230
230
  name: "Create catalog file",
231
231
  input: {
@@ -250,7 +250,7 @@ const examples$2 = [
250
250
  ];
251
251
  function createCatalogWriteAction() {
252
252
  return pluginScaffolderNode.createTemplateAction({
253
- id: id$2,
253
+ id: id$3,
254
254
  description: "Writes the catalog-info.yaml for your template",
255
255
  schema: {
256
256
  input: zod.z.object({
@@ -261,7 +261,7 @@ function createCatalogWriteAction() {
261
261
  )
262
262
  })
263
263
  },
264
- examples: examples$2,
264
+ examples: examples$3,
265
265
  supportsDryRun: true,
266
266
  async handler(ctx) {
267
267
  ctx.logStream.write(`Writing catalog-info.yaml`);
@@ -275,14 +275,14 @@ function createCatalogWriteAction() {
275
275
  });
276
276
  }
277
277
 
278
- const id$1 = "catalog:fetch";
279
- const examples$1 = [
278
+ const id$2 = "catalog:fetch";
279
+ const examples$2 = [
280
280
  {
281
281
  description: "Fetch entity by reference",
282
282
  example: yaml__default["default"].stringify({
283
283
  steps: [
284
284
  {
285
- action: id$1,
285
+ action: id$2,
286
286
  id: "fetch",
287
287
  name: "Fetch catalog entity",
288
288
  input: {
@@ -291,17 +291,31 @@ const examples$1 = [
291
291
  }
292
292
  ]
293
293
  })
294
+ },
295
+ {
296
+ description: "Fetch multiple entities by referencse",
297
+ example: yaml__default["default"].stringify({
298
+ steps: [
299
+ {
300
+ action: id$2,
301
+ id: "fetchMultiple",
302
+ name: "Fetch catalog entities",
303
+ input: {
304
+ entityRefs: ["component:default/name"]
305
+ }
306
+ }
307
+ ]
308
+ })
294
309
  }
295
310
  ];
296
311
  function createFetchCatalogEntityAction(options) {
297
312
  const { catalogClient } = options;
298
313
  return pluginScaffolderNode.createTemplateAction({
299
- id: id$1,
300
- description: "Returns entity from the catalog by entity reference",
301
- examples: examples$1,
314
+ id: id$2,
315
+ description: "Returns entity or entities from the catalog by entity reference(s)",
316
+ examples: examples$2,
302
317
  schema: {
303
318
  input: {
304
- required: ["entityRef"],
305
319
  type: "object",
306
320
  properties: {
307
321
  entityRef: {
@@ -309,9 +323,14 @@ function createFetchCatalogEntityAction(options) {
309
323
  title: "Entity reference",
310
324
  description: "Entity reference of the entity to get"
311
325
  },
326
+ entityRefs: {
327
+ type: "array",
328
+ title: "Entity references",
329
+ description: "Entity references of the entities to get"
330
+ },
312
331
  optional: {
313
332
  title: "Optional",
314
- description: "Permit the entity to optionally exist. Default: false",
333
+ description: "Allow the entity or entities to optionally exist. Default: false",
315
334
  type: "boolean"
316
335
  }
317
336
  }
@@ -322,40 +341,62 @@ function createFetchCatalogEntityAction(options) {
322
341
  entity: {
323
342
  title: "Entity found by the entity reference",
324
343
  type: "object",
325
- description: "Object containing same values used in the Entity schema."
344
+ description: "Object containing same values used in the Entity schema. Only when used with `entityRef` parameter."
345
+ },
346
+ entities: {
347
+ title: "Entities found by the entity references",
348
+ type: "array",
349
+ items: { type: "object" },
350
+ description: "Array containing objects with same values used in the Entity schema. Only when used with `entityRefs` parameter."
326
351
  }
327
352
  }
328
353
  }
329
354
  },
330
355
  async handler(ctx) {
331
- var _a;
332
- const { entityRef, optional } = ctx.input;
333
- let entity;
334
- try {
335
- entity = await catalogClient.getEntityByRef(entityRef, {
356
+ var _a, _b;
357
+ const { entityRef, entityRefs, optional } = ctx.input;
358
+ if (!entityRef && !entityRefs) {
359
+ if (optional) {
360
+ return;
361
+ }
362
+ throw new Error("Missing entity reference or references");
363
+ }
364
+ if (entityRef) {
365
+ const entity = await catalogClient.getEntityByRef(entityRef, {
336
366
  token: (_a = ctx.secrets) == null ? void 0 : _a.backstageToken
337
367
  });
338
- } catch (e) {
339
- if (!optional) {
340
- throw e;
368
+ if (!entity && !optional) {
369
+ throw new Error(`Entity ${entityRef} not found`);
341
370
  }
371
+ ctx.output("entity", entity != null ? entity : null);
342
372
  }
343
- if (!entity && !optional) {
344
- throw new Error(`Entity ${entityRef} not found`);
373
+ if (entityRefs) {
374
+ const entities = await catalogClient.getEntitiesByRefs(
375
+ { entityRefs },
376
+ {
377
+ token: (_b = ctx.secrets) == null ? void 0 : _b.backstageToken
378
+ }
379
+ );
380
+ const finalEntities = entities.items.map((e, i) => {
381
+ if (!e && !optional) {
382
+ throw new Error(`Entity ${entityRefs[i]} not found`);
383
+ }
384
+ return e != null ? e : null;
385
+ });
386
+ ctx.output("entities", finalEntities);
345
387
  }
346
- ctx.output("entity", entity != null ? entity : null);
347
388
  }
348
389
  });
349
390
  }
350
391
 
351
- const id = "debug:log";
352
- const examples = [
392
+ const id$1 = "debug:log";
393
+ const examples$1 = [
353
394
  {
354
395
  description: "Write a debug message",
355
396
  example: yaml__default["default"].stringify({
356
397
  steps: [
357
398
  {
358
- action: id,
399
+ action: id$1,
359
400
  id: "write-debug-line",
360
401
  name: 'Write "Hello Backstage!" log line',
361
402
  input: {
@@ -370,7 +411,7 @@ const examples = [
370
411
  example: yaml__default["default"].stringify({
371
412
  steps: [
372
413
  {
373
- action: id,
414
+ action: id$1,
374
415
  id: "write-workspace-directory",
375
416
  name: "List the workspace directory",
376
417
  input: {
@@ -383,9 +424,9 @@ const examples = [
383
424
  ];
384
425
  function createDebugLogAction() {
385
426
  return pluginScaffolderNode.createTemplateAction({
386
- id,
427
+ id: id$1,
387
428
  description: "Writes a message into the log or lists all files in the workspace.",
388
- examples,
429
+ examples: examples$1,
389
430
  schema: {
390
431
  input: {
391
432
  type: "object",
@@ -432,6 +473,98 @@ async function recursiveReadDir(dir) {
432
473
  return files.reduce((a, f) => a.concat(f), []);
433
474
  }
434
475
 
476
+ const id = "debug:wait";
477
+ const MAX_WAIT_TIME_IN_ISO = "T00:00:30";
478
+ const examples = [
479
+ {
480
+ description: "Waiting for 5 seconds",
481
+ example: yaml__default["default"].stringify({
482
+ steps: [
483
+ {
484
+ action: id,
485
+ id: "wait-5sec",
486
+ name: "Waiting for 5 seconds",
487
+ input: {
488
+ seconds: 5
489
+ }
490
+ }
491
+ ]
492
+ })
493
+ },
494
+ {
495
+ description: "Waiting for 5 minutes",
496
+ example: yaml__default["default"].stringify({
497
+ steps: [
498
+ {
499
+ action: id,
500
+ id: "wait-5min",
501
+ name: "Waiting for 5 minutes",
502
+ input: {
503
+ minutes: 5
504
+ }
505
+ }
506
+ ]
507
+ })
508
+ }
509
+ ];
510
+ function createWaitAction(options) {
511
+ const toDuration = (maxWaitTime) => {
512
+ if (maxWaitTime) {
513
+ if (maxWaitTime instanceof luxon.Duration) {
514
+ return maxWaitTime;
515
+ }
516
+ return luxon.Duration.fromObject(maxWaitTime);
517
+ }
518
+ return luxon.Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO);
519
+ };
520
+ return pluginScaffolderNode.createTemplateAction({
521
+ id,
522
+ description: "Waits for a certain period of time.",
523
+ examples,
524
+ schema: {
525
+ input: {
526
+ type: "object",
527
+ properties: {
528
+ minutes: {
529
+ title: "Waiting period in minutes.",
530
+ type: "number"
531
+ },
532
+ seconds: {
533
+ title: "Waiting period in seconds.",
534
+ type: "number"
535
+ },
536
+ milliseconds: {
537
+ title: "Waiting period in milliseconds.",
538
+ type: "number"
539
+ }
540
+ }
541
+ }
542
+ },
543
+ async handler(ctx) {
544
+ const delayTime = luxon.Duration.fromObject(ctx.input);
545
+ const maxWait = toDuration(options == null ? void 0 : options.maxWaitTime);
546
+ if (delayTime.minus(maxWait).toMillis() > 0) {
547
+ throw new Error(
548
+ `Waiting duration is longer than the maximum threshold of ${maxWait.toHuman()}`
549
+ );
550
+ }
551
+ await new Promise((resolve) => {
552
+ var _a;
553
+ const controller = new AbortController();
554
+ const timeoutHandle = setTimeout(abort, delayTime.toMillis());
555
+ (_a = ctx.signal) == null ? void 0 : _a.addEventListener("abort", abort);
556
+ function abort() {
557
+ var _a2;
558
+ (_a2 = ctx.signal) == null ? void 0 : _a2.removeEventListener("abort", abort);
559
+ clearTimeout(timeoutHandle);
560
+ controller.abort();
561
+ resolve("finished");
562
+ }
563
+ });
564
+ }
565
+ });
566
+ }
567
+
435
568
  async function fetchContents(options) {
436
569
  const { reader, integrations, baseUrl, fetchUrl = ".", outputPath } = options;
437
570
  let fetchUrlIsAbsolute = false;
@@ -3928,7 +4061,7 @@ const createPublishGitlabMergeRequestAction = (options) => {
3928
4061
  title: "Gitlab Project path",
3929
4062
  type: "string"
3930
4063
  },
3931
- mergeRequestURL: {
4064
+ mergeRequestUrl: {
3932
4065
  title: "MergeRequest(MR) URL",
3933
4066
  type: "string",
3934
4067
  description: "Link to the merge request in GitLab"
@@ -4100,6 +4233,7 @@ const createBuiltinActions = (options) => {
4100
4233
  config
4101
4234
  }),
4102
4235
  createDebugLogAction(),
4236
+ createWaitAction(),
4103
4237
  createCatalogRegisterAction({ catalogClient, integrations }),
4104
4238
  createFetchCatalogEntityAction({ catalogClient }),
4105
4239
  createCatalogWriteAction(),
@@ -4266,7 +4400,7 @@ class DatabaseTaskStore {
4266
4400
  const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
4267
4401
  status: "processing",
4268
4402
  last_heartbeat_at: this.db.fn.now(),
4269
- // remove the secrets when moving moving to processing state.
4403
+ // remove the secrets when moving to processing state.
4270
4404
  secrets: null
4271
4405
  });
4272
4406
  if (updateCount < 1) {
@@ -4314,7 +4448,7 @@ class DatabaseTaskStore {
4314
4448
  async completeTask(options) {
4315
4449
  const { taskId, status, eventBody } = options;
4316
4450
  let oldStatus;
4317
- if (status === "failed" || status === "completed") {
4451
+ if (["failed", "completed", "cancelled"].includes(status)) {
4318
4452
  oldStatus = "processing";
4319
4453
  } else {
4320
4454
  throw new Error(
@@ -4325,6 +4459,30 @@ class DatabaseTaskStore {
4325
4459
  const [task] = await tx("tasks").where({
4326
4460
  id: taskId
4327
4461
  }).limit(1).select();
4462
+ const updateTask = async (criteria) => {
4463
+ const updateCount = await tx("tasks").where(criteria).update({
4464
+ status
4465
+ });
4466
+ if (updateCount !== 1) {
4467
+ throw new errors.ConflictError(
4468
+ `Failed to update status to '${status}' for taskId ${taskId}`
4469
+ );
4470
+ }
4471
+ await tx("task_events").insert({
4472
+ task_id: taskId,
4473
+ event_type: "completion",
4474
+ body: JSON.stringify(eventBody)
4475
+ });
4476
+ };
4477
+ if (status === "cancelled") {
4478
+ await updateTask({
4479
+ id: taskId
4480
+ });
4481
+ return;
4482
+ }
4483
+ if (task.status === "cancelled") {
4484
+ return;
4485
+ }
4328
4486
  if (!task) {
4329
4487
  throw new Error(`No task with taskId ${taskId} found`);
4330
4488
  }
@@ -4333,21 +4491,9 @@ class DatabaseTaskStore {
4333
4491
  `Refusing to update status of run '${taskId}' to status '${status}' as it is currently '${task.status}', expected '${oldStatus}'`
4334
4492
  );
4335
4493
  }
4336
- const updateCount = await tx("tasks").where({
4494
+ await updateTask({
4337
4495
  id: taskId,
4338
4496
  status: oldStatus
4339
- }).update({
4340
- status
4341
- });
4342
- if (updateCount !== 1) {
4343
- throw new errors.ConflictError(
4344
- `Failed to update status to '${status}' for taskId ${taskId}`
4345
- );
4346
- }
4347
- await tx("task_events").insert({
4348
- task_id: taskId,
4349
- event_type: "completion",
4350
- body: JSON.stringify(eventBody)
4351
4497
  });
4352
4498
  });
4353
4499
  }
@@ -4415,24 +4561,37 @@ class DatabaseTaskStore {
4415
4561
  }
4416
4562
  });
4417
4563
  }
4564
+ async cancelTask(options) {
4565
+ const { taskId, body } = options;
4566
+ const serializedBody = JSON.stringify(body);
4567
+ await this.db("task_events").insert({
4568
+ task_id: taskId,
4569
+ event_type: "cancelled",
4570
+ body: serializedBody
4571
+ });
4572
+ }
4418
4573
  }
4419
4574
 
4420
4575
  class TaskManager {
4421
4576
  // Runs heartbeat internally
4422
- constructor(task, storage, logger) {
4577
+ constructor(task, storage, signal, logger) {
4423
4578
  this.task = task;
4424
4579
  this.storage = storage;
4580
+ this.signal = signal;
4425
4581
  this.logger = logger;
4426
4582
  this.isDone = false;
4427
4583
  }
4428
- static create(task, storage, logger) {
4429
- const agent = new TaskManager(task, storage, logger);
4584
+ static create(task, storage, abortSignal, logger) {
4585
+ const agent = new TaskManager(task, storage, abortSignal, logger);
4430
4586
  agent.startTimeout();
4431
4587
  return agent;
4432
4588
  }
4433
4589
  get spec() {
4434
4590
  return this.task.spec;
4435
4591
  }
4592
+ get cancelSignal() {
4593
+ return this.signal;
4594
+ }
4436
4595
  get secrets() {
4437
4596
  return this.task.secrets;
4438
4597
  }
@@ -4502,6 +4661,28 @@ class StorageTaskBroker {
4502
4661
  }
4503
4662
  return await this.storage.list({ createdBy: options == null ? void 0 : options.createdBy });
4504
4663
  }
4664
+ async registerCancellable(taskId, abortController) {
4665
+ let shouldUnsubscribe = false;
4666
+ const subscription = this.event$({ taskId, after: void 0 }).subscribe({
4667
+ error: (_) => {
4668
+ subscription.unsubscribe();
4669
+ },
4670
+ next: ({ events }) => {
4671
+ for (const event of events) {
4672
+ if (event.type === "cancelled") {
4673
+ abortController.abort();
4674
+ shouldUnsubscribe = true;
4675
+ }
4676
+ if (event.type === "completion") {
4677
+ shouldUnsubscribe = true;
4678
+ }
4679
+ }
4680
+ if (shouldUnsubscribe) {
4681
+ subscription.unsubscribe();
4682
+ }
4683
+ }
4684
+ });
4685
+ }
4505
4686
  /**
4506
4687
  * {@inheritdoc TaskBroker.claim}
4507
4688
  */
@@ -4509,6 +4690,8 @@ class StorageTaskBroker {
4509
4690
  for (; ; ) {
4510
4691
  const pendingTask = await this.storage.claimTask();
4511
4692
  if (pendingTask) {
4693
+ const abortController = new AbortController();
4694
+ await this.registerCancellable(pendingTask.id, abortController);
4512
4695
  return TaskManager.create(
4513
4696
  {
4514
4697
  taskId: pendingTask.id,
@@ -4517,6 +4700,7 @@ class StorageTaskBroker {
4517
4700
  createdBy: pendingTask.createdBy
4518
4701
  },
4519
4702
  this.storage,
4703
+ abortController.signal,
4520
4704
  this.logger
4521
4705
  );
4522
4706
  }
@@ -4591,6 +4775,19 @@ class StorageTaskBroker {
4591
4775
  this.deferredDispatch.resolve();
4592
4776
  this.deferredDispatch = defer();
4593
4777
  }
4778
+ async cancel(taskId) {
4779
+ var _a, _b;
4780
+ const { events } = await this.storage.listEvents({ taskId });
4781
+ const currentStepId = events.length > 0 ? events.filter(({ body }) => body == null ? void 0 : body.stepId).reduce((prev, curr) => prev.id > curr.id ? prev : curr).body.stepId : 0;
4782
+ await ((_b = (_a = this.storage).cancelTask) == null ? void 0 : _b.call(_a, {
4783
+ taskId,
4784
+ body: {
4785
+ message: `Step ${currentStepId} has been cancelled.`,
4786
+ stepId: currentStepId,
4787
+ status: "cancelled"
4788
+ }
4789
+ }));
4790
+ }
4594
4791
  }
4595
4792
 
4596
4793
  function isTruthy(value) {
@@ -4722,8 +4919,107 @@ class NunjucksWorkflowRunner {
4722
4919
  return value;
4723
4920
  });
4724
4921
  }
4725
- async execute(task) {
4922
+ async executeStep(task, step, context, renderTemplate, taskTrack, workspacePath) {
4726
4923
  var _a, _b, _c, _d, _e, _f, _g;
4924
+ const stepTrack = await this.tracker.stepStart(task, step);
4925
+ if (task.cancelSignal.aborted) {
4926
+ throw new Error(`Step ${step.name} has been cancelled.`);
4927
+ }
4928
+ try {
4929
+ if (step.if) {
4930
+ const ifResult = await this.render(step.if, context, renderTemplate);
4931
+ if (!isTruthy(ifResult)) {
4932
+ await stepTrack.skipFalsy();
4933
+ return;
4934
+ }
4935
+ }
4936
+ const action = this.options.actionRegistry.get(step.action);
4937
+ const { taskLogger, streamLogger } = createStepLogger({ task, step });
4938
+ if (task.isDryRun) {
4939
+ const redactedSecrets = Object.fromEntries(
4940
+ Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
4941
+ secret[0],
4942
+ "[REDACTED]"
4943
+ ])
4944
+ );
4945
+ const debugInput = (_b = step.input && this.render(
4946
+ step.input,
4947
+ {
4948
+ ...context,
4949
+ secrets: redactedSecrets
4950
+ },
4951
+ renderTemplate
4952
+ )) != null ? _b : {};
4953
+ taskLogger.info(
4954
+ `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
4955
+ debugInput,
4956
+ void 0,
4957
+ 2
4958
+ )}`
4959
+ );
4960
+ if (!action.supportsDryRun) {
4961
+ await taskTrack.skipDryRun(step, action);
4962
+ const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
4963
+ if (outputSchema) {
4964
+ context.steps[step.id] = {
4965
+ output: generateExampleOutput(outputSchema)
4966
+ };
4967
+ } else {
4968
+ context.steps[step.id] = { output: {} };
4969
+ }
4970
+ return;
4971
+ }
4972
+ }
4973
+ const input = (_e = step.input && this.render(
4974
+ step.input,
4975
+ { ...context, secrets: (_d = task.secrets) != null ? _d : {} },
4976
+ renderTemplate
4977
+ )) != null ? _e : {};
4978
+ if ((_f = action.schema) == null ? void 0 : _f.input) {
4979
+ const validateResult = jsonschema.validate(input, action.schema.input);
4980
+ if (!validateResult.valid) {
4981
+ const errors$1 = validateResult.errors.join(", ");
4982
+ throw new errors.InputError(
4983
+ `Invalid input passed to action ${action.id}, ${errors$1}`
4984
+ );
4985
+ }
4986
+ }
4987
+ const tmpDirs = new Array();
4988
+ const stepOutput = {};
4989
+ await action.handler({
4990
+ input,
4991
+ secrets: (_g = task.secrets) != null ? _g : {},
4992
+ logger: taskLogger,
4993
+ logStream: streamLogger,
4994
+ workspacePath,
4995
+ createTemporaryDirectory: async () => {
4996
+ const tmpDir = await fs__default["default"].mkdtemp(`${workspacePath}_step-${step.id}-`);
4997
+ tmpDirs.push(tmpDir);
4998
+ return tmpDir;
4999
+ },
5000
+ output(name, value) {
5001
+ stepOutput[name] = value;
5002
+ },
5003
+ templateInfo: task.spec.templateInfo,
5004
+ user: task.spec.user,
5005
+ isDryRun: task.isDryRun,
5006
+ signal: task.cancelSignal
5007
+ });
5008
+ for (const tmpDir of tmpDirs) {
5009
+ await fs__default["default"].remove(tmpDir);
5010
+ }
5011
+ context.steps[step.id] = { output: stepOutput };
5012
+ if (task.cancelSignal.aborted) {
5013
+ throw new Error(`Step ${step.name} has been cancelled.`);
5014
+ }
5015
+ await stepTrack.markSuccessful();
5016
+ } catch (err) {
5017
+ await taskTrack.markFailed(step, err);
5018
+ await stepTrack.markFailed();
5019
+ throw err;
5020
+ }
5021
+ }
5022
+ async execute(task) {
4727
5023
  if (!isValidTaskSpec(task.spec)) {
4728
5024
  throw new errors.InputError(
4729
5025
  "Wrong template version executed with the workflow engine"
@@ -4733,7 +5029,11 @@ class NunjucksWorkflowRunner {
4733
5029
  this.options.workingDirectory,
4734
5030
  await task.getWorkspaceName()
4735
5031
  );
4736
- const { integrations } = this.options;
5032
+ const {
5033
+ additionalTemplateFilters,
5034
+ additionalTemplateGlobals,
5035
+ integrations
5036
+ } = this.options;
4737
5037
  const renderTemplate = await SecureTemplater.loadRenderer({
4738
5038
  // TODO(blam): let's work out how we can deprecate this.
4739
5039
  // We shouldn't really need to be exposing these now we can deal with
@@ -4742,8 +5042,8 @@ class NunjucksWorkflowRunner {
4742
5042
  parseRepoUrl(url) {
4743
5043
  return parseRepoUrl(url, integrations);
4744
5044
  },
4745
- additionalTemplateFilters: this.options.additionalTemplateFilters,
4746
- additionalTemplateGlobals: this.options.additionalTemplateGlobals
5045
+ additionalTemplateFilters,
5046
+ additionalTemplateGlobals
4747
5047
  });
4748
5048
  try {
4749
5049
  const taskTrack = await this.tracker.taskStart(task);
@@ -4754,105 +5054,14 @@ class NunjucksWorkflowRunner {
4754
5054
  user: task.spec.user
4755
5055
  };
4756
5056
  for (const step of task.spec.steps) {
4757
- const stepTrack = await this.tracker.stepStart(task, step);
4758
- try {
4759
- if (step.if) {
4760
- const ifResult = await this.render(
4761
- step.if,
4762
- context,
4763
- renderTemplate
4764
- );
4765
- if (!isTruthy(ifResult)) {
4766
- await stepTrack.skipFalsy();
4767
- continue;
4768
- }
4769
- }
4770
- const action = this.options.actionRegistry.get(step.action);
4771
- const { taskLogger, streamLogger } = createStepLogger({ task, step });
4772
- if (task.isDryRun) {
4773
- const redactedSecrets = Object.fromEntries(
4774
- Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
4775
- secret[0],
4776
- "[REDACTED]"
4777
- ])
4778
- );
4779
- const debugInput = (_b = step.input && this.render(
4780
- step.input,
4781
- {
4782
- ...context,
4783
- secrets: redactedSecrets
4784
- },
4785
- renderTemplate
4786
- )) != null ? _b : {};
4787
- taskLogger.info(
4788
- `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
4789
- debugInput,
4790
- void 0,
4791
- 2
4792
- )}`
4793
- );
4794
- if (!action.supportsDryRun) {
4795
- await taskTrack.skipDryRun(step, action);
4796
- const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
4797
- if (outputSchema) {
4798
- context.steps[step.id] = {
4799
- output: generateExampleOutput(outputSchema)
4800
- };
4801
- } else {
4802
- context.steps[step.id] = { output: {} };
4803
- }
4804
- continue;
4805
- }
4806
- }
4807
- const input = (_e = step.input && this.render(
4808
- step.input,
4809
- { ...context, secrets: (_d = task.secrets) != null ? _d : {} },
4810
- renderTemplate
4811
- )) != null ? _e : {};
4812
- if ((_f = action.schema) == null ? void 0 : _f.input) {
4813
- const validateResult = jsonschema.validate(
4814
- input,
4815
- action.schema.input
4816
- );
4817
- if (!validateResult.valid) {
4818
- const errors$1 = validateResult.errors.join(", ");
4819
- throw new errors.InputError(
4820
- `Invalid input passed to action ${action.id}, ${errors$1}`
4821
- );
4822
- }
4823
- }
4824
- const tmpDirs = new Array();
4825
- const stepOutput = {};
4826
- await action.handler({
4827
- input,
4828
- secrets: (_g = task.secrets) != null ? _g : {},
4829
- logger: taskLogger,
4830
- logStream: streamLogger,
4831
- workspacePath,
4832
- createTemporaryDirectory: async () => {
4833
- const tmpDir = await fs__default["default"].mkdtemp(
4834
- `${workspacePath}_step-${step.id}-`
4835
- );
4836
- tmpDirs.push(tmpDir);
4837
- return tmpDir;
4838
- },
4839
- output(name, value) {
4840
- stepOutput[name] = value;
4841
- },
4842
- templateInfo: task.spec.templateInfo,
4843
- user: task.spec.user,
4844
- isDryRun: task.isDryRun
4845
- });
4846
- for (const tmpDir of tmpDirs) {
4847
- await fs__default["default"].remove(tmpDir);
4848
- }
4849
- context.steps[step.id] = { output: stepOutput };
4850
- await stepTrack.markSuccessful();
4851
- } catch (err) {
4852
- await taskTrack.markFailed(step, err);
4853
- await stepTrack.markFailed();
4854
- throw err;
4855
- }
5057
+ await this.executeStep(
5058
+ task,
5059
+ step,
5060
+ context,
5061
+ renderTemplate,
5062
+ taskTrack,
5063
+ workspacePath
5064
+ );
4856
5065
  }
4857
5066
  const output = this.render(task.spec.output, context, renderTemplate);
4858
5067
  await taskTrack.markSuccessful();
@@ -4919,8 +5128,21 @@ function scaffoldingTracker() {
4919
5128
  });
4920
5129
  taskTimer({ result: "failed" });
4921
5130
  }
5131
+ async function markCancelled(step) {
5132
+ await task.emitLog(`Step ${step.id} has been cancelled.`, {
5133
+ stepId: step.id,
5134
+ status: "cancelled"
5135
+ });
5136
+ taskCount.inc({
5137
+ template,
5138
+ user,
5139
+ result: "cancelled"
5140
+ });
5141
+ taskTimer({ result: "cancelled" });
5142
+ }
4922
5143
  return {
4923
5144
  skipDryRun,
5145
+ markCancelled,
4924
5146
  markSuccessful,
4925
5147
  markFailed
4926
5148
  };
@@ -4948,6 +5170,14 @@ function scaffoldingTracker() {
4948
5170
  });
4949
5171
  stepTimer({ result: "ok" });
4950
5172
  }
5173
+ async function markCancelled() {
5174
+ stepCount.inc({
5175
+ template,
5176
+ step: step.name,
5177
+ result: "cancelled"
5178
+ });
5179
+ stepTimer({ result: "cancelled" });
5180
+ }
4951
5181
  async function markFailed() {
4952
5182
  stepCount.inc({
4953
5183
  template,
@@ -4964,8 +5194,9 @@ function scaffoldingTracker() {
4964
5194
  stepTimer({ result: "skipped" });
4965
5195
  }
4966
5196
  return {
4967
- markSuccessful,
5197
+ markCancelled,
4968
5198
  markFailed,
5199
+ markSuccessful,
4969
5200
  skipFalsy
4970
5201
  };
4971
5202
  }
@@ -5089,6 +5320,7 @@ function createDryRunner(options) {
5089
5320
  );
5090
5321
  try {
5091
5322
  await deserializeDirectoryContents(contentsPath, input.directoryContents);
5323
+ const abortSignal = new AbortController().signal;
5092
5324
  const result = await workflowRunner.execute({
5093
5325
  spec: {
5094
5326
  ...input.spec,
@@ -5112,6 +5344,7 @@ function createDryRunner(options) {
5112
5344
  done: false,
5113
5345
  isDryRun: true,
5114
5346
  getWorkspaceName: async () => `dry-run-${dryRunId}`,
5347
+ cancelSignal: abortSignal,
5115
5348
  async emitLog(message, logMetadata) {
5116
5349
  if ((logMetadata == null ? void 0 : logMetadata.stepId) === dryRunId) {
5117
5350
  return;
@@ -5123,7 +5356,7 @@ function createDryRunner(options) {
5123
5356
  }
5124
5357
  });
5125
5358
  },
5126
- async complete() {
5359
+ complete: async () => {
5127
5360
  throw new Error("Not implemented");
5128
5361
  }
5129
5362
  });
@@ -5454,6 +5687,11 @@ async function createRouter(options) {
5454
5687
  }
5455
5688
  delete task.secrets;
5456
5689
  res.status(200).json(task);
5690
+ }).post("/v2/tasks/:taskId/cancel", async (req, res) => {
5691
+ var _a;
5692
+ const { taskId } = req.params;
5693
+ await ((_a = taskBroker.cancel) == null ? void 0 : _a.call(taskBroker, taskId));
5694
+ res.status(200).json({ status: "cancelled" });
5457
5695
  }).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
5458
5696
  const { taskId } = req.params;
5459
5697
  const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
@@ -5610,7 +5848,7 @@ class ScaffolderEntitiesProcessor {
5610
5848
  defaultNamespace: selfRef.namespace
5611
5849
  });
5612
5850
  emit(
5613
- pluginCatalogBackend.processingResult.relation({
5851
+ pluginCatalogNode.processingResult.relation({
5614
5852
  source: selfRef,
5615
5853
  type: catalogModel.RELATION_OWNED_BY,
5616
5854
  target: {
@@ -5621,7 +5859,7 @@ class ScaffolderEntitiesProcessor {
5621
5859
  })
5622
5860
  );
5623
5861
  emit(
5624
- pluginCatalogBackend.processingResult.relation({
5862
+ pluginCatalogNode.processingResult.relation({
5625
5863
  source: {
5626
5864
  kind: targetRef.kind,
5627
5865
  namespace: targetRef.namespace,
@@ -5670,6 +5908,7 @@ exports.createPublishGitlabAction = createPublishGitlabAction;
5670
5908
  exports.createPublishGitlabMergeRequestAction = createPublishGitlabMergeRequestAction;
5671
5909
  exports.createRouter = createRouter;
5672
5910
  exports.createTemplateAction = createTemplateAction;
5911
+ exports.createWaitAction = createWaitAction;
5673
5912
  exports.executeShellCommand = executeShellCommand;
5674
5913
  exports.fetchContents = fetchContents;
5675
5914
  //# sourceMappingURL=index.cjs.js.map