@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/alpha.cjs.js CHANGED
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var backendPluginApi = require('@backstage/backend-plugin-api');
6
6
  var alpha = require('@backstage/plugin-catalog-node/alpha');
7
7
  var catalogModel = require('@backstage/catalog-model');
8
- var pluginCatalogBackend = require('@backstage/plugin-catalog-backend');
8
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
9
9
  var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
10
10
  var backendCommon = require('@backstage/backend-common');
11
11
  var integration = require('@backstage/integration');
@@ -15,6 +15,7 @@ var yaml = require('yaml');
15
15
  var fs = require('fs-extra');
16
16
  var zod = require('zod');
17
17
  var path = require('path');
18
+ var luxon = require('luxon');
18
19
  var globby = require('globby');
19
20
  var isbinaryfile = require('isbinaryfile');
20
21
  var vm2 = require('vm2');
@@ -30,7 +31,6 @@ var fs$1 = require('fs');
30
31
  var limiterFactory = require('p-limit');
31
32
  var node = require('@gitbeaker/node');
32
33
  var uuid = require('uuid');
33
- var luxon = require('luxon');
34
34
  var ObservableImpl = require('zen-observable');
35
35
  var PQueue = require('p-queue');
36
36
  var winston = require('winston');
@@ -105,7 +105,7 @@ class ScaffolderEntitiesProcessor {
105
105
  defaultNamespace: selfRef.namespace
106
106
  });
107
107
  emit(
108
- pluginCatalogBackend.processingResult.relation({
108
+ pluginCatalogNode.processingResult.relation({
109
109
  source: selfRef,
110
110
  type: catalogModel.RELATION_OWNED_BY,
111
111
  target: {
@@ -116,7 +116,7 @@ class ScaffolderEntitiesProcessor {
116
116
  })
117
117
  );
118
118
  emit(
119
- pluginCatalogBackend.processingResult.relation({
119
+ pluginCatalogNode.processingResult.relation({
120
120
  source: {
121
121
  kind: targetRef.kind,
122
122
  namespace: targetRef.namespace,
@@ -147,14 +147,14 @@ const catalogModuleTemplateKind = backendPluginApi.createBackendModule({
147
147
  }
148
148
  });
149
149
 
150
- const id$3 = "catalog:register";
151
- const examples$3 = [
150
+ const id$4 = "catalog:register";
151
+ const examples$4 = [
152
152
  {
153
153
  description: "Register with the catalog",
154
154
  example: yaml__default["default"].stringify({
155
155
  steps: [
156
156
  {
157
- action: id$3,
157
+ action: id$4,
158
158
  id: "register-with-catalog",
159
159
  name: "Register with the catalog",
160
160
  input: {
@@ -168,9 +168,9 @@ const examples$3 = [
168
168
  function createCatalogRegisterAction(options) {
169
169
  const { catalogClient, integrations } = options;
170
170
  return pluginScaffolderNode.createTemplateAction({
171
- id: id$3,
171
+ id: id$4,
172
172
  description: "Registers entities from a catalog descriptor file in the workspace into the software catalog.",
173
- examples: examples$3,
173
+ examples: examples$4,
174
174
  schema: {
175
175
  input: {
176
176
  oneOf: [
@@ -288,14 +288,14 @@ function createCatalogRegisterAction(options) {
288
288
  });
289
289
  }
290
290
 
291
- const id$2 = "catalog:write";
292
- const examples$2 = [
291
+ const id$3 = "catalog:write";
292
+ const examples$3 = [
293
293
  {
294
294
  description: "Write a catalog yaml file",
295
295
  example: yaml__namespace.stringify({
296
296
  steps: [
297
297
  {
298
- action: id$2,
298
+ action: id$3,
299
299
  id: "create-catalog-info-file",
300
300
  name: "Create catalog file",
301
301
  input: {
@@ -320,7 +320,7 @@ const examples$2 = [
320
320
  ];
321
321
  function createCatalogWriteAction() {
322
322
  return pluginScaffolderNode.createTemplateAction({
323
- id: id$2,
323
+ id: id$3,
324
324
  description: "Writes the catalog-info.yaml for your template",
325
325
  schema: {
326
326
  input: zod.z.object({
@@ -331,7 +331,7 @@ function createCatalogWriteAction() {
331
331
  )
332
332
  })
333
333
  },
334
- examples: examples$2,
334
+ examples: examples$3,
335
335
  supportsDryRun: true,
336
336
  async handler(ctx) {
337
337
  ctx.logStream.write(`Writing catalog-info.yaml`);
@@ -345,14 +345,14 @@ function createCatalogWriteAction() {
345
345
  });
346
346
  }
347
347
 
348
- const id$1 = "catalog:fetch";
349
- const examples$1 = [
348
+ const id$2 = "catalog:fetch";
349
+ const examples$2 = [
350
350
  {
351
351
  description: "Fetch entity by reference",
352
352
  example: yaml__default["default"].stringify({
353
353
  steps: [
354
354
  {
355
- action: id$1,
355
+ action: id$2,
356
356
  id: "fetch",
357
357
  name: "Fetch catalog entity",
358
358
  input: {
@@ -361,17 +361,31 @@ const examples$1 = [
361
361
  }
362
362
  ]
363
363
  })
364
+ },
365
+ {
366
+ description: "Fetch multiple entities by referencse",
367
+ example: yaml__default["default"].stringify({
368
+ steps: [
369
+ {
370
+ action: id$2,
371
+ id: "fetchMultiple",
372
+ name: "Fetch catalog entities",
373
+ input: {
374
+ entityRefs: ["component:default/name"]
375
+ }
376
+ }
377
+ ]
378
+ })
364
379
  }
365
380
  ];
366
381
  function createFetchCatalogEntityAction(options) {
367
382
  const { catalogClient } = options;
368
383
  return pluginScaffolderNode.createTemplateAction({
369
- id: id$1,
370
- description: "Returns entity from the catalog by entity reference",
371
- examples: examples$1,
384
+ id: id$2,
385
+ description: "Returns entity or entities from the catalog by entity reference(s)",
386
+ examples: examples$2,
372
387
  schema: {
373
388
  input: {
374
- required: ["entityRef"],
375
389
  type: "object",
376
390
  properties: {
377
391
  entityRef: {
@@ -379,9 +393,14 @@ function createFetchCatalogEntityAction(options) {
379
393
  title: "Entity reference",
380
394
  description: "Entity reference of the entity to get"
381
395
  },
396
+ entityRefs: {
397
+ type: "array",
398
+ title: "Entity references",
399
+ description: "Entity references of the entities to get"
400
+ },
382
401
  optional: {
383
402
  title: "Optional",
384
- description: "Permit the entity to optionally exist. Default: false",
403
+ description: "Allow the entity or entities to optionally exist. Default: false",
385
404
  type: "boolean"
386
405
  }
387
406
  }
@@ -392,40 +411,62 @@ function createFetchCatalogEntityAction(options) {
392
411
  entity: {
393
412
  title: "Entity found by the entity reference",
394
413
  type: "object",
395
- description: "Object containing same values used in the Entity schema."
414
+ description: "Object containing same values used in the Entity schema. Only when used with `entityRef` parameter."
415
+ },
416
+ entities: {
417
+ title: "Entities found by the entity references",
418
+ type: "array",
419
+ items: { type: "object" },
420
+ description: "Array containing objects with same values used in the Entity schema. Only when used with `entityRefs` parameter."
396
421
  }
397
422
  }
398
423
  }
399
424
  },
400
425
  async handler(ctx) {
401
- var _a;
402
- const { entityRef, optional } = ctx.input;
403
- let entity;
404
- try {
405
- entity = await catalogClient.getEntityByRef(entityRef, {
426
+ var _a, _b;
427
+ const { entityRef, entityRefs, optional } = ctx.input;
428
+ if (!entityRef && !entityRefs) {
429
+ if (optional) {
430
+ return;
431
+ }
432
+ throw new Error("Missing entity reference or references");
433
+ }
434
+ if (entityRef) {
435
+ const entity = await catalogClient.getEntityByRef(entityRef, {
406
436
  token: (_a = ctx.secrets) == null ? void 0 : _a.backstageToken
407
437
  });
408
- } catch (e) {
409
- if (!optional) {
410
- throw e;
438
+ if (!entity && !optional) {
439
+ throw new Error(`Entity ${entityRef} not found`);
411
440
  }
441
+ ctx.output("entity", entity != null ? entity : null);
412
442
  }
413
- if (!entity && !optional) {
414
- throw new Error(`Entity ${entityRef} not found`);
443
+ if (entityRefs) {
444
+ const entities = await catalogClient.getEntitiesByRefs(
445
+ { entityRefs },
446
+ {
447
+ token: (_b = ctx.secrets) == null ? void 0 : _b.backstageToken
448
+ }
449
+ );
450
+ const finalEntities = entities.items.map((e, i) => {
451
+ if (!e && !optional) {
452
+ throw new Error(`Entity ${entityRefs[i]} not found`);
453
+ }
454
+ return e != null ? e : null;
455
+ });
456
+ ctx.output("entities", finalEntities);
415
457
  }
416
- ctx.output("entity", entity != null ? entity : null);
417
458
  }
418
459
  });
419
460
  }
420
461
 
421
- const id = "debug:log";
422
- const examples = [
462
+ const id$1 = "debug:log";
463
+ const examples$1 = [
423
464
  {
424
465
  description: "Write a debug message",
425
466
  example: yaml__default["default"].stringify({
426
467
  steps: [
427
468
  {
428
- action: id,
469
+ action: id$1,
429
470
  id: "write-debug-line",
430
471
  name: 'Write "Hello Backstage!" log line',
431
472
  input: {
@@ -440,7 +481,7 @@ const examples = [
440
481
  example: yaml__default["default"].stringify({
441
482
  steps: [
442
483
  {
443
- action: id,
484
+ action: id$1,
444
485
  id: "write-workspace-directory",
445
486
  name: "List the workspace directory",
446
487
  input: {
@@ -453,9 +494,9 @@ const examples = [
453
494
  ];
454
495
  function createDebugLogAction() {
455
496
  return pluginScaffolderNode.createTemplateAction({
456
- id,
497
+ id: id$1,
457
498
  description: "Writes a message into the log or lists all files in the workspace.",
458
- examples,
499
+ examples: examples$1,
459
500
  schema: {
460
501
  input: {
461
502
  type: "object",
@@ -502,6 +543,98 @@ async function recursiveReadDir(dir) {
502
543
  return files.reduce((a, f) => a.concat(f), []);
503
544
  }
504
545
 
546
+ const id = "debug:wait";
547
+ const MAX_WAIT_TIME_IN_ISO = "T00:00:30";
548
+ const examples = [
549
+ {
550
+ description: "Waiting for 5 seconds",
551
+ example: yaml__default["default"].stringify({
552
+ steps: [
553
+ {
554
+ action: id,
555
+ id: "wait-5sec",
556
+ name: "Waiting for 5 seconds",
557
+ input: {
558
+ seconds: 5
559
+ }
560
+ }
561
+ ]
562
+ })
563
+ },
564
+ {
565
+ description: "Waiting for 5 minutes",
566
+ example: yaml__default["default"].stringify({
567
+ steps: [
568
+ {
569
+ action: id,
570
+ id: "wait-5min",
571
+ name: "Waiting for 5 minutes",
572
+ input: {
573
+ minutes: 5
574
+ }
575
+ }
576
+ ]
577
+ })
578
+ }
579
+ ];
580
+ function createWaitAction(options) {
581
+ const toDuration = (maxWaitTime) => {
582
+ if (maxWaitTime) {
583
+ if (maxWaitTime instanceof luxon.Duration) {
584
+ return maxWaitTime;
585
+ }
586
+ return luxon.Duration.fromObject(maxWaitTime);
587
+ }
588
+ return luxon.Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO);
589
+ };
590
+ return pluginScaffolderNode.createTemplateAction({
591
+ id,
592
+ description: "Waits for a certain period of time.",
593
+ examples,
594
+ schema: {
595
+ input: {
596
+ type: "object",
597
+ properties: {
598
+ minutes: {
599
+ title: "Waiting period in minutes.",
600
+ type: "number"
601
+ },
602
+ seconds: {
603
+ title: "Waiting period in seconds.",
604
+ type: "number"
605
+ },
606
+ milliseconds: {
607
+ title: "Waiting period in milliseconds.",
608
+ type: "number"
609
+ }
610
+ }
611
+ }
612
+ },
613
+ async handler(ctx) {
614
+ const delayTime = luxon.Duration.fromObject(ctx.input);
615
+ const maxWait = toDuration(options == null ? void 0 : options.maxWaitTime);
616
+ if (delayTime.minus(maxWait).toMillis() > 0) {
617
+ throw new Error(
618
+ `Waiting duration is longer than the maximum threshold of ${maxWait.toHuman()}`
619
+ );
620
+ }
621
+ await new Promise((resolve) => {
622
+ var _a;
623
+ const controller = new AbortController();
624
+ const timeoutHandle = setTimeout(abort, delayTime.toMillis());
625
+ (_a = ctx.signal) == null ? void 0 : _a.addEventListener("abort", abort);
626
+ function abort() {
627
+ var _a2;
628
+ (_a2 = ctx.signal) == null ? void 0 : _a2.removeEventListener("abort", abort);
629
+ clearTimeout(timeoutHandle);
630
+ controller.abort();
631
+ resolve("finished");
632
+ }
633
+ });
634
+ }
635
+ });
636
+ }
637
+
505
638
  async function fetchContents(options) {
506
639
  const { reader, integrations, baseUrl, fetchUrl = ".", outputPath } = options;
507
640
  let fetchUrlIsAbsolute = false;
@@ -3970,7 +4103,7 @@ const createPublishGitlabMergeRequestAction = (options) => {
3970
4103
  title: "Gitlab Project path",
3971
4104
  type: "string"
3972
4105
  },
3973
- mergeRequestURL: {
4106
+ mergeRequestUrl: {
3974
4107
  title: "MergeRequest(MR) URL",
3975
4108
  type: "string",
3976
4109
  description: "Link to the merge request in GitLab"
@@ -4142,6 +4275,7 @@ const createBuiltinActions = (options) => {
4142
4275
  config
4143
4276
  }),
4144
4277
  createDebugLogAction(),
4278
+ createWaitAction(),
4145
4279
  createCatalogRegisterAction({ catalogClient, integrations }),
4146
4280
  createFetchCatalogEntityAction({ catalogClient }),
4147
4281
  createCatalogWriteAction(),
@@ -4308,7 +4442,7 @@ class DatabaseTaskStore {
4308
4442
  const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
4309
4443
  status: "processing",
4310
4444
  last_heartbeat_at: this.db.fn.now(),
4311
- // remove the secrets when moving moving to processing state.
4445
+ // remove the secrets when moving to processing state.
4312
4446
  secrets: null
4313
4447
  });
4314
4448
  if (updateCount < 1) {
@@ -4356,7 +4490,7 @@ class DatabaseTaskStore {
4356
4490
  async completeTask(options) {
4357
4491
  const { taskId, status, eventBody } = options;
4358
4492
  let oldStatus;
4359
- if (status === "failed" || status === "completed") {
4493
+ if (["failed", "completed", "cancelled"].includes(status)) {
4360
4494
  oldStatus = "processing";
4361
4495
  } else {
4362
4496
  throw new Error(
@@ -4367,6 +4501,30 @@ class DatabaseTaskStore {
4367
4501
  const [task] = await tx("tasks").where({
4368
4502
  id: taskId
4369
4503
  }).limit(1).select();
4504
+ const updateTask = async (criteria) => {
4505
+ const updateCount = await tx("tasks").where(criteria).update({
4506
+ status
4507
+ });
4508
+ if (updateCount !== 1) {
4509
+ throw new errors.ConflictError(
4510
+ `Failed to update status to '${status}' for taskId ${taskId}`
4511
+ );
4512
+ }
4513
+ await tx("task_events").insert({
4514
+ task_id: taskId,
4515
+ event_type: "completion",
4516
+ body: JSON.stringify(eventBody)
4517
+ });
4518
+ };
4519
+ if (status === "cancelled") {
4520
+ await updateTask({
4521
+ id: taskId
4522
+ });
4523
+ return;
4524
+ }
4525
+ if (task.status === "cancelled") {
4526
+ return;
4527
+ }
4370
4528
  if (!task) {
4371
4529
  throw new Error(`No task with taskId ${taskId} found`);
4372
4530
  }
@@ -4375,21 +4533,9 @@ class DatabaseTaskStore {
4375
4533
  `Refusing to update status of run '${taskId}' to status '${status}' as it is currently '${task.status}', expected '${oldStatus}'`
4376
4534
  );
4377
4535
  }
4378
- const updateCount = await tx("tasks").where({
4536
+ await updateTask({
4379
4537
  id: taskId,
4380
4538
  status: oldStatus
4381
- }).update({
4382
- status
4383
- });
4384
- if (updateCount !== 1) {
4385
- throw new errors.ConflictError(
4386
- `Failed to update status to '${status}' for taskId ${taskId}`
4387
- );
4388
- }
4389
- await tx("task_events").insert({
4390
- task_id: taskId,
4391
- event_type: "completion",
4392
- body: JSON.stringify(eventBody)
4393
4539
  });
4394
4540
  });
4395
4541
  }
@@ -4457,24 +4603,37 @@ class DatabaseTaskStore {
4457
4603
  }
4458
4604
  });
4459
4605
  }
4606
+ async cancelTask(options) {
4607
+ const { taskId, body } = options;
4608
+ const serializedBody = JSON.stringify(body);
4609
+ await this.db("task_events").insert({
4610
+ task_id: taskId,
4611
+ event_type: "cancelled",
4612
+ body: serializedBody
4613
+ });
4614
+ }
4460
4615
  }
4461
4616
 
4462
4617
  class TaskManager {
4463
4618
  // Runs heartbeat internally
4464
- constructor(task, storage, logger) {
4619
+ constructor(task, storage, signal, logger) {
4465
4620
  this.task = task;
4466
4621
  this.storage = storage;
4622
+ this.signal = signal;
4467
4623
  this.logger = logger;
4468
4624
  this.isDone = false;
4469
4625
  }
4470
- static create(task, storage, logger) {
4471
- const agent = new TaskManager(task, storage, logger);
4626
+ static create(task, storage, abortSignal, logger) {
4627
+ const agent = new TaskManager(task, storage, abortSignal, logger);
4472
4628
  agent.startTimeout();
4473
4629
  return agent;
4474
4630
  }
4475
4631
  get spec() {
4476
4632
  return this.task.spec;
4477
4633
  }
4634
+ get cancelSignal() {
4635
+ return this.signal;
4636
+ }
4478
4637
  get secrets() {
4479
4638
  return this.task.secrets;
4480
4639
  }
@@ -4544,6 +4703,28 @@ class StorageTaskBroker {
4544
4703
  }
4545
4704
  return await this.storage.list({ createdBy: options == null ? void 0 : options.createdBy });
4546
4705
  }
4706
+ async registerCancellable(taskId, abortController) {
4707
+ let shouldUnsubscribe = false;
4708
+ const subscription = this.event$({ taskId, after: void 0 }).subscribe({
4709
+ error: (_) => {
4710
+ subscription.unsubscribe();
4711
+ },
4712
+ next: ({ events }) => {
4713
+ for (const event of events) {
4714
+ if (event.type === "cancelled") {
4715
+ abortController.abort();
4716
+ shouldUnsubscribe = true;
4717
+ }
4718
+ if (event.type === "completion") {
4719
+ shouldUnsubscribe = true;
4720
+ }
4721
+ }
4722
+ if (shouldUnsubscribe) {
4723
+ subscription.unsubscribe();
4724
+ }
4725
+ }
4726
+ });
4727
+ }
4547
4728
  /**
4548
4729
  * {@inheritdoc TaskBroker.claim}
4549
4730
  */
@@ -4551,6 +4732,8 @@ class StorageTaskBroker {
4551
4732
  for (; ; ) {
4552
4733
  const pendingTask = await this.storage.claimTask();
4553
4734
  if (pendingTask) {
4735
+ const abortController = new AbortController();
4736
+ await this.registerCancellable(pendingTask.id, abortController);
4554
4737
  return TaskManager.create(
4555
4738
  {
4556
4739
  taskId: pendingTask.id,
@@ -4559,6 +4742,7 @@ class StorageTaskBroker {
4559
4742
  createdBy: pendingTask.createdBy
4560
4743
  },
4561
4744
  this.storage,
4745
+ abortController.signal,
4562
4746
  this.logger
4563
4747
  );
4564
4748
  }
@@ -4633,6 +4817,19 @@ class StorageTaskBroker {
4633
4817
  this.deferredDispatch.resolve();
4634
4818
  this.deferredDispatch = defer();
4635
4819
  }
4820
+ async cancel(taskId) {
4821
+ var _a, _b;
4822
+ const { events } = await this.storage.listEvents({ taskId });
4823
+ 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;
4824
+ await ((_b = (_a = this.storage).cancelTask) == null ? void 0 : _b.call(_a, {
4825
+ taskId,
4826
+ body: {
4827
+ message: `Step ${currentStepId} has been cancelled.`,
4828
+ stepId: currentStepId,
4829
+ status: "cancelled"
4830
+ }
4831
+ }));
4832
+ }
4636
4833
  }
4637
4834
 
4638
4835
  function isTruthy(value) {
@@ -4764,8 +4961,107 @@ class NunjucksWorkflowRunner {
4764
4961
  return value;
4765
4962
  });
4766
4963
  }
4767
- async execute(task) {
4964
+ async executeStep(task, step, context, renderTemplate, taskTrack, workspacePath) {
4768
4965
  var _a, _b, _c, _d, _e, _f, _g;
4966
+ const stepTrack = await this.tracker.stepStart(task, step);
4967
+ if (task.cancelSignal.aborted) {
4968
+ throw new Error(`Step ${step.name} has been cancelled.`);
4969
+ }
4970
+ try {
4971
+ if (step.if) {
4972
+ const ifResult = await this.render(step.if, context, renderTemplate);
4973
+ if (!isTruthy(ifResult)) {
4974
+ await stepTrack.skipFalsy();
4975
+ return;
4976
+ }
4977
+ }
4978
+ const action = this.options.actionRegistry.get(step.action);
4979
+ const { taskLogger, streamLogger } = createStepLogger({ task, step });
4980
+ if (task.isDryRun) {
4981
+ const redactedSecrets = Object.fromEntries(
4982
+ Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
4983
+ secret[0],
4984
+ "[REDACTED]"
4985
+ ])
4986
+ );
4987
+ const debugInput = (_b = step.input && this.render(
4988
+ step.input,
4989
+ {
4990
+ ...context,
4991
+ secrets: redactedSecrets
4992
+ },
4993
+ renderTemplate
4994
+ )) != null ? _b : {};
4995
+ taskLogger.info(
4996
+ `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
4997
+ debugInput,
4998
+ void 0,
4999
+ 2
5000
+ )}`
5001
+ );
5002
+ if (!action.supportsDryRun) {
5003
+ await taskTrack.skipDryRun(step, action);
5004
+ const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
5005
+ if (outputSchema) {
5006
+ context.steps[step.id] = {
5007
+ output: generateExampleOutput(outputSchema)
5008
+ };
5009
+ } else {
5010
+ context.steps[step.id] = { output: {} };
5011
+ }
5012
+ return;
5013
+ }
5014
+ }
5015
+ const input = (_e = step.input && this.render(
5016
+ step.input,
5017
+ { ...context, secrets: (_d = task.secrets) != null ? _d : {} },
5018
+ renderTemplate
5019
+ )) != null ? _e : {};
5020
+ if ((_f = action.schema) == null ? void 0 : _f.input) {
5021
+ const validateResult = jsonschema.validate(input, action.schema.input);
5022
+ if (!validateResult.valid) {
5023
+ const errors$1 = validateResult.errors.join(", ");
5024
+ throw new errors.InputError(
5025
+ `Invalid input passed to action ${action.id}, ${errors$1}`
5026
+ );
5027
+ }
5028
+ }
5029
+ const tmpDirs = new Array();
5030
+ const stepOutput = {};
5031
+ await action.handler({
5032
+ input,
5033
+ secrets: (_g = task.secrets) != null ? _g : {},
5034
+ logger: taskLogger,
5035
+ logStream: streamLogger,
5036
+ workspacePath,
5037
+ createTemporaryDirectory: async () => {
5038
+ const tmpDir = await fs__default["default"].mkdtemp(`${workspacePath}_step-${step.id}-`);
5039
+ tmpDirs.push(tmpDir);
5040
+ return tmpDir;
5041
+ },
5042
+ output(name, value) {
5043
+ stepOutput[name] = value;
5044
+ },
5045
+ templateInfo: task.spec.templateInfo,
5046
+ user: task.spec.user,
5047
+ isDryRun: task.isDryRun,
5048
+ signal: task.cancelSignal
5049
+ });
5050
+ for (const tmpDir of tmpDirs) {
5051
+ await fs__default["default"].remove(tmpDir);
5052
+ }
5053
+ context.steps[step.id] = { output: stepOutput };
5054
+ if (task.cancelSignal.aborted) {
5055
+ throw new Error(`Step ${step.name} has been cancelled.`);
5056
+ }
5057
+ await stepTrack.markSuccessful();
5058
+ } catch (err) {
5059
+ await taskTrack.markFailed(step, err);
5060
+ await stepTrack.markFailed();
5061
+ throw err;
5062
+ }
5063
+ }
5064
+ async execute(task) {
4769
5065
  if (!isValidTaskSpec(task.spec)) {
4770
5066
  throw new errors.InputError(
4771
5067
  "Wrong template version executed with the workflow engine"
@@ -4775,7 +5071,11 @@ class NunjucksWorkflowRunner {
4775
5071
  this.options.workingDirectory,
4776
5072
  await task.getWorkspaceName()
4777
5073
  );
4778
- const { integrations } = this.options;
5074
+ const {
5075
+ additionalTemplateFilters,
5076
+ additionalTemplateGlobals,
5077
+ integrations
5078
+ } = this.options;
4779
5079
  const renderTemplate = await SecureTemplater.loadRenderer({
4780
5080
  // TODO(blam): let's work out how we can deprecate this.
4781
5081
  // We shouldn't really need to be exposing these now we can deal with
@@ -4784,8 +5084,8 @@ class NunjucksWorkflowRunner {
4784
5084
  parseRepoUrl(url) {
4785
5085
  return parseRepoUrl(url, integrations);
4786
5086
  },
4787
- additionalTemplateFilters: this.options.additionalTemplateFilters,
4788
- additionalTemplateGlobals: this.options.additionalTemplateGlobals
5087
+ additionalTemplateFilters,
5088
+ additionalTemplateGlobals
4789
5089
  });
4790
5090
  try {
4791
5091
  const taskTrack = await this.tracker.taskStart(task);
@@ -4796,105 +5096,14 @@ class NunjucksWorkflowRunner {
4796
5096
  user: task.spec.user
4797
5097
  };
4798
5098
  for (const step of task.spec.steps) {
4799
- const stepTrack = await this.tracker.stepStart(task, step);
4800
- try {
4801
- if (step.if) {
4802
- const ifResult = await this.render(
4803
- step.if,
4804
- context,
4805
- renderTemplate
4806
- );
4807
- if (!isTruthy(ifResult)) {
4808
- await stepTrack.skipFalsy();
4809
- continue;
4810
- }
4811
- }
4812
- const action = this.options.actionRegistry.get(step.action);
4813
- const { taskLogger, streamLogger } = createStepLogger({ task, step });
4814
- if (task.isDryRun) {
4815
- const redactedSecrets = Object.fromEntries(
4816
- Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
4817
- secret[0],
4818
- "[REDACTED]"
4819
- ])
4820
- );
4821
- const debugInput = (_b = step.input && this.render(
4822
- step.input,
4823
- {
4824
- ...context,
4825
- secrets: redactedSecrets
4826
- },
4827
- renderTemplate
4828
- )) != null ? _b : {};
4829
- taskLogger.info(
4830
- `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
4831
- debugInput,
4832
- void 0,
4833
- 2
4834
- )}`
4835
- );
4836
- if (!action.supportsDryRun) {
4837
- await taskTrack.skipDryRun(step, action);
4838
- const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
4839
- if (outputSchema) {
4840
- context.steps[step.id] = {
4841
- output: generateExampleOutput(outputSchema)
4842
- };
4843
- } else {
4844
- context.steps[step.id] = { output: {} };
4845
- }
4846
- continue;
4847
- }
4848
- }
4849
- const input = (_e = step.input && this.render(
4850
- step.input,
4851
- { ...context, secrets: (_d = task.secrets) != null ? _d : {} },
4852
- renderTemplate
4853
- )) != null ? _e : {};
4854
- if ((_f = action.schema) == null ? void 0 : _f.input) {
4855
- const validateResult = jsonschema.validate(
4856
- input,
4857
- action.schema.input
4858
- );
4859
- if (!validateResult.valid) {
4860
- const errors$1 = validateResult.errors.join(", ");
4861
- throw new errors.InputError(
4862
- `Invalid input passed to action ${action.id}, ${errors$1}`
4863
- );
4864
- }
4865
- }
4866
- const tmpDirs = new Array();
4867
- const stepOutput = {};
4868
- await action.handler({
4869
- input,
4870
- secrets: (_g = task.secrets) != null ? _g : {},
4871
- logger: taskLogger,
4872
- logStream: streamLogger,
4873
- workspacePath,
4874
- createTemporaryDirectory: async () => {
4875
- const tmpDir = await fs__default["default"].mkdtemp(
4876
- `${workspacePath}_step-${step.id}-`
4877
- );
4878
- tmpDirs.push(tmpDir);
4879
- return tmpDir;
4880
- },
4881
- output(name, value) {
4882
- stepOutput[name] = value;
4883
- },
4884
- templateInfo: task.spec.templateInfo,
4885
- user: task.spec.user,
4886
- isDryRun: task.isDryRun
4887
- });
4888
- for (const tmpDir of tmpDirs) {
4889
- await fs__default["default"].remove(tmpDir);
4890
- }
4891
- context.steps[step.id] = { output: stepOutput };
4892
- await stepTrack.markSuccessful();
4893
- } catch (err) {
4894
- await taskTrack.markFailed(step, err);
4895
- await stepTrack.markFailed();
4896
- throw err;
4897
- }
5099
+ await this.executeStep(
5100
+ task,
5101
+ step,
5102
+ context,
5103
+ renderTemplate,
5104
+ taskTrack,
5105
+ workspacePath
5106
+ );
4898
5107
  }
4899
5108
  const output = this.render(task.spec.output, context, renderTemplate);
4900
5109
  await taskTrack.markSuccessful();
@@ -4961,8 +5170,21 @@ function scaffoldingTracker() {
4961
5170
  });
4962
5171
  taskTimer({ result: "failed" });
4963
5172
  }
5173
+ async function markCancelled(step) {
5174
+ await task.emitLog(`Step ${step.id} has been cancelled.`, {
5175
+ stepId: step.id,
5176
+ status: "cancelled"
5177
+ });
5178
+ taskCount.inc({
5179
+ template,
5180
+ user,
5181
+ result: "cancelled"
5182
+ });
5183
+ taskTimer({ result: "cancelled" });
5184
+ }
4964
5185
  return {
4965
5186
  skipDryRun,
5187
+ markCancelled,
4966
5188
  markSuccessful,
4967
5189
  markFailed
4968
5190
  };
@@ -4990,6 +5212,14 @@ function scaffoldingTracker() {
4990
5212
  });
4991
5213
  stepTimer({ result: "ok" });
4992
5214
  }
5215
+ async function markCancelled() {
5216
+ stepCount.inc({
5217
+ template,
5218
+ step: step.name,
5219
+ result: "cancelled"
5220
+ });
5221
+ stepTimer({ result: "cancelled" });
5222
+ }
4993
5223
  async function markFailed() {
4994
5224
  stepCount.inc({
4995
5225
  template,
@@ -5006,8 +5236,9 @@ function scaffoldingTracker() {
5006
5236
  stepTimer({ result: "skipped" });
5007
5237
  }
5008
5238
  return {
5009
- markSuccessful,
5239
+ markCancelled,
5010
5240
  markFailed,
5241
+ markSuccessful,
5011
5242
  skipFalsy
5012
5243
  };
5013
5244
  }
@@ -5131,6 +5362,7 @@ function createDryRunner(options) {
5131
5362
  );
5132
5363
  try {
5133
5364
  await deserializeDirectoryContents(contentsPath, input.directoryContents);
5365
+ const abortSignal = new AbortController().signal;
5134
5366
  const result = await workflowRunner.execute({
5135
5367
  spec: {
5136
5368
  ...input.spec,
@@ -5154,6 +5386,7 @@ function createDryRunner(options) {
5154
5386
  done: false,
5155
5387
  isDryRun: true,
5156
5388
  getWorkspaceName: async () => `dry-run-${dryRunId}`,
5389
+ cancelSignal: abortSignal,
5157
5390
  async emitLog(message, logMetadata) {
5158
5391
  if ((logMetadata == null ? void 0 : logMetadata.stepId) === dryRunId) {
5159
5392
  return;
@@ -5165,7 +5398,7 @@ function createDryRunner(options) {
5165
5398
  }
5166
5399
  });
5167
5400
  },
5168
- async complete() {
5401
+ complete: async () => {
5169
5402
  throw new Error("Not implemented");
5170
5403
  }
5171
5404
  });
@@ -5496,6 +5729,11 @@ async function createRouter(options) {
5496
5729
  }
5497
5730
  delete task.secrets;
5498
5731
  res.status(200).json(task);
5732
+ }).post("/v2/tasks/:taskId/cancel", async (req, res) => {
5733
+ var _a;
5734
+ const { taskId } = req.params;
5735
+ await ((_a = taskBroker.cancel) == null ? void 0 : _a.call(taskBroker, taskId));
5736
+ res.status(200).json({ status: "cancelled" });
5499
5737
  }).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
5500
5738
  const { taskId } = req.params;
5501
5739
  const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;