@backstage/plugin-scaffolder-backend 1.19.2-next.1 → 1.19.2-next.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.
@@ -0,0 +1,3207 @@
1
+ 'use strict';
2
+
3
+ var catalogModel = require('@backstage/catalog-model');
4
+ var config = require('@backstage/config');
5
+ var errors = require('@backstage/errors');
6
+ var integration = require('@backstage/integration');
7
+ var pluginScaffolderCommon = require('@backstage/plugin-scaffolder-common');
8
+ var alpha = require('@backstage/plugin-scaffolder-common/alpha');
9
+ var express = require('express');
10
+ var Router = require('express-promise-router');
11
+ var jsonschema = require('jsonschema');
12
+ var zod = require('zod');
13
+ var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
14
+ var yaml = require('yaml');
15
+ var fs = require('fs-extra');
16
+ var backendCommon = require('@backstage/backend-common');
17
+ var path = require('path');
18
+ var luxon = require('luxon');
19
+ var globby = require('globby');
20
+ var isbinaryfile = require('isbinaryfile');
21
+ var isolatedVm = require('isolated-vm');
22
+ var get = require('lodash/get');
23
+ var github = require('@backstage/plugin-scaffolder-backend-module-github');
24
+ var azure = require('@backstage/plugin-scaffolder-backend-module-azure');
25
+ var bitbucket = require('@backstage/plugin-scaffolder-backend-module-bitbucket');
26
+ var gerrit = require('@backstage/plugin-scaffolder-backend-module-gerrit');
27
+ var gitlab = require('@backstage/plugin-scaffolder-backend-module-gitlab');
28
+ var uuid = require('uuid');
29
+ var ObservableImpl = require('zen-observable');
30
+ var PQueue = require('p-queue');
31
+ var winston = require('winston');
32
+ var nunjucks = require('nunjucks');
33
+ var stream = require('stream');
34
+ var lodash = require('lodash');
35
+ var pluginPermissionNode = require('@backstage/plugin-permission-node');
36
+ var promClient = require('prom-client');
37
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
38
+ var url = require('url');
39
+ var os = require('os');
40
+
41
+ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
42
+
43
+ function _interopNamespace(e) {
44
+ if (e && e.__esModule) return e;
45
+ var n = Object.create(null);
46
+ if (e) {
47
+ Object.keys(e).forEach(function (k) {
48
+ if (k !== 'default') {
49
+ var d = Object.getOwnPropertyDescriptor(e, k);
50
+ Object.defineProperty(n, k, d.get ? d : {
51
+ enumerable: true,
52
+ get: function () { return e[k]; }
53
+ });
54
+ }
55
+ });
56
+ }
57
+ n["default"] = e;
58
+ return Object.freeze(n);
59
+ }
60
+
61
+ var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
62
+ var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
63
+ var yaml__default = /*#__PURE__*/_interopDefaultLegacy(yaml);
64
+ var yaml__namespace = /*#__PURE__*/_interopNamespace(yaml);
65
+ var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
66
+ var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
67
+ var globby__default = /*#__PURE__*/_interopDefaultLegacy(globby);
68
+ var get__default = /*#__PURE__*/_interopDefaultLegacy(get);
69
+ var ObservableImpl__default = /*#__PURE__*/_interopDefaultLegacy(ObservableImpl);
70
+ var PQueue__default = /*#__PURE__*/_interopDefaultLegacy(PQueue);
71
+ var winston__namespace = /*#__PURE__*/_interopNamespace(winston);
72
+ var nunjucks__default = /*#__PURE__*/_interopDefaultLegacy(nunjucks);
73
+ var os__default = /*#__PURE__*/_interopDefaultLegacy(os);
74
+
75
+ const examples$9 = [
76
+ {
77
+ description: "Register with the catalog",
78
+ example: yaml__default["default"].stringify({
79
+ steps: [
80
+ {
81
+ action: "catalog:register",
82
+ id: "register-with-catalog",
83
+ name: "Register with the catalog",
84
+ input: {
85
+ catalogInfoUrl: "http://github.com/backstage/backstage/blob/master/catalog-info.yaml"
86
+ }
87
+ }
88
+ ]
89
+ })
90
+ }
91
+ ];
92
+
93
+ const id$4 = "catalog:register";
94
+ function createCatalogRegisterAction(options) {
95
+ const { catalogClient, integrations } = options;
96
+ return pluginScaffolderNode.createTemplateAction({
97
+ id: id$4,
98
+ description: "Registers entities from a catalog descriptor file in the workspace into the software catalog.",
99
+ examples: examples$9,
100
+ schema: {
101
+ input: {
102
+ oneOf: [
103
+ {
104
+ type: "object",
105
+ required: ["catalogInfoUrl"],
106
+ properties: {
107
+ catalogInfoUrl: {
108
+ title: "Catalog Info URL",
109
+ description: "An absolute URL pointing to the catalog info file location",
110
+ type: "string"
111
+ },
112
+ optional: {
113
+ title: "Optional",
114
+ description: "Permit the registered location to optionally exist. Default: false",
115
+ type: "boolean"
116
+ }
117
+ }
118
+ },
119
+ {
120
+ type: "object",
121
+ required: ["repoContentsUrl"],
122
+ properties: {
123
+ repoContentsUrl: {
124
+ title: "Repository Contents URL",
125
+ description: "An absolute URL pointing to the root of a repository directory tree",
126
+ type: "string"
127
+ },
128
+ catalogInfoPath: {
129
+ title: "Fetch URL",
130
+ description: "A relative path from the repo root pointing to the catalog info file, defaults to /catalog-info.yaml",
131
+ type: "string"
132
+ },
133
+ optional: {
134
+ title: "Optional",
135
+ description: "Permit the registered location to optionally exist. Default: false",
136
+ type: "boolean"
137
+ }
138
+ }
139
+ }
140
+ ]
141
+ },
142
+ output: {
143
+ type: "object",
144
+ required: ["catalogInfoUrl"],
145
+ properties: {
146
+ entityRef: {
147
+ type: "string"
148
+ },
149
+ catalogInfoUrl: {
150
+ type: "string"
151
+ }
152
+ }
153
+ }
154
+ },
155
+ async handler(ctx) {
156
+ var _a, _b;
157
+ const { input } = ctx;
158
+ let catalogInfoUrl;
159
+ if ("catalogInfoUrl" in input) {
160
+ catalogInfoUrl = input.catalogInfoUrl;
161
+ } else {
162
+ const { repoContentsUrl, catalogInfoPath = "/catalog-info.yaml" } = input;
163
+ const integration = integrations.byUrl(repoContentsUrl);
164
+ if (!integration) {
165
+ throw new errors.InputError(
166
+ `No integration found for host ${repoContentsUrl}`
167
+ );
168
+ }
169
+ catalogInfoUrl = integration.resolveUrl({
170
+ base: repoContentsUrl,
171
+ url: catalogInfoPath
172
+ });
173
+ }
174
+ ctx.logger.info(`Registering ${catalogInfoUrl} in the catalog`);
175
+ try {
176
+ await catalogClient.addLocation(
177
+ {
178
+ type: "url",
179
+ target: catalogInfoUrl
180
+ },
181
+ ((_a = ctx.secrets) == null ? void 0 : _a.backstageToken) ? { token: ctx.secrets.backstageToken } : {}
182
+ );
183
+ } catch (e) {
184
+ if (!input.optional) {
185
+ throw e;
186
+ }
187
+ }
188
+ try {
189
+ const result = await catalogClient.addLocation(
190
+ {
191
+ dryRun: true,
192
+ type: "url",
193
+ target: catalogInfoUrl
194
+ },
195
+ ((_b = ctx.secrets) == null ? void 0 : _b.backstageToken) ? { token: ctx.secrets.backstageToken } : {}
196
+ );
197
+ if (result.entities.length) {
198
+ const { entities } = result;
199
+ let entity;
200
+ entity = entities.find(
201
+ (e) => !e.metadata.name.startsWith("generated-") && e.kind === "Component"
202
+ );
203
+ if (!entity) {
204
+ entity = entities.find(
205
+ (e) => !e.metadata.name.startsWith("generated-")
206
+ );
207
+ }
208
+ if (!entity) {
209
+ entity = entities[0];
210
+ }
211
+ ctx.output("entityRef", catalogModel.stringifyEntityRef(entity));
212
+ }
213
+ } catch (e) {
214
+ if (!input.optional) {
215
+ throw e;
216
+ }
217
+ }
218
+ ctx.output("catalogInfoUrl", catalogInfoUrl);
219
+ }
220
+ });
221
+ }
222
+
223
+ const examples$8 = [
224
+ {
225
+ description: "Write a catalog yaml file",
226
+ example: yaml__namespace.stringify({
227
+ steps: [
228
+ {
229
+ action: "catalog:write",
230
+ id: "create-catalog-info-file",
231
+ name: "Create catalog file",
232
+ input: {
233
+ entity: {
234
+ apiVersion: "backstage.io/v1alpha1",
235
+ kind: "Component",
236
+ metadata: {
237
+ name: "test",
238
+ annotations: {}
239
+ },
240
+ spec: {
241
+ type: "service",
242
+ lifecycle: "production",
243
+ owner: "default/owner"
244
+ }
245
+ }
246
+ }
247
+ }
248
+ ]
249
+ })
250
+ }
251
+ ];
252
+
253
+ const id$3 = "catalog:write";
254
+ function createCatalogWriteAction() {
255
+ return pluginScaffolderNode.createTemplateAction({
256
+ id: id$3,
257
+ description: "Writes the catalog-info.yaml for your template",
258
+ schema: {
259
+ input: zod.z.object({
260
+ filePath: zod.z.string().optional().describe("Defaults to catalog-info.yaml"),
261
+ // TODO: this should reference an zod entity validator if it existed.
262
+ entity: zod.z.record(zod.z.any()).describe(
263
+ "You can provide the same values used in the Entity schema."
264
+ )
265
+ })
266
+ },
267
+ examples: examples$8,
268
+ supportsDryRun: true,
269
+ async handler(ctx) {
270
+ ctx.logStream.write(`Writing catalog-info.yaml`);
271
+ const { filePath, entity } = ctx.input;
272
+ const path = filePath != null ? filePath : "catalog-info.yaml";
273
+ await fs__default["default"].writeFile(
274
+ backendCommon.resolveSafeChildPath(ctx.workspacePath, path),
275
+ yaml__namespace.stringify(entity)
276
+ );
277
+ }
278
+ });
279
+ }
280
+
281
+ const examples$7 = [
282
+ {
283
+ description: "Fetch entity by reference",
284
+ example: yaml__default["default"].stringify({
285
+ steps: [
286
+ {
287
+ action: "catalog:fetch",
288
+ id: "fetch",
289
+ name: "Fetch catalog entity",
290
+ input: {
291
+ entityRef: "component:default/name"
292
+ }
293
+ }
294
+ ]
295
+ })
296
+ },
297
+ {
298
+ description: "Fetch multiple entities by reference",
299
+ example: yaml__default["default"].stringify({
300
+ steps: [
301
+ {
302
+ action: "catalog:fetch",
303
+ id: "fetchMultiple",
304
+ name: "Fetch catalog entities",
305
+ input: {
306
+ entityRefs: ["component:default/name"]
307
+ }
308
+ }
309
+ ]
310
+ })
311
+ }
312
+ ];
313
+
314
+ const id$2 = "catalog:fetch";
315
+ function createFetchCatalogEntityAction(options) {
316
+ const { catalogClient } = options;
317
+ return pluginScaffolderNode.createTemplateAction({
318
+ id: id$2,
319
+ description: "Returns entity or entities from the catalog by entity reference(s)",
320
+ examples: examples$7,
321
+ supportsDryRun: true,
322
+ schema: {
323
+ input: zod.z.object({
324
+ entityRef: zod.z.string({
325
+ description: "Entity reference of the entity to get"
326
+ }).optional(),
327
+ entityRefs: zod.z.array(zod.z.string(), {
328
+ description: "Entity references of the entities to get"
329
+ }).optional(),
330
+ optional: zod.z.boolean({
331
+ description: "Allow the entity or entities to optionally exist. Default: false"
332
+ }).optional(),
333
+ defaultKind: zod.z.string({ description: "The default kind" }).optional(),
334
+ defaultNamespace: zod.z.string({ description: "The default namespace" }).optional()
335
+ }),
336
+ output: zod.z.object({
337
+ entity: zod.z.any({
338
+ description: "Object containing same values used in the Entity schema. Only when used with `entityRef` parameter."
339
+ }).optional(),
340
+ entities: zod.z.array(
341
+ zod.z.any({
342
+ description: "Array containing objects with same values used in the Entity schema. Only when used with `entityRefs` parameter."
343
+ })
344
+ ).optional()
345
+ })
346
+ },
347
+ async handler(ctx) {
348
+ var _a, _b;
349
+ const { entityRef, entityRefs, optional, defaultKind, defaultNamespace } = ctx.input;
350
+ if (!entityRef && !entityRefs) {
351
+ if (optional) {
352
+ return;
353
+ }
354
+ throw new Error("Missing entity reference or references");
355
+ }
356
+ if (entityRef) {
357
+ const entity = await catalogClient.getEntityByRef(
358
+ catalogModel.stringifyEntityRef(
359
+ catalogModel.parseEntityRef(entityRef, { defaultKind, defaultNamespace })
360
+ ),
361
+ {
362
+ token: (_a = ctx.secrets) == null ? void 0 : _a.backstageToken
363
+ }
364
+ );
365
+ if (!entity && !optional) {
366
+ throw new Error(`Entity ${entityRef} not found`);
367
+ }
368
+ ctx.output("entity", entity != null ? entity : null);
369
+ }
370
+ if (entityRefs) {
371
+ const entities = await catalogClient.getEntitiesByRefs(
372
+ {
373
+ entityRefs: entityRefs.map(
374
+ (ref) => catalogModel.stringifyEntityRef(
375
+ catalogModel.parseEntityRef(ref, { defaultKind, defaultNamespace })
376
+ )
377
+ )
378
+ },
379
+ {
380
+ token: (_b = ctx.secrets) == null ? void 0 : _b.backstageToken
381
+ }
382
+ );
383
+ const finalEntities = entities.items.map((e, i) => {
384
+ if (!e && !optional) {
385
+ throw new Error(`Entity ${entityRefs[i]} not found`);
386
+ }
387
+ return e != null ? e : null;
388
+ });
389
+ ctx.output("entities", finalEntities);
390
+ }
391
+ }
392
+ });
393
+ }
394
+
395
+ const examples$6 = [
396
+ {
397
+ description: "Write a debug message",
398
+ example: yaml__default["default"].stringify({
399
+ steps: [
400
+ {
401
+ action: "debug:log",
402
+ id: "write-debug-line",
403
+ name: 'Write "Hello Backstage!" log line',
404
+ input: {
405
+ message: "Hello Backstage!"
406
+ }
407
+ }
408
+ ]
409
+ })
410
+ },
411
+ {
412
+ description: "List the workspace directory",
413
+ example: yaml__default["default"].stringify({
414
+ steps: [
415
+ {
416
+ action: "debug:log",
417
+ id: "write-workspace-directory",
418
+ name: "List the workspace directory",
419
+ input: {
420
+ listWorkspace: true
421
+ }
422
+ }
423
+ ]
424
+ })
425
+ }
426
+ ];
427
+
428
+ const id$1 = "debug:log";
429
+ function createDebugLogAction() {
430
+ return pluginScaffolderNode.createTemplateAction({
431
+ id: id$1,
432
+ description: "Writes a message into the log or lists all files in the workspace.",
433
+ examples: examples$6,
434
+ schema: {
435
+ input: {
436
+ type: "object",
437
+ properties: {
438
+ message: {
439
+ title: "Message to output.",
440
+ type: "string"
441
+ },
442
+ listWorkspace: {
443
+ title: "List all files in the workspace, if true.",
444
+ type: "boolean"
445
+ },
446
+ extra: {
447
+ title: "Extra info"
448
+ }
449
+ }
450
+ }
451
+ },
452
+ supportsDryRun: true,
453
+ async handler(ctx) {
454
+ var _a, _b;
455
+ ctx.logger.info(JSON.stringify(ctx.input, null, 2));
456
+ if ((_a = ctx.input) == null ? void 0 : _a.message) {
457
+ ctx.logStream.write(ctx.input.message);
458
+ }
459
+ if ((_b = ctx.input) == null ? void 0 : _b.listWorkspace) {
460
+ const files = await recursiveReadDir(ctx.workspacePath);
461
+ ctx.logStream.write(
462
+ `Workspace:
463
+ ${files.map((f) => ` - ${path.relative(ctx.workspacePath, f)}`).join("\n")}`
464
+ );
465
+ }
466
+ }
467
+ });
468
+ }
469
+ async function recursiveReadDir(dir) {
470
+ const subdirs = await fs.readdir(dir);
471
+ const files = await Promise.all(
472
+ subdirs.map(async (subdir) => {
473
+ const res = path.join(dir, subdir);
474
+ return (await fs.stat(res)).isDirectory() ? recursiveReadDir(res) : [res];
475
+ })
476
+ );
477
+ return files.reduce((a, f) => a.concat(f), []);
478
+ }
479
+
480
+ const examples$5 = [
481
+ {
482
+ description: "Waiting for 50 milliseconds",
483
+ example: yaml__default["default"].stringify({
484
+ steps: [
485
+ {
486
+ action: "debug:wait",
487
+ id: "wait-milliseconds",
488
+ name: "Waiting for 50 milliseconds",
489
+ input: {
490
+ milliseconds: 50
491
+ }
492
+ }
493
+ ]
494
+ })
495
+ },
496
+ {
497
+ description: "Waiting for 5 seconds",
498
+ example: yaml__default["default"].stringify({
499
+ steps: [
500
+ {
501
+ action: "debug:wait",
502
+ id: "wait-5sec",
503
+ name: "Waiting for 5 seconds",
504
+ input: {
505
+ seconds: 5
506
+ }
507
+ }
508
+ ]
509
+ })
510
+ },
511
+ {
512
+ description: "Waiting for 1 minutes",
513
+ example: yaml__default["default"].stringify({
514
+ steps: [
515
+ {
516
+ action: "debug:wait",
517
+ id: "wait-1min",
518
+ name: "Waiting for 1 minutes",
519
+ input: {
520
+ minutes: 1
521
+ }
522
+ }
523
+ ]
524
+ })
525
+ }
526
+ ];
527
+
528
+ const id = "debug:wait";
529
+ const MAX_WAIT_TIME_IN_ISO = "T00:00:30";
530
+ function createWaitAction(options) {
531
+ const toDuration = (maxWaitTime) => {
532
+ if (maxWaitTime) {
533
+ if (maxWaitTime instanceof luxon.Duration) {
534
+ return maxWaitTime;
535
+ }
536
+ return luxon.Duration.fromObject(maxWaitTime);
537
+ }
538
+ return luxon.Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO);
539
+ };
540
+ return pluginScaffolderNode.createTemplateAction({
541
+ id,
542
+ description: "Waits for a certain period of time.",
543
+ examples: examples$5,
544
+ schema: {
545
+ input: {
546
+ type: "object",
547
+ properties: {
548
+ minutes: {
549
+ title: "Waiting period in minutes.",
550
+ type: "number"
551
+ },
552
+ seconds: {
553
+ title: "Waiting period in seconds.",
554
+ type: "number"
555
+ },
556
+ milliseconds: {
557
+ title: "Waiting period in milliseconds.",
558
+ type: "number"
559
+ }
560
+ }
561
+ }
562
+ },
563
+ async handler(ctx) {
564
+ const delayTime = luxon.Duration.fromObject(ctx.input);
565
+ const maxWait = toDuration(options == null ? void 0 : options.maxWaitTime);
566
+ if (delayTime.minus(maxWait).toMillis() > 0) {
567
+ throw new Error(
568
+ `Waiting duration is longer than the maximum threshold of ${maxWait.toHuman()}`
569
+ );
570
+ }
571
+ await new Promise((resolve) => {
572
+ var _a;
573
+ const controller = new AbortController();
574
+ const timeoutHandle = setTimeout(abort, delayTime.toMillis());
575
+ (_a = ctx.signal) == null ? void 0 : _a.addEventListener("abort", abort);
576
+ function abort() {
577
+ var _a2;
578
+ (_a2 = ctx.signal) == null ? void 0 : _a2.removeEventListener("abort", abort);
579
+ clearTimeout(timeoutHandle);
580
+ controller.abort();
581
+ resolve("finished");
582
+ }
583
+ });
584
+ }
585
+ });
586
+ }
587
+
588
+ const examples$4 = [
589
+ {
590
+ description: "Downloads content and places it in the workspace.",
591
+ example: yaml__default["default"].stringify({
592
+ steps: [
593
+ {
594
+ action: "fetch:plain",
595
+ id: "fetch-plain",
596
+ name: "Fetch plain",
597
+ input: {
598
+ url: "https://github.com/backstage/community/tree/main/backstage-community-sessions/assets"
599
+ }
600
+ }
601
+ ]
602
+ })
603
+ },
604
+ {
605
+ description: "Optionally, if you would prefer the data to be downloaded to a subdirectory in the workspace you may specify the \u2018targetPath\u2019 input option.",
606
+ example: yaml__default["default"].stringify({
607
+ steps: [
608
+ {
609
+ action: "fetch:plain",
610
+ id: "fetch-plain",
611
+ name: "Fetch plain",
612
+ input: {
613
+ url: "https://github.com/backstage/community/tree/main/backstage-community-sessions/assets",
614
+ targetPath: "fetched-data"
615
+ }
616
+ }
617
+ ]
618
+ })
619
+ }
620
+ ];
621
+
622
+ const ACTION_ID = "fetch:plain";
623
+ function createFetchPlainAction(options) {
624
+ const { reader, integrations } = options;
625
+ return pluginScaffolderNode.createTemplateAction({
626
+ id: ACTION_ID,
627
+ examples: examples$4,
628
+ description: "Downloads content and places it in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.",
629
+ schema: {
630
+ input: {
631
+ type: "object",
632
+ required: ["url"],
633
+ properties: {
634
+ url: {
635
+ title: "Fetch URL",
636
+ description: "Relative path or absolute URL pointing to the directory tree to fetch",
637
+ type: "string"
638
+ },
639
+ targetPath: {
640
+ title: "Target Path",
641
+ description: "Target path within the working directory to download the contents to.",
642
+ type: "string"
643
+ }
644
+ }
645
+ }
646
+ },
647
+ supportsDryRun: true,
648
+ async handler(ctx) {
649
+ var _a, _b;
650
+ ctx.logger.info("Fetching plain content from remote URL");
651
+ const targetPath = (_a = ctx.input.targetPath) != null ? _a : "./";
652
+ const outputPath = backendCommon.resolveSafeChildPath(ctx.workspacePath, targetPath);
653
+ await pluginScaffolderNode.fetchContents({
654
+ reader,
655
+ integrations,
656
+ baseUrl: (_b = ctx.templateInfo) == null ? void 0 : _b.baseUrl,
657
+ fetchUrl: ctx.input.url,
658
+ outputPath
659
+ });
660
+ }
661
+ });
662
+ }
663
+
664
+ const examples$3 = [
665
+ {
666
+ description: "Downloads a file and places it in the workspace.",
667
+ example: yaml__default["default"].stringify({
668
+ steps: [
669
+ {
670
+ action: "fetch:plain:file",
671
+ id: "fetch-plain-file",
672
+ name: "Fetch plain file",
673
+ input: {
674
+ url: "https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png",
675
+ targetPath: "target-path"
676
+ }
677
+ }
678
+ ]
679
+ })
680
+ }
681
+ ];
682
+
683
+ function createFetchPlainFileAction(options) {
684
+ const { reader, integrations } = options;
685
+ return pluginScaffolderNode.createTemplateAction({
686
+ id: "fetch:plain:file",
687
+ description: "Downloads single file and places it in the workspace.",
688
+ examples: examples$3,
689
+ schema: {
690
+ input: {
691
+ type: "object",
692
+ required: ["url", "targetPath"],
693
+ properties: {
694
+ url: {
695
+ title: "Fetch URL",
696
+ description: "Relative path or absolute URL pointing to the single file to fetch.",
697
+ type: "string"
698
+ },
699
+ targetPath: {
700
+ title: "Target Path",
701
+ description: "Target path within the working directory to download the file as.",
702
+ type: "string"
703
+ }
704
+ }
705
+ }
706
+ },
707
+ supportsDryRun: true,
708
+ async handler(ctx) {
709
+ var _a;
710
+ ctx.logger.info("Fetching plain content from remote URL");
711
+ const outputPath = backendCommon.resolveSafeChildPath(
712
+ ctx.workspacePath,
713
+ ctx.input.targetPath
714
+ );
715
+ await pluginScaffolderNode.fetchFile({
716
+ reader,
717
+ integrations,
718
+ baseUrl: (_a = ctx.templateInfo) == null ? void 0 : _a.baseUrl,
719
+ fetchUrl: ctx.input.url,
720
+ outputPath
721
+ });
722
+ }
723
+ });
724
+ }
725
+
726
+ const mkScript = (nunjucksSource) => `
727
+ const { render, renderCompat } = (() => {
728
+ const module = {};
729
+ const process = { env: {} };
730
+ const require = (pkg) => { if (pkg === 'events') { return function (){}; }};
731
+
732
+ ${nunjucksSource}
733
+
734
+ const env = module.exports.configure({
735
+ autoescape: false,
736
+ tags: {
737
+ variableStart: '\${{',
738
+ variableEnd: '}}',
739
+ },
740
+ });
741
+
742
+ const compatEnv = module.exports.configure({
743
+ autoescape: false,
744
+ tags: {
745
+ variableStart: '{{',
746
+ variableEnd: '}}',
747
+ },
748
+ });
749
+ compatEnv.addFilter('jsonify', compatEnv.getFilter('dump'));
750
+
751
+ for (const name of JSON.parse(availableTemplateFilters)) {
752
+ env.addFilter(name, (...args) => JSON.parse(callFilter(name, args)));
753
+ }
754
+ for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) {
755
+ env.addGlobal(name, value);
756
+ }
757
+ for (const name of JSON.parse(availableTemplateCallbacks)) {
758
+ env.addGlobal(name, (...args) => JSON.parse(callGlobal(name, args)));
759
+ }
760
+
761
+ let uninstallCompat = undefined;
762
+
763
+ function render(str, values) {
764
+ try {
765
+ if (uninstallCompat) {
766
+ uninstallCompat();
767
+ uninstallCompat = undefined;
768
+ }
769
+ return env.renderString(str, JSON.parse(values));
770
+ } catch (error) {
771
+ // Make sure errors don't leak anything
772
+ throw new Error(String(error.message));
773
+ }
774
+ }
775
+
776
+ function renderCompat(str, values) {
777
+ try {
778
+ if (!uninstallCompat) {
779
+ uninstallCompat = module.exports.installJinjaCompat();
780
+ }
781
+ return compatEnv.renderString(str, JSON.parse(values));
782
+ } catch (error) {
783
+ // Make sure errors don't leak anything
784
+ throw new Error(String(error.message));
785
+ }
786
+ }
787
+
788
+ return { render, renderCompat };
789
+ })();
790
+ `;
791
+ class SecureTemplater {
792
+ static async loadRenderer(options = {}) {
793
+ const {
794
+ cookiecutterCompat,
795
+ templateFilters = {},
796
+ templateGlobals = {}
797
+ } = options;
798
+ const isolate = new isolatedVm.Isolate({ memoryLimit: 128 });
799
+ const context = await isolate.createContext();
800
+ const contextGlobal = context.global;
801
+ const nunjucksSource = await fs__default["default"].readFile(
802
+ backendCommon.resolvePackagePath(
803
+ "@backstage/plugin-scaffolder-backend",
804
+ "assets/nunjucks.js.txt"
805
+ ),
806
+ "utf-8"
807
+ );
808
+ const nunjucksScript = await isolate.compileScript(
809
+ mkScript(nunjucksSource)
810
+ );
811
+ const availableFilters = Object.keys(templateFilters);
812
+ await contextGlobal.set(
813
+ "availableTemplateFilters",
814
+ JSON.stringify(availableFilters)
815
+ );
816
+ const globalCallbacks = [];
817
+ const globalValues = {};
818
+ for (const [name, value] of Object.entries(templateGlobals)) {
819
+ if (typeof value === "function") {
820
+ globalCallbacks.push(name);
821
+ } else {
822
+ globalValues[name] = value;
823
+ }
824
+ }
825
+ await contextGlobal.set(
826
+ "availableTemplateGlobals",
827
+ JSON.stringify(globalValues)
828
+ );
829
+ await contextGlobal.set(
830
+ "availableTemplateCallbacks",
831
+ JSON.stringify(globalCallbacks)
832
+ );
833
+ await contextGlobal.set(
834
+ "callFilter",
835
+ (filterName, args) => {
836
+ if (!Object.hasOwn(templateFilters, filterName)) {
837
+ return "";
838
+ }
839
+ return JSON.stringify(templateFilters[filterName](...args));
840
+ }
841
+ );
842
+ await contextGlobal.set(
843
+ "callGlobal",
844
+ (globalName, args) => {
845
+ if (!Object.hasOwn(templateGlobals, globalName)) {
846
+ return "";
847
+ }
848
+ const global = templateGlobals[globalName];
849
+ if (typeof global !== "function") {
850
+ return "";
851
+ }
852
+ return JSON.stringify(global(...args));
853
+ }
854
+ );
855
+ await nunjucksScript.run(context);
856
+ const render = (template, values) => {
857
+ if (!context) {
858
+ throw new Error("SecureTemplater has not been initialized");
859
+ }
860
+ contextGlobal.setSync("templateStr", String(template));
861
+ contextGlobal.setSync("templateValues", JSON.stringify(values));
862
+ if (cookiecutterCompat) {
863
+ return context.evalSync(`renderCompat(templateStr, templateValues)`);
864
+ }
865
+ return context.evalSync(`render(templateStr, templateValues)`);
866
+ };
867
+ return render;
868
+ }
869
+ }
870
+
871
+ const createDefaultFilters = ({
872
+ integrations
873
+ }) => {
874
+ return {
875
+ parseRepoUrl: (url) => pluginScaffolderNode.parseRepoUrl(url, integrations),
876
+ parseEntityRef: (ref, context) => catalogModel.parseEntityRef(ref, context),
877
+ pick: (obj, key) => get__default["default"](obj, key),
878
+ projectSlug: (repoUrl) => {
879
+ const { owner, repo } = pluginScaffolderNode.parseRepoUrl(repoUrl, integrations);
880
+ return `${owner}/${repo}`;
881
+ }
882
+ };
883
+ };
884
+
885
+ const examples$2 = [
886
+ {
887
+ description: "Downloads a skelaton directory that lives alongside the template file and fill it out with values.",
888
+ example: yaml__default["default"].stringify({
889
+ steps: [
890
+ {
891
+ action: "fetch:template",
892
+ id: "fetch-template",
893
+ name: "Fetch template",
894
+ input: {
895
+ url: "./skeleton",
896
+ targetPath: "./target",
897
+ values: {
898
+ name: "test-project",
899
+ count: 1234,
900
+ itemList: ["first", "second", "third"],
901
+ showDummyFile: false
902
+ }
903
+ }
904
+ }
905
+ ]
906
+ })
907
+ }
908
+ ];
909
+
910
+ function createFetchTemplateAction(options) {
911
+ const {
912
+ reader,
913
+ integrations,
914
+ additionalTemplateFilters,
915
+ additionalTemplateGlobals
916
+ } = options;
917
+ const defaultTemplateFilters = createDefaultFilters({ integrations });
918
+ return pluginScaffolderNode.createTemplateAction({
919
+ id: "fetch:template",
920
+ 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.",
921
+ examples: examples$2,
922
+ schema: {
923
+ input: {
924
+ type: "object",
925
+ required: ["url"],
926
+ properties: {
927
+ url: {
928
+ title: "Fetch URL",
929
+ description: "Relative path or absolute URL pointing to the directory tree to fetch",
930
+ type: "string"
931
+ },
932
+ targetPath: {
933
+ title: "Target Path",
934
+ description: "Target path within the working directory to download the contents to. Defaults to the working directory root.",
935
+ type: "string"
936
+ },
937
+ values: {
938
+ title: "Template Values",
939
+ description: "Values to pass on to the templating engine",
940
+ type: "object"
941
+ },
942
+ copyWithoutRender: {
943
+ title: "[Deprecated] Copy Without Render",
944
+ description: "An array of glob patterns. Any files or directories which match are copied without being processed as templates.",
945
+ type: "array",
946
+ items: {
947
+ type: "string"
948
+ }
949
+ },
950
+ copyWithoutTemplating: {
951
+ title: "Copy Without Templating",
952
+ description: "An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.",
953
+ type: "array",
954
+ items: {
955
+ type: "string"
956
+ }
957
+ },
958
+ cookiecutterCompat: {
959
+ title: "Cookiecutter compatibility mode",
960
+ description: "Enable features to maximise compatibility with templates built for fetch:cookiecutter",
961
+ type: "boolean"
962
+ },
963
+ templateFileExtension: {
964
+ title: "Template File Extension",
965
+ description: "If set, only files with the given extension will be templated. If set to `true`, the default extension `.njk` is used.",
966
+ type: ["string", "boolean"]
967
+ },
968
+ replace: {
969
+ title: "Replace files",
970
+ description: "If set, replace files in targetPath instead of skipping existing ones.",
971
+ type: "boolean"
972
+ }
973
+ }
974
+ }
975
+ },
976
+ supportsDryRun: true,
977
+ async handler(ctx) {
978
+ var _a, _b;
979
+ ctx.logger.info("Fetching template content from remote URL");
980
+ const workDir = await ctx.createTemporaryDirectory();
981
+ const templateDir = backendCommon.resolveSafeChildPath(workDir, "template");
982
+ const targetPath = (_a = ctx.input.targetPath) != null ? _a : "./";
983
+ const outputDir = backendCommon.resolveSafeChildPath(ctx.workspacePath, targetPath);
984
+ if (ctx.input.copyWithoutRender && ctx.input.copyWithoutTemplating) {
985
+ throw new errors.InputError(
986
+ "Fetch action input copyWithoutRender and copyWithoutTemplating can not be used at the same time"
987
+ );
988
+ }
989
+ let copyOnlyPatterns;
990
+ let renderFilename;
991
+ if (ctx.input.copyWithoutRender) {
992
+ ctx.logger.warn(
993
+ "[Deprecated] copyWithoutRender is deprecated Please use copyWithoutTemplating instead."
994
+ );
995
+ copyOnlyPatterns = ctx.input.copyWithoutRender;
996
+ renderFilename = false;
997
+ } else {
998
+ copyOnlyPatterns = ctx.input.copyWithoutTemplating;
999
+ renderFilename = true;
1000
+ }
1001
+ if (copyOnlyPatterns && !Array.isArray(copyOnlyPatterns)) {
1002
+ throw new errors.InputError(
1003
+ "Fetch action input copyWithoutRender/copyWithoutTemplating must be an Array"
1004
+ );
1005
+ }
1006
+ if (ctx.input.templateFileExtension && (copyOnlyPatterns || ctx.input.cookiecutterCompat)) {
1007
+ throw new errors.InputError(
1008
+ "Fetch action input extension incompatible with copyWithoutRender/copyWithoutTemplating and cookiecutterCompat"
1009
+ );
1010
+ }
1011
+ let extension = false;
1012
+ if (ctx.input.templateFileExtension) {
1013
+ extension = ctx.input.templateFileExtension === true ? ".njk" : ctx.input.templateFileExtension;
1014
+ if (!extension.startsWith(".")) {
1015
+ extension = `.${extension}`;
1016
+ }
1017
+ }
1018
+ await pluginScaffolderNode.fetchContents({
1019
+ reader,
1020
+ integrations,
1021
+ baseUrl: (_b = ctx.templateInfo) == null ? void 0 : _b.baseUrl,
1022
+ fetchUrl: ctx.input.url,
1023
+ outputPath: templateDir
1024
+ });
1025
+ ctx.logger.info("Listing files and directories in template");
1026
+ const allEntriesInTemplate = await globby__default["default"](`**/*`, {
1027
+ cwd: templateDir,
1028
+ dot: true,
1029
+ onlyFiles: false,
1030
+ markDirectories: true,
1031
+ followSymbolicLinks: false
1032
+ });
1033
+ const nonTemplatedEntries = new Set(
1034
+ await globby__default["default"](copyOnlyPatterns || [], {
1035
+ cwd: templateDir,
1036
+ dot: true,
1037
+ onlyFiles: false,
1038
+ markDirectories: true,
1039
+ followSymbolicLinks: false
1040
+ })
1041
+ );
1042
+ const { cookiecutterCompat, values } = ctx.input;
1043
+ const context = {
1044
+ [cookiecutterCompat ? "cookiecutter" : "values"]: values
1045
+ };
1046
+ ctx.logger.info(
1047
+ `Processing ${allEntriesInTemplate.length} template files/directories with input values`,
1048
+ ctx.input.values
1049
+ );
1050
+ const renderTemplate = await SecureTemplater.loadRenderer({
1051
+ cookiecutterCompat: ctx.input.cookiecutterCompat,
1052
+ templateFilters: {
1053
+ ...defaultTemplateFilters,
1054
+ ...additionalTemplateFilters
1055
+ },
1056
+ templateGlobals: additionalTemplateGlobals
1057
+ });
1058
+ for (const location of allEntriesInTemplate) {
1059
+ let renderContents;
1060
+ let localOutputPath = location;
1061
+ if (extension) {
1062
+ renderContents = path.extname(localOutputPath) === extension;
1063
+ if (renderContents) {
1064
+ localOutputPath = localOutputPath.slice(0, -extension.length);
1065
+ }
1066
+ localOutputPath = renderTemplate(localOutputPath, context);
1067
+ } else {
1068
+ renderContents = !nonTemplatedEntries.has(location);
1069
+ if (renderFilename) {
1070
+ localOutputPath = renderTemplate(localOutputPath, context);
1071
+ } else {
1072
+ localOutputPath = renderContents ? renderTemplate(localOutputPath, context) : localOutputPath;
1073
+ }
1074
+ }
1075
+ if (containsSkippedContent(localOutputPath)) {
1076
+ continue;
1077
+ }
1078
+ const outputPath = backendCommon.resolveSafeChildPath(outputDir, localOutputPath);
1079
+ if (fs__default["default"].existsSync(outputPath) && !ctx.input.replace) {
1080
+ continue;
1081
+ }
1082
+ if (!renderContents && !extension) {
1083
+ ctx.logger.info(
1084
+ `Copying file/directory ${location} without processing.`
1085
+ );
1086
+ }
1087
+ if (location.endsWith("/")) {
1088
+ ctx.logger.info(
1089
+ `Writing directory ${location} to template output path.`
1090
+ );
1091
+ await fs__default["default"].ensureDir(outputPath);
1092
+ } else {
1093
+ const inputFilePath = backendCommon.resolveSafeChildPath(templateDir, location);
1094
+ const stats = await fs__default["default"].promises.lstat(inputFilePath);
1095
+ if (stats.isSymbolicLink() || await isbinaryfile.isBinaryFile(inputFilePath)) {
1096
+ ctx.logger.info(
1097
+ `Copying file binary or symbolic link at ${location}, to template output path.`
1098
+ );
1099
+ await fs__default["default"].copy(inputFilePath, outputPath);
1100
+ } else {
1101
+ const statsObj = await fs__default["default"].stat(inputFilePath);
1102
+ ctx.logger.info(
1103
+ `Writing file ${location} to template output path with mode ${statsObj.mode}.`
1104
+ );
1105
+ const inputFileContents = await fs__default["default"].readFile(inputFilePath, "utf-8");
1106
+ await fs__default["default"].outputFile(
1107
+ outputPath,
1108
+ renderContents ? renderTemplate(inputFileContents, context) : inputFileContents,
1109
+ { mode: statsObj.mode }
1110
+ );
1111
+ }
1112
+ }
1113
+ }
1114
+ ctx.logger.info(`Template result written to ${outputDir}`);
1115
+ }
1116
+ });
1117
+ }
1118
+ function containsSkippedContent(localOutputPath) {
1119
+ return localOutputPath === "" || localOutputPath.startsWith("/") || localOutputPath.includes("//");
1120
+ }
1121
+
1122
+ const examples$1 = [
1123
+ {
1124
+ description: "Delete specified files",
1125
+ example: yaml__namespace.stringify({
1126
+ steps: [
1127
+ {
1128
+ action: "fs:delete",
1129
+ id: "deleteFiles",
1130
+ name: "Delete files",
1131
+ input: {
1132
+ files: ["file1.txt", "file2.txt"]
1133
+ }
1134
+ }
1135
+ ]
1136
+ })
1137
+ }
1138
+ ];
1139
+
1140
+ const createFilesystemDeleteAction = () => {
1141
+ return pluginScaffolderNode.createTemplateAction({
1142
+ id: "fs:delete",
1143
+ description: "Deletes files and directories from the workspace",
1144
+ examples: examples$1,
1145
+ schema: {
1146
+ input: {
1147
+ required: ["files"],
1148
+ type: "object",
1149
+ properties: {
1150
+ files: {
1151
+ title: "Files",
1152
+ description: "A list of files and directories that will be deleted",
1153
+ type: "array",
1154
+ items: {
1155
+ type: "string"
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+ },
1161
+ supportsDryRun: true,
1162
+ async handler(ctx) {
1163
+ var _a;
1164
+ if (!Array.isArray((_a = ctx.input) == null ? void 0 : _a.files)) {
1165
+ throw new errors.InputError("files must be an Array");
1166
+ }
1167
+ for (const file of ctx.input.files) {
1168
+ const filepath = backendCommon.resolveSafeChildPath(ctx.workspacePath, file);
1169
+ try {
1170
+ await fs__default["default"].remove(filepath);
1171
+ ctx.logger.info(`File ${filepath} deleted successfully`);
1172
+ } catch (err) {
1173
+ ctx.logger.error(`Failed to delete file ${filepath}:`, err);
1174
+ throw err;
1175
+ }
1176
+ }
1177
+ }
1178
+ });
1179
+ };
1180
+
1181
+ const examples = [
1182
+ {
1183
+ description: "Rename specified files ",
1184
+ example: yaml__namespace.stringify({
1185
+ steps: [
1186
+ {
1187
+ action: "fs:rename",
1188
+ id: "renameFiles",
1189
+ name: "Rename files",
1190
+ input: {
1191
+ files: [
1192
+ { from: "file1.txt", to: "file1Renamed.txt" },
1193
+ { from: "file2.txt", to: "file2Renamed.txt" },
1194
+ { from: "file3.txt", to: "file3Renamed.txt", overwrite: true }
1195
+ ]
1196
+ }
1197
+ }
1198
+ ]
1199
+ })
1200
+ }
1201
+ ];
1202
+
1203
+ const createFilesystemRenameAction = () => {
1204
+ return pluginScaffolderNode.createTemplateAction({
1205
+ id: "fs:rename",
1206
+ description: "Renames files and directories within the workspace",
1207
+ examples,
1208
+ schema: {
1209
+ input: {
1210
+ required: ["files"],
1211
+ type: "object",
1212
+ properties: {
1213
+ files: {
1214
+ title: "Files",
1215
+ description: "A list of file and directory names that will be renamed",
1216
+ type: "array",
1217
+ items: {
1218
+ type: "object",
1219
+ required: ["from", "to"],
1220
+ properties: {
1221
+ from: {
1222
+ type: "string",
1223
+ title: "The source location of the file to be renamed"
1224
+ },
1225
+ to: {
1226
+ type: "string",
1227
+ title: "The destination of the new file"
1228
+ },
1229
+ overwrite: {
1230
+ type: "boolean",
1231
+ title: "Overwrite existing file or directory, default is false"
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+ },
1239
+ supportsDryRun: true,
1240
+ async handler(ctx) {
1241
+ var _a, _b;
1242
+ if (!Array.isArray((_a = ctx.input) == null ? void 0 : _a.files)) {
1243
+ throw new errors.InputError("files must be an Array");
1244
+ }
1245
+ for (const file of ctx.input.files) {
1246
+ if (!file.from || !file.to) {
1247
+ throw new errors.InputError("each file must have a from and to property");
1248
+ }
1249
+ const sourceFilepath = backendCommon.resolveSafeChildPath(
1250
+ ctx.workspacePath,
1251
+ file.from
1252
+ );
1253
+ const destFilepath = backendCommon.resolveSafeChildPath(ctx.workspacePath, file.to);
1254
+ try {
1255
+ await fs__default["default"].move(sourceFilepath, destFilepath, {
1256
+ overwrite: (_b = file.overwrite) != null ? _b : false
1257
+ });
1258
+ ctx.logger.info(
1259
+ `File ${sourceFilepath} renamed to ${destFilepath} successfully`
1260
+ );
1261
+ } catch (err) {
1262
+ ctx.logger.error(
1263
+ `Failed to rename file ${sourceFilepath} to ${destFilepath}:`,
1264
+ err
1265
+ );
1266
+ throw err;
1267
+ }
1268
+ }
1269
+ }
1270
+ });
1271
+ };
1272
+
1273
+ const createBuiltinActions = (options) => {
1274
+ const {
1275
+ reader,
1276
+ integrations,
1277
+ catalogClient,
1278
+ config,
1279
+ additionalTemplateFilters,
1280
+ additionalTemplateGlobals
1281
+ } = options;
1282
+ const githubCredentialsProvider = integration.DefaultGithubCredentialsProvider.fromIntegrations(integrations);
1283
+ const actions = [
1284
+ createFetchPlainAction({
1285
+ reader,
1286
+ integrations
1287
+ }),
1288
+ createFetchPlainFileAction({
1289
+ reader,
1290
+ integrations
1291
+ }),
1292
+ createFetchTemplateAction({
1293
+ integrations,
1294
+ reader,
1295
+ additionalTemplateFilters,
1296
+ additionalTemplateGlobals
1297
+ }),
1298
+ gerrit.createPublishGerritAction({
1299
+ integrations,
1300
+ config
1301
+ }),
1302
+ gerrit.createPublishGerritReviewAction({
1303
+ integrations,
1304
+ config
1305
+ }),
1306
+ github.createPublishGithubAction({
1307
+ integrations,
1308
+ config,
1309
+ githubCredentialsProvider
1310
+ }),
1311
+ github.createPublishGithubPullRequestAction({
1312
+ integrations,
1313
+ githubCredentialsProvider
1314
+ }),
1315
+ gitlab.createPublishGitlabAction({
1316
+ integrations,
1317
+ config
1318
+ }),
1319
+ gitlab.createPublishGitlabMergeRequestAction({
1320
+ integrations
1321
+ }),
1322
+ bitbucket.createPublishBitbucketAction({
1323
+ integrations,
1324
+ config
1325
+ }),
1326
+ bitbucket.createPublishBitbucketCloudAction({
1327
+ integrations,
1328
+ config
1329
+ }),
1330
+ bitbucket.createPublishBitbucketServerAction({
1331
+ integrations,
1332
+ config
1333
+ }),
1334
+ bitbucket.createPublishBitbucketServerPullRequestAction({
1335
+ integrations,
1336
+ config
1337
+ }),
1338
+ azure.createPublishAzureAction({
1339
+ integrations,
1340
+ config
1341
+ }),
1342
+ createDebugLogAction(),
1343
+ createWaitAction(),
1344
+ createCatalogRegisterAction({ catalogClient, integrations }),
1345
+ createFetchCatalogEntityAction({ catalogClient }),
1346
+ createCatalogWriteAction(),
1347
+ createFilesystemDeleteAction(),
1348
+ createFilesystemRenameAction(),
1349
+ github.createGithubActionsDispatchAction({
1350
+ integrations,
1351
+ githubCredentialsProvider
1352
+ }),
1353
+ github.createGithubWebhookAction({
1354
+ integrations,
1355
+ githubCredentialsProvider
1356
+ }),
1357
+ github.createGithubIssuesLabelAction({
1358
+ integrations,
1359
+ githubCredentialsProvider
1360
+ }),
1361
+ github.createGithubRepoCreateAction({
1362
+ integrations,
1363
+ githubCredentialsProvider
1364
+ }),
1365
+ github.createGithubRepoPushAction({
1366
+ integrations,
1367
+ config,
1368
+ githubCredentialsProvider
1369
+ }),
1370
+ github.createGithubEnvironmentAction({
1371
+ integrations
1372
+ }),
1373
+ github.createGithubDeployKeyAction({
1374
+ integrations
1375
+ })
1376
+ ];
1377
+ return actions;
1378
+ };
1379
+
1380
+ var __defProp$4 = Object.defineProperty;
1381
+ var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1382
+ var __publicField$4 = (obj, key, value) => {
1383
+ __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
1384
+ return value;
1385
+ };
1386
+ class TemplateActionRegistry {
1387
+ constructor() {
1388
+ __publicField$4(this, "actions", /* @__PURE__ */ new Map());
1389
+ }
1390
+ register(action) {
1391
+ if (this.actions.has(action.id)) {
1392
+ throw new errors.ConflictError(
1393
+ `Template action with ID '${action.id}' has already been registered`
1394
+ );
1395
+ }
1396
+ this.actions.set(action.id, action);
1397
+ }
1398
+ get(actionId) {
1399
+ const action = this.actions.get(actionId);
1400
+ if (!action) {
1401
+ throw new errors.NotFoundError(
1402
+ `Template action with ID '${actionId}' is not registered.`
1403
+ );
1404
+ }
1405
+ return action;
1406
+ }
1407
+ list() {
1408
+ return [...this.actions.values()];
1409
+ }
1410
+ }
1411
+
1412
+ var __defProp$3 = Object.defineProperty;
1413
+ var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1414
+ var __publicField$3 = (obj, key, value) => {
1415
+ __defNormalProp$3(obj, typeof key !== "symbol" ? key + "" : key, value);
1416
+ return value;
1417
+ };
1418
+ const migrationsDir = backendCommon.resolvePackagePath(
1419
+ "@backstage/plugin-scaffolder-backend",
1420
+ "migrations"
1421
+ );
1422
+ function isPluginDatabaseManager(opt) {
1423
+ return opt.getClient !== void 0;
1424
+ }
1425
+ const parseSqlDateToIsoString = (input) => {
1426
+ if (typeof input === "string") {
1427
+ const parsed = luxon.DateTime.fromSQL(input, { zone: "UTC" });
1428
+ if (!parsed.isValid) {
1429
+ throw new Error(
1430
+ `Failed to parse database timestamp '${input}', ${parsed.invalidReason}: ${parsed.invalidExplanation}`
1431
+ );
1432
+ }
1433
+ return parsed.toISO();
1434
+ }
1435
+ return input;
1436
+ };
1437
+ class DatabaseTaskStore {
1438
+ constructor(client) {
1439
+ __publicField$3(this, "db");
1440
+ this.db = client;
1441
+ }
1442
+ static async create(options) {
1443
+ const { database } = options;
1444
+ const client = await this.getClient(database);
1445
+ await this.runMigrations(database, client);
1446
+ return new DatabaseTaskStore(client);
1447
+ }
1448
+ static async getClient(database) {
1449
+ if (isPluginDatabaseManager(database)) {
1450
+ return database.getClient();
1451
+ }
1452
+ return database;
1453
+ }
1454
+ static async runMigrations(database, client) {
1455
+ var _a;
1456
+ if (!isPluginDatabaseManager(database)) {
1457
+ await client.migrate.latest({
1458
+ directory: migrationsDir
1459
+ });
1460
+ return;
1461
+ }
1462
+ if (!((_a = database.migrations) == null ? void 0 : _a.skip)) {
1463
+ await client.migrate.latest({
1464
+ directory: migrationsDir
1465
+ });
1466
+ }
1467
+ }
1468
+ async list(options) {
1469
+ const queryBuilder = this.db("tasks");
1470
+ if (options.createdBy) {
1471
+ queryBuilder.where({
1472
+ created_by: options.createdBy
1473
+ });
1474
+ }
1475
+ const results = await queryBuilder.orderBy("created_at", "desc").select();
1476
+ const tasks = results.map((result) => {
1477
+ var _a;
1478
+ return {
1479
+ id: result.id,
1480
+ spec: JSON.parse(result.spec),
1481
+ status: result.status,
1482
+ createdBy: (_a = result.created_by) != null ? _a : void 0,
1483
+ lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
1484
+ createdAt: parseSqlDateToIsoString(result.created_at)
1485
+ };
1486
+ });
1487
+ return { tasks };
1488
+ }
1489
+ async getTask(taskId) {
1490
+ var _a;
1491
+ const [result] = await this.db("tasks").where({ id: taskId }).select();
1492
+ if (!result) {
1493
+ throw new errors.NotFoundError(`No task with id '${taskId}' found`);
1494
+ }
1495
+ try {
1496
+ const spec = JSON.parse(result.spec);
1497
+ const secrets = result.secrets ? JSON.parse(result.secrets) : void 0;
1498
+ return {
1499
+ id: result.id,
1500
+ spec,
1501
+ status: result.status,
1502
+ lastHeartbeatAt: parseSqlDateToIsoString(result.last_heartbeat_at),
1503
+ createdAt: parseSqlDateToIsoString(result.created_at),
1504
+ createdBy: (_a = result.created_by) != null ? _a : void 0,
1505
+ secrets
1506
+ };
1507
+ } catch (error) {
1508
+ throw new Error(`Failed to parse spec of task '${taskId}', ${error}`);
1509
+ }
1510
+ }
1511
+ async createTask(options) {
1512
+ var _a;
1513
+ const taskId = uuid.v4();
1514
+ await this.db("tasks").insert({
1515
+ id: taskId,
1516
+ spec: JSON.stringify(options.spec),
1517
+ secrets: options.secrets ? JSON.stringify(options.secrets) : void 0,
1518
+ created_by: (_a = options.createdBy) != null ? _a : null,
1519
+ status: "open"
1520
+ });
1521
+ return { taskId };
1522
+ }
1523
+ async claimTask() {
1524
+ return this.db.transaction(async (tx) => {
1525
+ var _a;
1526
+ const [task] = await tx("tasks").where({
1527
+ status: "open"
1528
+ }).limit(1).select();
1529
+ if (!task) {
1530
+ return void 0;
1531
+ }
1532
+ const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
1533
+ status: "processing",
1534
+ last_heartbeat_at: this.db.fn.now(),
1535
+ // remove the secrets when moving to processing state.
1536
+ secrets: null
1537
+ });
1538
+ if (updateCount < 1) {
1539
+ return void 0;
1540
+ }
1541
+ try {
1542
+ const spec = JSON.parse(task.spec);
1543
+ const secrets = task.secrets ? JSON.parse(task.secrets) : void 0;
1544
+ return {
1545
+ id: task.id,
1546
+ spec,
1547
+ status: "processing",
1548
+ lastHeartbeatAt: task.last_heartbeat_at,
1549
+ createdAt: task.created_at,
1550
+ createdBy: (_a = task.created_by) != null ? _a : void 0,
1551
+ secrets
1552
+ };
1553
+ } catch (error) {
1554
+ throw new Error(`Failed to parse spec of task '${task.id}', ${error}`);
1555
+ }
1556
+ });
1557
+ }
1558
+ async heartbeatTask(taskId) {
1559
+ const updateCount = await this.db("tasks").where({ id: taskId, status: "processing" }).update({
1560
+ last_heartbeat_at: this.db.fn.now()
1561
+ });
1562
+ if (updateCount === 0) {
1563
+ throw new errors.ConflictError(`No running task with taskId ${taskId} found`);
1564
+ }
1565
+ }
1566
+ async listStaleTasks(options) {
1567
+ const { timeoutS } = options;
1568
+ let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [
1569
+ this.db.fn.now()
1570
+ ]);
1571
+ if (this.db.client.config.client.includes("mysql")) {
1572
+ heartbeatInterval = this.db.raw(
1573
+ `date_sub(now(), interval ${timeoutS} second)`
1574
+ );
1575
+ } else if (this.db.client.config.client.includes("sqlite3")) {
1576
+ heartbeatInterval = this.db.raw(`datetime('now', ?)`, [
1577
+ `-${timeoutS} seconds`
1578
+ ]);
1579
+ }
1580
+ const rawRows = await this.db("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval);
1581
+ const tasks = rawRows.map((row) => ({
1582
+ taskId: row.id
1583
+ }));
1584
+ return { tasks };
1585
+ }
1586
+ async completeTask(options) {
1587
+ const { taskId, status, eventBody } = options;
1588
+ let oldStatus;
1589
+ if (["failed", "completed", "cancelled"].includes(status)) {
1590
+ oldStatus = "processing";
1591
+ } else {
1592
+ throw new Error(
1593
+ `Invalid status update of run '${taskId}' to status '${status}'`
1594
+ );
1595
+ }
1596
+ await this.db.transaction(async (tx) => {
1597
+ const [task] = await tx("tasks").where({
1598
+ id: taskId
1599
+ }).limit(1).select();
1600
+ const updateTask = async (criteria) => {
1601
+ const updateCount = await tx("tasks").where(criteria).update({
1602
+ status
1603
+ });
1604
+ if (updateCount !== 1) {
1605
+ throw new errors.ConflictError(
1606
+ `Failed to update status to '${status}' for taskId ${taskId}`
1607
+ );
1608
+ }
1609
+ await tx("task_events").insert({
1610
+ task_id: taskId,
1611
+ event_type: "completion",
1612
+ body: JSON.stringify(eventBody)
1613
+ });
1614
+ };
1615
+ if (status === "cancelled") {
1616
+ await updateTask({
1617
+ id: taskId
1618
+ });
1619
+ return;
1620
+ }
1621
+ if (task.status === "cancelled") {
1622
+ return;
1623
+ }
1624
+ if (!task) {
1625
+ throw new Error(`No task with taskId ${taskId} found`);
1626
+ }
1627
+ if (task.status !== oldStatus) {
1628
+ throw new errors.ConflictError(
1629
+ `Refusing to update status of run '${taskId}' to status '${status}' as it is currently '${task.status}', expected '${oldStatus}'`
1630
+ );
1631
+ }
1632
+ await updateTask({
1633
+ id: taskId,
1634
+ status: oldStatus
1635
+ });
1636
+ });
1637
+ }
1638
+ async emitLogEvent(options) {
1639
+ const { taskId, body } = options;
1640
+ const serializedBody = JSON.stringify(body);
1641
+ await this.db("task_events").insert({
1642
+ task_id: taskId,
1643
+ event_type: "log",
1644
+ body: serializedBody
1645
+ });
1646
+ }
1647
+ async listEvents(options) {
1648
+ const { taskId, after } = options;
1649
+ const rawEvents = await this.db("task_events").where({
1650
+ task_id: taskId
1651
+ }).andWhere((builder) => {
1652
+ if (typeof after === "number") {
1653
+ builder.where("id", ">", after).orWhere("event_type", "completion");
1654
+ }
1655
+ }).orderBy("id").select();
1656
+ const events = rawEvents.map((event) => {
1657
+ try {
1658
+ const body = JSON.parse(event.body);
1659
+ return {
1660
+ id: Number(event.id),
1661
+ taskId,
1662
+ body,
1663
+ type: event.event_type,
1664
+ createdAt: parseSqlDateToIsoString(event.created_at)
1665
+ };
1666
+ } catch (error) {
1667
+ throw new Error(
1668
+ `Failed to parse event body from event taskId=${taskId} id=${event.id}, ${error}`
1669
+ );
1670
+ }
1671
+ });
1672
+ return { events };
1673
+ }
1674
+ async shutdownTask(options) {
1675
+ const { taskId } = options;
1676
+ const message = `This task was marked as stale as it exceeded its timeout`;
1677
+ const statusStepEvents = (await this.listEvents({ taskId })).events.filter(
1678
+ ({ body }) => body == null ? void 0 : body.stepId
1679
+ );
1680
+ const completedSteps = statusStepEvents.filter(
1681
+ ({ body: { status } }) => status === "failed" || status === "completed"
1682
+ ).map((step) => step.body.stepId);
1683
+ const hungProcessingSteps = statusStepEvents.filter(({ body: { status } }) => status === "processing").map((event) => event.body.stepId).filter((step) => !completedSteps.includes(step));
1684
+ for (const step of hungProcessingSteps) {
1685
+ await this.emitLogEvent({
1686
+ taskId,
1687
+ body: {
1688
+ message,
1689
+ stepId: step,
1690
+ status: "failed"
1691
+ }
1692
+ });
1693
+ }
1694
+ await this.completeTask({
1695
+ taskId,
1696
+ status: "failed",
1697
+ eventBody: {
1698
+ message
1699
+ }
1700
+ });
1701
+ }
1702
+ async cancelTask(options) {
1703
+ const { taskId, body } = options;
1704
+ const serializedBody = JSON.stringify(body);
1705
+ await this.db("task_events").insert({
1706
+ task_id: taskId,
1707
+ event_type: "cancelled",
1708
+ body: serializedBody
1709
+ });
1710
+ }
1711
+ }
1712
+
1713
+ var __defProp$2 = Object.defineProperty;
1714
+ var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1715
+ var __publicField$2 = (obj, key, value) => {
1716
+ __defNormalProp$2(obj, typeof key !== "symbol" ? key + "" : key, value);
1717
+ return value;
1718
+ };
1719
+ class TaskManager {
1720
+ // Runs heartbeat internally
1721
+ constructor(task, storage, signal, logger) {
1722
+ this.task = task;
1723
+ this.storage = storage;
1724
+ this.signal = signal;
1725
+ this.logger = logger;
1726
+ __publicField$2(this, "isDone", false);
1727
+ __publicField$2(this, "heartbeatTimeoutId");
1728
+ }
1729
+ static create(task, storage, abortSignal, logger) {
1730
+ const agent = new TaskManager(task, storage, abortSignal, logger);
1731
+ agent.startTimeout();
1732
+ return agent;
1733
+ }
1734
+ get spec() {
1735
+ return this.task.spec;
1736
+ }
1737
+ get cancelSignal() {
1738
+ return this.signal;
1739
+ }
1740
+ get secrets() {
1741
+ return this.task.secrets;
1742
+ }
1743
+ get createdBy() {
1744
+ return this.task.createdBy;
1745
+ }
1746
+ async getWorkspaceName() {
1747
+ return this.task.taskId;
1748
+ }
1749
+ get done() {
1750
+ return this.isDone;
1751
+ }
1752
+ async emitLog(message, logMetadata) {
1753
+ await this.storage.emitLogEvent({
1754
+ taskId: this.task.taskId,
1755
+ body: { message, ...logMetadata }
1756
+ });
1757
+ }
1758
+ async complete(result, metadata) {
1759
+ await this.storage.completeTask({
1760
+ taskId: this.task.taskId,
1761
+ status: result === "failed" ? "failed" : "completed",
1762
+ eventBody: {
1763
+ message: `Run completed with status: ${result}`,
1764
+ ...metadata
1765
+ }
1766
+ });
1767
+ this.isDone = true;
1768
+ if (this.heartbeatTimeoutId) {
1769
+ clearTimeout(this.heartbeatTimeoutId);
1770
+ }
1771
+ }
1772
+ startTimeout() {
1773
+ this.heartbeatTimeoutId = setTimeout(async () => {
1774
+ try {
1775
+ await this.storage.heartbeatTask(this.task.taskId);
1776
+ this.startTimeout();
1777
+ } catch (error) {
1778
+ this.isDone = true;
1779
+ this.logger.error(
1780
+ `Heartbeat for task ${this.task.taskId} failed`,
1781
+ error
1782
+ );
1783
+ }
1784
+ }, 1e3);
1785
+ }
1786
+ }
1787
+ function defer() {
1788
+ let resolve = () => {
1789
+ };
1790
+ const promise = new Promise((_resolve) => {
1791
+ resolve = _resolve;
1792
+ });
1793
+ return { promise, resolve };
1794
+ }
1795
+ class StorageTaskBroker {
1796
+ constructor(storage, logger) {
1797
+ this.storage = storage;
1798
+ this.logger = logger;
1799
+ __publicField$2(this, "deferredDispatch", defer());
1800
+ }
1801
+ async list(options) {
1802
+ if (!this.storage.list) {
1803
+ throw new Error(
1804
+ "TaskStore does not implement the list method. Please implement the list method to be able to list tasks"
1805
+ );
1806
+ }
1807
+ return await this.storage.list({ createdBy: options == null ? void 0 : options.createdBy });
1808
+ }
1809
+ async registerCancellable(taskId, abortController) {
1810
+ let shouldUnsubscribe = false;
1811
+ const subscription = this.event$({ taskId, after: void 0 }).subscribe({
1812
+ error: (_) => {
1813
+ subscription.unsubscribe();
1814
+ },
1815
+ next: ({ events }) => {
1816
+ for (const event of events) {
1817
+ if (event.type === "cancelled") {
1818
+ abortController.abort();
1819
+ shouldUnsubscribe = true;
1820
+ }
1821
+ if (event.type === "completion") {
1822
+ shouldUnsubscribe = true;
1823
+ }
1824
+ }
1825
+ if (shouldUnsubscribe) {
1826
+ subscription.unsubscribe();
1827
+ }
1828
+ }
1829
+ });
1830
+ }
1831
+ /**
1832
+ * {@inheritdoc TaskBroker.claim}
1833
+ */
1834
+ async claim() {
1835
+ for (; ; ) {
1836
+ const pendingTask = await this.storage.claimTask();
1837
+ if (pendingTask) {
1838
+ const abortController = new AbortController();
1839
+ await this.registerCancellable(pendingTask.id, abortController);
1840
+ return TaskManager.create(
1841
+ {
1842
+ taskId: pendingTask.id,
1843
+ spec: pendingTask.spec,
1844
+ secrets: pendingTask.secrets,
1845
+ createdBy: pendingTask.createdBy
1846
+ },
1847
+ this.storage,
1848
+ abortController.signal,
1849
+ this.logger
1850
+ );
1851
+ }
1852
+ await this.waitForDispatch();
1853
+ }
1854
+ }
1855
+ /**
1856
+ * {@inheritdoc TaskBroker.dispatch}
1857
+ */
1858
+ async dispatch(options) {
1859
+ const taskRow = await this.storage.createTask(options);
1860
+ this.signalDispatch();
1861
+ return {
1862
+ taskId: taskRow.taskId
1863
+ };
1864
+ }
1865
+ /**
1866
+ * {@inheritdoc TaskBroker.get}
1867
+ */
1868
+ async get(taskId) {
1869
+ return this.storage.getTask(taskId);
1870
+ }
1871
+ /**
1872
+ * {@inheritdoc TaskBroker.event$}
1873
+ */
1874
+ event$(options) {
1875
+ return new ObservableImpl__default["default"]((observer) => {
1876
+ const { taskId } = options;
1877
+ let after = options.after;
1878
+ let cancelled = false;
1879
+ (async () => {
1880
+ while (!cancelled) {
1881
+ const result = await this.storage.listEvents({ taskId, after });
1882
+ const { events } = result;
1883
+ if (events.length) {
1884
+ after = events[events.length - 1].id;
1885
+ observer.next(result);
1886
+ }
1887
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1888
+ }
1889
+ })();
1890
+ return () => {
1891
+ cancelled = true;
1892
+ };
1893
+ });
1894
+ }
1895
+ /**
1896
+ * {@inheritdoc TaskBroker.vacuumTasks}
1897
+ */
1898
+ async vacuumTasks(options) {
1899
+ const { tasks } = await this.storage.listStaleTasks(options);
1900
+ await Promise.all(
1901
+ tasks.map(async (task) => {
1902
+ try {
1903
+ await this.storage.completeTask({
1904
+ taskId: task.taskId,
1905
+ status: "failed",
1906
+ eventBody: {
1907
+ message: "The task was cancelled because the task worker lost connection to the task broker"
1908
+ }
1909
+ });
1910
+ } catch (error) {
1911
+ this.logger.warn(`Failed to cancel task '${task.taskId}', ${error}`);
1912
+ }
1913
+ })
1914
+ );
1915
+ }
1916
+ waitForDispatch() {
1917
+ return this.deferredDispatch.promise;
1918
+ }
1919
+ signalDispatch() {
1920
+ this.deferredDispatch.resolve();
1921
+ this.deferredDispatch = defer();
1922
+ }
1923
+ async cancel(taskId) {
1924
+ var _a, _b;
1925
+ const { events } = await this.storage.listEvents({ taskId });
1926
+ 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;
1927
+ await ((_b = (_a = this.storage).cancelTask) == null ? void 0 : _b.call(_a, {
1928
+ taskId,
1929
+ body: {
1930
+ message: `Step ${currentStepId} has been cancelled.`,
1931
+ stepId: currentStepId,
1932
+ status: "cancelled"
1933
+ }
1934
+ }));
1935
+ }
1936
+ }
1937
+
1938
+ function isTruthy(value) {
1939
+ return lodash.isArray(value) ? value.length > 0 : !!value;
1940
+ }
1941
+ function generateExampleOutput(schema) {
1942
+ var _a, _b;
1943
+ const { examples } = schema;
1944
+ if (examples && Array.isArray(examples)) {
1945
+ return examples[0];
1946
+ }
1947
+ if (schema.type === "object") {
1948
+ return Object.fromEntries(
1949
+ Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
1950
+ key,
1951
+ generateExampleOutput(value)
1952
+ ])
1953
+ );
1954
+ } else if (schema.type === "array") {
1955
+ const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
1956
+ if (firstSchema) {
1957
+ return [generateExampleOutput(firstSchema)];
1958
+ }
1959
+ return [];
1960
+ } else if (schema.type === "string") {
1961
+ return "<example>";
1962
+ } else if (schema.type === "number") {
1963
+ return 0;
1964
+ } else if (schema.type === "boolean") {
1965
+ return false;
1966
+ }
1967
+ return "<unknown>";
1968
+ }
1969
+
1970
+ function createCounterMetric(config) {
1971
+ let metric = promClient.register.getSingleMetric(config.name);
1972
+ if (!metric) {
1973
+ metric = new promClient.Counter(config);
1974
+ promClient.register.registerMetric(metric);
1975
+ }
1976
+ return metric;
1977
+ }
1978
+ function createHistogramMetric(config) {
1979
+ let metric = promClient.register.getSingleMetric(config.name);
1980
+ if (!metric) {
1981
+ metric = new promClient.Histogram(config);
1982
+ promClient.register.registerMetric(metric);
1983
+ }
1984
+ return metric;
1985
+ }
1986
+
1987
+ const createTemplatePermissionRule = pluginPermissionNode.makeCreatePermissionRule();
1988
+ const hasTag = createTemplatePermissionRule({
1989
+ name: "HAS_TAG",
1990
+ resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
1991
+ description: `Match parameters or steps with the given tag`,
1992
+ paramsSchema: zod.z.object({
1993
+ tag: zod.z.string().describe("Name of the tag to match on")
1994
+ }),
1995
+ apply: (resource, { tag }) => {
1996
+ var _a, _b, _c;
1997
+ return (_c = (_b = (_a = resource["backstage:permissions"]) == null ? void 0 : _a.tags) == null ? void 0 : _b.includes(tag)) != null ? _c : false;
1998
+ },
1999
+ toQuery: () => ({})
2000
+ });
2001
+ const createActionPermissionRule = pluginPermissionNode.makeCreatePermissionRule();
2002
+ const hasActionId = createActionPermissionRule({
2003
+ name: "HAS_ACTION_ID",
2004
+ resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
2005
+ description: `Match actions with the given actionId`,
2006
+ paramsSchema: zod.z.object({
2007
+ actionId: zod.z.string().describe("Name of the actionId to match on")
2008
+ }),
2009
+ apply: (resource, { actionId }) => {
2010
+ return resource.action === actionId;
2011
+ },
2012
+ toQuery: () => ({})
2013
+ });
2014
+ buildHasProperty({
2015
+ name: "HAS_PROPERTY",
2016
+ valueSchema: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]),
2017
+ validateProperty: false
2018
+ });
2019
+ const hasBooleanProperty = buildHasProperty({
2020
+ name: "HAS_BOOLEAN_PROPERTY",
2021
+ valueSchema: zod.z.boolean()
2022
+ });
2023
+ const hasNumberProperty = buildHasProperty({
2024
+ name: "HAS_NUMBER_PROPERTY",
2025
+ valueSchema: zod.z.number()
2026
+ });
2027
+ const hasStringProperty = buildHasProperty({
2028
+ name: "HAS_STRING_PROPERTY",
2029
+ valueSchema: zod.z.string()
2030
+ });
2031
+ function buildHasProperty({
2032
+ name,
2033
+ valueSchema,
2034
+ validateProperty = true
2035
+ }) {
2036
+ return createActionPermissionRule({
2037
+ name,
2038
+ description: `Allow actions with the specified property`,
2039
+ resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
2040
+ paramsSchema: zod.z.object({
2041
+ key: zod.z.string().describe(`Property within the action parameters to match on`),
2042
+ value: valueSchema.describe(`Value of the given property to match on`)
2043
+ }),
2044
+ apply: (resource, { key, value }) => {
2045
+ const foundValue = lodash.get(resource.input, key);
2046
+ if (validateProperty && !valueSchema.safeParse(foundValue).success) {
2047
+ return false;
2048
+ }
2049
+ if (value !== void 0) {
2050
+ if (valueSchema.safeParse(value).success) {
2051
+ return value === foundValue;
2052
+ }
2053
+ return false;
2054
+ }
2055
+ return foundValue !== void 0;
2056
+ },
2057
+ toQuery: () => ({})
2058
+ });
2059
+ }
2060
+ const scaffolderTemplateRules = { hasTag };
2061
+ const scaffolderActionRules = {
2062
+ hasActionId,
2063
+ hasBooleanProperty,
2064
+ hasNumberProperty,
2065
+ hasStringProperty
2066
+ };
2067
+
2068
+ var __defProp$1 = Object.defineProperty;
2069
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
2070
+ var __publicField$1 = (obj, key, value) => {
2071
+ __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
2072
+ return value;
2073
+ };
2074
+ const isValidTaskSpec = (taskSpec) => {
2075
+ return taskSpec.apiVersion === "scaffolder.backstage.io/v1beta3";
2076
+ };
2077
+ const createStepLogger = ({
2078
+ task,
2079
+ step
2080
+ }) => {
2081
+ const metadata = { stepId: step.id };
2082
+ const taskLogger = winston__namespace.createLogger({
2083
+ level: process.env.LOG_LEVEL || "info",
2084
+ format: winston__namespace.format.combine(
2085
+ winston__namespace.format.colorize(),
2086
+ winston__namespace.format.simple()
2087
+ ),
2088
+ defaultMeta: {}
2089
+ });
2090
+ const streamLogger = new stream.PassThrough();
2091
+ streamLogger.on("data", async (data) => {
2092
+ const message = data.toString().trim();
2093
+ if ((message == null ? void 0 : message.length) > 1) {
2094
+ await task.emitLog(message, metadata);
2095
+ }
2096
+ });
2097
+ taskLogger.add(new winston__namespace.transports.Stream({ stream: streamLogger }));
2098
+ return { taskLogger, streamLogger };
2099
+ };
2100
+ const isActionAuthorized = pluginPermissionNode.createConditionAuthorizer(
2101
+ Object.values(scaffolderActionRules)
2102
+ );
2103
+ class NunjucksWorkflowRunner {
2104
+ constructor(options) {
2105
+ this.options = options;
2106
+ __publicField$1(this, "defaultTemplateFilters");
2107
+ __publicField$1(this, "tracker", scaffoldingTracker());
2108
+ this.defaultTemplateFilters = createDefaultFilters({
2109
+ integrations: this.options.integrations
2110
+ });
2111
+ }
2112
+ isSingleTemplateString(input) {
2113
+ var _a, _b;
2114
+ const { parser, nodes } = nunjucks__default["default"];
2115
+ const parsed = parser.parse(
2116
+ input,
2117
+ {},
2118
+ {
2119
+ autoescape: false,
2120
+ tags: {
2121
+ variableStart: "${{",
2122
+ variableEnd: "}}"
2123
+ }
2124
+ }
2125
+ );
2126
+ return parsed.children.length === 1 && !(((_b = (_a = parsed.children[0]) == null ? void 0 : _a.children) == null ? void 0 : _b[0]) instanceof nodes.TemplateData);
2127
+ }
2128
+ render(input, context, renderTemplate) {
2129
+ return JSON.parse(JSON.stringify(input), (_key, value) => {
2130
+ try {
2131
+ if (typeof value === "string") {
2132
+ try {
2133
+ if (this.isSingleTemplateString(value)) {
2134
+ const wrappedDumped = value.replace(
2135
+ /\${{(.+)}}/g,
2136
+ "${{ ( $1 ) | dump }}"
2137
+ );
2138
+ const templated2 = renderTemplate(wrappedDumped, context);
2139
+ if (templated2 === "") {
2140
+ return void 0;
2141
+ }
2142
+ return JSON.parse(templated2);
2143
+ }
2144
+ } catch (ex) {
2145
+ this.options.logger.error(
2146
+ `Failed to parse template string: ${value} with error ${ex.message}`
2147
+ );
2148
+ }
2149
+ const templated = renderTemplate(value, context);
2150
+ if (templated === "") {
2151
+ return void 0;
2152
+ }
2153
+ return templated;
2154
+ }
2155
+ } catch {
2156
+ return value;
2157
+ }
2158
+ return value;
2159
+ });
2160
+ }
2161
+ async executeStep(task, step, context, renderTemplate, taskTrack, workspacePath, decision) {
2162
+ var _a, _b, _c, _d, _e;
2163
+ const stepTrack = await this.tracker.stepStart(task, step);
2164
+ if (task.cancelSignal.aborted) {
2165
+ throw new Error(`Step ${step.name} has been cancelled.`);
2166
+ }
2167
+ try {
2168
+ if (step.if) {
2169
+ const ifResult = await this.render(step.if, context, renderTemplate);
2170
+ if (!isTruthy(ifResult)) {
2171
+ await stepTrack.skipFalsy();
2172
+ return;
2173
+ }
2174
+ }
2175
+ const action = this.options.actionRegistry.get(step.action);
2176
+ const { taskLogger, streamLogger } = createStepLogger({ task, step });
2177
+ if (task.isDryRun) {
2178
+ const redactedSecrets = Object.fromEntries(
2179
+ Object.entries((_a = task.secrets) != null ? _a : {}).map((secret) => [
2180
+ secret[0],
2181
+ "[REDACTED]"
2182
+ ])
2183
+ );
2184
+ const debugInput = (_b = step.input && this.render(
2185
+ step.input,
2186
+ {
2187
+ ...context,
2188
+ secrets: redactedSecrets
2189
+ },
2190
+ renderTemplate
2191
+ )) != null ? _b : {};
2192
+ taskLogger.info(
2193
+ `Running ${action.id} in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
2194
+ debugInput,
2195
+ void 0,
2196
+ 2
2197
+ )}`
2198
+ );
2199
+ if (!action.supportsDryRun) {
2200
+ await taskTrack.skipDryRun(step, action);
2201
+ const outputSchema = (_c = action.schema) == null ? void 0 : _c.output;
2202
+ if (outputSchema) {
2203
+ context.steps[step.id] = {
2204
+ output: generateExampleOutput(outputSchema)
2205
+ };
2206
+ } else {
2207
+ context.steps[step.id] = { output: {} };
2208
+ }
2209
+ return;
2210
+ }
2211
+ }
2212
+ const iterations = (step.each ? Object.entries(this.render(step.each, context, renderTemplate)).map(
2213
+ ([key, value]) => ({
2214
+ each: { key, value }
2215
+ })
2216
+ ) : [{}]).map((i) => {
2217
+ var _a2;
2218
+ return {
2219
+ ...i,
2220
+ // Secrets are only passed when templating the input to actions for security reasons
2221
+ input: step.input ? this.render(
2222
+ step.input,
2223
+ { ...context, secrets: (_a2 = task.secrets) != null ? _a2 : {}, ...i },
2224
+ renderTemplate
2225
+ ) : {}
2226
+ };
2227
+ });
2228
+ for (const iteration of iterations) {
2229
+ const actionId = `${action.id}${iteration.each ? `[${iteration.each.key}]` : ""}`;
2230
+ if ((_d = action.schema) == null ? void 0 : _d.input) {
2231
+ const validateResult = jsonschema.validate(
2232
+ iteration.input,
2233
+ action.schema.input
2234
+ );
2235
+ if (!validateResult.valid) {
2236
+ const errors$1 = validateResult.errors.join(", ");
2237
+ throw new errors.InputError(
2238
+ `Invalid input passed to action ${actionId}, ${errors$1}`
2239
+ );
2240
+ }
2241
+ }
2242
+ if (!isActionAuthorized(decision, {
2243
+ action: action.id,
2244
+ input: iteration.input
2245
+ })) {
2246
+ throw new errors.NotAllowedError(
2247
+ `Unauthorized action: ${actionId}. The action is not allowed. Input: ${JSON.stringify(
2248
+ iteration.input,
2249
+ null,
2250
+ 2
2251
+ )}`
2252
+ );
2253
+ }
2254
+ }
2255
+ const tmpDirs = new Array();
2256
+ const stepOutput = {};
2257
+ for (const iteration of iterations) {
2258
+ if (iteration.each) {
2259
+ taskLogger.info(
2260
+ `Running step each: ${JSON.stringify(
2261
+ iteration.each,
2262
+ (k, v) => k ? v.toString() : v,
2263
+ 0
2264
+ )}`
2265
+ );
2266
+ }
2267
+ await action.handler({
2268
+ input: iteration.input,
2269
+ secrets: (_e = task.secrets) != null ? _e : {},
2270
+ logger: taskLogger,
2271
+ logStream: streamLogger,
2272
+ workspacePath,
2273
+ createTemporaryDirectory: async () => {
2274
+ const tmpDir = await fs__default["default"].mkdtemp(
2275
+ `${workspacePath}_step-${step.id}-`
2276
+ );
2277
+ tmpDirs.push(tmpDir);
2278
+ return tmpDir;
2279
+ },
2280
+ output(name, value) {
2281
+ if (step.each) {
2282
+ stepOutput[name] = stepOutput[name] || [];
2283
+ stepOutput[name].push(value);
2284
+ } else {
2285
+ stepOutput[name] = value;
2286
+ }
2287
+ },
2288
+ templateInfo: task.spec.templateInfo,
2289
+ user: task.spec.user,
2290
+ isDryRun: task.isDryRun,
2291
+ signal: task.cancelSignal
2292
+ });
2293
+ }
2294
+ for (const tmpDir of tmpDirs) {
2295
+ await fs__default["default"].remove(tmpDir);
2296
+ }
2297
+ context.steps[step.id] = { output: stepOutput };
2298
+ if (task.cancelSignal.aborted) {
2299
+ throw new Error(`Step ${step.name} has been cancelled.`);
2300
+ }
2301
+ await stepTrack.markSuccessful();
2302
+ } catch (err) {
2303
+ await taskTrack.markFailed(step, err);
2304
+ await stepTrack.markFailed();
2305
+ throw err;
2306
+ }
2307
+ }
2308
+ async execute(task) {
2309
+ var _a;
2310
+ if (!isValidTaskSpec(task.spec)) {
2311
+ throw new errors.InputError(
2312
+ "Wrong template version executed with the workflow engine"
2313
+ );
2314
+ }
2315
+ const workspacePath = path__default["default"].join(
2316
+ this.options.workingDirectory,
2317
+ await task.getWorkspaceName()
2318
+ );
2319
+ const { additionalTemplateFilters, additionalTemplateGlobals } = this.options;
2320
+ const renderTemplate = await SecureTemplater.loadRenderer({
2321
+ templateFilters: {
2322
+ ...this.defaultTemplateFilters,
2323
+ ...additionalTemplateFilters
2324
+ },
2325
+ templateGlobals: additionalTemplateGlobals
2326
+ });
2327
+ try {
2328
+ const taskTrack = await this.tracker.taskStart(task);
2329
+ await fs__default["default"].ensureDir(workspacePath);
2330
+ const context = {
2331
+ parameters: task.spec.parameters,
2332
+ steps: {},
2333
+ user: task.spec.user
2334
+ };
2335
+ const [decision] = this.options.permissions && task.spec.steps.length ? await this.options.permissions.authorizeConditional(
2336
+ [{ permission: alpha.actionExecutePermission }],
2337
+ { token: (_a = task.secrets) == null ? void 0 : _a.backstageToken }
2338
+ ) : [{ result: pluginPermissionCommon.AuthorizeResult.ALLOW }];
2339
+ for (const step of task.spec.steps) {
2340
+ await this.executeStep(
2341
+ task,
2342
+ step,
2343
+ context,
2344
+ renderTemplate,
2345
+ taskTrack,
2346
+ workspacePath,
2347
+ decision
2348
+ );
2349
+ }
2350
+ const output = this.render(task.spec.output, context, renderTemplate);
2351
+ await taskTrack.markSuccessful();
2352
+ return { output };
2353
+ } finally {
2354
+ if (workspacePath) {
2355
+ await fs__default["default"].remove(workspacePath);
2356
+ }
2357
+ }
2358
+ }
2359
+ }
2360
+ function scaffoldingTracker() {
2361
+ const taskCount = createCounterMetric({
2362
+ name: "scaffolder_task_count",
2363
+ help: "Count of task runs",
2364
+ labelNames: ["template", "user", "result"]
2365
+ });
2366
+ const taskDuration = createHistogramMetric({
2367
+ name: "scaffolder_task_duration",
2368
+ help: "Duration of a task run",
2369
+ labelNames: ["template", "result"]
2370
+ });
2371
+ const stepCount = createCounterMetric({
2372
+ name: "scaffolder_step_count",
2373
+ help: "Count of step runs",
2374
+ labelNames: ["template", "step", "result"]
2375
+ });
2376
+ const stepDuration = createHistogramMetric({
2377
+ name: "scaffolder_step_duration",
2378
+ help: "Duration of a step runs",
2379
+ labelNames: ["template", "step", "result"]
2380
+ });
2381
+ async function taskStart(task) {
2382
+ var _a, _b;
2383
+ await task.emitLog(`Starting up task with ${task.spec.steps.length} steps`);
2384
+ const template = ((_a = task.spec.templateInfo) == null ? void 0 : _a.entityRef) || "";
2385
+ const user = ((_b = task.spec.user) == null ? void 0 : _b.ref) || "";
2386
+ const taskTimer = taskDuration.startTimer({
2387
+ template
2388
+ });
2389
+ async function skipDryRun(step, action) {
2390
+ task.emitLog(`Skipping because ${action.id} does not support dry-run`, {
2391
+ stepId: step.id,
2392
+ status: "skipped"
2393
+ });
2394
+ }
2395
+ async function markSuccessful() {
2396
+ taskCount.inc({
2397
+ template,
2398
+ user,
2399
+ result: "ok"
2400
+ });
2401
+ taskTimer({ result: "ok" });
2402
+ }
2403
+ async function markFailed(step, err) {
2404
+ await task.emitLog(String(err.stack), {
2405
+ stepId: step.id,
2406
+ status: "failed"
2407
+ });
2408
+ taskCount.inc({
2409
+ template,
2410
+ user,
2411
+ result: "failed"
2412
+ });
2413
+ taskTimer({ result: "failed" });
2414
+ }
2415
+ async function markCancelled(step) {
2416
+ await task.emitLog(`Step ${step.id} has been cancelled.`, {
2417
+ stepId: step.id,
2418
+ status: "cancelled"
2419
+ });
2420
+ taskCount.inc({
2421
+ template,
2422
+ user,
2423
+ result: "cancelled"
2424
+ });
2425
+ taskTimer({ result: "cancelled" });
2426
+ }
2427
+ return {
2428
+ skipDryRun,
2429
+ markCancelled,
2430
+ markSuccessful,
2431
+ markFailed
2432
+ };
2433
+ }
2434
+ async function stepStart(task, step) {
2435
+ var _a;
2436
+ await task.emitLog(`Beginning step ${step.name}`, {
2437
+ stepId: step.id,
2438
+ status: "processing"
2439
+ });
2440
+ const template = ((_a = task.spec.templateInfo) == null ? void 0 : _a.entityRef) || "";
2441
+ const stepTimer = stepDuration.startTimer({
2442
+ template,
2443
+ step: step.name
2444
+ });
2445
+ async function markSuccessful() {
2446
+ await task.emitLog(`Finished step ${step.name}`, {
2447
+ stepId: step.id,
2448
+ status: "completed"
2449
+ });
2450
+ stepCount.inc({
2451
+ template,
2452
+ step: step.name,
2453
+ result: "ok"
2454
+ });
2455
+ stepTimer({ result: "ok" });
2456
+ }
2457
+ async function markCancelled() {
2458
+ stepCount.inc({
2459
+ template,
2460
+ step: step.name,
2461
+ result: "cancelled"
2462
+ });
2463
+ stepTimer({ result: "cancelled" });
2464
+ }
2465
+ async function markFailed() {
2466
+ stepCount.inc({
2467
+ template,
2468
+ step: step.name,
2469
+ result: "failed"
2470
+ });
2471
+ stepTimer({ result: "failed" });
2472
+ }
2473
+ async function skipFalsy() {
2474
+ await task.emitLog(
2475
+ `Skipping step ${step.id} because its if condition was false`,
2476
+ { stepId: step.id, status: "skipped" }
2477
+ );
2478
+ stepTimer({ result: "skipped" });
2479
+ }
2480
+ return {
2481
+ markCancelled,
2482
+ markFailed,
2483
+ markSuccessful,
2484
+ skipFalsy
2485
+ };
2486
+ }
2487
+ return {
2488
+ taskStart,
2489
+ stepStart
2490
+ };
2491
+ }
2492
+
2493
+ var __defProp = Object.defineProperty;
2494
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
2495
+ var __publicField = (obj, key, value) => {
2496
+ __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
2497
+ return value;
2498
+ };
2499
+ class TaskWorker {
2500
+ constructor(options) {
2501
+ this.options = options;
2502
+ __publicField(this, "taskQueue");
2503
+ this.taskQueue = new PQueue__default["default"]({
2504
+ concurrency: options.concurrentTasksLimit
2505
+ });
2506
+ }
2507
+ static async create(options) {
2508
+ const {
2509
+ taskBroker,
2510
+ logger,
2511
+ actionRegistry,
2512
+ integrations,
2513
+ workingDirectory,
2514
+ additionalTemplateFilters,
2515
+ concurrentTasksLimit = 10,
2516
+ // from 1 to Infinity
2517
+ additionalTemplateGlobals,
2518
+ permissions
2519
+ } = options;
2520
+ const workflowRunner = new NunjucksWorkflowRunner({
2521
+ actionRegistry,
2522
+ integrations,
2523
+ logger,
2524
+ workingDirectory,
2525
+ additionalTemplateFilters,
2526
+ additionalTemplateGlobals,
2527
+ permissions
2528
+ });
2529
+ return new TaskWorker({
2530
+ taskBroker,
2531
+ runners: { workflowRunner },
2532
+ concurrentTasksLimit,
2533
+ permissions
2534
+ });
2535
+ }
2536
+ start() {
2537
+ (async () => {
2538
+ for (; ; ) {
2539
+ await this.onReadyToClaimTask();
2540
+ const task = await this.options.taskBroker.claim();
2541
+ this.taskQueue.add(() => this.runOneTask(task));
2542
+ }
2543
+ })();
2544
+ }
2545
+ onReadyToClaimTask() {
2546
+ if (this.taskQueue.pending < this.options.concurrentTasksLimit) {
2547
+ return Promise.resolve();
2548
+ }
2549
+ return new Promise((resolve) => {
2550
+ this.taskQueue.once("next", () => {
2551
+ resolve();
2552
+ });
2553
+ });
2554
+ }
2555
+ async runOneTask(task) {
2556
+ try {
2557
+ if (task.spec.apiVersion !== "scaffolder.backstage.io/v1beta3") {
2558
+ throw new Error(
2559
+ `Unsupported Template apiVersion ${task.spec.apiVersion}`
2560
+ );
2561
+ }
2562
+ const { output } = await this.options.runners.workflowRunner.execute(
2563
+ task
2564
+ );
2565
+ await task.complete("completed", { output });
2566
+ } catch (error) {
2567
+ errors.assertError(error);
2568
+ await task.complete("failed", {
2569
+ error: { name: error.name, message: error.message }
2570
+ });
2571
+ }
2572
+ }
2573
+ }
2574
+
2575
+ class DecoratedActionsRegistry extends TemplateActionRegistry {
2576
+ constructor(innerRegistry, extraActions) {
2577
+ super();
2578
+ this.innerRegistry = innerRegistry;
2579
+ for (const action of extraActions) {
2580
+ this.register(action);
2581
+ }
2582
+ }
2583
+ get(actionId) {
2584
+ try {
2585
+ return super.get(actionId);
2586
+ } catch {
2587
+ return this.innerRegistry.get(actionId);
2588
+ }
2589
+ }
2590
+ }
2591
+
2592
+ function createDryRunner(options) {
2593
+ return async function dryRun(input) {
2594
+ let contentPromise;
2595
+ const workflowRunner = new NunjucksWorkflowRunner({
2596
+ ...options,
2597
+ actionRegistry: new DecoratedActionsRegistry(options.actionRegistry, [
2598
+ pluginScaffolderNode.createTemplateAction({
2599
+ id: "dry-run:extract",
2600
+ supportsDryRun: true,
2601
+ async handler(ctx) {
2602
+ contentPromise = pluginScaffolderNode.serializeDirectoryContents(ctx.workspacePath);
2603
+ await contentPromise.catch(() => {
2604
+ });
2605
+ }
2606
+ })
2607
+ ])
2608
+ });
2609
+ const dryRunId = uuid.v4();
2610
+ const log = new Array();
2611
+ const contentsPath = backendCommon.resolveSafeChildPath(
2612
+ options.workingDirectory,
2613
+ `dry-run-content-${dryRunId}`
2614
+ );
2615
+ try {
2616
+ await pluginScaffolderNode.deserializeDirectoryContents(contentsPath, input.directoryContents);
2617
+ const abortSignal = new AbortController().signal;
2618
+ const result = await workflowRunner.execute({
2619
+ spec: {
2620
+ ...input.spec,
2621
+ steps: [
2622
+ ...input.spec.steps,
2623
+ {
2624
+ id: dryRunId,
2625
+ name: "dry-run:extract",
2626
+ action: "dry-run:extract"
2627
+ }
2628
+ ],
2629
+ templateInfo: {
2630
+ entityRef: "template:default/dry-run",
2631
+ baseUrl: url.pathToFileURL(
2632
+ backendCommon.resolveSafeChildPath(contentsPath, "template.yaml")
2633
+ ).toString()
2634
+ }
2635
+ },
2636
+ secrets: input.secrets,
2637
+ // No need to update this at the end of the run, so just hard-code it
2638
+ done: false,
2639
+ isDryRun: true,
2640
+ getWorkspaceName: async () => `dry-run-${dryRunId}`,
2641
+ cancelSignal: abortSignal,
2642
+ async emitLog(message, logMetadata) {
2643
+ if ((logMetadata == null ? void 0 : logMetadata.stepId) === dryRunId) {
2644
+ return;
2645
+ }
2646
+ log.push({
2647
+ body: {
2648
+ ...logMetadata,
2649
+ message
2650
+ }
2651
+ });
2652
+ },
2653
+ complete: async () => {
2654
+ throw new Error("Not implemented");
2655
+ }
2656
+ });
2657
+ if (!contentPromise) {
2658
+ throw new Error("Content extraction step was skipped");
2659
+ }
2660
+ const directoryContents = await contentPromise;
2661
+ return {
2662
+ log,
2663
+ directoryContents,
2664
+ output: result.output
2665
+ };
2666
+ } finally {
2667
+ await fs__default["default"].remove(contentsPath);
2668
+ }
2669
+ };
2670
+ }
2671
+
2672
+ async function getWorkingDirectory(config, logger) {
2673
+ if (!config.has("backend.workingDirectory")) {
2674
+ return os__default["default"].tmpdir();
2675
+ }
2676
+ const workingDirectory = config.getString("backend.workingDirectory");
2677
+ try {
2678
+ await fs__default["default"].access(workingDirectory, fs__default["default"].constants.F_OK | fs__default["default"].constants.W_OK);
2679
+ logger.info(`using working directory: ${workingDirectory}`);
2680
+ } catch (err) {
2681
+ errors.assertError(err);
2682
+ logger.error(
2683
+ `working directory ${workingDirectory} ${err.code === "ENOENT" ? "does not exist" : "is not writable"}`
2684
+ );
2685
+ throw err;
2686
+ }
2687
+ return workingDirectory;
2688
+ }
2689
+ function getEntityBaseUrl(entity) {
2690
+ var _a, _b;
2691
+ let location = (_a = entity.metadata.annotations) == null ? void 0 : _a[catalogModel.ANNOTATION_SOURCE_LOCATION];
2692
+ if (!location) {
2693
+ location = (_b = entity.metadata.annotations) == null ? void 0 : _b[catalogModel.ANNOTATION_LOCATION];
2694
+ }
2695
+ if (!location) {
2696
+ return void 0;
2697
+ }
2698
+ const { type, target } = catalogModel.parseLocationRef(location);
2699
+ if (type === "url") {
2700
+ return target;
2701
+ } else if (type === "file") {
2702
+ return `file://${target}`;
2703
+ }
2704
+ return void 0;
2705
+ }
2706
+ async function findTemplate(options) {
2707
+ const { entityRef, token, catalogApi } = options;
2708
+ if (entityRef.kind.toLocaleLowerCase("en-US") !== "template") {
2709
+ throw new errors.InputError(`Invalid kind, only 'Template' kind is supported`);
2710
+ }
2711
+ const template = await catalogApi.getEntityByRef(entityRef, { token });
2712
+ if (!template) {
2713
+ throw new errors.NotFoundError(
2714
+ `Template ${catalogModel.stringifyEntityRef(entityRef)} not found`
2715
+ );
2716
+ }
2717
+ return template;
2718
+ }
2719
+
2720
+ function isTemplatePermissionRuleInput(permissionRule) {
2721
+ return permissionRule.resourceType === alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE;
2722
+ }
2723
+ function isActionPermissionRuleInput(permissionRule) {
2724
+ return permissionRule.resourceType === alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION;
2725
+ }
2726
+ function isSupportedTemplate(entity) {
2727
+ return entity.apiVersion === "scaffolder.backstage.io/v1beta3";
2728
+ }
2729
+ function buildDefaultIdentityClient(options) {
2730
+ return {
2731
+ getIdentity: async ({ request }) => {
2732
+ var _a;
2733
+ const header = request.headers.authorization;
2734
+ const { logger } = options;
2735
+ if (!header) {
2736
+ return void 0;
2737
+ }
2738
+ try {
2739
+ const token = (_a = header.match(/^Bearer\s(\S+\.\S+\.\S+)$/i)) == null ? void 0 : _a[1];
2740
+ if (!token) {
2741
+ throw new TypeError("Expected Bearer with JWT");
2742
+ }
2743
+ const [_header, rawPayload, _signature] = token.split(".");
2744
+ const payload = JSON.parse(
2745
+ Buffer.from(rawPayload, "base64").toString()
2746
+ );
2747
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
2748
+ throw new TypeError("Malformed JWT payload");
2749
+ }
2750
+ const sub = payload.sub;
2751
+ if (typeof sub !== "string") {
2752
+ throw new TypeError("Expected string sub claim");
2753
+ }
2754
+ if (sub === "backstage-server") {
2755
+ return void 0;
2756
+ }
2757
+ catalogModel.parseEntityRef(sub);
2758
+ return {
2759
+ identity: {
2760
+ userEntityRef: sub,
2761
+ ownershipEntityRefs: [],
2762
+ type: "user"
2763
+ },
2764
+ token
2765
+ };
2766
+ } catch (e) {
2767
+ logger.error(`Invalid authorization header: ${errors.stringifyError(e)}`);
2768
+ return void 0;
2769
+ }
2770
+ }
2771
+ };
2772
+ }
2773
+ const readDuration = (config$1, key, defaultValue) => {
2774
+ if (config$1.has(key)) {
2775
+ return config.readDurationFromConfig(config$1, { key });
2776
+ }
2777
+ return defaultValue;
2778
+ };
2779
+ async function createRouter(options) {
2780
+ var _a;
2781
+ const router = Router__default["default"]();
2782
+ router.use(express__default["default"].json({ limit: "10MB" }));
2783
+ const {
2784
+ logger: parentLogger,
2785
+ config,
2786
+ reader,
2787
+ database,
2788
+ catalogClient,
2789
+ actions,
2790
+ taskWorkers,
2791
+ scheduler,
2792
+ additionalTemplateFilters,
2793
+ additionalTemplateGlobals,
2794
+ permissions,
2795
+ permissionRules
2796
+ } = options;
2797
+ const concurrentTasksLimit = (_a = options.concurrentTasksLimit) != null ? _a : options.config.getOptionalNumber("scaffolder.concurrentTasksLimit");
2798
+ const logger = parentLogger.child({ plugin: "scaffolder" });
2799
+ const identity = options.identity || buildDefaultIdentityClient(options);
2800
+ const workingDirectory = await getWorkingDirectory(config, logger);
2801
+ const integrations = integration.ScmIntegrations.fromConfig(config);
2802
+ let taskBroker;
2803
+ if (!options.taskBroker) {
2804
+ const databaseTaskStore = await DatabaseTaskStore.create({ database });
2805
+ taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
2806
+ if (scheduler && databaseTaskStore.listStaleTasks) {
2807
+ await scheduler.scheduleTask({
2808
+ id: "close_stale_tasks",
2809
+ frequency: readDuration(
2810
+ config,
2811
+ "scaffolder.taskTimeoutJanitorFrequency",
2812
+ {
2813
+ minutes: 5
2814
+ }
2815
+ ),
2816
+ timeout: { minutes: 15 },
2817
+ fn: async () => {
2818
+ const { tasks } = await databaseTaskStore.listStaleTasks({
2819
+ timeoutS: luxon.Duration.fromObject(
2820
+ readDuration(config, "scaffolder.taskTimeout", {
2821
+ hours: 24
2822
+ })
2823
+ ).as("seconds")
2824
+ });
2825
+ for (const task of tasks) {
2826
+ await databaseTaskStore.shutdownTask(task);
2827
+ logger.info(`Successfully closed stale task ${task.taskId}`);
2828
+ }
2829
+ }
2830
+ });
2831
+ }
2832
+ } else {
2833
+ taskBroker = options.taskBroker;
2834
+ }
2835
+ const actionRegistry = new TemplateActionRegistry();
2836
+ const workers = [];
2837
+ if (concurrentTasksLimit !== 0) {
2838
+ for (let i = 0; i < (taskWorkers || 1); i++) {
2839
+ const worker = await TaskWorker.create({
2840
+ taskBroker,
2841
+ actionRegistry,
2842
+ integrations,
2843
+ logger,
2844
+ workingDirectory,
2845
+ additionalTemplateFilters,
2846
+ additionalTemplateGlobals,
2847
+ concurrentTasksLimit,
2848
+ permissions
2849
+ });
2850
+ workers.push(worker);
2851
+ }
2852
+ }
2853
+ const actionsToRegister = Array.isArray(actions) ? actions : createBuiltinActions({
2854
+ integrations,
2855
+ catalogClient,
2856
+ reader,
2857
+ config,
2858
+ additionalTemplateFilters,
2859
+ additionalTemplateGlobals
2860
+ });
2861
+ actionsToRegister.forEach((action) => actionRegistry.register(action));
2862
+ workers.forEach((worker) => worker.start());
2863
+ const dryRunner = createDryRunner({
2864
+ actionRegistry,
2865
+ integrations,
2866
+ logger,
2867
+ workingDirectory,
2868
+ additionalTemplateFilters,
2869
+ additionalTemplateGlobals,
2870
+ permissions
2871
+ });
2872
+ const templateRules = Object.values(
2873
+ scaffolderTemplateRules
2874
+ );
2875
+ const actionRules = Object.values(
2876
+ scaffolderActionRules
2877
+ );
2878
+ if (permissionRules) {
2879
+ templateRules.push(
2880
+ ...permissionRules.filter(isTemplatePermissionRuleInput)
2881
+ );
2882
+ actionRules.push(...permissionRules.filter(isActionPermissionRuleInput));
2883
+ }
2884
+ const isAuthorized = pluginPermissionNode.createConditionAuthorizer(Object.values(templateRules));
2885
+ const permissionIntegrationRouter = pluginPermissionNode.createPermissionIntegrationRouter({
2886
+ resources: [
2887
+ {
2888
+ resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
2889
+ permissions: alpha.scaffolderTemplatePermissions,
2890
+ rules: templateRules
2891
+ },
2892
+ {
2893
+ resourceType: alpha.RESOURCE_TYPE_SCAFFOLDER_ACTION,
2894
+ permissions: alpha.scaffolderActionPermissions,
2895
+ rules: actionRules
2896
+ }
2897
+ ]
2898
+ });
2899
+ router.use(permissionIntegrationRouter);
2900
+ router.get(
2901
+ "/v2/templates/:namespace/:kind/:name/parameter-schema",
2902
+ async (req, res) => {
2903
+ var _a2, _b;
2904
+ const userIdentity = await identity.getIdentity({
2905
+ request: req
2906
+ });
2907
+ const token = userIdentity == null ? void 0 : userIdentity.token;
2908
+ const template = await authorizeTemplate(req.params, token);
2909
+ const parameters = [(_a2 = template.spec.parameters) != null ? _a2 : []].flat();
2910
+ const presentation = template.spec.presentation;
2911
+ res.json({
2912
+ title: (_b = template.metadata.title) != null ? _b : template.metadata.name,
2913
+ ...presentation ? { presentation } : {},
2914
+ description: template.metadata.description,
2915
+ "ui:options": template.metadata["ui:options"],
2916
+ steps: parameters.map((schema) => {
2917
+ var _a3;
2918
+ return {
2919
+ title: (_a3 = schema.title) != null ? _a3 : "Please enter the following information",
2920
+ description: schema.description,
2921
+ schema
2922
+ };
2923
+ })
2924
+ });
2925
+ }
2926
+ ).get("/v2/actions", async (_req, res) => {
2927
+ const actionsList = actionRegistry.list().map((action) => {
2928
+ return {
2929
+ id: action.id,
2930
+ description: action.description,
2931
+ examples: action.examples,
2932
+ schema: action.schema
2933
+ };
2934
+ });
2935
+ res.json(actionsList);
2936
+ }).post("/v2/tasks", async (req, res) => {
2937
+ var _a2, _b;
2938
+ const templateRef = req.body.templateRef;
2939
+ const { kind, namespace, name } = catalogModel.parseEntityRef(templateRef, {
2940
+ defaultKind: "template"
2941
+ });
2942
+ const callerIdentity = await identity.getIdentity({
2943
+ request: req
2944
+ });
2945
+ const token = callerIdentity == null ? void 0 : callerIdentity.token;
2946
+ const userEntityRef = callerIdentity == null ? void 0 : callerIdentity.identity.userEntityRef;
2947
+ const userEntity = userEntityRef ? await catalogClient.getEntityByRef(userEntityRef, { token }) : void 0;
2948
+ let auditLog = `Scaffolding task for ${templateRef}`;
2949
+ if (userEntityRef) {
2950
+ auditLog += ` created by ${userEntityRef}`;
2951
+ }
2952
+ logger.info(auditLog);
2953
+ const values = req.body.values;
2954
+ const template = await authorizeTemplate(
2955
+ { kind, namespace, name },
2956
+ token
2957
+ );
2958
+ for (const parameters of [(_a2 = template.spec.parameters) != null ? _a2 : []].flat()) {
2959
+ const result2 = jsonschema.validate(values, parameters);
2960
+ if (!result2.valid) {
2961
+ res.status(400).json({ errors: result2.errors });
2962
+ return;
2963
+ }
2964
+ }
2965
+ const baseUrl = getEntityBaseUrl(template);
2966
+ const taskSpec = {
2967
+ apiVersion: template.apiVersion,
2968
+ steps: template.spec.steps.map((step, index) => {
2969
+ var _a3, _b2;
2970
+ return {
2971
+ ...step,
2972
+ id: (_a3 = step.id) != null ? _a3 : `step-${index + 1}`,
2973
+ name: (_b2 = step.name) != null ? _b2 : step.action
2974
+ };
2975
+ }),
2976
+ output: (_b = template.spec.output) != null ? _b : {},
2977
+ parameters: values,
2978
+ user: {
2979
+ entity: userEntity,
2980
+ ref: userEntityRef
2981
+ },
2982
+ templateInfo: {
2983
+ entityRef: catalogModel.stringifyEntityRef({ kind, name, namespace }),
2984
+ baseUrl,
2985
+ entity: {
2986
+ metadata: template.metadata
2987
+ }
2988
+ }
2989
+ };
2990
+ const result = await taskBroker.dispatch({
2991
+ spec: taskSpec,
2992
+ createdBy: userEntityRef,
2993
+ secrets: {
2994
+ ...req.body.secrets,
2995
+ backstageToken: token
2996
+ }
2997
+ });
2998
+ res.status(201).json({ id: result.taskId });
2999
+ }).get("/v2/tasks", async (req, res) => {
3000
+ const [userEntityRef] = [req.query.createdBy].flat();
3001
+ if (typeof userEntityRef !== "string" && typeof userEntityRef !== "undefined") {
3002
+ throw new errors.InputError("createdBy query parameter must be a string");
3003
+ }
3004
+ if (!taskBroker.list) {
3005
+ throw new Error(
3006
+ "TaskBroker does not support listing tasks, please implement the list method on the TaskBroker."
3007
+ );
3008
+ }
3009
+ const tasks = await taskBroker.list({
3010
+ createdBy: userEntityRef
3011
+ });
3012
+ res.status(200).json(tasks);
3013
+ }).get("/v2/tasks/:taskId", async (req, res) => {
3014
+ const { taskId } = req.params;
3015
+ const task = await taskBroker.get(taskId);
3016
+ if (!task) {
3017
+ throw new errors.NotFoundError(`Task with id ${taskId} does not exist`);
3018
+ }
3019
+ delete task.secrets;
3020
+ res.status(200).json(task);
3021
+ }).post("/v2/tasks/:taskId/cancel", async (req, res) => {
3022
+ var _a2;
3023
+ const { taskId } = req.params;
3024
+ await ((_a2 = taskBroker.cancel) == null ? void 0 : _a2.call(taskBroker, taskId));
3025
+ res.status(200).json({ status: "cancelled" });
3026
+ }).get("/v2/tasks/:taskId/eventstream", async (req, res) => {
3027
+ const { taskId } = req.params;
3028
+ const after = req.query.after !== void 0 ? Number(req.query.after) : void 0;
3029
+ logger.debug(`Event stream observing taskId '${taskId}' opened`);
3030
+ res.writeHead(200, {
3031
+ Connection: "keep-alive",
3032
+ "Cache-Control": "no-cache",
3033
+ "Content-Type": "text/event-stream"
3034
+ });
3035
+ const subscription = taskBroker.event$({ taskId, after }).subscribe({
3036
+ error: (error) => {
3037
+ logger.error(
3038
+ `Received error from event stream when observing taskId '${taskId}', ${error}`
3039
+ );
3040
+ res.end();
3041
+ },
3042
+ next: ({ events }) => {
3043
+ var _a2;
3044
+ let shouldUnsubscribe = false;
3045
+ for (const event of events) {
3046
+ res.write(
3047
+ `event: ${event.type}
3048
+ data: ${JSON.stringify(event)}
3049
+
3050
+ `
3051
+ );
3052
+ if (event.type === "completion") {
3053
+ shouldUnsubscribe = true;
3054
+ }
3055
+ }
3056
+ (_a2 = res.flush) == null ? void 0 : _a2.call(res);
3057
+ if (shouldUnsubscribe) {
3058
+ subscription.unsubscribe();
3059
+ res.end();
3060
+ }
3061
+ }
3062
+ });
3063
+ req.on("close", () => {
3064
+ subscription.unsubscribe();
3065
+ logger.debug(`Event stream observing taskId '${taskId}' closed`);
3066
+ });
3067
+ }).get("/v2/tasks/:taskId/events", async (req, res) => {
3068
+ const { taskId } = req.params;
3069
+ const after = Number(req.query.after) || void 0;
3070
+ const timeout = setTimeout(() => {
3071
+ res.json([]);
3072
+ }, 3e4);
3073
+ const subscription = taskBroker.event$({ taskId, after }).subscribe({
3074
+ error: (error) => {
3075
+ logger.error(
3076
+ `Received error from event stream when observing taskId '${taskId}', ${error}`
3077
+ );
3078
+ },
3079
+ next: ({ events }) => {
3080
+ clearTimeout(timeout);
3081
+ subscription.unsubscribe();
3082
+ res.json(events);
3083
+ }
3084
+ });
3085
+ req.on("close", () => {
3086
+ subscription.unsubscribe();
3087
+ clearTimeout(timeout);
3088
+ });
3089
+ }).post("/v2/dry-run", async (req, res) => {
3090
+ var _a2, _b, _c, _d;
3091
+ const bodySchema = zod.z.object({
3092
+ template: zod.z.unknown(),
3093
+ values: zod.z.record(zod.z.unknown()),
3094
+ secrets: zod.z.record(zod.z.string()).optional(),
3095
+ directoryContents: zod.z.array(
3096
+ zod.z.object({ path: zod.z.string(), base64Content: zod.z.string() })
3097
+ )
3098
+ });
3099
+ const body = await bodySchema.parseAsync(req.body).catch((e) => {
3100
+ throw new errors.InputError(`Malformed request: ${e}`);
3101
+ });
3102
+ const template = body.template;
3103
+ if (!await pluginScaffolderCommon.templateEntityV1beta3Validator.check(template)) {
3104
+ throw new errors.InputError("Input template is not a template");
3105
+ }
3106
+ const token = (_a2 = await identity.getIdentity({
3107
+ request: req
3108
+ })) == null ? void 0 : _a2.token;
3109
+ for (const parameters of [(_b = template.spec.parameters) != null ? _b : []].flat()) {
3110
+ const result2 = jsonschema.validate(body.values, parameters);
3111
+ if (!result2.valid) {
3112
+ res.status(400).json({ errors: result2.errors });
3113
+ return;
3114
+ }
3115
+ }
3116
+ const steps = template.spec.steps.map((step, index) => {
3117
+ var _a3, _b2;
3118
+ return {
3119
+ ...step,
3120
+ id: (_a3 = step.id) != null ? _a3 : `step-${index + 1}`,
3121
+ name: (_b2 = step.name) != null ? _b2 : step.action
3122
+ };
3123
+ });
3124
+ const result = await dryRunner({
3125
+ spec: {
3126
+ apiVersion: template.apiVersion,
3127
+ steps,
3128
+ output: (_c = template.spec.output) != null ? _c : {},
3129
+ parameters: body.values
3130
+ },
3131
+ directoryContents: ((_d = body.directoryContents) != null ? _d : []).map((file) => ({
3132
+ path: file.path,
3133
+ content: Buffer.from(file.base64Content, "base64")
3134
+ })),
3135
+ secrets: {
3136
+ ...body.secrets,
3137
+ ...token && { backstageToken: token }
3138
+ }
3139
+ });
3140
+ res.status(200).json({
3141
+ ...result,
3142
+ steps,
3143
+ directoryContents: result.directoryContents.map((file) => ({
3144
+ path: file.path,
3145
+ executable: file.executable,
3146
+ base64Content: file.content.toString("base64")
3147
+ }))
3148
+ });
3149
+ });
3150
+ const app = express__default["default"]();
3151
+ app.set("logger", logger);
3152
+ app.use("/", router);
3153
+ async function authorizeTemplate(entityRef, token) {
3154
+ const template = await findTemplate({
3155
+ catalogApi: catalogClient,
3156
+ entityRef,
3157
+ token
3158
+ });
3159
+ if (!isSupportedTemplate(template)) {
3160
+ throw new errors.InputError(
3161
+ `Unsupported apiVersion field in schema entity, ${template.apiVersion}`
3162
+ );
3163
+ }
3164
+ if (!permissions) {
3165
+ return template;
3166
+ }
3167
+ const [parameterDecision, stepDecision] = await permissions.authorizeConditional(
3168
+ [
3169
+ { permission: alpha.templateParameterReadPermission },
3170
+ { permission: alpha.templateStepReadPermission }
3171
+ ],
3172
+ { token }
3173
+ );
3174
+ if (Array.isArray(template.spec.parameters)) {
3175
+ template.spec.parameters = template.spec.parameters.filter(
3176
+ (step) => isAuthorized(parameterDecision, step)
3177
+ );
3178
+ } else if (template.spec.parameters && !isAuthorized(parameterDecision, template.spec.parameters)) {
3179
+ template.spec.parameters = void 0;
3180
+ }
3181
+ template.spec.steps = template.spec.steps.filter(
3182
+ (step) => isAuthorized(stepDecision, step)
3183
+ );
3184
+ return template;
3185
+ }
3186
+ return app;
3187
+ }
3188
+
3189
+ exports.DatabaseTaskStore = DatabaseTaskStore;
3190
+ exports.TaskManager = TaskManager;
3191
+ exports.TaskWorker = TaskWorker;
3192
+ exports.TemplateActionRegistry = TemplateActionRegistry;
3193
+ exports.createBuiltinActions = createBuiltinActions;
3194
+ exports.createCatalogRegisterAction = createCatalogRegisterAction;
3195
+ exports.createCatalogWriteAction = createCatalogWriteAction;
3196
+ exports.createDebugLogAction = createDebugLogAction;
3197
+ exports.createFetchCatalogEntityAction = createFetchCatalogEntityAction;
3198
+ exports.createFetchPlainAction = createFetchPlainAction;
3199
+ exports.createFetchPlainFileAction = createFetchPlainFileAction;
3200
+ exports.createFetchTemplateAction = createFetchTemplateAction;
3201
+ exports.createFilesystemDeleteAction = createFilesystemDeleteAction;
3202
+ exports.createFilesystemRenameAction = createFilesystemRenameAction;
3203
+ exports.createRouter = createRouter;
3204
+ exports.createWaitAction = createWaitAction;
3205
+ exports.scaffolderActionRules = scaffolderActionRules;
3206
+ exports.scaffolderTemplateRules = scaffolderTemplateRules;
3207
+ //# sourceMappingURL=router-46321f3f.cjs.js.map