@backstage/plugin-scaffolder-backend 1.6.0-next.3 → 1.7.0-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
@@ -403,6 +403,16 @@ const { render, renderCompat } = (() => {
403
403
  }
404
404
  }
405
405
 
406
+ if (typeof additionalTemplateGlobals !== 'undefined') {
407
+ for (const [globalName, global] of Object.entries(additionalTemplateGlobals)) {
408
+ if (typeof global === 'function') {
409
+ env.addGlobal(globalName, (...args) => JSON.parse(global(...args)));
410
+ } else {
411
+ env.addGlobal(globalName, JSON.parse(global));
412
+ }
413
+ }
414
+ }
415
+
406
416
  let uninstallCompat = undefined;
407
417
 
408
418
  function render(str, values) {
@@ -435,7 +445,12 @@ const { render, renderCompat } = (() => {
435
445
  `;
436
446
  class SecureTemplater {
437
447
  static async loadRenderer(options = {}) {
438
- const { parseRepoUrl, cookiecutterCompat, additionalTemplateFilters } = options;
448
+ const {
449
+ parseRepoUrl,
450
+ cookiecutterCompat,
451
+ additionalTemplateFilters,
452
+ additionalTemplateGlobals
453
+ } = options;
439
454
  const sandbox = {};
440
455
  if (parseRepoUrl) {
441
456
  sandbox.parseRepoUrl = (url) => JSON.stringify(parseRepoUrl(url));
@@ -448,6 +463,19 @@ class SecureTemplater {
448
463
  ])
449
464
  );
450
465
  }
466
+ if (additionalTemplateGlobals) {
467
+ sandbox.additionalTemplateGlobals = Object.fromEntries(
468
+ Object.entries(additionalTemplateGlobals).filter(([_, global]) => !!global).map(([globalName, global]) => {
469
+ if (typeof global === "function") {
470
+ return [
471
+ globalName,
472
+ (...args) => JSON.stringify(global(...args))
473
+ ];
474
+ }
475
+ return [globalName, JSON.stringify(global)];
476
+ })
477
+ );
478
+ }
451
479
  const vm = new vm2.VM({ sandbox });
452
480
  const nunjucksSource = await fs__default["default"].readFile(
453
481
  backendCommon.resolvePackagePath(
@@ -473,7 +501,12 @@ class SecureTemplater {
473
501
  }
474
502
 
475
503
  function createFetchTemplateAction(options) {
476
- const { reader, integrations, additionalTemplateFilters } = options;
504
+ const {
505
+ reader,
506
+ integrations,
507
+ additionalTemplateFilters,
508
+ additionalTemplateGlobals
509
+ } = options;
477
510
  return createTemplateAction({
478
511
  id: "fetch:template",
479
512
  description: "Downloads a skeleton, templates variables into file and directory names and content, and places the result in the workspace, or optionally in a subdirectory specified by the 'targetPath' input option.",
@@ -606,7 +639,8 @@ function createFetchTemplateAction(options) {
606
639
  );
607
640
  const renderTemplate = await SecureTemplater.loadRenderer({
608
641
  cookiecutterCompat: ctx.input.cookiecutterCompat,
609
- additionalTemplateFilters
642
+ additionalTemplateFilters,
643
+ additionalTemplateGlobals
610
644
  });
611
645
  for (const location of allEntriesInTemplate) {
612
646
  let renderContents;
@@ -1039,7 +1073,7 @@ async function getOctokitOptions(options) {
1039
1073
  previews: ["nebula-preview"]
1040
1074
  };
1041
1075
  }
1042
- async function createGithubRepoWithCollaboratorsAndTopics(client, repo, owner, repoVisibility, description, homepage, deleteBranchOnMerge, allowMergeCommit, allowSquashMerge, allowRebaseMerge, access, collaborators, topics, logger) {
1076
+ async function createGithubRepoWithCollaboratorsAndTopics(client, repo, owner, repoVisibility, description, homepage, deleteBranchOnMerge, allowMergeCommit, allowSquashMerge, allowRebaseMerge, allowAutoMerge, access, collaborators, topics, logger) {
1043
1077
  const user = await client.rest.users.getByUsername({
1044
1078
  username: owner
1045
1079
  });
@@ -1053,6 +1087,7 @@ async function createGithubRepoWithCollaboratorsAndTopics(client, repo, owner, r
1053
1087
  allow_merge_commit: allowMergeCommit,
1054
1088
  allow_squash_merge: allowSquashMerge,
1055
1089
  allow_rebase_merge: allowRebaseMerge,
1090
+ allow_auto_merge: allowAutoMerge,
1056
1091
  homepage
1057
1092
  }) : client.rest.repos.createForAuthenticatedUser({
1058
1093
  name: repo,
@@ -1062,6 +1097,7 @@ async function createGithubRepoWithCollaboratorsAndTopics(client, repo, owner, r
1062
1097
  allow_merge_commit: allowMergeCommit,
1063
1098
  allow_squash_merge: allowSquashMerge,
1064
1099
  allow_rebase_merge: allowRebaseMerge,
1100
+ allow_auto_merge: allowAutoMerge,
1065
1101
  homepage
1066
1102
  });
1067
1103
  let newRepo;
@@ -1390,6 +1426,11 @@ const allowRebaseMerge = {
1390
1426
  type: "boolean",
1391
1427
  description: `Allow rebase merges. The default value is 'true'`
1392
1428
  };
1429
+ const allowAutoMerge = {
1430
+ title: "Allow Auto Merges",
1431
+ type: "boolean",
1432
+ description: `Allow individual PRs to merge automatically when all merge requirements are met. The default value is 'false'`
1433
+ };
1393
1434
  const collaborators = {
1394
1435
  title: "Collaborators",
1395
1436
  description: "Provide additional users or teams with permissions",
@@ -1484,6 +1525,7 @@ function createGithubRepoCreateAction(options) {
1484
1525
  allowMergeCommit: allowMergeCommit,
1485
1526
  allowSquashMerge: allowSquashMerge,
1486
1527
  allowRebaseMerge: allowRebaseMerge,
1528
+ allowAutoMerge: allowAutoMerge,
1487
1529
  collaborators: collaborators,
1488
1530
  token: token,
1489
1531
  topics: topics
@@ -1508,6 +1550,7 @@ function createGithubRepoCreateAction(options) {
1508
1550
  allowMergeCommit = true,
1509
1551
  allowSquashMerge = true,
1510
1552
  allowRebaseMerge = true,
1553
+ allowAutoMerge = false,
1511
1554
  collaborators,
1512
1555
  topics,
1513
1556
  token: providedToken
@@ -1534,6 +1577,7 @@ function createGithubRepoCreateAction(options) {
1534
1577
  allowMergeCommit,
1535
1578
  allowSquashMerge,
1536
1579
  allowRebaseMerge,
1580
+ allowAutoMerge,
1537
1581
  access,
1538
1582
  collaborators,
1539
1583
  topics,
@@ -2841,6 +2885,7 @@ function createPublishGithubAction(options) {
2841
2885
  allowMergeCommit: allowMergeCommit,
2842
2886
  allowSquashMerge: allowSquashMerge,
2843
2887
  allowRebaseMerge: allowRebaseMerge,
2888
+ allowAutoMerge: allowAutoMerge,
2844
2889
  sourcePath: sourcePath,
2845
2890
  collaborators: collaborators,
2846
2891
  token: token,
@@ -2874,6 +2919,7 @@ function createPublishGithubAction(options) {
2874
2919
  allowMergeCommit = true,
2875
2920
  allowSquashMerge = true,
2876
2921
  allowRebaseMerge = true,
2922
+ allowAutoMerge = false,
2877
2923
  collaborators,
2878
2924
  topics,
2879
2925
  token: providedToken
@@ -2900,6 +2946,7 @@ function createPublishGithubAction(options) {
2900
2946
  allowMergeCommit,
2901
2947
  allowSquashMerge,
2902
2948
  allowRebaseMerge,
2949
+ allowAutoMerge,
2903
2950
  access,
2904
2951
  collaborators,
2905
2952
  topics,
@@ -3551,7 +3598,8 @@ const createBuiltinActions = (options) => {
3551
3598
  integrations,
3552
3599
  catalogClient,
3553
3600
  config,
3554
- additionalTemplateFilters
3601
+ additionalTemplateFilters,
3602
+ additionalTemplateGlobals
3555
3603
  } = options;
3556
3604
  const githubCredentialsProvider = integration.DefaultGithubCredentialsProvider.fromIntegrations(integrations);
3557
3605
  const actions = [
@@ -3562,7 +3610,8 @@ const createBuiltinActions = (options) => {
3562
3610
  createFetchTemplateAction({
3563
3611
  integrations,
3564
3612
  reader,
3565
- additionalTemplateFilters
3613
+ additionalTemplateFilters,
3614
+ additionalTemplateGlobals
3566
3615
  }),
3567
3616
  createPublishGerritAction({
3568
3617
  integrations,
@@ -3805,8 +3854,7 @@ class DatabaseTaskStore {
3805
3854
  const rawRows = await this.db("tasks").where("status", "processing").andWhere(
3806
3855
  "last_heartbeat_at",
3807
3856
  "<=",
3808
- this.db.client.config.client.includes("sqlite3") ? this.db.raw(`datetime('now', ?)`, [`-${timeoutS} seconds`]) : this.db.raw(`dateadd('second', ?, ?)`, [
3809
- `-${timeoutS}`,
3857
+ this.db.client.config.client.includes("sqlite3") ? this.db.raw(`datetime('now', ?)`, [`-${timeoutS} seconds`]) : this.db.raw(`? - interval '${timeoutS} seconds'`, [
3810
3858
  this.db.fn.now()
3811
3859
  ])
3812
3860
  );
@@ -3891,6 +3939,33 @@ class DatabaseTaskStore {
3891
3939
  });
3892
3940
  return { events };
3893
3941
  }
3942
+ async shutdownTask({ taskId }) {
3943
+ const message = `This task was marked as stale as it exceeded its timeout`;
3944
+ const statusStepEvents = (await this.listEvents({ taskId })).events.filter(
3945
+ ({ body }) => body == null ? void 0 : body.stepId
3946
+ );
3947
+ const completedSteps = statusStepEvents.filter(
3948
+ ({ body: { status } }) => status === "failed" || status === "completed"
3949
+ ).map((step) => step.body.stepId);
3950
+ const hungProcessingSteps = statusStepEvents.filter(({ body: { status } }) => status === "processing").map((event) => event.body.stepId).filter((step) => !completedSteps.includes(step));
3951
+ for (const step of hungProcessingSteps) {
3952
+ await this.emitLogEvent({
3953
+ taskId,
3954
+ body: {
3955
+ message,
3956
+ stepId: step,
3957
+ status: "failed"
3958
+ }
3959
+ });
3960
+ }
3961
+ await this.completeTask({
3962
+ taskId,
3963
+ status: "failed",
3964
+ eventBody: {
3965
+ message
3966
+ }
3967
+ });
3968
+ }
3894
3969
  }
3895
3970
 
3896
3971
  class TaskManager {
@@ -4166,7 +4241,7 @@ class NunjucksWorkflowRunner {
4166
4241
  });
4167
4242
  }
4168
4243
  async execute(task) {
4169
- var _a, _b, _c, _d, _e;
4244
+ var _a, _b, _c, _d, _e, _f, _g;
4170
4245
  if (!isValidTaskSpec(task.spec)) {
4171
4246
  throw new errors.InputError(
4172
4247
  "Wrong template version executed with the workflow engine"
@@ -4181,7 +4256,8 @@ class NunjucksWorkflowRunner {
4181
4256
  parseRepoUrl(url) {
4182
4257
  return parseRepoUrl(url, integrations);
4183
4258
  },
4184
- additionalTemplateFilters: this.options.additionalTemplateFilters
4259
+ additionalTemplateFilters: this.options.additionalTemplateFilters,
4260
+ additionalTemplateGlobals: this.options.additionalTemplateGlobals
4185
4261
  });
4186
4262
  try {
4187
4263
  await fs__default["default"].ensureDir(workspacePath);
@@ -4215,30 +4291,53 @@ class NunjucksWorkflowRunner {
4215
4291
  });
4216
4292
  const action = this.options.actionRegistry.get(step.action);
4217
4293
  const { taskLogger, streamLogger } = createStepLogger({ task, step });
4218
- if (task.isDryRun && !action.supportsDryRun) {
4219
- task.emitLog(
4220
- `Skipping because ${action.id} does not support dry-run`,
4294
+ if (task.isDryRun) {
4295
+ const redactedSecrets = Object.fromEntries(
4296
+ Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
4297
+ secret[0],
4298
+ "[REDACTED]"
4299
+ ])
4300
+ );
4301
+ const debugInput = (_b = step.input && this.render(
4302
+ step.input,
4221
4303
  {
4222
- stepId: step.id,
4223
- status: "skipped"
4224
- }
4304
+ ...context,
4305
+ secrets: redactedSecrets
4306
+ },
4307
+ renderTemplate
4308
+ )) != null ? _b : {};
4309
+ taskLogger.info(
4310
+ `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
4311
+ debugInput,
4312
+ void 0,
4313
+ 2
4314
+ )}`
4225
4315
  );
4226
- const outputSchema = (_a = action.schema) == null ? void 0 : _a.output;
4227
- if (outputSchema) {
4228
- context.steps[step.id] = {
4229
- output: generateExampleOutput(outputSchema)
4230
- };
4231
- } else {
4232
- context.steps[step.id] = { output: {} };
4316
+ if (!action.supportsDryRun) {
4317
+ task.emitLog(
4318
+ `Skipping because ${action.id} does not support dry-run`,
4319
+ {
4320
+ stepId: step.id,
4321
+ status: "skipped"
4322
+ }
4323
+ );
4324
+ const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
4325
+ if (outputSchema) {
4326
+ context.steps[step.id] = {
4327
+ output: generateExampleOutput(outputSchema)
4328
+ };
4329
+ } else {
4330
+ context.steps[step.id] = { output: {} };
4331
+ }
4332
+ continue;
4233
4333
  }
4234
- continue;
4235
4334
  }
4236
- const input = (_c = step.input && this.render(
4335
+ const input = (_e = step.input && this.render(
4237
4336
  step.input,
4238
- { ...context, secrets: (_b = task.secrets) != null ? _b : {} },
4337
+ { ...context, secrets: (_d = task.secrets) != null ? _d : {} },
4239
4338
  renderTemplate
4240
- )) != null ? _c : {};
4241
- if ((_d = action.schema) == null ? void 0 : _d.input) {
4339
+ )) != null ? _e : {};
4340
+ if ((_f = action.schema) == null ? void 0 : _f.input) {
4242
4341
  const validateResult = jsonschema.validate(
4243
4342
  input,
4244
4343
  action.schema.input
@@ -4254,7 +4353,7 @@ class NunjucksWorkflowRunner {
4254
4353
  const stepOutput = {};
4255
4354
  await action.handler({
4256
4355
  input,
4257
- secrets: (_e = task.secrets) != null ? _e : {},
4356
+ secrets: (_g = task.secrets) != null ? _g : {},
4258
4357
  logger: taskLogger,
4259
4358
  logStream: streamLogger,
4260
4359
  workspacePath,
@@ -4268,7 +4367,9 @@ class NunjucksWorkflowRunner {
4268
4367
  output(name, value) {
4269
4368
  stepOutput[name] = value;
4270
4369
  },
4271
- templateInfo: task.spec.templateInfo
4370
+ templateInfo: task.spec.templateInfo,
4371
+ user: task.spec.user,
4372
+ isDryRun: task.isDryRun
4272
4373
  });
4273
4374
  for (const tmpDir of tmpDirs) {
4274
4375
  await fs__default["default"].remove(tmpDir);
@@ -4307,14 +4408,16 @@ class TaskWorker {
4307
4408
  actionRegistry,
4308
4409
  integrations,
4309
4410
  workingDirectory,
4310
- additionalTemplateFilters
4411
+ additionalTemplateFilters,
4412
+ additionalTemplateGlobals
4311
4413
  } = options;
4312
4414
  const workflowRunner = new NunjucksWorkflowRunner({
4313
4415
  actionRegistry,
4314
4416
  integrations,
4315
4417
  logger,
4316
4418
  workingDirectory,
4317
- additionalTemplateFilters
4419
+ additionalTemplateFilters,
4420
+ additionalTemplateGlobals
4318
4421
  });
4319
4422
  return new TaskWorker({
4320
4423
  taskBroker,
@@ -4538,7 +4641,7 @@ function buildDefaultIdentityClient({
4538
4641
  }
4539
4642
  async function createRouter(options) {
4540
4643
  const router = Router__default["default"]();
4541
- router.use(express__default["default"].json());
4644
+ router.use(express__default["default"].json({ limit: "10MB" }));
4542
4645
  const {
4543
4646
  logger: parentLogger,
4544
4647
  config,
@@ -4547,7 +4650,9 @@ async function createRouter(options) {
4547
4650
  catalogClient,
4548
4651
  actions,
4549
4652
  taskWorkers,
4550
- additionalTemplateFilters
4653
+ scheduler,
4654
+ additionalTemplateFilters,
4655
+ additionalTemplateGlobals
4551
4656
  } = options;
4552
4657
  const logger = parentLogger.child({ plugin: "scaffolder" });
4553
4658
  const identity = options.identity || buildDefaultIdentityClient({ logger });
@@ -4557,6 +4662,22 @@ async function createRouter(options) {
4557
4662
  if (!options.taskBroker) {
4558
4663
  const databaseTaskStore = await DatabaseTaskStore.create({ database });
4559
4664
  taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
4665
+ if (scheduler && databaseTaskStore.listStaleTasks) {
4666
+ await scheduler.scheduleTask({
4667
+ id: "close_stale_tasks",
4668
+ frequency: { cron: "*/5 * * * *" },
4669
+ timeout: { minutes: 15 },
4670
+ fn: async () => {
4671
+ const { tasks } = await databaseTaskStore.listStaleTasks({
4672
+ timeoutS: 86400
4673
+ });
4674
+ for (const task of tasks) {
4675
+ await databaseTaskStore.shutdownTask(task);
4676
+ logger.info(`Successfully closed stale task ${task.taskId}`);
4677
+ }
4678
+ }
4679
+ });
4680
+ }
4560
4681
  } else {
4561
4682
  taskBroker = options.taskBroker;
4562
4683
  }
@@ -4569,7 +4690,8 @@ async function createRouter(options) {
4569
4690
  integrations,
4570
4691
  logger,
4571
4692
  workingDirectory,
4572
- additionalTemplateFilters
4693
+ additionalTemplateFilters,
4694
+ additionalTemplateGlobals
4573
4695
  });
4574
4696
  workers.push(worker);
4575
4697
  }
@@ -4578,7 +4700,8 @@ async function createRouter(options) {
4578
4700
  catalogClient,
4579
4701
  reader,
4580
4702
  config,
4581
- additionalTemplateFilters
4703
+ additionalTemplateFilters,
4704
+ additionalTemplateGlobals
4582
4705
  });
4583
4706
  actionsToRegister.forEach((action) => actionRegistry.register(action));
4584
4707
  workers.forEach((worker) => worker.start());
@@ -4587,7 +4710,8 @@ async function createRouter(options) {
4587
4710
  integrations,
4588
4711
  logger,
4589
4712
  workingDirectory,
4590
- additionalTemplateFilters
4713
+ additionalTemplateFilters,
4714
+ additionalTemplateGlobals
4591
4715
  });
4592
4716
  router.get(
4593
4717
  "/v2/templates/:namespace/:kind/:name/parameter-schema",
@@ -4980,7 +5104,12 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
4980
5104
  httpRouter,
4981
5105
  catalogClient
4982
5106
  }) {
4983
- const { additionalTemplateFilters, taskBroker, taskWorkers } = options;
5107
+ const {
5108
+ additionalTemplateFilters,
5109
+ taskBroker,
5110
+ taskWorkers,
5111
+ additionalTemplateGlobals
5112
+ } = options;
4984
5113
  const log = backendPluginApi.loggerToWinstonLogger(logger);
4985
5114
  const actions = options.actions || [
4986
5115
  ...actionsExtensions.actions,
@@ -4989,7 +5118,8 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
4989
5118
  catalogClient,
4990
5119
  reader,
4991
5120
  config,
4992
- additionalTemplateFilters
5121
+ additionalTemplateFilters,
5122
+ additionalTemplateGlobals
4993
5123
  })
4994
5124
  ];
4995
5125
  const actionIds = actions.map((action) => action.id).join(", ");
@@ -5005,7 +5135,8 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
5005
5135
  actions,
5006
5136
  taskBroker,
5007
5137
  taskWorkers,
5008
- additionalTemplateFilters
5138
+ additionalTemplateFilters,
5139
+ additionalTemplateGlobals
5009
5140
  });
5010
5141
  httpRouter.use(router);
5011
5142
  }