@backstage/plugin-scaffolder-backend 4.0.3-next.1 → 4.0.3

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.
Files changed (25) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/ScaffolderPlugin.cjs.js +1 -1
  3. package/dist/scaffolder/actions/builtin/fetch/template.cjs.js +3 -1
  4. package/dist/scaffolder/actions/builtin/fetch/template.cjs.js.map +1 -1
  5. package/dist/scaffolder/actions/builtin/fetch/templateActionHandler.cjs.js +70 -68
  6. package/dist/scaffolder/actions/builtin/fetch/templateActionHandler.cjs.js.map +1 -1
  7. package/dist/scaffolder/actions/builtin/fetch/templateFile.cjs.js +4 -1
  8. package/dist/scaffolder/actions/builtin/fetch/templateFile.cjs.js.map +1 -1
  9. package/dist/scaffolder/actions/builtin/fetch/templateFileActionHandler.cjs.js +14 -30
  10. package/dist/scaffolder/actions/builtin/fetch/templateFileActionHandler.cjs.js.map +1 -1
  11. package/dist/scaffolder/dryrun/createDryRunner.cjs.js +6 -0
  12. package/dist/scaffolder/dryrun/createDryRunner.cjs.js.map +1 -1
  13. package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js +103 -109
  14. package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js.map +1 -1
  15. package/dist/scaffolder/tasks/TaskWorker.cjs.js +5 -2
  16. package/dist/scaffolder/tasks/TaskWorker.cjs.js.map +1 -1
  17. package/dist/service/router.cjs.js +1 -1
  18. package/dist/util/templating.cjs.js +12 -0
  19. package/dist/util/templating.cjs.js.map +1 -1
  20. package/package.json +21 -26
  21. package/assets/nunjucks.js.txt +0 -10385
  22. package/dist/lib/templating/SecureTemplater.cjs.js +0 -188
  23. package/dist/lib/templating/SecureTemplater.cjs.js.map +0 -1
  24. package/dist/lib/templating/helpers.cjs.js +0 -13
  25. package/dist/lib/templating/helpers.cjs.js.map +0 -1
@@ -4,9 +4,8 @@ var errors = require('@backstage/errors');
4
4
  var fs = require('fs-extra');
5
5
  var jsonschema = require('jsonschema');
6
6
  var path = require('node:path');
7
- var nunjucks = require('nunjucks');
7
+ var nunjitsu = require('nunjitsu');
8
8
  var winston = require('winston');
9
- var SecureTemplater = require('../../lib/templating/SecureTemplater.cjs.js');
10
9
  var helper = require('./helper.cjs.js');
11
10
  var pluginPermissionCommon = require('@backstage/plugin-permission-common');
12
11
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
@@ -40,12 +39,22 @@ function _interopNamespaceCompat(e) {
40
39
 
41
40
  var fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
42
41
  var path__default = /*#__PURE__*/_interopDefaultCompat(path);
43
- var nunjucks__default = /*#__PURE__*/_interopDefaultCompat(nunjucks);
44
42
  var winston__namespace = /*#__PURE__*/_interopNamespaceCompat(winston);
45
43
 
46
44
  const isValidTaskSpec = (taskSpec) => {
47
45
  return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
48
46
  };
47
+ function stringifyEachLogValue(value) {
48
+ if (Array.isArray(value)) {
49
+ return value.map(
50
+ (item) => item === null || item === void 0 ? "" : stringifyEachLogValue(item)
51
+ ).join(",");
52
+ }
53
+ if (value !== null && typeof value === "object") {
54
+ return "[object Object]";
55
+ }
56
+ return String(value);
57
+ }
49
58
  const createStepLogger = ({
50
59
  task,
51
60
  step,
@@ -96,17 +105,19 @@ const isActionAuthorized = pluginPermissionNode.createConditionAuthorizer(
96
105
  Object.values(rules.scaffolderActionRules)
97
106
  );
98
107
  class NunjucksWorkflowRunner {
99
- defaultTemplateFilters;
108
+ defaultTemplateCapabilities;
100
109
  options;
101
110
  environment = { parameters: {}, secrets: {} };
102
111
  tracker;
103
112
  constructor(options) {
104
113
  this.options = options;
105
- this.defaultTemplateFilters = templating.convertFiltersToRecord(
106
- createDefaultFilters.createDefaultFilters({
107
- integrations: this.options.integrations
108
- })
109
- );
114
+ this.defaultTemplateCapabilities = templating.collectTemplateCapabilities({
115
+ filters: templating.convertFiltersToRecord(
116
+ createDefaultFilters.createDefaultFilters({
117
+ integrations: this.options.integrations
118
+ })
119
+ )
120
+ });
110
121
  this.tracker = scaffoldingTracker(options.metrics);
111
122
  }
112
123
  async getEnvironmentConfig() {
@@ -122,47 +133,11 @@ class NunjucksWorkflowRunner {
122
133
  secrets: {}
123
134
  };
124
135
  }
125
- isSingleTemplateString(input) {
126
- const { parser, nodes } = nunjucks__default.default;
127
- const parsed = parser.parse(
128
- input,
129
- {},
130
- {
131
- autoescape: false,
132
- tags: {
133
- variableStart: "${{",
134
- variableEnd: "}}"
135
- }
136
- }
137
- );
138
- return parsed.children.length === 1 && !(parsed.children[0]?.children?.[0] instanceof nodes.TemplateData);
139
- }
140
- render(input, context, renderTemplate) {
136
+ render(input, context, templateRenderer) {
141
137
  return JSON.parse(JSON.stringify(input), (_key, value) => {
142
138
  try {
143
139
  if (typeof value === "string") {
144
- try {
145
- if (this.isSingleTemplateString(value)) {
146
- const wrappedDumped = value.replace(
147
- /\${{(.+)}}/g,
148
- "${{ ( $1 ) | dump }}"
149
- );
150
- const templated2 = renderTemplate(wrappedDumped, context);
151
- if (templated2 === "") {
152
- return void 0;
153
- }
154
- return JSON.parse(templated2);
155
- }
156
- } catch (ex) {
157
- this.options.logger.error(
158
- `Failed to parse template string: ${value} with error ${ex.message}`
159
- );
160
- }
161
- const templated = renderTemplate(value, context);
162
- if (templated === "") {
163
- return void 0;
164
- }
165
- return templated;
140
+ return templateRenderer.renderValue(value, context);
166
141
  }
167
142
  } catch {
168
143
  return value;
@@ -170,7 +145,7 @@ class NunjucksWorkflowRunner {
170
145
  return value;
171
146
  });
172
147
  }
173
- async executeStep(task, step, context, renderTemplate, taskTrack, workspacePath, decision) {
148
+ async executeStep(task, step, context, preparedContext, templateRenderer, taskTrack, workspacePath, decision) {
174
149
  const stepTrack = await this.tracker.stepStart(task, step);
175
150
  if (task.cancelSignal.aborted) {
176
151
  throw new Error(
@@ -178,9 +153,9 @@ class NunjucksWorkflowRunner {
178
153
  );
179
154
  }
180
155
  try {
181
- if (step.if === false || typeof step.if === "string" && step.each === void 0 && !helper.isTruthy(this.render(step.if, context, renderTemplate))) {
156
+ if (step.if === false || typeof step.if === "string" && step.each === void 0 && !helper.isTruthy(this.render(step.if, preparedContext, templateRenderer))) {
182
157
  await stepTrack.skipFalsy();
183
- return;
158
+ return preparedContext;
184
159
  }
185
160
  const action = await this.options.actionRegistry.get(step.action, {
186
161
  credentials: await task.getInitiatorCredentials()
@@ -204,18 +179,11 @@ class NunjucksWorkflowRunner {
204
179
  "***"
205
180
  ])
206
181
  );
207
- const debugInput = (step.input && this.render(
208
- step.input,
209
- {
210
- ...context,
211
- environment: {
212
- parameters: this.environment?.parameters || {},
213
- secrets: redactedEnvironmentSecrets
214
- },
215
- secrets: redactedSecrets
216
- },
217
- renderTemplate
218
- )) ?? {};
182
+ const dryRunContext = preparedContext.withValue(["environment"], {
183
+ parameters: this.environment?.parameters || {},
184
+ secrets: redactedEnvironmentSecrets
185
+ }).withValue(["secrets"], redactedSecrets);
186
+ const debugInput = (step.input && this.render(step.input, dryRunContext, templateRenderer)) ?? {};
219
187
  taskLogger.info(
220
188
  `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
221
189
  debugInput,
@@ -233,7 +201,10 @@ class NunjucksWorkflowRunner {
233
201
  } else {
234
202
  context.steps[step.id] = { output: {} };
235
203
  }
236
- return;
204
+ return preparedContext.withValue(
205
+ ["steps", step.id],
206
+ context.steps[step.id]
207
+ );
237
208
  }
238
209
  }
239
210
  const preIterationContext = {
@@ -244,7 +215,14 @@ class NunjucksWorkflowRunner {
244
215
  },
245
216
  secrets: task.secrets ?? {}
246
217
  };
247
- const resolvedEach = step.each && this.render(step.each, preIterationContext, renderTemplate);
218
+ const preparedPreIterationContext = preparedContext.withValue(
219
+ ["environment"],
220
+ preIterationContext.environment
221
+ ).withValue(
222
+ ["secrets"],
223
+ preIterationContext.secrets
224
+ );
225
+ const resolvedEach = step.each && this.render(step.each, preparedPreIterationContext, templateRenderer);
248
226
  if (step.each && !resolvedEach) {
249
227
  throw new errors.InputError(
250
228
  `Invalid value on action ${action.id}.each parameter, "${step.each}" cannot be resolved to a value`
@@ -253,13 +231,17 @@ class NunjucksWorkflowRunner {
253
231
  const iterations = (resolvedEach ? Object.entries(resolvedEach).map(([key, value]) => ({
254
232
  each: { key, value }
255
233
  })) : [{}]).map((i) => {
256
- const fullContext = { ...preIterationContext, ...i };
257
- const shouldRun = !("each" in i) || !step.if || helper.isTruthy(this.render(step.if, fullContext, renderTemplate));
234
+ const preparedFullContext = "each" in i ? preparedPreIterationContext.withValue(
235
+ ["each"],
236
+ i.each
237
+ ) : preparedPreIterationContext;
238
+ const shouldRun = !("each" in i) || !step.if || helper.isTruthy(this.render(step.if, preparedFullContext, templateRenderer));
258
239
  return {
259
240
  ...i,
241
+ preparedContext: preparedFullContext,
260
242
  shouldRun,
261
243
  // Secrets are only passed when templating the input to actions for security reasons
262
- input: step.input ? this.render(step.input, fullContext, renderTemplate) : {}
244
+ input: step.input ? this.render(step.input, preparedFullContext, templateRenderer) : {}
263
245
  };
264
246
  });
265
247
  for (const iteration of iterations) {
@@ -301,7 +283,7 @@ class NunjucksWorkflowRunner {
301
283
  taskLogger.info(
302
284
  `Skipping step each: ${JSON.stringify(
303
285
  iteration.each,
304
- (k, v) => k ? String(v) : v,
286
+ (key, value) => key ? stringifyEachLogValue(value) : value,
305
287
  0
306
288
  )}`
307
289
  );
@@ -310,7 +292,7 @@ class NunjucksWorkflowRunner {
310
292
  taskLogger.info(
311
293
  `Running step each: ${JSON.stringify(
312
294
  iteration.each,
313
- (k, v) => k ? String(v) : v,
295
+ (key, value) => key ? stringifyEachLogValue(value) : value,
314
296
  0
315
297
  )}`
316
298
  );
@@ -319,19 +301,11 @@ class NunjucksWorkflowRunner {
319
301
  const hasSecrets = Object.keys(task.secrets ?? {}).length > 0 || Object.keys(this.environment?.secrets ?? {}).length > 0;
320
302
  if (hasSecrets) {
321
303
  try {
322
- const contextNoSecrets = {
323
- ...preIterationContext,
324
- ...iteration.each ? { each: iteration.each } : {},
325
- secrets: {},
326
- environment: {
327
- ...preIterationContext.environment,
328
- secrets: {}
329
- }
330
- };
304
+ const preparedContextNoSecrets = iteration.preparedContext.withValue(["secrets"], {}).withValue(["environment", "secrets"], {});
331
305
  const inputWithoutSecrets = this.render(
332
306
  step.input,
333
- contextNoSecrets,
334
- renderTemplate
307
+ preparedContextNoSecrets,
308
+ templateRenderer
335
309
  );
336
310
  taskLogger.addRedactions(
337
311
  collectSecretRedactions(iteration.input, inputWithoutSecrets)
@@ -354,28 +328,38 @@ class NunjucksWorkflowRunner {
354
328
  const key = `v1.task.checkpoint.${step.id}.${checkpointKey}`;
355
329
  try {
356
330
  let prevValue;
357
- if (prevTaskState) {
358
- const prevState = prevTaskState.state?.checkpoints?.[key];
359
- if (prevState && prevState.status === "success") {
360
- prevValue = prevState.value;
331
+ let value;
332
+ try {
333
+ if (prevTaskState) {
334
+ const prevState = prevTaskState.state?.checkpoints?.[key];
335
+ if (prevState && prevState.status === "success") {
336
+ prevValue = prevState.value;
337
+ }
361
338
  }
339
+ value = prevValue !== void 0 ? prevValue : await fn();
340
+ } catch (err) {
341
+ try {
342
+ await task.updateCheckpoint?.({
343
+ key,
344
+ status: "failed",
345
+ reason: errors.stringifyError(err)
346
+ });
347
+ } catch (persistenceError) {
348
+ throw new AggregateError(
349
+ [err, persistenceError],
350
+ `Checkpoint '${checkpointKey}' failed and its failure state could not be persisted`
351
+ );
352
+ }
353
+ throw err;
362
354
  }
363
- const value = prevValue ? prevValue : await fn();
364
- if (!prevValue) {
365
- task.updateCheckpoint?.({
355
+ if (prevValue === void 0) {
356
+ await task.updateCheckpoint?.({
366
357
  key,
367
358
  status: "success",
368
359
  value: value ?? {}
369
360
  });
370
361
  }
371
362
  return value;
372
- } catch (err) {
373
- task.updateCheckpoint?.({
374
- key,
375
- status: "failed",
376
- reason: errors.stringifyError(err)
377
- });
378
- throw err;
379
363
  } finally {
380
364
  await task.serializeWorkspace?.({ path: workspacePath });
381
365
  }
@@ -410,12 +394,17 @@ class NunjucksWorkflowRunner {
410
394
  await fs__default.default.remove(tmpDir);
411
395
  }
412
396
  context.steps[step.id] = { output: stepOutput };
397
+ const updatedPreparedContext = preparedContext.withValue(
398
+ ["steps", step.id],
399
+ context.steps[step.id]
400
+ );
413
401
  if (task.cancelSignal.aborted) {
414
402
  throw new Error(
415
403
  `Step ${step.id} (${step.name}) of task ${task.taskId} has been cancelled.`
416
404
  );
417
405
  }
418
406
  await stepTrack.markSuccessful();
407
+ return updatedPreparedContext;
419
408
  } catch (err) {
420
409
  await taskTrack.markFailed(step, err);
421
410
  await stepTrack.markFailed();
@@ -432,17 +421,18 @@ class NunjucksWorkflowRunner {
432
421
  }
433
422
  const taskId = await task.getWorkspaceName();
434
423
  const workspacePath = path__default.default.join(this.options.workingDirectory, taskId);
435
- const { additionalTemplateFilters, additionalTemplateGlobals } = this.options;
424
+ const { templateCapabilities } = this.options;
436
425
  this.environment = await this.getEnvironmentConfig();
437
426
  const taskState = { failed: false };
438
427
  const statusCheckInvoked = { value: false };
439
- const { render: renderTemplate, dispose } = await SecureTemplater.SecureTemplater.loadRenderer({
440
- templateFilters: {
441
- ...this.defaultTemplateFilters,
442
- ...additionalTemplateFilters
428
+ const templateRenderer = nunjitsu.createTemplateRenderer({
429
+ allowRegexExecution: true,
430
+ filters: {
431
+ ...this.defaultTemplateCapabilities.filters,
432
+ ...templateCapabilities?.filters
443
433
  },
444
- templateGlobals: {
445
- ...additionalTemplateGlobals,
434
+ globals: {
435
+ ...templateCapabilities?.globals,
446
436
  always: () => {
447
437
  statusCheckInvoked.value = true;
448
438
  return true;
@@ -471,6 +461,9 @@ class NunjucksWorkflowRunner {
471
461
  }
472
462
  }
473
463
  };
464
+ let preparedContext = templateRenderer.prepareContext(
465
+ context
466
+ );
474
467
  const [decision] = this.options.permissions && task.spec.steps.length ? await this.options.permissions.authorizeConditional(
475
468
  [{ permission: alpha.actionExecutePermission }],
476
469
  { credentials: await task.getInitiatorCredentials() }
@@ -487,7 +480,7 @@ class NunjucksWorkflowRunner {
487
480
  continue;
488
481
  }
489
482
  statusCheckInvoked.value = false;
490
- this.render(step.if, context, renderTemplate);
483
+ this.render(step.if, preparedContext, templateRenderer);
491
484
  if (!statusCheckInvoked.value) {
492
485
  await task.emitLog(
493
486
  `Skipping step ${step.id} because a previous step failed`,
@@ -497,11 +490,12 @@ class NunjucksWorkflowRunner {
497
490
  }
498
491
  }
499
492
  try {
500
- await this.executeStep(
493
+ preparedContext = await this.executeStep(
501
494
  task,
502
495
  step,
503
496
  context,
504
- renderTemplate,
497
+ preparedContext,
498
+ templateRenderer,
505
499
  taskTrack,
506
500
  workspacePath,
507
501
  decision
@@ -534,7 +528,11 @@ ${error.stack}`,
534
528
  }
535
529
  throw firstError;
536
530
  }
537
- const output = this.render(task.spec.output, context, renderTemplate);
531
+ const output = this.render(
532
+ task.spec.output,
533
+ preparedContext,
534
+ templateRenderer
535
+ );
538
536
  if (Array.isArray(output?.links)) {
539
537
  output.links = helper.filterConditionalItems(output.links);
540
538
  }
@@ -545,10 +543,6 @@ ${error.stack}`,
545
543
  await task.cleanWorkspace?.();
546
544
  return { output };
547
545
  } finally {
548
- try {
549
- dispose();
550
- } catch {
551
- }
552
546
  if (workspacePath) {
553
547
  await fs__default.default.remove(workspacePath);
554
548
  }