@backstage/plugin-scaffolder-backend 1.3.0-next.0 → 1.3.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
@@ -18,8 +18,8 @@ var azureDevopsNodeApi = require('azure-devops-node-api');
18
18
  var fetch = require('node-fetch');
19
19
  var crypto = require('crypto');
20
20
  var octokit = require('octokit');
21
- var lodash = require('lodash');
22
21
  var octokitPluginCreatePullRequest = require('octokit-plugin-create-pull-request');
22
+ var limiterFactory = require('p-limit');
23
23
  var node = require('@gitbeaker/node');
24
24
  var webhooks = require('@octokit/webhooks');
25
25
  var uuid = require('uuid');
@@ -27,12 +27,15 @@ var luxon = require('luxon');
27
27
  var ObservableImpl = require('zen-observable');
28
28
  var winston = require('winston');
29
29
  var nunjucks = require('nunjucks');
30
+ var lodash = require('lodash');
30
31
  var jsonschema = require('jsonschema');
32
+ var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
31
33
  var express = require('express');
32
34
  var Router = require('express-promise-router');
35
+ var zod = require('zod');
36
+ var url = require('url');
33
37
  var os = require('os');
34
38
  var pluginCatalogBackend = require('@backstage/plugin-catalog-backend');
35
- var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
36
39
 
37
40
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
38
41
 
@@ -60,6 +63,7 @@ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
60
63
  var globby__default = /*#__PURE__*/_interopDefaultLegacy(globby);
61
64
  var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
62
65
  var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto);
66
+ var limiterFactory__default = /*#__PURE__*/_interopDefaultLegacy(limiterFactory);
63
67
  var ObservableImpl__default = /*#__PURE__*/_interopDefaultLegacy(ObservableImpl);
64
68
  var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
65
69
  var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
@@ -117,6 +121,18 @@ function createCatalogRegisterAction(options) {
117
121
  }
118
122
  }
119
123
  ]
124
+ },
125
+ output: {
126
+ type: "object",
127
+ required: ["catalogInfoUrl"],
128
+ properties: {
129
+ entityRef: {
130
+ type: "string"
131
+ },
132
+ catalogInfoUrl: {
133
+ type: "string"
134
+ }
135
+ }
120
136
  }
121
137
  },
122
138
  async handler(ctx) {
@@ -190,6 +206,7 @@ function createCatalogWriteAction() {
190
206
  }
191
207
  }
192
208
  },
209
+ supportsDryRun: true,
193
210
  async handler(ctx) {
194
211
  ctx.logStream.write(`Writing catalog-info.yaml`);
195
212
  const { filePath, entity } = ctx.input;
@@ -221,6 +238,7 @@ function createDebugLogAction() {
221
238
  }
222
239
  }
223
240
  },
241
+ supportsDryRun: true,
224
242
  async handler(ctx) {
225
243
  var _a, _b;
226
244
  ctx.logger.info(JSON.stringify(ctx.input, null, 2));
@@ -306,6 +324,7 @@ function createFetchPlainAction(options) {
306
324
  }
307
325
  }
308
326
  },
327
+ supportsDryRun: true,
309
328
  async handler(ctx) {
310
329
  var _a, _b;
311
330
  ctx.logger.info("Fetching plain content from remote URL");
@@ -472,6 +491,7 @@ function createFetchTemplateAction(options) {
472
491
  }
473
492
  }
474
493
  },
494
+ supportsDryRun: true,
475
495
  async handler(ctx) {
476
496
  var _a, _b;
477
497
  ctx.logger.info("Fetching template content from remote URL");
@@ -504,13 +524,15 @@ function createFetchTemplateAction(options) {
504
524
  cwd: templateDir,
505
525
  dot: true,
506
526
  onlyFiles: false,
507
- markDirectories: true
527
+ markDirectories: true,
528
+ followSymbolicLinks: false
508
529
  });
509
530
  const nonTemplatedEntries = new Set((await Promise.all((ctx.input.copyWithoutRender || []).map((pattern) => globby__default["default"](pattern, {
510
531
  cwd: templateDir,
511
532
  dot: true,
512
533
  onlyFiles: false,
513
- markDirectories: true
534
+ markDirectories: true,
535
+ followSymbolicLinks: false
514
536
  })))).flat());
515
537
  const { cookiecutterCompat, values } = ctx.input;
516
538
  const context = {
@@ -585,6 +607,7 @@ const createFilesystemDeleteAction = () => {
585
607
  }
586
608
  }
587
609
  },
610
+ supportsDryRun: true,
588
611
  async handler(ctx) {
589
612
  var _a;
590
613
  if (!Array.isArray((_a = ctx.input) == null ? void 0 : _a.files)) {
@@ -639,6 +662,7 @@ const createFilesystemRenameAction = () => {
639
662
  }
640
663
  }
641
664
  },
665
+ supportsDryRun: true,
642
666
  async handler(ctx) {
643
667
  var _a, _b;
644
668
  if (!Array.isArray((_a = ctx.input) == null ? void 0 : _a.files)) {
@@ -827,11 +851,6 @@ const parseRepoUrl = (repoUrl, integrations) => {
827
851
  }
828
852
  return { host, owner, repo, organization, workspace, project };
829
853
  };
830
- const isExecutable = (fileMode) => {
831
- const executeBitMask = 73;
832
- const res = fileMode & executeBitMask;
833
- return res > 0;
834
- };
835
854
 
836
855
  function createPublishAzureAction(options) {
837
856
  const { integrations, config } = options;
@@ -1796,6 +1815,11 @@ function createPublishGithubAction(options) {
1796
1815
  type: "string",
1797
1816
  description: `Sets the default branch on the repository. The default value is 'master'`
1798
1817
  },
1818
+ protectDefaultBranch: {
1819
+ title: "Protect Default Branch",
1820
+ type: "boolean",
1821
+ description: `Protect the default branch after creating the repository. The default value is 'true'`
1822
+ },
1799
1823
  deleteBranchOnMerge: {
1800
1824
  title: "Delete Branch On Merge",
1801
1825
  type: "boolean",
@@ -1838,22 +1862,36 @@ function createPublishGithubAction(options) {
1838
1862
  },
1839
1863
  collaborators: {
1840
1864
  title: "Collaborators",
1841
- description: "Provide additional users with permissions",
1865
+ description: "Provide additional users or teams with permissions",
1842
1866
  type: "array",
1843
1867
  items: {
1844
1868
  type: "object",
1845
- required: ["username", "access"],
1869
+ additionalProperties: false,
1870
+ required: ["access"],
1846
1871
  properties: {
1847
1872
  access: {
1848
1873
  type: "string",
1849
1874
  description: "The type of access for the user",
1850
1875
  enum: ["push", "pull", "admin", "maintain", "triage"]
1851
1876
  },
1877
+ user: {
1878
+ type: "string",
1879
+ description: "The name of the user that will be added as a collaborator"
1880
+ },
1852
1881
  username: {
1853
1882
  type: "string",
1854
- description: "The username or group"
1883
+ description: "Deprecated. Use the `team` or `user` field instead."
1884
+ },
1885
+ team: {
1886
+ type: "string",
1887
+ description: "The name of the team that will be added as a collaborator"
1855
1888
  }
1856
- }
1889
+ },
1890
+ oneOf: [
1891
+ { required: ["user"] },
1892
+ { required: ["username"] },
1893
+ { required: ["team"] }
1894
+ ]
1857
1895
  }
1858
1896
  },
1859
1897
  token: {
@@ -1893,6 +1931,7 @@ function createPublishGithubAction(options) {
1893
1931
  requiredStatusCheckContexts = [],
1894
1932
  repoVisibility = "private",
1895
1933
  defaultBranch = "master",
1934
+ protectDefaultBranch = true,
1896
1935
  deleteBranchOnMerge = false,
1897
1936
  gitCommitMessage = "initial commit",
1898
1937
  gitAuthorName,
@@ -1965,21 +2004,37 @@ function createPublishGithubAction(options) {
1965
2004
  });
1966
2005
  }
1967
2006
  if (collaborators) {
1968
- for (const {
1969
- access: permission,
1970
- username: team_slug
1971
- } of collaborators) {
2007
+ for (const collaborator of collaborators) {
1972
2008
  try {
1973
- await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
1974
- org: owner,
1975
- team_slug,
1976
- owner,
1977
- repo,
1978
- permission
1979
- });
2009
+ if ("user" in collaborator) {
2010
+ await client.rest.repos.addCollaborator({
2011
+ owner,
2012
+ repo,
2013
+ username: collaborator.user,
2014
+ permission: collaborator.access
2015
+ });
2016
+ } else if ("username" in collaborator) {
2017
+ ctx.logger.warn("The field `username` is deprecated in favor of `team` and will be removed in the future.");
2018
+ await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
2019
+ org: owner,
2020
+ team_slug: collaborator.username,
2021
+ owner,
2022
+ repo,
2023
+ permission: collaborator.access
2024
+ });
2025
+ } else if ("team" in collaborator) {
2026
+ await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
2027
+ org: owner,
2028
+ team_slug: collaborator.team,
2029
+ owner,
2030
+ repo,
2031
+ permission: collaborator.access
2032
+ });
2033
+ }
1980
2034
  } catch (e) {
1981
2035
  errors.assertError(e);
1982
- ctx.logger.warn(`Skipping ${permission} access for ${team_slug}, ${e.message}`);
2036
+ const name = extractCollaboratorName(collaborator);
2037
+ ctx.logger.warn(`Skipping ${collaborator.access} access for ${name}, ${e.message}`);
1983
2038
  }
1984
2039
  }
1985
2040
  }
@@ -2013,25 +2068,69 @@ function createPublishGithubAction(options) {
2013
2068
  commitMessage: gitCommitMessage ? gitCommitMessage : config.getOptionalString("scaffolder.defaultCommitMessage"),
2014
2069
  gitAuthorInfo
2015
2070
  });
2016
- try {
2017
- await enableBranchProtectionOnDefaultRepoBranch({
2018
- owner,
2019
- client,
2020
- repoName: newRepo.name,
2021
- logger: ctx.logger,
2022
- defaultBranch,
2023
- requireCodeOwnerReviews,
2024
- requiredStatusCheckContexts
2025
- });
2026
- } catch (e) {
2027
- errors.assertError(e);
2028
- ctx.logger.warn(`Skipping: default branch protection on '${newRepo.name}', ${e.message}`);
2071
+ if (protectDefaultBranch) {
2072
+ try {
2073
+ await enableBranchProtectionOnDefaultRepoBranch({
2074
+ owner,
2075
+ client,
2076
+ repoName: newRepo.name,
2077
+ logger: ctx.logger,
2078
+ defaultBranch,
2079
+ requireCodeOwnerReviews,
2080
+ requiredStatusCheckContexts
2081
+ });
2082
+ } catch (e) {
2083
+ errors.assertError(e);
2084
+ ctx.logger.warn(`Skipping: default branch protection on '${newRepo.name}', ${e.message}`);
2085
+ }
2029
2086
  }
2030
2087
  ctx.output("remoteUrl", remoteUrl);
2031
2088
  ctx.output("repoContentsUrl", repoContentsUrl);
2032
2089
  }
2033
2090
  });
2034
2091
  }
2092
+ function extractCollaboratorName(collaborator) {
2093
+ if ("username" in collaborator)
2094
+ return collaborator.username;
2095
+ if ("user" in collaborator)
2096
+ return collaborator.user;
2097
+ return collaborator.team;
2098
+ }
2099
+
2100
+ const DEFAULT_GLOB_PATTERNS = ["./**", "!.git"];
2101
+ const isExecutable = (fileMode) => {
2102
+ if (!fileMode) {
2103
+ return false;
2104
+ }
2105
+ const executeBitMask = 73;
2106
+ const res = fileMode & executeBitMask;
2107
+ return res > 0;
2108
+ };
2109
+ async function serializeDirectoryContents(sourcePath, options) {
2110
+ var _a;
2111
+ const paths = await globby__default["default"]((_a = options == null ? void 0 : options.globPatterns) != null ? _a : DEFAULT_GLOB_PATTERNS, {
2112
+ cwd: sourcePath,
2113
+ dot: true,
2114
+ gitignore: options == null ? void 0 : options.gitignore,
2115
+ followSymbolicLinks: false,
2116
+ objectMode: true,
2117
+ stats: true
2118
+ });
2119
+ const limiter = limiterFactory__default["default"](10);
2120
+ return Promise.all(paths.map(async ({ path: path$1, stats }) => ({
2121
+ path: path$1,
2122
+ content: await limiter(async () => fs__default["default"].readFile(path.join(sourcePath, path$1))),
2123
+ executable: isExecutable(stats == null ? void 0 : stats.mode)
2124
+ })));
2125
+ }
2126
+
2127
+ async function deserializeDirectoryContents(targetPath, files) {
2128
+ for (const file of files) {
2129
+ const filePath = backendCommon.resolveSafeChildPath(targetPath, file.path);
2130
+ await fs__default["default"].ensureDir(path.dirname(filePath));
2131
+ await fs__default["default"].writeFile(filePath, file.content);
2132
+ }
2133
+ }
2035
2134
 
2036
2135
  class GithubResponseError extends errors.CustomErrorBase {
2037
2136
  }
@@ -2148,38 +2247,28 @@ const createPublishGithubPullRequestAction = ({
2148
2247
  token: providedToken
2149
2248
  });
2150
2249
  const fileRoot = sourcePath ? backendCommon.resolveSafeChildPath(ctx.workspacePath, sourcePath) : ctx.workspacePath;
2151
- const localFilePaths = await globby__default["default"](["./**", "./**/.*", "!.git"], {
2152
- cwd: fileRoot,
2153
- gitignore: true,
2154
- dot: true
2250
+ const directoryContents = await serializeDirectoryContents(fileRoot, {
2251
+ gitignore: true
2155
2252
  });
2156
- const fileContents = await Promise.all(localFilePaths.map((filePath) => {
2157
- const absPath = backendCommon.resolveSafeChildPath(fileRoot, filePath);
2158
- const base64EncodedContent = fs__default["default"].readFileSync(absPath).toString("base64");
2159
- const fileStat = fs__default["default"].statSync(absPath);
2160
- const githubTreeItemMode = isExecutable(fileStat.mode) ? "100755" : "100644";
2161
- const encoding = "base64";
2162
- return {
2163
- encoding,
2164
- content: base64EncodedContent,
2165
- mode: githubTreeItemMode
2166
- };
2167
- }));
2168
- const repoFilePaths = localFilePaths.map((repoFilePath) => {
2169
- return targetPath ? `${targetPath}/${repoFilePath}` : repoFilePath;
2170
- });
2171
- const changes = [
2253
+ const files = Object.fromEntries(directoryContents.map((file) => [
2254
+ targetPath ? path__default["default"].posix.join(targetPath, file.path) : file.path,
2172
2255
  {
2173
- files: lodash.zipObject(repoFilePaths, fileContents),
2174
- commit: title
2256
+ mode: file.executable ? "100755" : "100644",
2257
+ encoding: "base64",
2258
+ content: file.content.toString("base64")
2175
2259
  }
2176
- ];
2260
+ ]));
2177
2261
  try {
2178
2262
  const response = await client.createPullRequest({
2179
2263
  owner,
2180
2264
  repo,
2181
2265
  title,
2182
- changes,
2266
+ changes: [
2267
+ {
2268
+ files,
2269
+ commit: title
2270
+ }
2271
+ ],
2183
2272
  body: description,
2184
2273
  head: branchName,
2185
2274
  draft
@@ -2327,7 +2416,7 @@ const createPublishGitlabMergeRequestAction = (options) => {
2327
2416
  id: "publish:gitlab:merge-request",
2328
2417
  schema: {
2329
2418
  input: {
2330
- required: ["projectid", "repoUrl", "targetPath", "branchName"],
2419
+ required: ["repoUrl", "targetPath", "branchName"],
2331
2420
  type: "object",
2332
2421
  properties: {
2333
2422
  repoUrl: {
@@ -2374,6 +2463,10 @@ const createPublishGitlabMergeRequestAction = (options) => {
2374
2463
  title: "Gitlab Project id/Name(slug)",
2375
2464
  type: "string"
2376
2465
  },
2466
+ projectPath: {
2467
+ title: "Gitlab Project path",
2468
+ type: "string"
2469
+ },
2377
2470
  mergeRequestURL: {
2378
2471
  title: "MergeRequest(MR) URL",
2379
2472
  type: "string",
@@ -2385,9 +2478,14 @@ const createPublishGitlabMergeRequestAction = (options) => {
2385
2478
  async handler(ctx) {
2386
2479
  var _a;
2387
2480
  const repoUrl = ctx.input.repoUrl;
2388
- const { host } = parseRepoUrl(repoUrl, integrations);
2481
+ const { host, owner, repo } = parseRepoUrl(repoUrl, integrations);
2482
+ const projectPath = `${owner}/${repo}`;
2483
+ if (ctx.input.projectid) {
2484
+ const deprecationWarning = `Property "projectid" is deprecated and no longer to needed to create a MR`;
2485
+ ctx.logger.warn(deprecationWarning);
2486
+ console.warn(deprecationWarning);
2487
+ }
2389
2488
  const integrationConfig = integrations.gitlab.byHost(host);
2390
- const actions = [];
2391
2489
  const destinationBranch = ctx.input.branchName;
2392
2490
  if (!integrationConfig) {
2393
2491
  throw new errors.InputError(`No matching integration configuration for host ${host}, please check your integrations config`);
@@ -2401,40 +2499,35 @@ const createPublishGitlabMergeRequestAction = (options) => {
2401
2499
  host: integrationConfig.config.baseUrl,
2402
2500
  [tokenType]: token
2403
2501
  });
2404
- const fileRoot = ctx.workspacePath;
2405
- const localFilePaths = await globby__default["default"]([`${ctx.input.targetPath}/**`], {
2406
- cwd: fileRoot,
2407
- gitignore: true,
2408
- dot: true
2502
+ const targetPath = backendCommon.resolveSafeChildPath(ctx.workspacePath, ctx.input.targetPath);
2503
+ const fileContents = await serializeDirectoryContents(targetPath, {
2504
+ gitignore: true
2409
2505
  });
2410
- const fileContents = await Promise.all(localFilePaths.map((p) => fs.readFile(backendCommon.resolveSafeChildPath(fileRoot, p))));
2411
- const repoFilePaths = localFilePaths.map((repoFilePath) => {
2412
- return repoFilePath;
2413
- });
2414
- for (let i = 0; i < repoFilePaths.length; i++) {
2415
- actions.push({
2416
- action: "create",
2417
- filePath: repoFilePaths[i],
2418
- content: fileContents[i].toString()
2419
- });
2420
- }
2421
- const projects = await api.Projects.show(ctx.input.projectid);
2506
+ const actions = fileContents.map((file) => ({
2507
+ action: "create",
2508
+ filePath: path__default["default"].posix.join(ctx.input.targetPath, file.path),
2509
+ encoding: "base64",
2510
+ content: file.content.toString("base64"),
2511
+ execute_filemode: file.executable
2512
+ }));
2513
+ const projects = await api.Projects.show(projectPath);
2422
2514
  const { default_branch: defaultBranch } = projects;
2423
2515
  try {
2424
- await api.Branches.create(ctx.input.projectid, destinationBranch, String(defaultBranch));
2516
+ await api.Branches.create(projectPath, destinationBranch, String(defaultBranch));
2425
2517
  } catch (e) {
2426
2518
  throw new errors.InputError(`The branch creation failed ${e}`);
2427
2519
  }
2428
2520
  try {
2429
- await api.Commits.create(ctx.input.projectid, destinationBranch, ctx.input.title, actions);
2521
+ await api.Commits.create(projectPath, destinationBranch, ctx.input.title, actions);
2430
2522
  } catch (e) {
2431
2523
  throw new errors.InputError(`Committing the changes to ${destinationBranch} failed ${e}`);
2432
2524
  }
2433
2525
  try {
2434
- const mergeRequestUrl = await api.MergeRequests.create(ctx.input.projectid, destinationBranch, String(defaultBranch), ctx.input.title, { description: ctx.input.description }).then((mergeRequest) => {
2526
+ const mergeRequestUrl = await api.MergeRequests.create(projectPath, destinationBranch, String(defaultBranch), ctx.input.title, { description: ctx.input.description }).then((mergeRequest) => {
2435
2527
  return mergeRequest.web_url;
2436
2528
  });
2437
- ctx.output("projectid", ctx.input.projectid);
2529
+ ctx.output("projectid", projectPath);
2530
+ ctx.output("projectPath", projectPath);
2438
2531
  ctx.output("mergeRequestUrl", mergeRequestUrl);
2439
2532
  } catch (e) {
2440
2533
  throw new errors.InputError(`Merge request creation failed${e}`);
@@ -2789,6 +2882,12 @@ class TemplateActionRegistry {
2789
2882
  }
2790
2883
 
2791
2884
  const migrationsDir = backendCommon.resolvePackagePath("@backstage/plugin-scaffolder-backend", "migrations");
2885
+ const parseSqlDateToIsoString = (input) => {
2886
+ if (typeof input === "string") {
2887
+ return luxon.DateTime.fromSQL(input, { zone: "UTC" }).toISO();
2888
+ }
2889
+ return input;
2890
+ };
2792
2891
  class DatabaseTaskStore {
2793
2892
  static async create(options) {
2794
2893
  await options.database.migrate.latest({
@@ -2799,6 +2898,27 @@ class DatabaseTaskStore {
2799
2898
  constructor(options) {
2800
2899
  this.db = options.database;
2801
2900
  }
2901
+ async list(options) {
2902
+ const queryBuilder = this.db("tasks");
2903
+ if (options.createdBy) {
2904
+ queryBuilder.where({
2905
+ created_by: options.createdBy
2906
+ });
2907
+ }
2908
+ const results = await queryBuilder.orderBy("created_at", "desc").select();
2909
+ const tasks = results.map((result) => {
2910
+ var _a;
2911
+ return {
2912
+ id: result.id,
2913
+ spec: JSON.parse(result.spec),
2914
+ status: result.status,
2915
+ createdBy: (_a = result.created_by) != null ? _a : void 0,
2916
+ lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
2917
+ createdAt: parseSqlDateToIsoString(result.created_at)
2918
+ };
2919
+ });
2920
+ return { tasks };
2921
+ }
2802
2922
  async getTask(taskId) {
2803
2923
  var _a;
2804
2924
  const [result] = await this.db("tasks").where({ id: taskId }).select();
@@ -2812,8 +2932,8 @@ class DatabaseTaskStore {
2812
2932
  id: result.id,
2813
2933
  spec,
2814
2934
  status: result.status,
2815
- lastHeartbeatAt: result.last_heartbeat_at,
2816
- createdAt: result.created_at,
2935
+ lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
2936
+ createdAt: parseSqlDateToIsoString(result.created_at),
2817
2937
  createdBy: (_a = result.created_by) != null ? _a : void 0,
2818
2938
  secrets
2819
2939
  };
@@ -2950,7 +3070,7 @@ class DatabaseTaskStore {
2950
3070
  taskId,
2951
3071
  body,
2952
3072
  type: event.event_type,
2953
- createdAt: typeof event.created_at === "string" ? luxon.DateTime.fromSQL(event.created_at, { zone: "UTC" }).toISO() : event.created_at
3073
+ createdAt: parseSqlDateToIsoString(event.created_at)
2954
3074
  };
2955
3075
  } catch (error) {
2956
3076
  throw new Error(`Failed to parse event body from event taskId=${taskId} id=${event.id}, ${error}`);
@@ -3033,6 +3153,12 @@ class StorageTaskBroker {
3033
3153
  this.logger = logger;
3034
3154
  this.deferredDispatch = defer();
3035
3155
  }
3156
+ async list(options) {
3157
+ if (!this.storage.list) {
3158
+ throw new Error("TaskStore does not implement the list method. Please implement the list method to be able to list tasks");
3159
+ }
3160
+ return await this.storage.list({ createdBy: options == null ? void 0 : options.createdBy });
3161
+ }
3036
3162
  async claim() {
3037
3163
  for (; ; ) {
3038
3164
  const pendingTask = await this.storage.claimTask();
@@ -3106,6 +3232,32 @@ class StorageTaskBroker {
3106
3232
  function isTruthy(value) {
3107
3233
  return lodash.isArray(value) ? value.length > 0 : !!value;
3108
3234
  }
3235
+ function generateExampleOutput(schema) {
3236
+ var _a, _b;
3237
+ const { examples } = schema;
3238
+ if (examples && Array.isArray(examples)) {
3239
+ return examples[0];
3240
+ }
3241
+ if (schema.type === "object") {
3242
+ return Object.fromEntries(Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
3243
+ key,
3244
+ generateExampleOutput(value)
3245
+ ]));
3246
+ } else if (schema.type === "array") {
3247
+ const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
3248
+ if (firstSchema) {
3249
+ return [generateExampleOutput(firstSchema)];
3250
+ }
3251
+ return [];
3252
+ } else if (schema.type === "string") {
3253
+ return "<example>";
3254
+ } else if (schema.type === "number") {
3255
+ return 0;
3256
+ } else if (schema.type === "boolean") {
3257
+ return false;
3258
+ }
3259
+ return "<unknown>";
3260
+ }
3109
3261
 
3110
3262
  const isValidTaskSpec = (taskSpec) => {
3111
3263
  return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
@@ -3175,7 +3327,7 @@ class NunjucksWorkflowRunner {
3175
3327
  });
3176
3328
  }
3177
3329
  async execute(task) {
3178
- var _a, _b, _c, _d;
3330
+ var _a, _b, _c, _d, _e;
3179
3331
  if (!isValidTaskSpec(task.spec)) {
3180
3332
  throw new errors.InputError("Wrong template version executed with the workflow engine");
3181
3333
  }
@@ -3210,8 +3362,23 @@ class NunjucksWorkflowRunner {
3210
3362
  });
3211
3363
  const action = this.options.actionRegistry.get(step.action);
3212
3364
  const { taskLogger, streamLogger } = createStepLogger({ task, step });
3213
- const input = (_b = step.input && this.render(step.input, { ...context, secrets: (_a = task.secrets) != null ? _a : {} }, renderTemplate)) != null ? _b : {};
3214
- if ((_c = action.schema) == null ? void 0 : _c.input) {
3365
+ if (task.isDryRun && !action.supportsDryRun) {
3366
+ task.emitLog(`Skipping because ${action.id} does not support dry-run`, {
3367
+ stepId: step.id,
3368
+ status: "skipped"
3369
+ });
3370
+ const outputSchema = (_a = action.schema) == null ? void 0 : _a.output;
3371
+ if (outputSchema) {
3372
+ context.steps[step.id] = {
3373
+ output: generateExampleOutput(outputSchema)
3374
+ };
3375
+ } else {
3376
+ context.steps[step.id] = { output: {} };
3377
+ }
3378
+ continue;
3379
+ }
3380
+ const input = (_c = step.input && this.render(step.input, { ...context, secrets: (_b = task.secrets) != null ? _b : {} }, renderTemplate)) != null ? _c : {};
3381
+ if ((_d = action.schema) == null ? void 0 : _d.input) {
3215
3382
  const validateResult = jsonschema.validate(input, action.schema.input);
3216
3383
  if (!validateResult.valid) {
3217
3384
  const errors$1 = validateResult.errors.join(", ");
@@ -3222,7 +3389,7 @@ class NunjucksWorkflowRunner {
3222
3389
  const stepOutput = {};
3223
3390
  await action.handler({
3224
3391
  input,
3225
- secrets: (_d = task.secrets) != null ? _d : {},
3392
+ secrets: (_e = task.secrets) != null ? _e : {},
3226
3393
  logger: taskLogger,
3227
3394
  logStream: streamLogger,
3228
3395
  workspacePath,
@@ -3311,6 +3478,95 @@ class TaskWorker {
3311
3478
  }
3312
3479
  }
3313
3480
 
3481
+ class DecoratedActionsRegistry extends TemplateActionRegistry {
3482
+ constructor(innerRegistry, extraActions) {
3483
+ super();
3484
+ this.innerRegistry = innerRegistry;
3485
+ for (const action of extraActions) {
3486
+ this.register(action);
3487
+ }
3488
+ }
3489
+ get(actionId) {
3490
+ try {
3491
+ return super.get(actionId);
3492
+ } catch {
3493
+ return this.innerRegistry.get(actionId);
3494
+ }
3495
+ }
3496
+ }
3497
+
3498
+ function createDryRunner(options) {
3499
+ return async function dryRun(input) {
3500
+ let contentPromise;
3501
+ const workflowRunner = new NunjucksWorkflowRunner({
3502
+ ...options,
3503
+ actionRegistry: new DecoratedActionsRegistry(options.actionRegistry, [
3504
+ createTemplateAction({
3505
+ id: "dry-run:extract",
3506
+ supportsDryRun: true,
3507
+ async handler(ctx) {
3508
+ contentPromise = serializeDirectoryContents(ctx.workspacePath);
3509
+ await contentPromise.catch(() => {
3510
+ });
3511
+ }
3512
+ })
3513
+ ])
3514
+ });
3515
+ const dryRunId = uuid.v4();
3516
+ const log = new Array();
3517
+ const contentsPath = backendCommon.resolveSafeChildPath(options.workingDirectory, `dry-run-content-${dryRunId}`);
3518
+ try {
3519
+ await deserializeDirectoryContents(contentsPath, input.directoryContents);
3520
+ const result = await workflowRunner.execute({
3521
+ spec: {
3522
+ ...input.spec,
3523
+ steps: [
3524
+ ...input.spec.steps,
3525
+ {
3526
+ id: dryRunId,
3527
+ name: "dry-run:extract",
3528
+ action: "dry-run:extract"
3529
+ }
3530
+ ],
3531
+ templateInfo: {
3532
+ entityRef: "template:default/dry-run",
3533
+ baseUrl: url.pathToFileURL(backendCommon.resolveSafeChildPath(contentsPath, "template.yaml")).toString()
3534
+ }
3535
+ },
3536
+ secrets: input.secrets,
3537
+ done: false,
3538
+ isDryRun: true,
3539
+ getWorkspaceName: async () => `dry-run-${dryRunId}`,
3540
+ async emitLog(message, logMetadata) {
3541
+ if ((logMetadata == null ? void 0 : logMetadata.stepId) === dryRunId) {
3542
+ return;
3543
+ }
3544
+ log.push({
3545
+ body: {
3546
+ ...logMetadata,
3547
+ message
3548
+ }
3549
+ });
3550
+ },
3551
+ async complete() {
3552
+ throw new Error("Not implemented");
3553
+ }
3554
+ });
3555
+ if (!contentPromise) {
3556
+ throw new Error("Content extraction step was skipped");
3557
+ }
3558
+ const directoryContents = await contentPromise;
3559
+ return {
3560
+ log,
3561
+ directoryContents,
3562
+ output: result.output
3563
+ };
3564
+ } finally {
3565
+ await fs__default["default"].remove(contentsPath);
3566
+ }
3567
+ };
3568
+ }
3569
+
3314
3570
  async function getWorkingDirectory(config, logger) {
3315
3571
  if (!config.has("backend.workingDirectory")) {
3316
3572
  return os__default["default"].tmpdir();
@@ -3345,9 +3601,6 @@ function getEntityBaseUrl(entity) {
3345
3601
  }
3346
3602
  async function findTemplate(options) {
3347
3603
  const { entityRef, token, catalogApi } = options;
3348
- if (entityRef.namespace.toLocaleLowerCase("en-US") !== catalogModel.DEFAULT_NAMESPACE) {
3349
- throw new errors.InputError(`Invalid namespace, only '${catalogModel.DEFAULT_NAMESPACE}' namespace is supported`);
3350
- }
3351
3604
  if (entityRef.kind.toLocaleLowerCase("en-US") !== "template") {
3352
3605
  throw new errors.InputError(`Invalid kind, only 'Template' kind is supported`);
3353
3606
  }
@@ -3408,6 +3661,13 @@ async function createRouter(options) {
3408
3661
  });
3409
3662
  actionsToRegister.forEach((action) => actionRegistry.register(action));
3410
3663
  workers.forEach((worker) => worker.start());
3664
+ const dryRunner = createDryRunner({
3665
+ actionRegistry,
3666
+ integrations,
3667
+ logger,
3668
+ workingDirectory,
3669
+ additionalTemplateFilters
3670
+ });
3411
3671
  router.get("/v2/templates/:namespace/:kind/:name/parameter-schema", async (req, res) => {
3412
3672
  var _a, _b;
3413
3673
  const { namespace, kind, name } = req.params;
@@ -3500,6 +3760,18 @@ async function createRouter(options) {
3500
3760
  }
3501
3761
  });
3502
3762
  res.status(201).json({ id: result.taskId });
3763
+ }).get("/v2/tasks", async (req, res) => {
3764
+ const [userEntityRef] = [req.query.createdBy].flat();
3765
+ if (typeof userEntityRef !== "string" && typeof userEntityRef !== "undefined") {
3766
+ throw new errors.InputError("createdBy query parameter must be a string");
3767
+ }
3768
+ if (!taskBroker.list) {
3769
+ throw new Error("TaskBroker does not support listing tasks, please implement the list method on the TaskBroker.");
3770
+ }
3771
+ const tasks = await taskBroker.list({
3772
+ createdBy: userEntityRef
3773
+ });
3774
+ res.status(200).json(tasks);
3503
3775
  }).get("/v2/tasks/:taskId", async (req, res) => {
3504
3776
  const { taskId } = req.params;
3505
3777
  const task = await taskBroker.get(taskId);
@@ -3562,6 +3834,62 @@ data: ${JSON.stringify(event)}
3562
3834
  subscription.unsubscribe();
3563
3835
  clearTimeout(timeout);
3564
3836
  });
3837
+ }).post("/v2/dry-run", async (req, res) => {
3838
+ var _a, _b, _c;
3839
+ const bodySchema = zod.z.object({
3840
+ template: zod.z.unknown(),
3841
+ values: zod.z.record(zod.z.unknown()),
3842
+ secrets: zod.z.record(zod.z.string()).optional(),
3843
+ directoryContents: zod.z.array(zod.z.object({ path: zod.z.string(), base64Content: zod.z.string() }))
3844
+ });
3845
+ const body = await bodySchema.parseAsync(req.body).catch((e) => {
3846
+ throw new errors.InputError(`Malformed request: ${e}`);
3847
+ });
3848
+ const template = body.template;
3849
+ if (!await pluginScaffolderCommon.templateEntityV1beta3Validator.check(template)) {
3850
+ throw new errors.InputError("Input template is not a template");
3851
+ }
3852
+ const { token } = parseBearerToken(req.headers.authorization);
3853
+ for (const parameters of [(_a = template.spec.parameters) != null ? _a : []].flat()) {
3854
+ const result2 = jsonschema.validate(body.values, parameters);
3855
+ if (!result2.valid) {
3856
+ res.status(400).json({ errors: result2.errors });
3857
+ return;
3858
+ }
3859
+ }
3860
+ const steps = template.spec.steps.map((step, index) => {
3861
+ var _a2, _b2;
3862
+ return {
3863
+ ...step,
3864
+ id: (_a2 = step.id) != null ? _a2 : `step-${index + 1}`,
3865
+ name: (_b2 = step.name) != null ? _b2 : step.action
3866
+ };
3867
+ });
3868
+ const result = await dryRunner({
3869
+ spec: {
3870
+ apiVersion: template.apiVersion,
3871
+ steps,
3872
+ output: (_b = template.spec.output) != null ? _b : {},
3873
+ parameters: body.values
3874
+ },
3875
+ directoryContents: ((_c = body.directoryContents) != null ? _c : []).map((file) => ({
3876
+ path: file.path,
3877
+ content: Buffer.from(file.base64Content, "base64")
3878
+ })),
3879
+ secrets: {
3880
+ ...body.secrets,
3881
+ ...token && { backstageToken: token }
3882
+ }
3883
+ });
3884
+ res.status(200).json({
3885
+ ...result,
3886
+ steps,
3887
+ directoryContents: result.directoryContents.map((file) => ({
3888
+ path: file.path,
3889
+ executable: file.executable,
3890
+ base64Content: file.content.toString("base64")
3891
+ }))
3892
+ });
3565
3893
  });
3566
3894
  const app = express__default["default"]();
3567
3895
  app.set("logger", logger);