@backstage/plugin-scaffolder-backend 1.12.0 → 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: {
@@ -367,7 +367,7 @@ const examples$1 = [
367
367
  example: yaml__default["default"].stringify({
368
368
  steps: [
369
369
  {
370
- action: id$1,
370
+ action: id$2,
371
371
  id: "fetchMultiple",
372
372
  name: "Fetch catalog entities",
373
373
  input: {
@@ -381,9 +381,9 @@ const examples$1 = [
381
381
  function createFetchCatalogEntityAction(options) {
382
382
  const { catalogClient } = options;
383
383
  return pluginScaffolderNode.createTemplateAction({
384
- id: id$1,
384
+ id: id$2,
385
385
  description: "Returns entity or entities from the catalog by entity reference(s)",
386
- examples: examples$1,
386
+ examples: examples$2,
387
387
  schema: {
388
388
  input: {
389
389
  type: "object",
@@ -459,14 +459,14 @@ function createFetchCatalogEntityAction(options) {
459
459
  });
460
460
  }
461
461
 
462
- const id = "debug:log";
463
- const examples = [
462
+ const id$1 = "debug:log";
463
+ const examples$1 = [
464
464
  {
465
465
  description: "Write a debug message",
466
466
  example: yaml__default["default"].stringify({
467
467
  steps: [
468
468
  {
469
- action: id,
469
+ action: id$1,
470
470
  id: "write-debug-line",
471
471
  name: 'Write "Hello Backstage!" log line',
472
472
  input: {
@@ -481,7 +481,7 @@ const examples = [
481
481
  example: yaml__default["default"].stringify({
482
482
  steps: [
483
483
  {
484
- action: id,
484
+ action: id$1,
485
485
  id: "write-workspace-directory",
486
486
  name: "List the workspace directory",
487
487
  input: {
@@ -494,9 +494,9 @@ const examples = [
494
494
  ];
495
495
  function createDebugLogAction() {
496
496
  return pluginScaffolderNode.createTemplateAction({
497
- id,
497
+ id: id$1,
498
498
  description: "Writes a message into the log or lists all files in the workspace.",
499
- examples,
499
+ examples: examples$1,
500
500
  schema: {
501
501
  input: {
502
502
  type: "object",
@@ -543,6 +543,98 @@ async function recursiveReadDir(dir) {
543
543
  return files.reduce((a, f) => a.concat(f), []);
544
544
  }
545
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
+
546
638
  async function fetchContents(options) {
547
639
  const { reader, integrations, baseUrl, fetchUrl = ".", outputPath } = options;
548
640
  let fetchUrlIsAbsolute = false;
@@ -4011,7 +4103,7 @@ const createPublishGitlabMergeRequestAction = (options) => {
4011
4103
  title: "Gitlab Project path",
4012
4104
  type: "string"
4013
4105
  },
4014
- mergeRequestURL: {
4106
+ mergeRequestUrl: {
4015
4107
  title: "MergeRequest(MR) URL",
4016
4108
  type: "string",
4017
4109
  description: "Link to the merge request in GitLab"
@@ -4183,6 +4275,7 @@ const createBuiltinActions = (options) => {
4183
4275
  config
4184
4276
  }),
4185
4277
  createDebugLogAction(),
4278
+ createWaitAction(),
4186
4279
  createCatalogRegisterAction({ catalogClient, integrations }),
4187
4280
  createFetchCatalogEntityAction({ catalogClient }),
4188
4281
  createCatalogWriteAction(),
@@ -4349,7 +4442,7 @@ class DatabaseTaskStore {
4349
4442
  const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
4350
4443
  status: "processing",
4351
4444
  last_heartbeat_at: this.db.fn.now(),
4352
- // remove the secrets when moving moving to processing state.
4445
+ // remove the secrets when moving to processing state.
4353
4446
  secrets: null
4354
4447
  });
4355
4448
  if (updateCount < 1) {
@@ -4397,7 +4490,7 @@ class DatabaseTaskStore {
4397
4490
  async completeTask(options) {
4398
4491
  const { taskId, status, eventBody } = options;
4399
4492
  let oldStatus;
4400
- if (status === "failed" || status === "completed") {
4493
+ if (["failed", "completed", "cancelled"].includes(status)) {
4401
4494
  oldStatus = "processing";
4402
4495
  } else {
4403
4496
  throw new Error(
@@ -4408,6 +4501,30 @@ class DatabaseTaskStore {
4408
4501
  const [task] = await tx("tasks").where({
4409
4502
  id: taskId
4410
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
+ }
4411
4528
  if (!task) {
4412
4529
  throw new Error(`No task with taskId ${taskId} found`);
4413
4530
  }
@@ -4416,21 +4533,9 @@ class DatabaseTaskStore {
4416
4533
  `Refusing to update status of run '${taskId}' to status '${status}' as it is currently '${task.status}', expected '${oldStatus}'`
4417
4534
  );
4418
4535
  }
4419
- const updateCount = await tx("tasks").where({
4536
+ await updateTask({
4420
4537
  id: taskId,
4421
4538
  status: oldStatus
4422
- }).update({
4423
- status
4424
- });
4425
- if (updateCount !== 1) {
4426
- throw new errors.ConflictError(
4427
- `Failed to update status to '${status}' for taskId ${taskId}`
4428
- );
4429
- }
4430
- await tx("task_events").insert({
4431
- task_id: taskId,
4432
- event_type: "completion",
4433
- body: JSON.stringify(eventBody)
4434
4539
  });
4435
4540
  });
4436
4541
  }
@@ -4498,24 +4603,37 @@ class DatabaseTaskStore {
4498
4603
  }
4499
4604
  });
4500
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
+ }
4501
4615
  }
4502
4616
 
4503
4617
  class TaskManager {
4504
4618
  // Runs heartbeat internally
4505
- constructor(task, storage, logger) {
4619
+ constructor(task, storage, signal, logger) {
4506
4620
  this.task = task;
4507
4621
  this.storage = storage;
4622
+ this.signal = signal;
4508
4623
  this.logger = logger;
4509
4624
  this.isDone = false;
4510
4625
  }
4511
- static create(task, storage, logger) {
4512
- const agent = new TaskManager(task, storage, logger);
4626
+ static create(task, storage, abortSignal, logger) {
4627
+ const agent = new TaskManager(task, storage, abortSignal, logger);
4513
4628
  agent.startTimeout();
4514
4629
  return agent;
4515
4630
  }
4516
4631
  get spec() {
4517
4632
  return this.task.spec;
4518
4633
  }
4634
+ get cancelSignal() {
4635
+ return this.signal;
4636
+ }
4519
4637
  get secrets() {
4520
4638
  return this.task.secrets;
4521
4639
  }
@@ -4585,6 +4703,28 @@ class StorageTaskBroker {
4585
4703
  }
4586
4704
  return await this.storage.list({ createdBy: options == null ? void 0 : options.createdBy });
4587
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
+ }
4588
4728
  /**
4589
4729
  * {@inheritdoc TaskBroker.claim}
4590
4730
  */
@@ -4592,6 +4732,8 @@ class StorageTaskBroker {
4592
4732
  for (; ; ) {
4593
4733
  const pendingTask = await this.storage.claimTask();
4594
4734
  if (pendingTask) {
4735
+ const abortController = new AbortController();
4736
+ await this.registerCancellable(pendingTask.id, abortController);
4595
4737
  return TaskManager.create(
4596
4738
  {
4597
4739
  taskId: pendingTask.id,
@@ -4600,6 +4742,7 @@ class StorageTaskBroker {
4600
4742
  createdBy: pendingTask.createdBy
4601
4743
  },
4602
4744
  this.storage,
4745
+ abortController.signal,
4603
4746
  this.logger
4604
4747
  );
4605
4748
  }
@@ -4674,6 +4817,19 @@ class StorageTaskBroker {
4674
4817
  this.deferredDispatch.resolve();
4675
4818
  this.deferredDispatch = defer();
4676
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
+ }
4677
4833
  }
4678
4834
 
4679
4835
  function isTruthy(value) {
@@ -4805,8 +4961,107 @@ class NunjucksWorkflowRunner {
4805
4961
  return value;
4806
4962
  });
4807
4963
  }
4808
- async execute(task) {
4964
+ async executeStep(task, step, context, renderTemplate, taskTrack, workspacePath) {
4809
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) {
4810
5065
  if (!isValidTaskSpec(task.spec)) {
4811
5066
  throw new errors.InputError(
4812
5067
  "Wrong template version executed with the workflow engine"
@@ -4816,7 +5071,11 @@ class NunjucksWorkflowRunner {
4816
5071
  this.options.workingDirectory,
4817
5072
  await task.getWorkspaceName()
4818
5073
  );
4819
- const { integrations } = this.options;
5074
+ const {
5075
+ additionalTemplateFilters,
5076
+ additionalTemplateGlobals,
5077
+ integrations
5078
+ } = this.options;
4820
5079
  const renderTemplate = await SecureTemplater.loadRenderer({
4821
5080
  // TODO(blam): let's work out how we can deprecate this.
4822
5081
  // We shouldn't really need to be exposing these now we can deal with
@@ -4825,8 +5084,8 @@ class NunjucksWorkflowRunner {
4825
5084
  parseRepoUrl(url) {
4826
5085
  return parseRepoUrl(url, integrations);
4827
5086
  },
4828
- additionalTemplateFilters: this.options.additionalTemplateFilters,
4829
- additionalTemplateGlobals: this.options.additionalTemplateGlobals
5087
+ additionalTemplateFilters,
5088
+ additionalTemplateGlobals
4830
5089
  });
4831
5090
  try {
4832
5091
  const taskTrack = await this.tracker.taskStart(task);
@@ -4837,105 +5096,14 @@ class NunjucksWorkflowRunner {
4837
5096
  user: task.spec.user
4838
5097
  };
4839
5098
  for (const step of task.spec.steps) {
4840
- const stepTrack = await this.tracker.stepStart(task, step);
4841
- try {
4842
- if (step.if) {
4843
- const ifResult = await this.render(
4844
- step.if,
4845
- context,
4846
- renderTemplate
4847
- );
4848
- if (!isTruthy(ifResult)) {
4849
- await stepTrack.skipFalsy();
4850
- continue;
4851
- }
4852
- }
4853
- const action = this.options.actionRegistry.get(step.action);
4854
- const { taskLogger, streamLogger } = createStepLogger({ task, step });
4855
- if (task.isDryRun) {
4856
- const redactedSecrets = Object.fromEntries(
4857
- Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
4858
- secret[0],
4859
- "[REDACTED]"
4860
- ])
4861
- );
4862
- const debugInput = (_b = step.input && this.render(
4863
- step.input,
4864
- {
4865
- ...context,
4866
- secrets: redactedSecrets
4867
- },
4868
- renderTemplate
4869
- )) != null ? _b : {};
4870
- taskLogger.info(
4871
- `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
4872
- debugInput,
4873
- void 0,
4874
- 2
4875
- )}`
4876
- );
4877
- if (!action.supportsDryRun) {
4878
- await taskTrack.skipDryRun(step, action);
4879
- const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
4880
- if (outputSchema) {
4881
- context.steps[step.id] = {
4882
- output: generateExampleOutput(outputSchema)
4883
- };
4884
- } else {
4885
- context.steps[step.id] = { output: {} };
4886
- }
4887
- continue;
4888
- }
4889
- }
4890
- const input = (_e = step.input && this.render(
4891
- step.input,
4892
- { ...context, secrets: (_d = task.secrets) != null ? _d : {} },
4893
- renderTemplate
4894
- )) != null ? _e : {};
4895
- if ((_f = action.schema) == null ? void 0 : _f.input) {
4896
- const validateResult = jsonschema.validate(
4897
- input,
4898
- action.schema.input
4899
- );
4900
- if (!validateResult.valid) {
4901
- const errors$1 = validateResult.errors.join(", ");
4902
- throw new errors.InputError(
4903
- `Invalid input passed to action ${action.id}, ${errors$1}`
4904
- );
4905
- }
4906
- }
4907
- const tmpDirs = new Array();
4908
- const stepOutput = {};
4909
- await action.handler({
4910
- input,
4911
- secrets: (_g = task.secrets) != null ? _g : {},
4912
- logger: taskLogger,
4913
- logStream: streamLogger,
4914
- workspacePath,
4915
- createTemporaryDirectory: async () => {
4916
- const tmpDir = await fs__default["default"].mkdtemp(
4917
- `${workspacePath}_step-${step.id}-`
4918
- );
4919
- tmpDirs.push(tmpDir);
4920
- return tmpDir;
4921
- },
4922
- output(name, value) {
4923
- stepOutput[name] = value;
4924
- },
4925
- templateInfo: task.spec.templateInfo,
4926
- user: task.spec.user,
4927
- isDryRun: task.isDryRun
4928
- });
4929
- for (const tmpDir of tmpDirs) {
4930
- await fs__default["default"].remove(tmpDir);
4931
- }
4932
- context.steps[step.id] = { output: stepOutput };
4933
- await stepTrack.markSuccessful();
4934
- } catch (err) {
4935
- await taskTrack.markFailed(step, err);
4936
- await stepTrack.markFailed();
4937
- throw err;
4938
- }
5099
+ await this.executeStep(
5100
+ task,
5101
+ step,
5102
+ context,
5103
+ renderTemplate,
5104
+ taskTrack,
5105
+ workspacePath
5106
+ );
4939
5107
  }
4940
5108
  const output = this.render(task.spec.output, context, renderTemplate);
4941
5109
  await taskTrack.markSuccessful();
@@ -5002,8 +5170,21 @@ function scaffoldingTracker() {
5002
5170
  });
5003
5171
  taskTimer({ result: "failed" });
5004
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
+ }
5005
5185
  return {
5006
5186
  skipDryRun,
5187
+ markCancelled,
5007
5188
  markSuccessful,
5008
5189
  markFailed
5009
5190
  };
@@ -5031,6 +5212,14 @@ function scaffoldingTracker() {
5031
5212
  });
5032
5213
  stepTimer({ result: "ok" });
5033
5214
  }
5215
+ async function markCancelled() {
5216
+ stepCount.inc({
5217
+ template,
5218
+ step: step.name,
5219
+ result: "cancelled"
5220
+ });
5221
+ stepTimer({ result: "cancelled" });
5222
+ }
5034
5223
  async function markFailed() {
5035
5224
  stepCount.inc({
5036
5225
  template,
@@ -5047,8 +5236,9 @@ function scaffoldingTracker() {
5047
5236
  stepTimer({ result: "skipped" });
5048
5237
  }
5049
5238
  return {
5050
- markSuccessful,
5239
+ markCancelled,
5051
5240
  markFailed,
5241
+ markSuccessful,
5052
5242
  skipFalsy
5053
5243
  };
5054
5244
  }
@@ -5172,6 +5362,7 @@ function createDryRunner(options) {
5172
5362
  );
5173
5363
  try {
5174
5364
  await deserializeDirectoryContents(contentsPath, input.directoryContents);
5365
+ const abortSignal = new AbortController().signal;
5175
5366
  const result = await workflowRunner.execute({
5176
5367
  spec: {
5177
5368
  ...input.spec,
@@ -5195,6 +5386,7 @@ function createDryRunner(options) {
5195
5386
  done: false,
5196
5387
  isDryRun: true,
5197
5388
  getWorkspaceName: async () => `dry-run-${dryRunId}`,
5389
+ cancelSignal: abortSignal,
5198
5390
  async emitLog(message, logMetadata) {
5199
5391
  if ((logMetadata == null ? void 0 : logMetadata.stepId) === dryRunId) {
5200
5392
  return;
@@ -5206,7 +5398,7 @@ function createDryRunner(options) {
5206
5398
  }
5207
5399
  });
5208
5400
  },
5209
- async complete() {
5401
+ complete: async () => {
5210
5402
  throw new Error("Not implemented");
5211
5403
  }
5212
5404
  });
@@ -5537,6 +5729,11 @@ async function createRouter(options) {
5537
5729
  }
5538
5730
  delete task.secrets;
5539
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" });
5540
5737
  }).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
5541
5738
  const { taskId } = req.params;
5542
5739
  const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;