@backstage/plugin-scaffolder-backend 3.4.1-next.0 → 4.0.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/actions/listScaffolderTasksAction.cjs.js +15 -3
- package/dist/actions/listScaffolderTasksAction.cjs.js.map +1 -1
- package/dist/lib/templating/SecureTemplater.cjs.js +17 -4
- package/dist/lib/templating/SecureTemplater.cjs.js.map +1 -1
- package/dist/scaffolder/actions/builtin/fetch/templateActionHandler.cjs.js +49 -43
- package/dist/scaffolder/actions/builtin/fetch/templateActionHandler.cjs.js.map +1 -1
- package/dist/scaffolder/actions/builtin/fetch/templateFileActionHandler.cjs.js +13 -9
- package/dist/scaffolder/actions/builtin/fetch/templateFileActionHandler.cjs.js.map +1 -1
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js +2 -2
- package/dist/scaffolder/dryrun/createDryRunner.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/DatabaseTaskStore.cjs.js +2 -2
- package/dist/scaffolder/tasks/DatabaseTaskStore.cjs.js.map +1 -1
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js +83 -19
- package/dist/scaffolder/tasks/NunjucksWorkflowRunner.cjs.js.map +1 -1
- package/dist/service/router.cjs.js +3 -3
- package/dist/service/router.cjs.js.map +1 -1
- package/dist/service/rules.cjs.js +1 -1
- package/dist/service/rules.cjs.js.map +1 -1
- package/package.json +11 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @backstage/plugin-scaffolder-backend
|
|
2
2
|
|
|
3
|
+
## 4.0.0-next.2
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- c78b3b6: Add explicit memory management to SecureTemplater usage
|
|
8
|
+
|
|
9
|
+
### Minor Changes
|
|
10
|
+
|
|
11
|
+
- 8006acf: The template parameter schema response now exposes a `formDecorators` field
|
|
12
|
+
instead of `EXPERIMENTAL_formDecorators`. Templates that still declare
|
|
13
|
+
`spec.EXPERIMENTAL_formDecorators` are read transparently and surfaced under
|
|
14
|
+
the new field.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 1ecc3ca: Fixed spelling mistakes in internal code
|
|
19
|
+
- Updated dependencies
|
|
20
|
+
- @backstage/backend-plugin-api@1.9.1-next.1
|
|
21
|
+
- @backstage/plugin-scaffolder-common@2.2.0-next.1
|
|
22
|
+
- @backstage/plugin-scaffolder-node@0.13.3-next.2
|
|
23
|
+
|
|
24
|
+
## 3.5.0-next.1
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- 77bee9f: Updated the `list-scaffolder-tasks` action to support the new "status" filter parameter, allowing the action to return tasks matching a specific status.
|
|
29
|
+
- 07e08be: Added `always()` and `failure()` status check functions for scaffolder steps. These functions can be used in the if field of a step to control execution after failures. `always()` ensures a step runs regardless of previous step outcomes, while `failure()` runs a step only when a previous step has failed.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- e9b78e9: Removed the `uuid` dependency and replaced usage with the built-in `crypto.randomUUID()`.
|
|
34
|
+
- Updated dependencies
|
|
35
|
+
- @backstage/catalog-model@1.8.1-next.1
|
|
36
|
+
- @backstage/plugin-catalog-node@2.2.1-next.1
|
|
37
|
+
- @backstage/plugin-scaffolder-node@0.13.3-next.1
|
|
38
|
+
- @backstage/plugin-permission-common@0.9.9-next.1
|
|
39
|
+
|
|
3
40
|
## 3.4.1-next.0
|
|
4
41
|
|
|
5
42
|
### Patch Changes
|
|
@@ -20,7 +20,7 @@ This allows you to list scaffolder tasks that have been created.
|
|
|
20
20
|
Each task has a unique id, specification, and status (one of open, processing, completed, failed, cancelled, skipped).
|
|
21
21
|
Each task includes a timestamp for when it was created, and an optional last heartbeat timestamp indicating the most recent activity.
|
|
22
22
|
Set owned to true to return only tasks created by the current user; omit or set to false for all tasks the credentials can see.
|
|
23
|
-
Pagination is supported via limit and offset.
|
|
23
|
+
Filtering by one or multiple statuses is supported. Pagination is supported via limit and offset.
|
|
24
24
|
`,
|
|
25
25
|
schema: {
|
|
26
26
|
input: (z) => z.object({
|
|
@@ -28,7 +28,18 @@ Pagination is supported via limit and offset.
|
|
|
28
28
|
"If true, return only tasks created by the current user. Requires a user identity."
|
|
29
29
|
),
|
|
30
30
|
limit: z.number().int().min(1).max(1e3).describe("The maximum number of tasks to return for pagination").optional(),
|
|
31
|
-
offset: z.number().int().min(0).describe("The offset to start from for pagination").optional()
|
|
31
|
+
offset: z.number().int().min(0).describe("The offset to start from for pagination").optional(),
|
|
32
|
+
status: (() => {
|
|
33
|
+
const statusEnum = z.enum([
|
|
34
|
+
"open",
|
|
35
|
+
"processing",
|
|
36
|
+
"completed",
|
|
37
|
+
"failed",
|
|
38
|
+
"cancelled",
|
|
39
|
+
"skipped"
|
|
40
|
+
]);
|
|
41
|
+
return z.union([statusEnum, z.array(statusEnum).nonempty()]).optional().describe("Filter tasks by status, or an array of statuses");
|
|
42
|
+
})()
|
|
32
43
|
}),
|
|
33
44
|
output: (z) => z.object({
|
|
34
45
|
tasks: z.array(
|
|
@@ -56,7 +67,8 @@ Pagination is supported via limit and offset.
|
|
|
56
67
|
{
|
|
57
68
|
createdBy,
|
|
58
69
|
limit: input.limit,
|
|
59
|
-
offset: input.offset
|
|
70
|
+
offset: input.offset,
|
|
71
|
+
status: input.status
|
|
60
72
|
},
|
|
61
73
|
{ credentials }
|
|
62
74
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"listScaffolderTasksAction.cjs.js","sources":["../../src/actions/listScaffolderTasksAction.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { NotAllowedError } from '@backstage/errors';\nimport { ScaffolderService } from '@backstage/plugin-scaffolder-node';\n\nexport const createListScaffolderTasksAction = ({\n actionsRegistry,\n auth,\n scaffolderService,\n}: {\n actionsRegistry: ActionsRegistryService;\n auth: AuthService;\n scaffolderService: ScaffolderService;\n}) => {\n actionsRegistry.register({\n name: 'list-scaffolder-tasks',\n title: 'List Scaffolder Tasks',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description: `\nThis allows you to list scaffolder tasks that have been created.\nEach task has a unique id, specification, and status (one of open, processing, completed, failed, cancelled, skipped).\nEach task includes a timestamp for when it was created, and an optional last heartbeat timestamp indicating the most recent activity.\nSet owned to true to return only tasks created by the current user; omit or set to false for all tasks the credentials can see.\
|
|
1
|
+
{"version":3,"file":"listScaffolderTasksAction.cjs.js","sources":["../../src/actions/listScaffolderTasksAction.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';\nimport { AuthService } from '@backstage/backend-plugin-api';\nimport { NotAllowedError } from '@backstage/errors';\nimport { ScaffolderService } from '@backstage/plugin-scaffolder-node';\n\nexport const createListScaffolderTasksAction = ({\n actionsRegistry,\n auth,\n scaffolderService,\n}: {\n actionsRegistry: ActionsRegistryService;\n auth: AuthService;\n scaffolderService: ScaffolderService;\n}) => {\n actionsRegistry.register({\n name: 'list-scaffolder-tasks',\n title: 'List Scaffolder Tasks',\n attributes: {\n destructive: false,\n readOnly: true,\n idempotent: true,\n },\n description: `\nThis allows you to list scaffolder tasks that have been created.\nEach task has a unique id, specification, and status (one of open, processing, completed, failed, cancelled, skipped).\nEach task includes a timestamp for when it was created, and an optional last heartbeat timestamp indicating the most recent activity.\nSet owned to true to return only tasks created by the current user; omit or set to false for all tasks the credentials can see.\nFiltering by one or multiple statuses is supported. Pagination is supported via limit and offset.\n `,\n schema: {\n input: z =>\n z.object({\n owned: z\n .boolean()\n .optional()\n .default(false)\n .describe(\n 'If true, return only tasks created by the current user. Requires a user identity.',\n ),\n limit: z\n .number()\n .int()\n .min(1)\n .max(1000)\n .describe('The maximum number of tasks to return for pagination')\n .optional(),\n offset: z\n .number()\n .int()\n .min(0)\n .describe('The offset to start from for pagination')\n .optional(),\n status: (() => {\n const statusEnum = z.enum([\n 'open',\n 'processing',\n 'completed',\n 'failed',\n 'cancelled',\n 'skipped',\n ]);\n return z\n .union([statusEnum, z.array(statusEnum).nonempty()])\n .optional()\n .describe('Filter tasks by status, or an array of statuses');\n })(),\n }),\n output: z =>\n z\n .object({\n tasks: z\n .array(\n z.object({\n id: z.string().describe('The task identifier'),\n spec: z.unknown().describe('The task specification'),\n status: z\n .string()\n .describe(\n 'Task status: open, processing, completed, failed, cancelled, or skipped',\n ),\n createdAt: z\n .string()\n .describe('Timestamp when the task was created'),\n lastHeartbeatAt: z\n .string()\n .optional()\n .describe('Timestamp of the last heartbeat'),\n }),\n )\n .describe('The list of scaffolder tasks'),\n totalTasks: z\n .number()\n .describe('Total number of tasks matching the filter'),\n })\n .describe('Object containing a tasks array and totalTasks count'),\n },\n action: async ({ input, credentials }) => {\n if (input.owned && !auth.isPrincipal(credentials, 'user')) {\n throw new NotAllowedError(\n 'Filtering by owned tasks requires a user identity.',\n );\n }\n\n const createdBy =\n input.owned && auth.isPrincipal(credentials, 'user')\n ? credentials.principal.userEntityRef\n : undefined;\n\n const { items, totalItems } = await scaffolderService.listTasks(\n {\n createdBy,\n limit: input.limit,\n offset: input.offset,\n status: input.status,\n },\n { credentials },\n );\n\n return {\n output: {\n tasks: items.map(task => ({\n id: task.id,\n spec: task.spec,\n status: task.status,\n createdAt: task.createdAt,\n lastHeartbeatAt: task.lastHeartbeatAt,\n })),\n totalTasks: totalItems,\n },\n };\n },\n });\n};\n"],"names":["NotAllowedError"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var isolatedVm = require('isolated-vm');
|
|
4
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
5
4
|
var fs = require('fs-extra');
|
|
5
|
+
var isolatedVm = require('isolated-vm');
|
|
6
6
|
var helpers = require('./helpers.cjs.js');
|
|
7
7
|
|
|
8
8
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
@@ -157,8 +157,10 @@ class SecureTemplater {
|
|
|
157
157
|
);
|
|
158
158
|
await nunjucksScript.run(context);
|
|
159
159
|
const render = (template, values) => {
|
|
160
|
-
if (!context) {
|
|
161
|
-
throw new Error(
|
|
160
|
+
if (!context || isolate.isDisposed) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
"SecureTemplater has not been initialized or has been disposed"
|
|
163
|
+
);
|
|
162
164
|
}
|
|
163
165
|
contextGlobal.setSync("templateStr", String(template));
|
|
164
166
|
contextGlobal.setSync("templateValues", JSON.stringify(values));
|
|
@@ -167,7 +169,18 @@ class SecureTemplater {
|
|
|
167
169
|
}
|
|
168
170
|
return context.evalSync(`render(templateStr, templateValues)`);
|
|
169
171
|
};
|
|
170
|
-
return
|
|
172
|
+
return {
|
|
173
|
+
render,
|
|
174
|
+
dispose: () => {
|
|
175
|
+
if (context && !isolate.isDisposed) {
|
|
176
|
+
try {
|
|
177
|
+
context.release();
|
|
178
|
+
isolate.dispose();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
186
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecureTemplater.cjs.js","sources":["../../../src/lib/templating/SecureTemplater.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Isolate } from 'isolated-vm';\nimport { resolvePackagePath } from '@backstage/backend-plugin-api';\nimport {\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport fs from 'fs-extra';\nimport { JsonValue } from '@backstage/types';\nimport { getMajorNodeVersion, isNoNodeSnapshotOptionProvided } from './helpers';\n\n// language=JavaScript\nconst mkScript = (nunjucksSource: string) => `\nconst { render, renderCompat } = (() => {\n const module = {};\n const process = { env: {} };\n const require = (pkg) => { if (pkg === 'events') { return function (){}; }};\n\n ${nunjucksSource}\n\n const env = module.exports.configure({\n autoescape: false,\n ...JSON.parse(nunjucksConfigs),\n tags: {\n variableStart: '\\${{',\n variableEnd: '}}',\n },\n });\n\n const compatEnv = module.exports.configure({\n autoescape: false,\n ...JSON.parse(nunjucksConfigs),\n tags: {\n variableStart: '{{',\n variableEnd: '}}',\n },\n });\n compatEnv.addFilter('jsonify', compatEnv.getFilter('dump'));\n\n const handleFunctionResult = (value) => {\n return value === '' ? undefined : JSON.parse(value);\n };\n for (const name of JSON.parse(availableTemplateFilters)) {\n env.addFilter(name, (...args) => handleFunctionResult(callFilter(name, args)));\n }\n for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) {\n env.addGlobal(name, value);\n }\n for (const name of JSON.parse(availableTemplateCallbacks)) {\n env.addGlobal(name, (...args) => handleFunctionResult(callGlobal(name, args)));\n }\n\n let uninstallCompat = undefined;\n\n function render(str, values) {\n try {\n if (uninstallCompat) {\n uninstallCompat();\n uninstallCompat = undefined;\n }\n return env.renderString(str, JSON.parse(values));\n } catch (error) {\n // Make sure errors don't leak anything\n throw new Error(String(error.message));\n }\n }\n\n function renderCompat(str, values) {\n try {\n if (!uninstallCompat) {\n uninstallCompat = module.exports.installJinjaCompat();\n }\n return compatEnv.renderString(str, JSON.parse(values));\n } catch (error) {\n // Make sure errors don't leak anything\n throw new Error(String(error.message));\n }\n }\n\n return { render, renderCompat };\n})();\n`;\n\ninterface SecureTemplaterOptions {\n /* Enables jinja compatibility and the \"jsonify\" filter */\n cookiecutterCompat?: boolean;\n /* Extra user-provided nunjucks filters */\n templateFilters?: Record<string, TemplateFilter>;\n /* Extra user-provided nunjucks globals */\n templateGlobals?: Record<string, TemplateGlobal>;\n nunjucksConfigs?: { trimBlocks?: boolean; lstripBlocks?: boolean };\n}\n\nexport type SecureTemplateRenderer = (\n template: string,\n values: unknown,\n) => string;\n\nexport class SecureTemplater {\n static async loadRenderer(options: SecureTemplaterOptions = {}) {\n const {\n cookiecutterCompat,\n templateFilters = {},\n templateGlobals = {},\n nunjucksConfigs = {},\n } = options;\n\n const nodeVersion = getMajorNodeVersion();\n if (nodeVersion >= 20 && !isNoNodeSnapshotOptionProvided()) {\n throw new Error(\n `When using Node.js version 20 or newer, the scaffolder backend plugin requires that it be started with the --no-node-snapshot option. \n Please make sure that you have NODE_OPTIONS=--no-node-snapshot in your environment.`,\n );\n }\n\n const isolate = new Isolate({ memoryLimit: 128 });\n const context = await isolate.createContext();\n const contextGlobal = context.global;\n\n const nunjucksSource = await fs.readFile(\n resolvePackagePath(\n '@backstage/plugin-scaffolder-backend',\n 'assets/nunjucks.js.txt',\n ),\n 'utf-8',\n );\n\n const nunjucksScript = await isolate.compileScript(\n mkScript(nunjucksSource),\n );\n\n await contextGlobal.set('nunjucksConfigs', JSON.stringify(nunjucksConfigs));\n\n const availableFilters = Object.keys(templateFilters);\n\n await contextGlobal.set(\n 'availableTemplateFilters',\n JSON.stringify(availableFilters),\n );\n\n const globalCallbacks = [];\n const globalValues: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(templateGlobals)) {\n if (typeof value === 'function') {\n globalCallbacks.push(name);\n } else {\n globalValues[name] = value;\n }\n }\n\n await contextGlobal.set(\n 'availableTemplateGlobals',\n JSON.stringify(globalValues),\n );\n await contextGlobal.set(\n 'availableTemplateCallbacks',\n JSON.stringify(globalCallbacks),\n );\n\n await contextGlobal.set(\n 'callFilter',\n (filterName: string, args: JsonValue[]) => {\n if (!Object.hasOwn(templateFilters, filterName)) {\n return '';\n }\n const [input, ...rest] = args;\n const rz = templateFilters[filterName](input, ...rest);\n return rz === undefined ? '' : JSON.stringify(rz);\n },\n );\n\n await contextGlobal.set(\n 'callGlobal',\n (globalName: string, args: JsonValue[]) => {\n if (!Object.hasOwn(templateGlobals, globalName)) {\n return '';\n }\n const global = templateGlobals[globalName];\n if (typeof global !== 'function') {\n return '';\n }\n const rz = global(...args);\n return rz === undefined ? '' : JSON.stringify(rz);\n },\n );\n\n await nunjucksScript.run(context);\n\n const render: SecureTemplateRenderer = (template, values) => {\n if (!context) {\n throw new Error('SecureTemplater has not been initialized');\n }\n\n contextGlobal.setSync('templateStr', String(template));\n contextGlobal.setSync('templateValues', JSON.stringify(values));\n\n if (cookiecutterCompat) {\n return context.evalSync(`renderCompat(templateStr, templateValues)`);\n }\n\n return context.evalSync(`render(templateStr, templateValues)`);\n };\n return render;\n }\n}\n"],"names":["getMajorNodeVersion","isNoNodeSnapshotOptionProvided","Isolate","fs","resolvePackagePath"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SecureTemplater.cjs.js","sources":["../../../src/lib/templating/SecureTemplater.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolvePackagePath } from '@backstage/backend-plugin-api';\nimport {\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport { JsonValue } from '@backstage/types';\nimport fs from 'fs-extra';\nimport { Isolate } from 'isolated-vm';\nimport { getMajorNodeVersion, isNoNodeSnapshotOptionProvided } from './helpers';\n\n// language=JavaScript\nconst mkScript = (nunjucksSource: string) => `\nconst { render, renderCompat } = (() => {\n const module = {};\n const process = { env: {} };\n const require = (pkg) => { if (pkg === 'events') { return function (){}; }};\n\n ${nunjucksSource}\n\n const env = module.exports.configure({\n autoescape: false,\n ...JSON.parse(nunjucksConfigs),\n tags: {\n variableStart: '\\${{',\n variableEnd: '}}',\n },\n });\n\n const compatEnv = module.exports.configure({\n autoescape: false,\n ...JSON.parse(nunjucksConfigs),\n tags: {\n variableStart: '{{',\n variableEnd: '}}',\n },\n });\n compatEnv.addFilter('jsonify', compatEnv.getFilter('dump'));\n\n const handleFunctionResult = (value) => {\n return value === '' ? undefined : JSON.parse(value);\n };\n for (const name of JSON.parse(availableTemplateFilters)) {\n env.addFilter(name, (...args) => handleFunctionResult(callFilter(name, args)));\n }\n for (const [name, value] of Object.entries(JSON.parse(availableTemplateGlobals))) {\n env.addGlobal(name, value);\n }\n for (const name of JSON.parse(availableTemplateCallbacks)) {\n env.addGlobal(name, (...args) => handleFunctionResult(callGlobal(name, args)));\n }\n\n let uninstallCompat = undefined;\n\n function render(str, values) {\n try {\n if (uninstallCompat) {\n uninstallCompat();\n uninstallCompat = undefined;\n }\n return env.renderString(str, JSON.parse(values));\n } catch (error) {\n // Make sure errors don't leak anything\n throw new Error(String(error.message));\n }\n }\n\n function renderCompat(str, values) {\n try {\n if (!uninstallCompat) {\n uninstallCompat = module.exports.installJinjaCompat();\n }\n return compatEnv.renderString(str, JSON.parse(values));\n } catch (error) {\n // Make sure errors don't leak anything\n throw new Error(String(error.message));\n }\n }\n\n return { render, renderCompat };\n})();\n`;\n\ninterface SecureTemplaterOptions {\n /* Enables jinja compatibility and the \"jsonify\" filter */\n cookiecutterCompat?: boolean;\n /* Extra user-provided nunjucks filters */\n templateFilters?: Record<string, TemplateFilter>;\n /* Extra user-provided nunjucks globals */\n templateGlobals?: Record<string, TemplateGlobal>;\n nunjucksConfigs?: { trimBlocks?: boolean; lstripBlocks?: boolean };\n}\n\nexport type SecureTemplateRenderer = (\n template: string,\n values: unknown,\n) => string;\n\nexport class SecureTemplater {\n static async loadRenderer(options: SecureTemplaterOptions = {}) {\n const {\n cookiecutterCompat,\n templateFilters = {},\n templateGlobals = {},\n nunjucksConfigs = {},\n } = options;\n\n const nodeVersion = getMajorNodeVersion();\n if (nodeVersion >= 20 && !isNoNodeSnapshotOptionProvided()) {\n throw new Error(\n `When using Node.js version 20 or newer, the scaffolder backend plugin requires that it be started with the --no-node-snapshot option. \n Please make sure that you have NODE_OPTIONS=--no-node-snapshot in your environment.`,\n );\n }\n\n const isolate = new Isolate({ memoryLimit: 128 });\n const context = await isolate.createContext();\n const contextGlobal = context.global;\n\n const nunjucksSource = await fs.readFile(\n resolvePackagePath(\n '@backstage/plugin-scaffolder-backend',\n 'assets/nunjucks.js.txt',\n ),\n 'utf-8',\n );\n\n const nunjucksScript = await isolate.compileScript(\n mkScript(nunjucksSource),\n );\n\n await contextGlobal.set('nunjucksConfigs', JSON.stringify(nunjucksConfigs));\n\n const availableFilters = Object.keys(templateFilters);\n\n await contextGlobal.set(\n 'availableTemplateFilters',\n JSON.stringify(availableFilters),\n );\n\n const globalCallbacks = [];\n const globalValues: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(templateGlobals)) {\n if (typeof value === 'function') {\n globalCallbacks.push(name);\n } else {\n globalValues[name] = value;\n }\n }\n\n await contextGlobal.set(\n 'availableTemplateGlobals',\n JSON.stringify(globalValues),\n );\n await contextGlobal.set(\n 'availableTemplateCallbacks',\n JSON.stringify(globalCallbacks),\n );\n\n await contextGlobal.set(\n 'callFilter',\n (filterName: string, args: JsonValue[]) => {\n if (!Object.hasOwn(templateFilters, filterName)) {\n return '';\n }\n const [input, ...rest] = args;\n const rz = templateFilters[filterName](input, ...rest);\n return rz === undefined ? '' : JSON.stringify(rz);\n },\n );\n\n await contextGlobal.set(\n 'callGlobal',\n (globalName: string, args: JsonValue[]) => {\n if (!Object.hasOwn(templateGlobals, globalName)) {\n return '';\n }\n const global = templateGlobals[globalName];\n if (typeof global !== 'function') {\n return '';\n }\n const rz = global(...args);\n return rz === undefined ? '' : JSON.stringify(rz);\n },\n );\n\n await nunjucksScript.run(context);\n\n const render: SecureTemplateRenderer = (template, values) => {\n if (!context || isolate.isDisposed) {\n throw new Error(\n 'SecureTemplater has not been initialized or has been disposed',\n );\n }\n\n contextGlobal.setSync('templateStr', String(template));\n contextGlobal.setSync('templateValues', JSON.stringify(values));\n\n if (cookiecutterCompat) {\n return context.evalSync(`renderCompat(templateStr, templateValues)`);\n }\n\n return context.evalSync(`render(templateStr, templateValues)`);\n };\n\n return {\n render,\n dispose: () => {\n if (context && !isolate.isDisposed) {\n try {\n context.release();\n isolate.dispose();\n } catch (error) {\n // Ignore errors during dispose, as there's not much we can do about it\n }\n }\n },\n };\n }\n}\n"],"names":["getMajorNodeVersion","isNoNodeSnapshotOptionProvided","Isolate","fs","resolvePackagePath"],"mappings":"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"}
|
|
@@ -57,7 +57,7 @@ async function createTemplateActionHandler(options) {
|
|
|
57
57
|
`Processing ${allEntriesInTemplate.length} template files/directories with input values`,
|
|
58
58
|
ctx.input.values
|
|
59
59
|
);
|
|
60
|
-
const renderTemplate = await SecureTemplater.SecureTemplater.loadRenderer({
|
|
60
|
+
const { render: renderTemplate, dispose } = await SecureTemplater.SecureTemplater.loadRenderer({
|
|
61
61
|
cookiecutterCompat: ctx.input.cookiecutterCompat,
|
|
62
62
|
templateFilters,
|
|
63
63
|
templateGlobals,
|
|
@@ -66,57 +66,63 @@ async function createTemplateActionHandler(options) {
|
|
|
66
66
|
lstripBlocks: ctx.input.lstripBlocks
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else {
|
|
79
|
-
renderContents = !nonTemplatedEntries.has(location);
|
|
80
|
-
if (renderFilename) {
|
|
69
|
+
try {
|
|
70
|
+
for (const location of allEntriesInTemplate) {
|
|
71
|
+
let renderContents;
|
|
72
|
+
let localOutputPath = location;
|
|
73
|
+
if (extension) {
|
|
74
|
+
renderContents = path.extname(localOutputPath) === extension;
|
|
75
|
+
if (renderContents) {
|
|
76
|
+
localOutputPath = localOutputPath.slice(0, -extension.length);
|
|
77
|
+
}
|
|
81
78
|
localOutputPath = renderTemplate(localOutputPath, context);
|
|
82
79
|
} else {
|
|
83
|
-
|
|
80
|
+
renderContents = !nonTemplatedEntries.has(location);
|
|
81
|
+
if (renderFilename) {
|
|
82
|
+
localOutputPath = renderTemplate(localOutputPath, context);
|
|
83
|
+
}
|
|
84
84
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!renderContents && !extension) {
|
|
94
|
-
ctx.logger.info(`Copying file/directory ${location} without processing.`);
|
|
95
|
-
}
|
|
96
|
-
if (location.endsWith("/")) {
|
|
97
|
-
ctx.logger.info(`Writing directory ${location} to template output path.`);
|
|
98
|
-
await fs__default.default.ensureDir(outputPath);
|
|
99
|
-
} else {
|
|
100
|
-
const inputFilePath = backendPluginApi.resolveSafeChildPath(templateDir, location);
|
|
101
|
-
const stats = await fs__default.default.promises.lstat(inputFilePath);
|
|
102
|
-
if (stats.isSymbolicLink() || await isbinaryfile.isBinaryFile(inputFilePath)) {
|
|
85
|
+
if (containsSkippedContent(localOutputPath)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const outputPath = backendPluginApi.resolveSafeChildPath(outputDir, localOutputPath);
|
|
89
|
+
if (fs__default.default.existsSync(outputPath) && !ctx.input.replace) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!renderContents && !extension) {
|
|
103
93
|
ctx.logger.info(
|
|
104
|
-
`Copying file
|
|
94
|
+
`Copying file/directory ${location} without processing.`
|
|
105
95
|
);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const statsObj = await fs__default.default.stat(inputFilePath);
|
|
96
|
+
}
|
|
97
|
+
if (location.endsWith("/")) {
|
|
109
98
|
ctx.logger.info(
|
|
110
|
-
`Writing
|
|
111
|
-
);
|
|
112
|
-
const inputFileContents = await fs__default.default.readFile(inputFilePath, "utf-8");
|
|
113
|
-
await fs__default.default.outputFile(
|
|
114
|
-
outputPath,
|
|
115
|
-
renderContents ? renderTemplate(inputFileContents, context) : inputFileContents,
|
|
116
|
-
{ mode: statsObj.mode }
|
|
99
|
+
`Writing directory ${location} to template output path.`
|
|
117
100
|
);
|
|
101
|
+
await fs__default.default.ensureDir(outputPath);
|
|
102
|
+
} else {
|
|
103
|
+
const inputFilePath = backendPluginApi.resolveSafeChildPath(templateDir, location);
|
|
104
|
+
const stats = await fs__default.default.promises.lstat(inputFilePath);
|
|
105
|
+
if (stats.isSymbolicLink() || await isbinaryfile.isBinaryFile(inputFilePath)) {
|
|
106
|
+
ctx.logger.info(
|
|
107
|
+
`Copying file binary or symbolic link at ${location}, to template output path.`
|
|
108
|
+
);
|
|
109
|
+
await fs__default.default.copy(inputFilePath, outputPath);
|
|
110
|
+
} else {
|
|
111
|
+
const statsObj = await fs__default.default.stat(inputFilePath);
|
|
112
|
+
ctx.logger.info(
|
|
113
|
+
`Writing file ${location} to template output path with mode ${statsObj.mode}.`
|
|
114
|
+
);
|
|
115
|
+
const inputFileContents = await fs__default.default.readFile(inputFilePath, "utf-8");
|
|
116
|
+
await fs__default.default.outputFile(
|
|
117
|
+
outputPath,
|
|
118
|
+
renderContents ? renderTemplate(inputFileContents, context) : inputFileContents,
|
|
119
|
+
{ mode: statsObj.mode }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
118
122
|
}
|
|
119
123
|
}
|
|
124
|
+
} finally {
|
|
125
|
+
dispose();
|
|
120
126
|
}
|
|
121
127
|
ctx.logger.info(`Template result written to ${outputDir}`);
|
|
122
128
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templateActionHandler.cjs.js","sources":["../../../../../src/scaffolder/actions/builtin/fetch/templateActionHandler.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n isChildPath,\n resolveSafeChildPath,\n} from '@backstage/backend-plugin-api';\nimport { InputError } from '@backstage/errors';\nimport { ScmIntegrations } from '@backstage/integration';\nimport {\n ActionContext,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport fs from 'fs-extra';\nimport globby from 'globby';\nimport { isBinaryFile } from 'isbinaryfile';\nimport { createDefaultFilters } from '../../../../lib/templating/filters/createDefaultFilters';\nimport { convertFiltersToRecord } from '../../../../util/templating';\nimport { SecureTemplater } from '../../../../lib/templating/SecureTemplater';\nimport { extname } from 'node:path';\n\nexport type TemplateActionInput = {\n targetPath?: string;\n values: any;\n templateFileExtension?: string | boolean;\n copyWithoutTemplating?: string[];\n cookiecutterCompat?: boolean;\n replace?: boolean;\n trimBlocks?: boolean;\n lstripBlocks?: boolean;\n};\n\nexport async function createTemplateActionHandler<\n I extends TemplateActionInput,\n>(options: {\n ctx: ActionContext<I, any, any>;\n resolveTemplate: () => Promise<string>;\n integrations: ScmIntegrations;\n additionalTemplateFilters?: Record<string, TemplateFilter>;\n additionalTemplateGlobals?: Record<string, TemplateGlobal>;\n}) {\n const {\n resolveTemplate,\n integrations,\n additionalTemplateFilters,\n additionalTemplateGlobals: templateGlobals,\n ctx,\n } = options;\n\n const templateFilters = {\n ...convertFiltersToRecord(createDefaultFilters({ integrations })),\n ...additionalTemplateFilters,\n };\n\n const { outputDir, copyOnlyPatterns, renderFilename, extension } =\n resolveTemplateActionSettings(ctx);\n\n const templateDir = await resolveTemplate();\n\n if (isChildPath(templateDir, outputDir)) {\n throw new InputError('targetPath must not be within template path');\n }\n\n ctx.logger.info('Listing files and directories in template');\n const allEntriesInTemplate = await globby(`**/*`, {\n cwd: templateDir,\n dot: true,\n onlyFiles: false,\n markDirectories: true,\n followSymbolicLinks: false,\n });\n\n const nonTemplatedEntries = new Set(\n await globby(copyOnlyPatterns || [], {\n cwd: templateDir,\n dot: true,\n onlyFiles: false,\n markDirectories: true,\n followSymbolicLinks: false,\n }),\n );\n\n // Cookiecutter prefixes all parameters in templates with\n // `cookiecutter.`. To replicate this, we wrap our parameters\n // in an object with a `cookiecutter` property when compat\n // mode is enabled.\n const { cookiecutterCompat, values } = ctx.input;\n const context = {\n [cookiecutterCompat ? 'cookiecutter' : 'values']: values,\n };\n\n ctx.logger.info(\n `Processing ${allEntriesInTemplate.length} template files/directories with input values`,\n ctx.input.values,\n );\n\n const renderTemplate = await SecureTemplater.loadRenderer({\n cookiecutterCompat: ctx.input.cookiecutterCompat,\n templateFilters,\n templateGlobals,\n nunjucksConfigs: {\n trimBlocks: ctx.input.trimBlocks,\n lstripBlocks: ctx.input.lstripBlocks,\n },\n });\n\n for (const location of allEntriesInTemplate) {\n let renderContents: boolean;\n\n let localOutputPath = location;\n if (extension) {\n renderContents = extname(localOutputPath) === extension;\n if (renderContents) {\n localOutputPath = localOutputPath.slice(0, -extension.length);\n }\n // extension is mutual exclusive with copyWithoutRender/copyWithoutTemplating,\n // therefore the output path is always rendered.\n localOutputPath = renderTemplate(localOutputPath, context);\n } else {\n renderContents = !nonTemplatedEntries.has(location);\n // The logic here is a bit tangled because it depends on two variables.\n // If renderFilename is true, which means copyWithoutTemplating is used,\n // then the path is always rendered.\n // If renderFilename is false, which means copyWithoutRender is used,\n // then matched file/directory won't be processed, same as before.\n if (renderFilename) {\n localOutputPath = renderTemplate(localOutputPath, context);\n } else {\n localOutputPath = renderContents\n ? renderTemplate(localOutputPath, context)\n : localOutputPath;\n }\n }\n\n if (containsSkippedContent(localOutputPath)) {\n continue;\n }\n\n const outputPath = resolveSafeChildPath(outputDir, localOutputPath);\n if (fs.existsSync(outputPath) && !ctx.input.replace) {\n continue;\n }\n\n if (!renderContents && !extension) {\n ctx.logger.info(`Copying file/directory ${location} without processing.`);\n }\n\n if (location.endsWith('/')) {\n ctx.logger.info(`Writing directory ${location} to template output path.`);\n await fs.ensureDir(outputPath);\n } else {\n const inputFilePath = resolveSafeChildPath(templateDir, location);\n const stats = await fs.promises.lstat(inputFilePath);\n\n if (stats.isSymbolicLink() || (await isBinaryFile(inputFilePath))) {\n ctx.logger.info(\n `Copying file binary or symbolic link at ${location}, to template output path.`,\n );\n await fs.copy(inputFilePath, outputPath);\n } else {\n const statsObj = await fs.stat(inputFilePath);\n ctx.logger.info(\n `Writing file ${location} to template output path with mode ${statsObj.mode}.`,\n );\n const inputFileContents = await fs.readFile(inputFilePath, 'utf-8');\n await fs.outputFile(\n outputPath,\n renderContents\n ? renderTemplate(inputFileContents, context)\n : inputFileContents,\n { mode: statsObj.mode },\n );\n }\n }\n }\n ctx.logger.info(`Template result written to ${outputDir}`);\n}\n\nfunction resolveTemplateActionSettings<I extends TemplateActionInput>(\n ctx: ActionContext<I, any, any>,\n): {\n outputDir: string;\n copyOnlyPatterns?: string[];\n renderFilename: boolean;\n extension: string | false;\n} {\n const targetPath = ctx.input.targetPath ?? './';\n const outputDir = resolveSafeChildPath(ctx.workspacePath, targetPath);\n\n const copyOnlyPatterns = ctx.input.copyWithoutTemplating;\n const renderFilename = true;\n\n if (copyOnlyPatterns && !Array.isArray(copyOnlyPatterns)) {\n throw new InputError(\n 'Fetch action input copyWithoutTemplating must be an Array',\n );\n }\n if (\n ctx.input.templateFileExtension &&\n (copyOnlyPatterns || ctx.input.cookiecutterCompat)\n ) {\n throw new InputError(\n 'Fetch action input extension incompatible with copyWithoutTemplating and cookiecutterCompat',\n );\n }\n let extension: string | false = false;\n if (ctx.input.templateFileExtension) {\n extension =\n ctx.input.templateFileExtension === true\n ? '.njk'\n : ctx.input.templateFileExtension;\n if (!extension.startsWith('.')) {\n extension = `.${extension}`;\n }\n }\n return {\n outputDir,\n copyOnlyPatterns,\n renderFilename,\n extension,\n };\n}\n\nfunction containsSkippedContent(localOutputPath: string): boolean {\n // if the path is empty means that there is a file skipped in the root\n // if the path starts with a separator it means that the root directory has been skipped\n // if the path includes // means that there is a subdirectory skipped\n // All paths returned are considered with / separator because of globby returning the linux separator for all os'.\n return (\n localOutputPath === '' ||\n localOutputPath.startsWith('/') ||\n localOutputPath.includes('//')\n );\n}\n"],"names":["convertFiltersToRecord","createDefaultFilters","isChildPath","InputError","globby","SecureTemplater","extname","resolveSafeChildPath","fs","isBinaryFile"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"templateActionHandler.cjs.js","sources":["../../../../../src/scaffolder/actions/builtin/fetch/templateActionHandler.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport {\n isChildPath,\n resolveSafeChildPath,\n} from '@backstage/backend-plugin-api';\nimport { InputError } from '@backstage/errors';\nimport { ScmIntegrations } from '@backstage/integration';\nimport {\n ActionContext,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport fs from 'fs-extra';\nimport globby from 'globby';\nimport { isBinaryFile } from 'isbinaryfile';\nimport { createDefaultFilters } from '../../../../lib/templating/filters/createDefaultFilters';\nimport { convertFiltersToRecord } from '../../../../util/templating';\nimport { SecureTemplater } from '../../../../lib/templating/SecureTemplater';\nimport { extname } from 'node:path';\n\nexport type TemplateActionInput = {\n targetPath?: string;\n values: any;\n templateFileExtension?: string | boolean;\n copyWithoutTemplating?: string[];\n cookiecutterCompat?: boolean;\n replace?: boolean;\n trimBlocks?: boolean;\n lstripBlocks?: boolean;\n};\n\nexport async function createTemplateActionHandler<\n I extends TemplateActionInput,\n>(options: {\n ctx: ActionContext<I, any, any>;\n resolveTemplate: () => Promise<string>;\n integrations: ScmIntegrations;\n additionalTemplateFilters?: Record<string, TemplateFilter>;\n additionalTemplateGlobals?: Record<string, TemplateGlobal>;\n}) {\n const {\n resolveTemplate,\n integrations,\n additionalTemplateFilters,\n additionalTemplateGlobals: templateGlobals,\n ctx,\n } = options;\n\n const templateFilters = {\n ...convertFiltersToRecord(createDefaultFilters({ integrations })),\n ...additionalTemplateFilters,\n };\n\n const { outputDir, copyOnlyPatterns, renderFilename, extension } =\n resolveTemplateActionSettings(ctx);\n\n const templateDir = await resolveTemplate();\n\n if (isChildPath(templateDir, outputDir)) {\n throw new InputError('targetPath must not be within template path');\n }\n\n ctx.logger.info('Listing files and directories in template');\n const allEntriesInTemplate = await globby(`**/*`, {\n cwd: templateDir,\n dot: true,\n onlyFiles: false,\n markDirectories: true,\n followSymbolicLinks: false,\n });\n\n const nonTemplatedEntries = new Set(\n await globby(copyOnlyPatterns || [], {\n cwd: templateDir,\n dot: true,\n onlyFiles: false,\n markDirectories: true,\n followSymbolicLinks: false,\n }),\n );\n\n // Cookiecutter prefixes all parameters in templates with\n // `cookiecutter.`. To replicate this, we wrap our parameters\n // in an object with a `cookiecutter` property when compat\n // mode is enabled.\n const { cookiecutterCompat, values } = ctx.input;\n const context = {\n [cookiecutterCompat ? 'cookiecutter' : 'values']: values,\n };\n\n ctx.logger.info(\n `Processing ${allEntriesInTemplate.length} template files/directories with input values`,\n ctx.input.values,\n );\n\n const { render: renderTemplate, dispose } =\n await SecureTemplater.loadRenderer({\n cookiecutterCompat: ctx.input.cookiecutterCompat,\n templateFilters,\n templateGlobals,\n nunjucksConfigs: {\n trimBlocks: ctx.input.trimBlocks,\n lstripBlocks: ctx.input.lstripBlocks,\n },\n });\n try {\n for (const location of allEntriesInTemplate) {\n let renderContents: boolean;\n\n let localOutputPath = location;\n if (extension) {\n renderContents = extname(localOutputPath) === extension;\n if (renderContents) {\n localOutputPath = localOutputPath.slice(0, -extension.length);\n }\n // extension is mutual exclusive with copyWithoutRender/copyWithoutTemplating,\n // therefore the output path is always rendered.\n localOutputPath = renderTemplate(localOutputPath, context);\n } else {\n renderContents = !nonTemplatedEntries.has(location);\n // The logic here is a bit tangled because it depends on two variables.\n // If renderFilename is true, which means copyWithoutTemplating is used,\n // then the path is always rendered.\n // If renderFilename is false, which means copyWithoutRender is used,\n // then matched file/directory won't be processed, same as before.\n if (renderFilename) {\n localOutputPath = renderTemplate(localOutputPath, context);\n } else {\n localOutputPath = renderContents\n ? renderTemplate(localOutputPath, context)\n : localOutputPath;\n }\n }\n\n if (containsSkippedContent(localOutputPath)) {\n continue;\n }\n\n const outputPath = resolveSafeChildPath(outputDir, localOutputPath);\n if (fs.existsSync(outputPath) && !ctx.input.replace) {\n continue;\n }\n\n if (!renderContents && !extension) {\n ctx.logger.info(\n `Copying file/directory ${location} without processing.`,\n );\n }\n\n if (location.endsWith('/')) {\n ctx.logger.info(\n `Writing directory ${location} to template output path.`,\n );\n await fs.ensureDir(outputPath);\n } else {\n const inputFilePath = resolveSafeChildPath(templateDir, location);\n const stats = await fs.promises.lstat(inputFilePath);\n\n if (stats.isSymbolicLink() || (await isBinaryFile(inputFilePath))) {\n ctx.logger.info(\n `Copying file binary or symbolic link at ${location}, to template output path.`,\n );\n await fs.copy(inputFilePath, outputPath);\n } else {\n const statsObj = await fs.stat(inputFilePath);\n ctx.logger.info(\n `Writing file ${location} to template output path with mode ${statsObj.mode}.`,\n );\n const inputFileContents = await fs.readFile(inputFilePath, 'utf-8');\n await fs.outputFile(\n outputPath,\n renderContents\n ? renderTemplate(inputFileContents, context)\n : inputFileContents,\n { mode: statsObj.mode },\n );\n }\n }\n }\n } finally {\n dispose();\n }\n ctx.logger.info(`Template result written to ${outputDir}`);\n}\n\nfunction resolveTemplateActionSettings<I extends TemplateActionInput>(\n ctx: ActionContext<I, any, any>,\n): {\n outputDir: string;\n copyOnlyPatterns?: string[];\n renderFilename: boolean;\n extension: string | false;\n} {\n const targetPath = ctx.input.targetPath ?? './';\n const outputDir = resolveSafeChildPath(ctx.workspacePath, targetPath);\n\n const copyOnlyPatterns = ctx.input.copyWithoutTemplating;\n const renderFilename = true;\n\n if (copyOnlyPatterns && !Array.isArray(copyOnlyPatterns)) {\n throw new InputError(\n 'Fetch action input copyWithoutTemplating must be an Array',\n );\n }\n if (\n ctx.input.templateFileExtension &&\n (copyOnlyPatterns || ctx.input.cookiecutterCompat)\n ) {\n throw new InputError(\n 'Fetch action input extension incompatible with copyWithoutTemplating and cookiecutterCompat',\n );\n }\n let extension: string | false = false;\n if (ctx.input.templateFileExtension) {\n extension =\n ctx.input.templateFileExtension === true\n ? '.njk'\n : ctx.input.templateFileExtension;\n if (!extension.startsWith('.')) {\n extension = `.${extension}`;\n }\n }\n return {\n outputDir,\n copyOnlyPatterns,\n renderFilename,\n extension,\n };\n}\n\nfunction containsSkippedContent(localOutputPath: string): boolean {\n // if the path is empty means that there is a file skipped in the root\n // if the path starts with a separator it means that the root directory has been skipped\n // if the path includes // means that there is a subdirectory skipped\n // All paths returned are considered with / separator because of globby returning the linux separator for all os'.\n return (\n localOutputPath === '' ||\n localOutputPath.startsWith('/') ||\n localOutputPath.includes('//')\n );\n}\n"],"names":["convertFiltersToRecord","createDefaultFilters","isChildPath","InputError","globby","SecureTemplater","extname","resolveSafeChildPath","fs","isBinaryFile"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var fs = require('fs-extra');
|
|
4
|
-
var createDefaultFilters = require('../../../../lib/templating/filters/createDefaultFilters.cjs.js');
|
|
5
|
-
var templating = require('../../../../util/templating.cjs.js');
|
|
6
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var fs = require('fs-extra');
|
|
7
5
|
var path = require('node:path');
|
|
6
|
+
var createDefaultFilters = require('../../../../lib/templating/filters/createDefaultFilters.cjs.js');
|
|
8
7
|
var SecureTemplater = require('../../../../lib/templating/SecureTemplater.cjs.js');
|
|
8
|
+
var templating = require('../../../../util/templating.cjs.js');
|
|
9
9
|
|
|
10
10
|
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
11
11
|
|
|
@@ -43,7 +43,7 @@ async function createTemplateFileActionHandler(options) {
|
|
|
43
43
|
`Processing template file with input values`,
|
|
44
44
|
ctx.input.values
|
|
45
45
|
);
|
|
46
|
-
const renderTemplate = await SecureTemplater.SecureTemplater.loadRenderer({
|
|
46
|
+
const { render: renderTemplate, dispose } = await SecureTemplater.SecureTemplater.loadRenderer({
|
|
47
47
|
cookiecutterCompat,
|
|
48
48
|
templateFilters,
|
|
49
49
|
templateGlobals,
|
|
@@ -52,11 +52,15 @@ async function createTemplateFileActionHandler(options) {
|
|
|
52
52
|
lstripBlocks: ctx.input.lstripBlocks
|
|
53
53
|
}
|
|
54
54
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
try {
|
|
56
|
+
const contents = await fs__default.default.readFile(filePath, "utf-8");
|
|
57
|
+
const result = renderTemplate(contents, context);
|
|
58
|
+
await fs__default.default.ensureDir(path__default.default.dirname(outputPath));
|
|
59
|
+
await fs__default.default.outputFile(outputPath, result);
|
|
60
|
+
ctx.logger.info(`Template file has been written to ${outputPath}`);
|
|
61
|
+
} finally {
|
|
62
|
+
dispose();
|
|
63
|
+
}
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
exports.createTemplateFileActionHandler = createTemplateFileActionHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templateFileActionHandler.cjs.js","sources":["../../../../../src/scaffolder/actions/builtin/fetch/templateFileActionHandler.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ScmIntegrations } from '@backstage/integration';\nimport {\n ActionContext,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport fs from 'fs-extra';\nimport { createDefaultFilters } from '../../../../lib/templating/filters/createDefaultFilters';\nimport {
|
|
1
|
+
{"version":3,"file":"templateFileActionHandler.cjs.js","sources":["../../../../../src/scaffolder/actions/builtin/fetch/templateFileActionHandler.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { resolveSafeChildPath } from '@backstage/backend-plugin-api';\nimport { ScmIntegrations } from '@backstage/integration';\nimport {\n ActionContext,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport fs from 'fs-extra';\nimport path from 'node:path';\nimport { createDefaultFilters } from '../../../../lib/templating/filters/createDefaultFilters';\nimport { SecureTemplater } from '../../../../lib/templating/SecureTemplater';\nimport { convertFiltersToRecord } from '../../../../util/templating';\n\nexport type TemplateFileActionInput = {\n targetPath: string;\n values: any;\n cookiecutterCompat?: boolean;\n replace?: boolean;\n trimBlocks?: boolean;\n lstripBlocks?: boolean;\n};\n\nexport async function createTemplateFileActionHandler<\n I extends TemplateFileActionInput = TemplateFileActionInput,\n>(options: {\n ctx: ActionContext<I, any, any>;\n resolveTemplateFile: () => Promise<string>;\n integrations: ScmIntegrations;\n additionalTemplateFilters?: Record<string, TemplateFilter>;\n additionalTemplateGlobals?: Record<string, TemplateGlobal>;\n}) {\n const {\n resolveTemplateFile,\n integrations,\n additionalTemplateFilters,\n additionalTemplateGlobals: templateGlobals,\n ctx,\n } = options;\n\n const templateFilters = {\n ...convertFiltersToRecord(createDefaultFilters({ integrations })),\n ...additionalTemplateFilters,\n };\n\n const outputPath = resolveSafeChildPath(\n ctx.workspacePath,\n ctx.input.targetPath,\n );\n\n if (fs.existsSync(outputPath) && !ctx.input.replace) {\n ctx.logger.info(\n `File ${ctx.input.targetPath} already exists in workspace, not replacing.`,\n );\n return;\n }\n const filePath = await resolveTemplateFile();\n\n const { cookiecutterCompat, values } = ctx.input;\n const context = {\n [cookiecutterCompat ? 'cookiecutter' : 'values']: values,\n };\n\n ctx.logger.info(\n `Processing template file with input values`,\n ctx.input.values,\n );\n\n const { render: renderTemplate, dispose } =\n await SecureTemplater.loadRenderer({\n cookiecutterCompat,\n templateFilters,\n templateGlobals,\n nunjucksConfigs: {\n trimBlocks: ctx.input.trimBlocks,\n lstripBlocks: ctx.input.lstripBlocks,\n },\n });\n\n try {\n const contents = await fs.readFile(filePath, 'utf-8');\n const result = renderTemplate(contents, context);\n await fs.ensureDir(path.dirname(outputPath));\n await fs.outputFile(outputPath, result);\n\n ctx.logger.info(`Template file has been written to ${outputPath}`);\n } finally {\n dispose();\n }\n}\n"],"names":["convertFiltersToRecord","createDefaultFilters","resolveSafeChildPath","fs","SecureTemplater","path"],"mappings":";;;;;;;;;;;;;;AAqCA,eAAsB,gCAEpB,OAAA,EAMC;AACD,EAAA,MAAM;AAAA,IACJ,mBAAA;AAAA,IACA,YAAA;AAAA,IACA,yBAAA;AAAA,IACA,yBAAA,EAA2B,eAAA;AAAA,IAC3B;AAAA,GACF,GAAI,OAAA;AAEJ,EAAA,MAAM,eAAA,GAAkB;AAAA,IACtB,GAAGA,iCAAA,CAAuBC,yCAAA,CAAqB,EAAE,YAAA,EAAc,CAAC,CAAA;AAAA,IAChE,GAAG;AAAA,GACL;AAEA,EAAA,MAAM,UAAA,GAAaC,qCAAA;AAAA,IACjB,GAAA,CAAI,aAAA;AAAA,IACJ,IAAI,KAAA,CAAM;AAAA,GACZ;AAEA,EAAA,IAAIC,oBAAG,UAAA,CAAW,UAAU,KAAK,CAAC,GAAA,CAAI,MAAM,OAAA,EAAS;AACnD,IAAA,GAAA,CAAI,MAAA,CAAO,IAAA;AAAA,MACT,CAAA,KAAA,EAAQ,GAAA,CAAI,KAAA,CAAM,UAAU,CAAA,4CAAA;AAAA,KAC9B;AACA,IAAA;AAAA,EACF;AACA,EAAA,MAAM,QAAA,GAAW,MAAM,mBAAA,EAAoB;AAE3C,EAAA,MAAM,EAAE,kBAAA,EAAoB,MAAA,EAAO,GAAI,GAAA,CAAI,KAAA;AAC3C,EAAA,MAAM,OAAA,GAAU;AAAA,IACd,CAAC,kBAAA,GAAqB,cAAA,GAAiB,QAAQ,GAAG;AAAA,GACpD;AAEA,EAAA,GAAA,CAAI,MAAA,CAAO,IAAA;AAAA,IACT,CAAA,0CAAA,CAAA;AAAA,IACA,IAAI,KAAA,CAAM;AAAA,GACZ;AAEA,EAAA,MAAM,EAAE,MAAA,EAAQ,cAAA,EAAgB,SAAQ,GACtC,MAAMC,gCAAgB,YAAA,CAAa;AAAA,IACjC,kBAAA;AAAA,IACA,eAAA;AAAA,IACA,eAAA;AAAA,IACA,eAAA,EAAiB;AAAA,MACf,UAAA,EAAY,IAAI,KAAA,CAAM,UAAA;AAAA,MACtB,YAAA,EAAc,IAAI,KAAA,CAAM;AAAA;AAC1B,GACD,CAAA;AAEH,EAAA,IAAI;AACF,IAAA,MAAM,QAAA,GAAW,MAAMD,mBAAA,CAAG,QAAA,CAAS,UAAU,OAAO,CAAA;AACpD,IAAA,MAAM,MAAA,GAAS,cAAA,CAAe,QAAA,EAAU,OAAO,CAAA;AAC/C,IAAA,MAAMA,mBAAA,CAAG,SAAA,CAAUE,qBAAA,CAAK,OAAA,CAAQ,UAAU,CAAC,CAAA;AAC3C,IAAA,MAAMF,mBAAA,CAAG,UAAA,CAAW,UAAA,EAAY,MAAM,CAAA;AAEtC,IAAA,GAAA,CAAI,MAAA,CAAO,IAAA,CAAK,CAAA,kCAAA,EAAqC,UAAU,CAAA,CAAE,CAAA;AAAA,EACnE,CAAA,SAAE;AACA,IAAA,OAAA,EAAQ;AAAA,EACV;AACF;;;;"}
|
|
@@ -4,7 +4,7 @@ var pluginScaffolderNode = require('@backstage/plugin-scaffolder-node');
|
|
|
4
4
|
var fs = require('fs-extra');
|
|
5
5
|
var path = require('node:path');
|
|
6
6
|
var node_url = require('node:url');
|
|
7
|
-
var
|
|
7
|
+
var node_crypto = require('node:crypto');
|
|
8
8
|
var NunjucksWorkflowRunner = require('../tasks/NunjucksWorkflowRunner.cjs.js');
|
|
9
9
|
var DecoratedActionsRegistry = require('./DecoratedActionsRegistry.cjs.js');
|
|
10
10
|
|
|
@@ -37,7 +37,7 @@ function createDryRunner(options) {
|
|
|
37
37
|
}
|
|
38
38
|
const basePath = node_url.fileURLToPath(new URL(baseUrl));
|
|
39
39
|
const contentsPath = path__default.default.dirname(basePath);
|
|
40
|
-
const dryRunId =
|
|
40
|
+
const dryRunId = node_crypto.randomUUID();
|
|
41
41
|
const log = new Array();
|
|
42
42
|
try {
|
|
43
43
|
await pluginScaffolderNode.deserializeDirectoryContents(contentsPath, input.directoryContents);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createDryRunner.cjs.js","sources":["../../../src/scaffolder/dryrun/createDryRunner.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuditorService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { MetricsService } from '@backstage/backend-plugin-api/alpha';\nimport type { UserEntity } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport {\n ScaffolderTaskStatus,\n TaskSpec,\n TemplateInfo,\n} from '@backstage/plugin-scaffolder-common';\nimport {\n createTemplateAction,\n deserializeDirectoryContents,\n SerializedFile,\n serializeDirectoryContents,\n TaskSecrets,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport { JsonObject } from '@backstage/types';\nimport fs from 'fs-extra';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {
|
|
1
|
+
{"version":3,"file":"createDryRunner.cjs.js","sources":["../../../src/scaffolder/dryrun/createDryRunner.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n AuditorService,\n BackstageCredentials,\n LoggerService,\n} from '@backstage/backend-plugin-api';\nimport type { MetricsService } from '@backstage/backend-plugin-api/alpha';\nimport type { UserEntity } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { PermissionEvaluator } from '@backstage/plugin-permission-common';\nimport {\n ScaffolderTaskStatus,\n TaskSpec,\n TemplateInfo,\n} from '@backstage/plugin-scaffolder-common';\nimport {\n createTemplateAction,\n deserializeDirectoryContents,\n SerializedFile,\n serializeDirectoryContents,\n TaskSecrets,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport { JsonObject } from '@backstage/types';\nimport fs from 'fs-extra';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { randomUUID as uuid } from 'node:crypto';\nimport { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner';\nimport { DecoratedActionsRegistry } from './DecoratedActionsRegistry';\nimport { TemplateActionRegistry } from '../actions';\n\ninterface DryRunInput {\n spec: TaskSpec;\n templateInfo: TemplateInfo;\n secrets?: TaskSecrets;\n directoryContents: SerializedFile[];\n credentials: BackstageCredentials;\n user?: {\n entity?: UserEntity;\n ref?: string;\n };\n}\n\ninterface DryRunResult {\n log: Array<{\n body: {\n message: string;\n stepId?: string;\n status?: ScaffolderTaskStatus;\n };\n }>;\n directoryContents: SerializedFile[];\n output: JsonObject;\n}\n\n/** @internal */\nexport type TemplateTesterCreateOptions = {\n logger: LoggerService;\n auditor?: AuditorService;\n integrations: ScmIntegrations;\n actionRegistry: TemplateActionRegistry;\n workingDirectory: string;\n additionalTemplateFilters?: Record<string, TemplateFilter>;\n additionalTemplateGlobals?: Record<string, TemplateGlobal>;\n permissions?: PermissionEvaluator;\n config?: Config;\n metrics: MetricsService;\n};\n\n/**\n * Executes a dry-run of the provided template.\n *\n * The provided content will be extracted into a temporary directory\n * which is then use as the base for any relative file fetch paths.\n *\n * @internal\n */\nexport function createDryRunner(options: TemplateTesterCreateOptions) {\n return async function dryRun(input: DryRunInput): Promise<DryRunResult> {\n let contentPromise;\n\n const workflowRunner = new NunjucksWorkflowRunner({\n ...options,\n actionRegistry: new DecoratedActionsRegistry(options.actionRegistry, [\n createTemplateAction({\n id: 'dry-run:extract',\n supportsDryRun: true,\n async handler(ctx) {\n contentPromise = serializeDirectoryContents(ctx.workspacePath);\n await contentPromise.catch(() => {});\n },\n }),\n ]),\n config: options.config,\n });\n\n // Extracting contentsPath and dryRunId from the baseUrl\n const baseUrl = input.templateInfo.baseUrl;\n if (!baseUrl) {\n throw new Error('baseUrl is required');\n }\n const basePath = fileURLToPath(new URL(baseUrl));\n const contentsPath = path.dirname(basePath);\n const dryRunId = uuid();\n\n const log = new Array<{\n body: {\n message: string;\n stepId?: string;\n status?: ScaffolderTaskStatus;\n };\n }>();\n\n try {\n await deserializeDirectoryContents(contentsPath, input.directoryContents);\n\n const abortSignal = new AbortController().signal;\n const result = await workflowRunner.execute({\n taskId: dryRunId,\n spec: {\n ...input.spec,\n steps: [\n ...input.spec.steps,\n {\n id: dryRunId,\n name: 'dry-run:extract',\n action: 'dry-run:extract',\n },\n ],\n templateInfo: input.templateInfo,\n },\n secrets: input.secrets,\n getInitiatorCredentials: () => Promise.resolve(input.credentials),\n // No need to update this at the end of the run, so just hard-code it\n done: false,\n isDryRun: true,\n getWorkspaceName: async () => `dry-run-${dryRunId}`,\n cancelSignal: abortSignal,\n async emitLog(message: string, logMetadata?: JsonObject) {\n if (logMetadata?.stepId === dryRunId) {\n return;\n }\n log.push({\n body: {\n ...logMetadata,\n message,\n },\n });\n },\n complete: async () => {\n throw new Error('Not implemented');\n },\n });\n\n if (!contentPromise) {\n throw new Error('Content extraction step was skipped');\n }\n const directoryContents = await contentPromise;\n\n return {\n log,\n directoryContents,\n output: result.output,\n };\n } finally {\n await fs.remove(contentsPath);\n }\n };\n}\n"],"names":["NunjucksWorkflowRunner","DecoratedActionsRegistry","createTemplateAction","serializeDirectoryContents","fileURLToPath","path","uuid","deserializeDirectoryContents","fs"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
4
|
var errors = require('@backstage/errors');
|
|
5
|
-
var
|
|
5
|
+
var node_crypto = require('node:crypto');
|
|
6
6
|
var luxon = require('luxon');
|
|
7
7
|
var taskRecoveryHelper = require('./taskRecoveryHelper.cjs.js');
|
|
8
8
|
var dbUtil = require('./dbUtil.cjs.js');
|
|
@@ -203,7 +203,7 @@ class DatabaseTaskStore {
|
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
async createTask(options) {
|
|
206
|
-
const taskId =
|
|
206
|
+
const taskId = node_crypto.randomUUID();
|
|
207
207
|
await this.db("tasks").insert({
|
|
208
208
|
id: taskId,
|
|
209
209
|
spec: JSON.stringify(options.spec),
|