@backstage/plugin-scaffolder-backend 1.20.0 → 1.21.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # @backstage/plugin-scaffolder-backend
2
2
 
3
+ ## 1.21.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 11b9a08: Introduced the first version of recoverable tasks.
8
+ - e9a5228: The built-in module list has been trimmed down when using the new Backend System. Provider specific modules should now be installed with `backend.add` to provide additional actions to the scaffolder. These modules are as follows:
9
+
10
+ - `@backstage/plugin-scaffolder-backend-module-github`
11
+ - `@backstage/plugin-scaffolder-backend-module-gitlab`
12
+ - `@backstage/plugin-scaffolder-backend-module-bitbucket`
13
+ - `@backstage/plugin-scaffolder-backend-module-gitea`
14
+ - `@backstage/plugin-scaffolder-backend-module-gerrit`
15
+ - `@backstage/plugin-scaffolder-backend-module-confluence-to-markdown`
16
+ - `@backstage/plugin-scaffolder-backend-module-cookiecutter`
17
+ - `@backstage/plugin-scaffolder-backend-module-rails`
18
+ - `@backstage/plugin-scaffolder-backend-module-sentry`
19
+ - `@backstage/plugin-scaffolder-backend-module-yeoman`
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies
24
+ - @backstage/backend-common@0.21.0-next.0
25
+ - @backstage/plugin-scaffolder-backend-module-bitbucket@0.1.2-next.0
26
+ - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.2-next.0
27
+ - @backstage/plugin-scaffolder-backend-module-github@0.1.2-next.0
28
+ - @backstage/plugin-scaffolder-backend-module-gitlab@0.2.13-next.0
29
+ - @backstage/plugin-scaffolder-backend-module-azure@0.1.2-next.0
30
+ - @backstage/catalog-client@1.6.0-next.0
31
+ - @backstage/plugin-scaffolder-node@0.3.0-next.0
32
+ - @backstage/plugin-scaffolder-common@1.5.0-next.0
33
+ - @backstage/backend-tasks@0.5.15-next.0
34
+ - @backstage/plugin-auth-node@0.4.4-next.0
35
+ - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.7-next.0
36
+ - @backstage/plugin-catalog-node@1.6.2-next.0
37
+ - @backstage/plugin-permission-node@0.7.21-next.0
38
+ - @backstage/backend-plugin-api@0.6.10-next.0
39
+ - @backstage/catalog-model@1.4.3
40
+ - @backstage/config@1.1.1
41
+ - @backstage/errors@1.2.3
42
+ - @backstage/integration@1.8.0
43
+ - @backstage/types@1.1.1
44
+ - @backstage/plugin-permission-common@0.7.12
45
+
3
46
  ## 1.20.0
4
47
 
5
48
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-scaffolder-backend",
3
- "version": "1.20.0",
3
+ "version": "1.21.0-next.0",
4
4
  "main": "../dist/alpha.cjs.js",
5
5
  "types": "../dist/alpha.d.ts"
6
6
  }
package/config.d.ts CHANGED
@@ -40,6 +40,22 @@ export interface Config {
40
40
  */
41
41
  concurrentTasksLimit?: number;
42
42
 
43
+ /**
44
+ * Sets the tasks recoverability on system start up.
45
+ *
46
+ * If not specified, the default value is false.
47
+ */
48
+ EXPERIMENTAL_recoverTasks?: boolean;
49
+
50
+ /**
51
+ * Every task which is in progress state and having a last heartbeat longer than a specified timeout is going to
52
+ * be attempted to recover.
53
+ *
54
+ * If not specified, the default value is 5 seconds.
55
+ *
56
+ */
57
+ EXPERIMENTAL_recoverTasksTimeout?: HumanDuration;
58
+
43
59
  /**
44
60
  * Makes sure to auto-expire and clean up things that time out or for other reasons should not be left lingering.
45
61
  *
package/dist/alpha.cjs.js CHANGED
@@ -4,41 +4,41 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var alpha = require('@backstage/plugin-scaffolder-common/alpha');
6
6
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
7
- var router = require('./cjs/router-e667d04e.cjs.js');
7
+ var router = require('./cjs/router-842a762b.cjs.js');
8
8
  var backendPluginApi = require('@backstage/backend-plugin-api');
9
9
  var backendCommon = require('@backstage/backend-common');
10
10
  var integration = require('@backstage/integration');
11
11
  var alpha$2 = require('@backstage/plugin-catalog-node/alpha');
12
12
  var alpha$1 = require('@backstage/plugin-scaffolder-node/alpha');
13
- require('@backstage/errors');
14
- require('@backstage/catalog-model');
15
- require('@backstage/plugin-scaffolder-node');
16
- require('fs-extra');
17
- require('yaml');
18
- require('zod');
19
- require('path');
20
- require('luxon');
21
- require('globby');
22
- require('isbinaryfile');
23
- require('isolated-vm');
24
- require('lodash/get');
25
13
  require('@backstage/plugin-scaffolder-backend-module-github');
26
- require('@backstage/plugin-scaffolder-backend-module-gitlab');
27
14
  require('@backstage/plugin-scaffolder-backend-module-azure');
28
15
  require('@backstage/plugin-scaffolder-backend-module-bitbucket');
29
16
  require('@backstage/plugin-scaffolder-backend-module-gerrit');
17
+ require('@backstage/plugin-scaffolder-backend-module-gitlab');
18
+ require('@backstage/errors');
30
19
  require('zen-observable');
31
- require('p-queue');
32
20
  require('@backstage/config');
21
+ require('lodash');
22
+ require('p-queue');
23
+ require('@backstage/catalog-model');
33
24
  require('@backstage/plugin-scaffolder-common');
34
25
  require('express');
35
26
  require('express-promise-router');
36
27
  require('jsonschema');
28
+ require('zod');
29
+ require('@backstage/plugin-scaffolder-node');
30
+ require('yaml');
31
+ require('fs-extra');
32
+ require('path');
33
+ require('luxon');
34
+ require('globby');
35
+ require('isbinaryfile');
36
+ require('isolated-vm');
37
+ require('lodash/get');
37
38
  require('uuid');
38
39
  require('winston');
39
40
  require('nunjucks');
40
41
  require('stream');
41
- require('lodash');
42
42
  require('prom-client');
43
43
  require('@backstage/plugin-permission-common');
44
44
  require('url');
@@ -91,6 +91,7 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
91
91
  deps: {
92
92
  logger: backendPluginApi.coreServices.logger,
93
93
  config: backendPluginApi.coreServices.rootConfig,
94
+ lifecycle: backendPluginApi.coreServices.rootLifecycle,
94
95
  reader: backendPluginApi.coreServices.urlReader,
95
96
  permissions: backendPluginApi.coreServices.permissions,
96
97
  database: backendPluginApi.coreServices.database,
@@ -100,6 +101,7 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
100
101
  async init({
101
102
  logger,
102
103
  config,
104
+ lifecycle,
103
105
  reader,
104
106
  database,
105
107
  httpRouter,
@@ -107,16 +109,33 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
107
109
  permissions
108
110
  }) {
109
111
  const log = backendCommon.loggerToWinstonLogger(logger);
112
+ const integrations = integration.ScmIntegrations.fromConfig(config);
110
113
  const actions = [
114
+ // actions provided from other modules
111
115
  ...addedActions,
112
- ...router.createBuiltinActions({
113
- integrations: integration.ScmIntegrations.fromConfig(config),
114
- catalogClient,
116
+ // built-in actions for the scaffolder
117
+ router.createFetchPlainAction({
118
+ reader,
119
+ integrations
120
+ }),
121
+ router.createFetchPlainFileAction({
122
+ reader,
123
+ integrations
124
+ }),
125
+ router.createFetchTemplateAction({
126
+ integrations,
115
127
  reader,
116
- config,
117
128
  additionalTemplateFilters,
118
129
  additionalTemplateGlobals
119
- })
130
+ }),
131
+ router.createDebugLogAction(),
132
+ router.createWaitAction(),
133
+ // todo(blam): maybe these should be a -catalog module?
134
+ router.createCatalogRegisterAction({ catalogClient, integrations }),
135
+ router.createFetchCatalogEntityAction({ catalogClient }),
136
+ router.createCatalogWriteAction(),
137
+ router.createFilesystemDeleteAction(),
138
+ router.createFilesystemRenameAction()
120
139
  ];
121
140
  const actionIds = actions.map((action) => action.id).join(", ");
122
141
  log.info(
@@ -128,6 +147,7 @@ const scaffolderPlugin = backendPluginApi.createBackendPlugin({
128
147
  database,
129
148
  catalogClient,
130
149
  reader,
150
+ lifecycle,
131
151
  actions,
132
152
  taskBroker,
133
153
  additionalTemplateFilters,
@@ -1 +1 @@
1
- {"version":3,"file":"alpha.cjs.js","sources":["../src/service/conditionExports.ts","../src/ScaffolderPlugin.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 RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,\n RESOURCE_TYPE_SCAFFOLDER_ACTION,\n} from '@backstage/plugin-scaffolder-common/alpha';\nimport { createConditionExports } from '@backstage/plugin-permission-node';\nimport { scaffolderTemplateRules, scaffolderActionRules } from './rules';\n\nconst templateConditionExports = createConditionExports({\n pluginId: 'scaffolder',\n resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,\n rules: scaffolderTemplateRules,\n});\n\nconst actionsConditionExports = createConditionExports({\n pluginId: 'scaffolder',\n resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION,\n rules: scaffolderActionRules,\n});\n\n/**\n * `createScaffolderTemplateConditionalDecision` can be used when authoring policies to\n * create conditional decisions. It requires a permission of type\n * `ResourcePermission<'scaffolder-template'>` to be passed as the first parameter.\n * It's recommended that you use the provided `isResourcePermission` and\n * `isPermission` helper methods to narrow the type of the permission passed to\n * the handle method as shown below.\n *\n * ```\n * // MyAuthorizationPolicy.ts\n * ...\n * import { createScaffolderPolicyDecision } from '@backstage/plugin-scaffolder-backend';\n * import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common';\n *\n * class MyAuthorizationPolicy implements PermissionPolicy {\n * async handle(request, user) {\n * ...\n *\n * if (isResourcePermission(request.permission, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE)) {\n * return createScaffolderConditionalDecision(\n * request.permission,\n * { anyOf: [...insert conditions here...] }\n * );\n * }\n *\n * ...\n * }\n *\n * ```\n *\n * @alpha\n */\nexport const createScaffolderTemplateConditionalDecision =\n templateConditionExports.createConditionalDecision;\n\n/**\n * These conditions are used when creating conditional decisions for scaffolder\n * templates that are returned by authorization policies.\n *\n * @alpha\n */\nexport const scaffolderTemplateConditions = templateConditionExports.conditions;\n\n/**\n * @alpha\n */\nexport const createScaffolderActionConditionalDecision =\n actionsConditionExports.createConditionalDecision;\n\n/**\n *\n * These conditions are used when creating conditional decisions for scaffolder\n * actions that are returned by authorization policies.\n *\n * @alpha\n */\nexport const scaffolderActionConditions = actionsConditionExports.conditions;\n","/*\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 createBackendPlugin,\n coreServices,\n} from '@backstage/backend-plugin-api';\nimport { loggerToWinstonLogger } from '@backstage/backend-common';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';\nimport {\n TaskBroker,\n TemplateAction,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport {\n scaffolderActionsExtensionPoint,\n scaffolderTaskBrokerExtensionPoint,\n scaffolderTemplatingExtensionPoint,\n} from '@backstage/plugin-scaffolder-node/alpha';\nimport { createBuiltinActions } from './scaffolder';\nimport { createRouter } from './service/router';\n\n/**\n * Scaffolder plugin\n *\n * @alpha\n */\nexport const scaffolderPlugin = createBackendPlugin({\n pluginId: 'scaffolder',\n register(env) {\n const addedActions = new Array<TemplateAction<any, any>>();\n env.registerExtensionPoint(scaffolderActionsExtensionPoint, {\n addActions(...newActions: TemplateAction<any>[]) {\n addedActions.push(...newActions);\n },\n });\n\n let taskBroker: TaskBroker | undefined;\n env.registerExtensionPoint(scaffolderTaskBrokerExtensionPoint, {\n setTaskBroker(newTaskBroker) {\n if (taskBroker) {\n throw new Error('Task broker may only be set once');\n }\n taskBroker = newTaskBroker;\n },\n });\n\n const additionalTemplateFilters: Record<string, TemplateFilter> = {};\n const additionalTemplateGlobals: Record<string, TemplateGlobal> = {};\n env.registerExtensionPoint(scaffolderTemplatingExtensionPoint, {\n addTemplateFilters(newFilters) {\n Object.assign(additionalTemplateFilters, newFilters);\n },\n addTemplateGlobals(newGlobals) {\n Object.assign(additionalTemplateGlobals, newGlobals);\n },\n });\n\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n reader: coreServices.urlReader,\n permissions: coreServices.permissions,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n catalogClient: catalogServiceRef,\n },\n async init({\n logger,\n config,\n reader,\n database,\n httpRouter,\n catalogClient,\n permissions,\n }) {\n const log = loggerToWinstonLogger(logger);\n\n const actions = [\n ...addedActions,\n ...createBuiltinActions({\n integrations: ScmIntegrations.fromConfig(config),\n catalogClient,\n reader,\n config,\n additionalTemplateFilters,\n additionalTemplateGlobals,\n }),\n ];\n\n const actionIds = actions.map(action => action.id).join(', ');\n log.info(\n `Starting scaffolder with the following actions enabled ${actionIds}`,\n );\n\n const router = await createRouter({\n logger: log,\n config,\n database,\n catalogClient,\n reader,\n actions,\n taskBroker,\n additionalTemplateFilters,\n additionalTemplateGlobals,\n permissions,\n });\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createConditionExports","RESOURCE_TYPE_SCAFFOLDER_TEMPLATE","scaffolderTemplateRules","RESOURCE_TYPE_SCAFFOLDER_ACTION","scaffolderActionRules","createBackendPlugin","scaffolderActionsExtensionPoint","scaffolderTaskBrokerExtensionPoint","scaffolderTemplatingExtensionPoint","coreServices","catalogServiceRef","loggerToWinstonLogger","createBuiltinActions","ScmIntegrations","router","createRouter"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,2BAA2BA,2CAAuB,CAAA;AAAA,EACtD,QAAU,EAAA,YAAA;AAAA,EACV,YAAc,EAAAC,uCAAA;AAAA,EACd,KAAO,EAAAC,8BAAA;AACT,CAAC,CAAA,CAAA;AAED,MAAM,0BAA0BF,2CAAuB,CAAA;AAAA,EACrD,QAAU,EAAA,YAAA;AAAA,EACV,YAAc,EAAAG,qCAAA;AAAA,EACd,KAAO,EAAAC,4BAAA;AACT,CAAC,CAAA,CAAA;AAkCM,MAAM,8CACX,wBAAyB,CAAA,0BAAA;AAQpB,MAAM,+BAA+B,wBAAyB,CAAA,WAAA;AAK9D,MAAM,4CACX,uBAAwB,CAAA,0BAAA;AASnB,MAAM,6BAA6B,uBAAwB,CAAA;;ACjD3D,MAAM,mBAAmBC,oCAAoB,CAAA;AAAA,EAClD,QAAU,EAAA,YAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAM,MAAA,YAAA,GAAe,IAAI,KAAgC,EAAA,CAAA;AACzD,IAAA,GAAA,CAAI,uBAAuBC,uCAAiC,EAAA;AAAA,MAC1D,cAAc,UAAmC,EAAA;AAC/C,QAAa,YAAA,CAAA,IAAA,CAAK,GAAG,UAAU,CAAA,CAAA;AAAA,OACjC;AAAA,KACD,CAAA,CAAA;AAED,IAAI,IAAA,UAAA,CAAA;AACJ,IAAA,GAAA,CAAI,uBAAuBC,0CAAoC,EAAA;AAAA,MAC7D,cAAc,aAAe,EAAA;AAC3B,QAAA,IAAI,UAAY,EAAA;AACd,UAAM,MAAA,IAAI,MAAM,kCAAkC,CAAA,CAAA;AAAA,SACpD;AACA,QAAa,UAAA,GAAA,aAAA,CAAA;AAAA,OACf;AAAA,KACD,CAAA,CAAA;AAED,IAAA,MAAM,4BAA4D,EAAC,CAAA;AACnE,IAAA,MAAM,4BAA4D,EAAC,CAAA;AACnE,IAAA,GAAA,CAAI,uBAAuBC,0CAAoC,EAAA;AAAA,MAC7D,mBAAmB,UAAY,EAAA;AAC7B,QAAO,MAAA,CAAA,MAAA,CAAO,2BAA2B,UAAU,CAAA,CAAA;AAAA,OACrD;AAAA,MACA,mBAAmB,UAAY,EAAA;AAC7B,QAAO,MAAA,CAAA,MAAA,CAAO,2BAA2B,UAAU,CAAA,CAAA;AAAA,OACrD;AAAA,KACD,CAAA,CAAA;AAED,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,QAAQC,6BAAa,CAAA,MAAA;AAAA,QACrB,QAAQA,6BAAa,CAAA,UAAA;AAAA,QACrB,QAAQA,6BAAa,CAAA,SAAA;AAAA,QACrB,aAAaA,6BAAa,CAAA,WAAA;AAAA,QAC1B,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,aAAe,EAAAC,yBAAA;AAAA,OACjB;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,MAAA;AAAA,QACA,MAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,aAAA;AAAA,QACA,WAAA;AAAA,OACC,EAAA;AACD,QAAM,MAAA,GAAA,GAAMC,oCAAsB,MAAM,CAAA,CAAA;AAExC,QAAA,MAAM,OAAU,GAAA;AAAA,UACd,GAAG,YAAA;AAAA,UACH,GAAGC,2BAAqB,CAAA;AAAA,YACtB,YAAA,EAAcC,2BAAgB,CAAA,UAAA,CAAW,MAAM,CAAA;AAAA,YAC/C,aAAA;AAAA,YACA,MAAA;AAAA,YACA,MAAA;AAAA,YACA,yBAAA;AAAA,YACA,yBAAA;AAAA,WACD,CAAA;AAAA,SACH,CAAA;AAEA,QAAM,MAAA,SAAA,GAAY,QAAQ,GAAI,CAAA,CAAA,MAAA,KAAU,OAAO,EAAE,CAAA,CAAE,KAAK,IAAI,CAAA,CAAA;AAC5D,QAAI,GAAA,CAAA,IAAA;AAAA,UACF,0DAA0D,SAAS,CAAA,CAAA;AAAA,SACrE,CAAA;AAEA,QAAM,MAAAC,QAAA,GAAS,MAAMC,mBAAa,CAAA;AAAA,UAChC,MAAQ,EAAA,GAAA;AAAA,UACR,MAAA;AAAA,UACA,QAAA;AAAA,UACA,aAAA;AAAA,UACA,MAAA;AAAA,UACA,OAAA;AAAA,UACA,UAAA;AAAA,UACA,yBAAA;AAAA,UACA,yBAAA;AAAA,UACA,WAAA;AAAA,SACD,CAAA,CAAA;AACD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA,CAAA;AAAA,OACvB;AAAA,KACD,CAAA,CAAA;AAAA,GACH;AACF,CAAC;;;;;;;;"}
1
+ {"version":3,"file":"alpha.cjs.js","sources":["../src/service/conditionExports.ts","../src/ScaffolderPlugin.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 RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,\n RESOURCE_TYPE_SCAFFOLDER_ACTION,\n} from '@backstage/plugin-scaffolder-common/alpha';\nimport { createConditionExports } from '@backstage/plugin-permission-node';\nimport { scaffolderTemplateRules, scaffolderActionRules } from './rules';\n\nconst templateConditionExports = createConditionExports({\n pluginId: 'scaffolder',\n resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,\n rules: scaffolderTemplateRules,\n});\n\nconst actionsConditionExports = createConditionExports({\n pluginId: 'scaffolder',\n resourceType: RESOURCE_TYPE_SCAFFOLDER_ACTION,\n rules: scaffolderActionRules,\n});\n\n/**\n * `createScaffolderTemplateConditionalDecision` can be used when authoring policies to\n * create conditional decisions. It requires a permission of type\n * `ResourcePermission<'scaffolder-template'>` to be passed as the first parameter.\n * It's recommended that you use the provided `isResourcePermission` and\n * `isPermission` helper methods to narrow the type of the permission passed to\n * the handle method as shown below.\n *\n * ```\n * // MyAuthorizationPolicy.ts\n * ...\n * import { createScaffolderPolicyDecision } from '@backstage/plugin-scaffolder-backend';\n * import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common';\n *\n * class MyAuthorizationPolicy implements PermissionPolicy {\n * async handle(request, user) {\n * ...\n *\n * if (isResourcePermission(request.permission, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE)) {\n * return createScaffolderConditionalDecision(\n * request.permission,\n * { anyOf: [...insert conditions here...] }\n * );\n * }\n *\n * ...\n * }\n *\n * ```\n *\n * @alpha\n */\nexport const createScaffolderTemplateConditionalDecision =\n templateConditionExports.createConditionalDecision;\n\n/**\n * These conditions are used when creating conditional decisions for scaffolder\n * templates that are returned by authorization policies.\n *\n * @alpha\n */\nexport const scaffolderTemplateConditions = templateConditionExports.conditions;\n\n/**\n * @alpha\n */\nexport const createScaffolderActionConditionalDecision =\n actionsConditionExports.createConditionalDecision;\n\n/**\n *\n * These conditions are used when creating conditional decisions for scaffolder\n * actions that are returned by authorization policies.\n *\n * @alpha\n */\nexport const scaffolderActionConditions = actionsConditionExports.conditions;\n","/*\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 createBackendPlugin,\n coreServices,\n} from '@backstage/backend-plugin-api';\nimport { loggerToWinstonLogger } from '@backstage/backend-common';\nimport { ScmIntegrations } from '@backstage/integration';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node/alpha';\nimport {\n TaskBroker,\n TemplateAction,\n TemplateFilter,\n TemplateGlobal,\n} from '@backstage/plugin-scaffolder-node';\nimport {\n scaffolderActionsExtensionPoint,\n scaffolderTaskBrokerExtensionPoint,\n scaffolderTemplatingExtensionPoint,\n} from '@backstage/plugin-scaffolder-node/alpha';\nimport {\n createCatalogRegisterAction,\n createCatalogWriteAction,\n createDebugLogAction,\n createFetchCatalogEntityAction,\n createFetchPlainAction,\n createFetchPlainFileAction,\n createFetchTemplateAction,\n createFilesystemDeleteAction,\n createFilesystemRenameAction,\n createWaitAction,\n} from './scaffolder';\nimport { createRouter } from './service/router';\n\n/**\n * Scaffolder plugin\n *\n * @alpha\n */\nexport const scaffolderPlugin = createBackendPlugin({\n pluginId: 'scaffolder',\n register(env) {\n const addedActions = new Array<TemplateAction<any, any>>();\n env.registerExtensionPoint(scaffolderActionsExtensionPoint, {\n addActions(...newActions: TemplateAction<any>[]) {\n addedActions.push(...newActions);\n },\n });\n\n let taskBroker: TaskBroker | undefined;\n env.registerExtensionPoint(scaffolderTaskBrokerExtensionPoint, {\n setTaskBroker(newTaskBroker) {\n if (taskBroker) {\n throw new Error('Task broker may only be set once');\n }\n taskBroker = newTaskBroker;\n },\n });\n\n const additionalTemplateFilters: Record<string, TemplateFilter> = {};\n const additionalTemplateGlobals: Record<string, TemplateGlobal> = {};\n env.registerExtensionPoint(scaffolderTemplatingExtensionPoint, {\n addTemplateFilters(newFilters) {\n Object.assign(additionalTemplateFilters, newFilters);\n },\n addTemplateGlobals(newGlobals) {\n Object.assign(additionalTemplateGlobals, newGlobals);\n },\n });\n\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n lifecycle: coreServices.rootLifecycle,\n reader: coreServices.urlReader,\n permissions: coreServices.permissions,\n database: coreServices.database,\n httpRouter: coreServices.httpRouter,\n catalogClient: catalogServiceRef,\n },\n async init({\n logger,\n config,\n lifecycle,\n reader,\n database,\n httpRouter,\n catalogClient,\n permissions,\n }) {\n const log = loggerToWinstonLogger(logger);\n const integrations = ScmIntegrations.fromConfig(config);\n\n const actions = [\n // actions provided from other modules\n ...addedActions,\n\n // built-in actions for the scaffolder\n createFetchPlainAction({\n reader,\n integrations,\n }),\n createFetchPlainFileAction({\n reader,\n integrations,\n }),\n createFetchTemplateAction({\n integrations,\n reader,\n additionalTemplateFilters,\n additionalTemplateGlobals,\n }),\n createDebugLogAction(),\n createWaitAction(),\n // todo(blam): maybe these should be a -catalog module?\n createCatalogRegisterAction({ catalogClient, integrations }),\n createFetchCatalogEntityAction({ catalogClient }),\n createCatalogWriteAction(),\n createFilesystemDeleteAction(),\n createFilesystemRenameAction(),\n ];\n\n const actionIds = actions.map(action => action.id).join(', ');\n\n log.info(\n `Starting scaffolder with the following actions enabled ${actionIds}`,\n );\n\n const router = await createRouter({\n logger: log,\n config,\n database,\n catalogClient,\n reader,\n lifecycle,\n actions,\n taskBroker,\n additionalTemplateFilters,\n additionalTemplateGlobals,\n permissions,\n });\n httpRouter.use(router);\n },\n });\n },\n});\n"],"names":["createConditionExports","RESOURCE_TYPE_SCAFFOLDER_TEMPLATE","scaffolderTemplateRules","RESOURCE_TYPE_SCAFFOLDER_ACTION","scaffolderActionRules","createBackendPlugin","scaffolderActionsExtensionPoint","scaffolderTaskBrokerExtensionPoint","scaffolderTemplatingExtensionPoint","coreServices","catalogServiceRef","loggerToWinstonLogger","ScmIntegrations","createFetchPlainAction","createFetchPlainFileAction","createFetchTemplateAction","createDebugLogAction","createWaitAction","createCatalogRegisterAction","createFetchCatalogEntityAction","createCatalogWriteAction","createFilesystemDeleteAction","createFilesystemRenameAction","router","createRouter"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,2BAA2BA,2CAAuB,CAAA;AAAA,EACtD,QAAU,EAAA,YAAA;AAAA,EACV,YAAc,EAAAC,uCAAA;AAAA,EACd,KAAO,EAAAC,8BAAA;AACT,CAAC,CAAA,CAAA;AAED,MAAM,0BAA0BF,2CAAuB,CAAA;AAAA,EACrD,QAAU,EAAA,YAAA;AAAA,EACV,YAAc,EAAAG,qCAAA;AAAA,EACd,KAAO,EAAAC,4BAAA;AACT,CAAC,CAAA,CAAA;AAkCM,MAAM,8CACX,wBAAyB,CAAA,0BAAA;AAQpB,MAAM,+BAA+B,wBAAyB,CAAA,WAAA;AAK9D,MAAM,4CACX,uBAAwB,CAAA,0BAAA;AASnB,MAAM,6BAA6B,uBAAwB,CAAA;;ACtC3D,MAAM,mBAAmBC,oCAAoB,CAAA;AAAA,EAClD,QAAU,EAAA,YAAA;AAAA,EACV,SAAS,GAAK,EAAA;AACZ,IAAM,MAAA,YAAA,GAAe,IAAI,KAAgC,EAAA,CAAA;AACzD,IAAA,GAAA,CAAI,uBAAuBC,uCAAiC,EAAA;AAAA,MAC1D,cAAc,UAAmC,EAAA;AAC/C,QAAa,YAAA,CAAA,IAAA,CAAK,GAAG,UAAU,CAAA,CAAA;AAAA,OACjC;AAAA,KACD,CAAA,CAAA;AAED,IAAI,IAAA,UAAA,CAAA;AACJ,IAAA,GAAA,CAAI,uBAAuBC,0CAAoC,EAAA;AAAA,MAC7D,cAAc,aAAe,EAAA;AAC3B,QAAA,IAAI,UAAY,EAAA;AACd,UAAM,MAAA,IAAI,MAAM,kCAAkC,CAAA,CAAA;AAAA,SACpD;AACA,QAAa,UAAA,GAAA,aAAA,CAAA;AAAA,OACf;AAAA,KACD,CAAA,CAAA;AAED,IAAA,MAAM,4BAA4D,EAAC,CAAA;AACnE,IAAA,MAAM,4BAA4D,EAAC,CAAA;AACnE,IAAA,GAAA,CAAI,uBAAuBC,0CAAoC,EAAA;AAAA,MAC7D,mBAAmB,UAAY,EAAA;AAC7B,QAAO,MAAA,CAAA,MAAA,CAAO,2BAA2B,UAAU,CAAA,CAAA;AAAA,OACrD;AAAA,MACA,mBAAmB,UAAY,EAAA;AAC7B,QAAO,MAAA,CAAA,MAAA,CAAO,2BAA2B,UAAU,CAAA,CAAA;AAAA,OACrD;AAAA,KACD,CAAA,CAAA;AAED,IAAA,GAAA,CAAI,YAAa,CAAA;AAAA,MACf,IAAM,EAAA;AAAA,QACJ,QAAQC,6BAAa,CAAA,MAAA;AAAA,QACrB,QAAQA,6BAAa,CAAA,UAAA;AAAA,QACrB,WAAWA,6BAAa,CAAA,aAAA;AAAA,QACxB,QAAQA,6BAAa,CAAA,SAAA;AAAA,QACrB,aAAaA,6BAAa,CAAA,WAAA;AAAA,QAC1B,UAAUA,6BAAa,CAAA,QAAA;AAAA,QACvB,YAAYA,6BAAa,CAAA,UAAA;AAAA,QACzB,aAAe,EAAAC,yBAAA;AAAA,OACjB;AAAA,MACA,MAAM,IAAK,CAAA;AAAA,QACT,MAAA;AAAA,QACA,MAAA;AAAA,QACA,SAAA;AAAA,QACA,MAAA;AAAA,QACA,QAAA;AAAA,QACA,UAAA;AAAA,QACA,aAAA;AAAA,QACA,WAAA;AAAA,OACC,EAAA;AACD,QAAM,MAAA,GAAA,GAAMC,oCAAsB,MAAM,CAAA,CAAA;AACxC,QAAM,MAAA,YAAA,GAAeC,2BAAgB,CAAA,UAAA,CAAW,MAAM,CAAA,CAAA;AAEtD,QAAA,MAAM,OAAU,GAAA;AAAA;AAAA,UAEd,GAAG,YAAA;AAAA;AAAA,UAGHC,6BAAuB,CAAA;AAAA,YACrB,MAAA;AAAA,YACA,YAAA;AAAA,WACD,CAAA;AAAA,UACDC,iCAA2B,CAAA;AAAA,YACzB,MAAA;AAAA,YACA,YAAA;AAAA,WACD,CAAA;AAAA,UACDC,gCAA0B,CAAA;AAAA,YACxB,YAAA;AAAA,YACA,MAAA;AAAA,YACA,yBAAA;AAAA,YACA,yBAAA;AAAA,WACD,CAAA;AAAA,UACDC,2BAAqB,EAAA;AAAA,UACrBC,uBAAiB,EAAA;AAAA;AAAA,UAEjBC,kCAA4B,CAAA,EAAE,aAAe,EAAA,YAAA,EAAc,CAAA;AAAA,UAC3DC,qCAAA,CAA+B,EAAE,aAAA,EAAe,CAAA;AAAA,UAChDC,+BAAyB,EAAA;AAAA,UACzBC,mCAA6B,EAAA;AAAA,UAC7BC,mCAA6B,EAAA;AAAA,SAC/B,CAAA;AAEA,QAAM,MAAA,SAAA,GAAY,QAAQ,GAAI,CAAA,CAAA,MAAA,KAAU,OAAO,EAAE,CAAA,CAAE,KAAK,IAAI,CAAA,CAAA;AAE5D,QAAI,GAAA,CAAA,IAAA;AAAA,UACF,0DAA0D,SAAS,CAAA,CAAA;AAAA,SACrE,CAAA;AAEA,QAAM,MAAAC,QAAA,GAAS,MAAMC,mBAAa,CAAA;AAAA,UAChC,MAAQ,EAAA,GAAA;AAAA,UACR,MAAA;AAAA,UACA,QAAA;AAAA,UACA,aAAA;AAAA,UACA,MAAA;AAAA,UACA,SAAA;AAAA,UACA,OAAA;AAAA,UACA,UAAA;AAAA,UACA,yBAAA;AAAA,UACA,yBAAA;AAAA,UACA,WAAA;AAAA,SACD,CAAA,CAAA;AACD,QAAA,UAAA,CAAW,IAAID,QAAM,CAAA,CAAA;AAAA,OACvB;AAAA,KACD,CAAA,CAAA;AAAA,GACH;AACF,CAAC;;;;;;;;"}
@@ -27,11 +27,11 @@ var gerrit = require('@backstage/plugin-scaffolder-backend-module-gerrit');
27
27
  var gitlab = require('@backstage/plugin-scaffolder-backend-module-gitlab');
28
28
  var uuid = require('uuid');
29
29
  var ObservableImpl = require('zen-observable');
30
+ var lodash = require('lodash');
30
31
  var PQueue = require('p-queue');
31
32
  var winston = require('winston');
32
33
  var nunjucks = require('nunjucks');
33
34
  var stream = require('stream');
34
- var lodash = require('lodash');
35
35
  var pluginPermissionNode = require('@backstage/plugin-permission-node');
36
36
  var promClient = require('prom-client');
37
37
  var pluginPermissionCommon = require('@backstage/plugin-permission-common');
@@ -1419,6 +1419,36 @@ class TemplateActionRegistry {
1419
1419
  }
1420
1420
  }
1421
1421
 
1422
+ const trimEventsTillLastRecovery = (events) => {
1423
+ const recoveredEventInd = events.slice().reverse().findIndex((event) => event.type === "recovered");
1424
+ if (recoveredEventInd >= 0) {
1425
+ const ind = events.length - recoveredEventInd - 1;
1426
+ const { recoverStrategy } = events[ind].body;
1427
+ if (recoverStrategy === "startOver") {
1428
+ return {
1429
+ events: recoveredEventInd === 0 ? [] : events.slice(ind)
1430
+ };
1431
+ }
1432
+ }
1433
+ return { events };
1434
+ };
1435
+
1436
+ const intervalFromNowTill = (timeoutS, knex) => {
1437
+ let heartbeatInterval = knex.raw(`? - interval '${timeoutS} seconds'`, [
1438
+ knex.fn.now()
1439
+ ]);
1440
+ if (knex.client.config.client.includes("mysql")) {
1441
+ heartbeatInterval = knex.raw(
1442
+ `date_sub(now(), interval ${timeoutS} second)`
1443
+ );
1444
+ } else if (knex.client.config.client.includes("sqlite3")) {
1445
+ heartbeatInterval = knex.raw(`datetime('now', ?)`, [
1446
+ `-${timeoutS} seconds`
1447
+ ]);
1448
+ }
1449
+ return heartbeatInterval;
1450
+ };
1451
+
1422
1452
  var __defProp$3 = Object.defineProperty;
1423
1453
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1424
1454
  var __publicField$3 = (obj, key, value) => {
@@ -1455,6 +1485,28 @@ class DatabaseTaskStore {
1455
1485
  await this.runMigrations(database, client);
1456
1486
  return new DatabaseTaskStore(client);
1457
1487
  }
1488
+ isRecoverableTask(spec) {
1489
+ var _a, _b;
1490
+ return ["startOver"].includes(
1491
+ (_b = (_a = spec.EXPERIMENTAL_recovery) == null ? void 0 : _a.EXPERIMENTAL_strategy) != null ? _b : "none"
1492
+ );
1493
+ }
1494
+ parseSpec({ spec, id }) {
1495
+ try {
1496
+ return JSON.parse(spec);
1497
+ } catch (error) {
1498
+ throw new Error(`Failed to parse spec of task '${id}', ${error}`);
1499
+ }
1500
+ }
1501
+ parseTaskSecrets(taskRow) {
1502
+ try {
1503
+ return taskRow.secrets ? JSON.parse(taskRow.secrets) : void 0;
1504
+ } catch (error) {
1505
+ throw new Error(
1506
+ `Failed to parse secrets of task '${taskRow.id}', ${error}`
1507
+ );
1508
+ }
1509
+ }
1458
1510
  static async getClient(database) {
1459
1511
  if (isPluginDatabaseManager(database)) {
1460
1512
  return database.getClient();
@@ -1539,30 +1591,26 @@ class DatabaseTaskStore {
1539
1591
  if (!task) {
1540
1592
  return void 0;
1541
1593
  }
1594
+ const spec = this.parseSpec(task);
1542
1595
  const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
1543
1596
  status: "processing",
1544
1597
  last_heartbeat_at: this.db.fn.now(),
1545
- // remove the secrets when moving to processing state.
1546
- secrets: null
1598
+ // remove the secrets for non-recoverable tasks when moving to processing state.
1599
+ secrets: this.isRecoverableTask(spec) ? task.secrets : null
1547
1600
  });
1548
1601
  if (updateCount < 1) {
1549
1602
  return void 0;
1550
1603
  }
1551
- try {
1552
- const spec = JSON.parse(task.spec);
1553
- const secrets = task.secrets ? JSON.parse(task.secrets) : void 0;
1554
- return {
1555
- id: task.id,
1556
- spec,
1557
- status: "processing",
1558
- lastHeartbeatAt: task.last_heartbeat_at,
1559
- createdAt: task.created_at,
1560
- createdBy: (_a = task.created_by) != null ? _a : void 0,
1561
- secrets
1562
- };
1563
- } catch (error) {
1564
- throw new Error(`Failed to parse spec of task '${task.id}', ${error}`);
1565
- }
1604
+ const secrets = this.parseTaskSecrets(task);
1605
+ return {
1606
+ id: task.id,
1607
+ spec,
1608
+ status: "processing",
1609
+ lastHeartbeatAt: task.last_heartbeat_at,
1610
+ createdAt: task.created_at,
1611
+ createdBy: (_a = task.created_by) != null ? _a : void 0,
1612
+ secrets
1613
+ };
1566
1614
  });
1567
1615
  }
1568
1616
  async heartbeatTask(taskId) {
@@ -1575,20 +1623,10 @@ class DatabaseTaskStore {
1575
1623
  }
1576
1624
  async listStaleTasks(options) {
1577
1625
  const { timeoutS } = options;
1578
- let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [
1579
- this.db.fn.now()
1580
- ]);
1581
- if (this.db.client.config.client.includes("mysql")) {
1582
- heartbeatInterval = this.db.raw(
1583
- `date_sub(now(), interval ${timeoutS} second)`
1584
- );
1585
- } else if (this.db.client.config.client.includes("sqlite3")) {
1586
- heartbeatInterval = this.db.raw(`datetime('now', ?)`, [
1587
- `-${timeoutS} seconds`
1588
- ]);
1589
- }
1626
+ const heartbeatInterval = intervalFromNowTill(timeoutS, this.db);
1590
1627
  const rawRows = await this.db("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval);
1591
1628
  const tasks = rawRows.map((row) => ({
1629
+ recovery: JSON.parse(row.spec).EXPERIMENTAL_recovery,
1592
1630
  taskId: row.id
1593
1631
  }));
1594
1632
  return { tasks };
@@ -1609,7 +1647,8 @@ class DatabaseTaskStore {
1609
1647
  }).limit(1).select();
1610
1648
  const updateTask = async (criteria) => {
1611
1649
  const updateCount = await tx("tasks").where(criteria).update({
1612
- status
1650
+ status,
1651
+ secrets: null
1613
1652
  });
1614
1653
  if (updateCount !== 1) {
1615
1654
  throw new errors.ConflictError(
@@ -1679,7 +1718,7 @@ class DatabaseTaskStore {
1679
1718
  );
1680
1719
  }
1681
1720
  });
1682
- return { events };
1721
+ return trimEventsTillLastRecovery(events);
1683
1722
  }
1684
1723
  async shutdownTask(options) {
1685
1724
  const { taskId } = options;
@@ -1718,7 +1757,72 @@ class DatabaseTaskStore {
1718
1757
  body: serializedBody
1719
1758
  });
1720
1759
  }
1760
+ async recoverTasks(options) {
1761
+ const taskIdsToRecover = [];
1762
+ const timeoutS = luxon.Duration.fromObject(options.timeout).as("seconds");
1763
+ await this.db.transaction(async (tx) => {
1764
+ var _a, _b;
1765
+ const heartbeatInterval = intervalFromNowTill(timeoutS, this.db);
1766
+ const result = await tx("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval).update(
1767
+ {
1768
+ status: "open",
1769
+ last_heartbeat_at: this.db.fn.now()
1770
+ },
1771
+ ["id", "spec"]
1772
+ );
1773
+ taskIdsToRecover.push(...result.map((i) => i.id));
1774
+ for (const { id, spec } of result) {
1775
+ const taskSpec = JSON.parse(spec);
1776
+ await this.db("task_events").insert({
1777
+ task_id: id,
1778
+ event_type: "recovered",
1779
+ body: JSON.stringify({
1780
+ recoverStrategy: (_b = (_a = taskSpec.EXPERIMENTAL_recovery) == null ? void 0 : _a.EXPERIMENTAL_strategy) != null ? _b : "none"
1781
+ })
1782
+ });
1783
+ }
1784
+ });
1785
+ return { ids: taskIdsToRecover };
1786
+ }
1787
+ }
1788
+
1789
+ function isTruthy(value) {
1790
+ return lodash.isArray(value) ? value.length > 0 : !!value;
1791
+ }
1792
+ function generateExampleOutput(schema) {
1793
+ var _a, _b;
1794
+ const { examples } = schema;
1795
+ if (examples && Array.isArray(examples)) {
1796
+ return examples[0];
1797
+ }
1798
+ if (schema.type === "object") {
1799
+ return Object.fromEntries(
1800
+ Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
1801
+ key,
1802
+ generateExampleOutput(value)
1803
+ ])
1804
+ );
1805
+ } else if (schema.type === "array") {
1806
+ const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
1807
+ if (firstSchema) {
1808
+ return [generateExampleOutput(firstSchema)];
1809
+ }
1810
+ return [];
1811
+ } else if (schema.type === "string") {
1812
+ return "<example>";
1813
+ } else if (schema.type === "number") {
1814
+ return 0;
1815
+ } else if (schema.type === "boolean") {
1816
+ return false;
1817
+ }
1818
+ return "<unknown>";
1721
1819
  }
1820
+ const readDuration$1 = (config$1, key, defaultValue) => {
1821
+ if (config$1 == null ? void 0 : config$1.has(key)) {
1822
+ return config.readDurationFromConfig(config$1, { key });
1823
+ }
1824
+ return defaultValue;
1825
+ };
1722
1826
 
1723
1827
  var __defProp$2 = Object.defineProperty;
1724
1828
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -1803,9 +1907,10 @@ function defer() {
1803
1907
  return { promise, resolve };
1804
1908
  }
1805
1909
  class StorageTaskBroker {
1806
- constructor(storage, logger) {
1910
+ constructor(storage, logger, config) {
1807
1911
  this.storage = storage;
1808
1912
  this.logger = logger;
1913
+ this.config = config;
1809
1914
  __publicField$2(this, "deferredDispatch", defer());
1810
1915
  }
1811
1916
  async list(options) {
@@ -1838,6 +1943,26 @@ class StorageTaskBroker {
1838
1943
  }
1839
1944
  });
1840
1945
  }
1946
+ async recoverTasks() {
1947
+ var _a, _b, _c, _d;
1948
+ const enabled = (_a = this.config && this.config.getOptionalBoolean(
1949
+ "scaffolder.EXPERIMENTAL_recoverTasks"
1950
+ )) != null ? _a : false;
1951
+ if (enabled) {
1952
+ const defaultTimeout = { seconds: 30 };
1953
+ const timeout = readDuration$1(
1954
+ this.config,
1955
+ "scaffolder.EXPERIMENTAL_recoverTasksTimeout",
1956
+ defaultTimeout
1957
+ );
1958
+ const { ids: recoveredTaskIds } = (_d = await ((_c = (_b = this.storage).recoverTasks) == null ? void 0 : _c.call(_b, {
1959
+ timeout
1960
+ }))) != null ? _d : { ids: [] };
1961
+ if (recoveredTaskIds.length > 0) {
1962
+ this.signalDispatch();
1963
+ }
1964
+ }
1965
+ }
1841
1966
  /**
1842
1967
  * {@inheritdoc TaskBroker.claim}
1843
1968
  */
@@ -1945,38 +2070,6 @@ class StorageTaskBroker {
1945
2070
  }
1946
2071
  }
1947
2072
 
1948
- function isTruthy(value) {
1949
- return lodash.isArray(value) ? value.length > 0 : !!value;
1950
- }
1951
- function generateExampleOutput(schema) {
1952
- var _a, _b;
1953
- const { examples } = schema;
1954
- if (examples && Array.isArray(examples)) {
1955
- return examples[0];
1956
- }
1957
- if (schema.type === "object") {
1958
- return Object.fromEntries(
1959
- Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
1960
- key,
1961
- generateExampleOutput(value)
1962
- ])
1963
- );
1964
- } else if (schema.type === "array") {
1965
- const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
1966
- if (firstSchema) {
1967
- return [generateExampleOutput(firstSchema)];
1968
- }
1969
- return [];
1970
- } else if (schema.type === "string") {
1971
- return "<example>";
1972
- } else if (schema.type === "number") {
1973
- return 0;
1974
- } else if (schema.type === "boolean") {
1975
- return false;
1976
- }
1977
- return "<unknown>";
1978
- }
1979
-
1980
2073
  function createCounterMetric(config) {
1981
2074
  let metric = promClient.register.getSingleMetric(config.name);
1982
2075
  if (!metric) {
@@ -2510,6 +2603,10 @@ class TaskWorker {
2510
2603
  constructor(options) {
2511
2604
  this.options = options;
2512
2605
  __publicField(this, "taskQueue");
2606
+ __publicField(this, "logger");
2607
+ __publicField(this, "stopWorkers");
2608
+ this.stopWorkers = false;
2609
+ this.logger = options.logger;
2513
2610
  this.taskQueue = new PQueue__default["default"]({
2514
2611
  concurrency: options.concurrentTasksLimit
2515
2612
  });
@@ -2543,15 +2640,34 @@ class TaskWorker {
2543
2640
  permissions
2544
2641
  });
2545
2642
  }
2643
+ async recoverTasks() {
2644
+ var _a, _b, _c;
2645
+ try {
2646
+ await ((_b = (_a = this.options.taskBroker).recoverTasks) == null ? void 0 : _b.call(_a));
2647
+ } catch (err) {
2648
+ (_c = this.logger) == null ? void 0 : _c.error(errors.stringifyError(err));
2649
+ }
2650
+ }
2546
2651
  start() {
2547
2652
  (async () => {
2548
- for (; ; ) {
2653
+ while (!this.stopWorkers) {
2654
+ await new Promise((resolve) => setTimeout(resolve, 1e4));
2655
+ await this.recoverTasks();
2656
+ }
2657
+ })();
2658
+ (async () => {
2659
+ while (!this.stopWorkers) {
2549
2660
  await this.onReadyToClaimTask();
2550
- const task = await this.options.taskBroker.claim();
2551
- this.taskQueue.add(() => this.runOneTask(task));
2661
+ if (!this.stopWorkers) {
2662
+ const task = await this.options.taskBroker.claim();
2663
+ void this.taskQueue.add(() => this.runOneTask(task));
2664
+ }
2552
2665
  }
2553
2666
  })();
2554
2667
  }
2668
+ stop() {
2669
+ this.stopWorkers = true;
2670
+ }
2555
2671
  onReadyToClaimTask() {
2556
2672
  if (this.taskQueue.pending < this.options.concurrentTasksLimit) {
2557
2673
  return Promise.resolve();
@@ -2812,7 +2928,7 @@ async function createRouter(options) {
2812
2928
  let taskBroker;
2813
2929
  if (!options.taskBroker) {
2814
2930
  const databaseTaskStore = await DatabaseTaskStore.create({ database });
2815
- taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
2931
+ taskBroker = new StorageTaskBroker(databaseTaskStore, logger, config);
2816
2932
  if (scheduler && databaseTaskStore.listStaleTasks) {
2817
2933
  await scheduler.scheduleTask({
2818
2934
  id: "close_stale_tasks",
@@ -2869,7 +2985,16 @@ async function createRouter(options) {
2869
2985
  additionalTemplateGlobals
2870
2986
  });
2871
2987
  actionsToRegister.forEach((action) => actionRegistry.register(action));
2872
- workers.forEach((worker) => worker.start());
2988
+ const launchWorkers = () => workers.forEach((worker) => worker.start());
2989
+ const shutdownWorkers = () => {
2990
+ workers.forEach((worker) => worker.stop());
2991
+ };
2992
+ if (options.lifecycle) {
2993
+ options.lifecycle.addStartupHook(launchWorkers);
2994
+ options.lifecycle.addShutdownHook(shutdownWorkers);
2995
+ } else {
2996
+ launchWorkers();
2997
+ }
2873
2998
  const dryRunner = createDryRunner({
2874
2999
  actionRegistry,
2875
3000
  integrations,
@@ -2983,6 +3108,7 @@ async function createRouter(options) {
2983
3108
  name: (_b2 = step.name) != null ? _b2 : step.action
2984
3109
  };
2985
3110
  }),
3111
+ EXPERIMENTAL_recovery: template.spec.EXPERIMENTAL_recovery,
2986
3112
  output: (_b = template.spec.output) != null ? _b : {},
2987
3113
  parameters: values,
2988
3114
  user: {
@@ -3214,4 +3340,4 @@ exports.createRouter = createRouter;
3214
3340
  exports.createWaitAction = createWaitAction;
3215
3341
  exports.scaffolderActionRules = scaffolderActionRules;
3216
3342
  exports.scaffolderTemplateRules = scaffolderTemplateRules;
3217
- //# sourceMappingURL=router-e667d04e.cjs.js.map
3343
+ //# sourceMappingURL=router-842a762b.cjs.js.map