@backstage/plugin-scaffolder-backend 1.19.3-next.2 → 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.
@@ -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');
@@ -1319,6 +1319,9 @@ const createBuiltinActions = (options) => {
1319
1319
  gitlab.createPublishGitlabMergeRequestAction({
1320
1320
  integrations
1321
1321
  }),
1322
+ gitlab.createGitlabRepoPushAction({
1323
+ integrations
1324
+ }),
1322
1325
  bitbucket.createPublishBitbucketAction({
1323
1326
  integrations,
1324
1327
  config
@@ -1376,6 +1379,9 @@ const createBuiltinActions = (options) => {
1376
1379
  github.createGithubAutolinksAction({
1377
1380
  integrations,
1378
1381
  githubCredentialsProvider
1382
+ }),
1383
+ bitbucket.createBitbucketPipelinesRunAction({
1384
+ integrations
1379
1385
  })
1380
1386
  ];
1381
1387
  return actions;
@@ -1413,6 +1419,36 @@ class TemplateActionRegistry {
1413
1419
  }
1414
1420
  }
1415
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
+
1416
1452
  var __defProp$3 = Object.defineProperty;
1417
1453
  var __defNormalProp$3 = (obj, key, value) => key in obj ? __defProp$3(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
1418
1454
  var __publicField$3 = (obj, key, value) => {
@@ -1449,6 +1485,28 @@ class DatabaseTaskStore {
1449
1485
  await this.runMigrations(database, client);
1450
1486
  return new DatabaseTaskStore(client);
1451
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
+ }
1452
1510
  static async getClient(database) {
1453
1511
  if (isPluginDatabaseManager(database)) {
1454
1512
  return database.getClient();
@@ -1533,30 +1591,26 @@ class DatabaseTaskStore {
1533
1591
  if (!task) {
1534
1592
  return void 0;
1535
1593
  }
1594
+ const spec = this.parseSpec(task);
1536
1595
  const updateCount = await tx("tasks").where({ id: task.id, status: "open" }).update({
1537
1596
  status: "processing",
1538
1597
  last_heartbeat_at: this.db.fn.now(),
1539
- // remove the secrets when moving to processing state.
1540
- secrets: null
1598
+ // remove the secrets for non-recoverable tasks when moving to processing state.
1599
+ secrets: this.isRecoverableTask(spec) ? task.secrets : null
1541
1600
  });
1542
1601
  if (updateCount < 1) {
1543
1602
  return void 0;
1544
1603
  }
1545
- try {
1546
- const spec = JSON.parse(task.spec);
1547
- const secrets = task.secrets ? JSON.parse(task.secrets) : void 0;
1548
- return {
1549
- id: task.id,
1550
- spec,
1551
- status: "processing",
1552
- lastHeartbeatAt: task.last_heartbeat_at,
1553
- createdAt: task.created_at,
1554
- createdBy: (_a = task.created_by) != null ? _a : void 0,
1555
- secrets
1556
- };
1557
- } catch (error) {
1558
- throw new Error(`Failed to parse spec of task '${task.id}', ${error}`);
1559
- }
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
+ };
1560
1614
  });
1561
1615
  }
1562
1616
  async heartbeatTask(taskId) {
@@ -1569,20 +1623,10 @@ class DatabaseTaskStore {
1569
1623
  }
1570
1624
  async listStaleTasks(options) {
1571
1625
  const { timeoutS } = options;
1572
- let heartbeatInterval = this.db.raw(`? - interval '${timeoutS} seconds'`, [
1573
- this.db.fn.now()
1574
- ]);
1575
- if (this.db.client.config.client.includes("mysql")) {
1576
- heartbeatInterval = this.db.raw(
1577
- `date_sub(now(), interval ${timeoutS} second)`
1578
- );
1579
- } else if (this.db.client.config.client.includes("sqlite3")) {
1580
- heartbeatInterval = this.db.raw(`datetime('now', ?)`, [
1581
- `-${timeoutS} seconds`
1582
- ]);
1583
- }
1626
+ const heartbeatInterval = intervalFromNowTill(timeoutS, this.db);
1584
1627
  const rawRows = await this.db("tasks").where("status", "processing").andWhere("last_heartbeat_at", "<=", heartbeatInterval);
1585
1628
  const tasks = rawRows.map((row) => ({
1629
+ recovery: JSON.parse(row.spec).EXPERIMENTAL_recovery,
1586
1630
  taskId: row.id
1587
1631
  }));
1588
1632
  return { tasks };
@@ -1603,7 +1647,8 @@ class DatabaseTaskStore {
1603
1647
  }).limit(1).select();
1604
1648
  const updateTask = async (criteria) => {
1605
1649
  const updateCount = await tx("tasks").where(criteria).update({
1606
- status
1650
+ status,
1651
+ secrets: null
1607
1652
  });
1608
1653
  if (updateCount !== 1) {
1609
1654
  throw new errors.ConflictError(
@@ -1673,7 +1718,7 @@ class DatabaseTaskStore {
1673
1718
  );
1674
1719
  }
1675
1720
  });
1676
- return { events };
1721
+ return trimEventsTillLastRecovery(events);
1677
1722
  }
1678
1723
  async shutdownTask(options) {
1679
1724
  const { taskId } = options;
@@ -1712,7 +1757,72 @@ class DatabaseTaskStore {
1712
1757
  body: serializedBody
1713
1758
  });
1714
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>";
1715
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
+ };
1716
1826
 
1717
1827
  var __defProp$2 = Object.defineProperty;
1718
1828
  var __defNormalProp$2 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -1797,9 +1907,10 @@ function defer() {
1797
1907
  return { promise, resolve };
1798
1908
  }
1799
1909
  class StorageTaskBroker {
1800
- constructor(storage, logger) {
1910
+ constructor(storage, logger, config) {
1801
1911
  this.storage = storage;
1802
1912
  this.logger = logger;
1913
+ this.config = config;
1803
1914
  __publicField$2(this, "deferredDispatch", defer());
1804
1915
  }
1805
1916
  async list(options) {
@@ -1832,6 +1943,26 @@ class StorageTaskBroker {
1832
1943
  }
1833
1944
  });
1834
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
+ }
1835
1966
  /**
1836
1967
  * {@inheritdoc TaskBroker.claim}
1837
1968
  */
@@ -1939,38 +2070,6 @@ class StorageTaskBroker {
1939
2070
  }
1940
2071
  }
1941
2072
 
1942
- function isTruthy(value) {
1943
- return lodash.isArray(value) ? value.length > 0 : !!value;
1944
- }
1945
- function generateExampleOutput(schema) {
1946
- var _a, _b;
1947
- const { examples } = schema;
1948
- if (examples && Array.isArray(examples)) {
1949
- return examples[0];
1950
- }
1951
- if (schema.type === "object") {
1952
- return Object.fromEntries(
1953
- Object.entries((_a = schema.properties) != null ? _a : {}).map(([key, value]) => [
1954
- key,
1955
- generateExampleOutput(value)
1956
- ])
1957
- );
1958
- } else if (schema.type === "array") {
1959
- const [firstSchema] = (_b = [schema.items]) == null ? void 0 : _b.flat();
1960
- if (firstSchema) {
1961
- return [generateExampleOutput(firstSchema)];
1962
- }
1963
- return [];
1964
- } else if (schema.type === "string") {
1965
- return "<example>";
1966
- } else if (schema.type === "number") {
1967
- return 0;
1968
- } else if (schema.type === "boolean") {
1969
- return false;
1970
- }
1971
- return "<unknown>";
1972
- }
1973
-
1974
2073
  function createCounterMetric(config) {
1975
2074
  let metric = promClient.register.getSingleMetric(config.name);
1976
2075
  if (!metric) {
@@ -2504,6 +2603,10 @@ class TaskWorker {
2504
2603
  constructor(options) {
2505
2604
  this.options = options;
2506
2605
  __publicField(this, "taskQueue");
2606
+ __publicField(this, "logger");
2607
+ __publicField(this, "stopWorkers");
2608
+ this.stopWorkers = false;
2609
+ this.logger = options.logger;
2507
2610
  this.taskQueue = new PQueue__default["default"]({
2508
2611
  concurrency: options.concurrentTasksLimit
2509
2612
  });
@@ -2537,15 +2640,34 @@ class TaskWorker {
2537
2640
  permissions
2538
2641
  });
2539
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
+ }
2540
2651
  start() {
2541
2652
  (async () => {
2542
- 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) {
2543
2660
  await this.onReadyToClaimTask();
2544
- const task = await this.options.taskBroker.claim();
2545
- 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
+ }
2546
2665
  }
2547
2666
  })();
2548
2667
  }
2668
+ stop() {
2669
+ this.stopWorkers = true;
2670
+ }
2549
2671
  onReadyToClaimTask() {
2550
2672
  if (this.taskQueue.pending < this.options.concurrentTasksLimit) {
2551
2673
  return Promise.resolve();
@@ -2806,7 +2928,7 @@ async function createRouter(options) {
2806
2928
  let taskBroker;
2807
2929
  if (!options.taskBroker) {
2808
2930
  const databaseTaskStore = await DatabaseTaskStore.create({ database });
2809
- taskBroker = new StorageTaskBroker(databaseTaskStore, logger);
2931
+ taskBroker = new StorageTaskBroker(databaseTaskStore, logger, config);
2810
2932
  if (scheduler && databaseTaskStore.listStaleTasks) {
2811
2933
  await scheduler.scheduleTask({
2812
2934
  id: "close_stale_tasks",
@@ -2863,7 +2985,16 @@ async function createRouter(options) {
2863
2985
  additionalTemplateGlobals
2864
2986
  });
2865
2987
  actionsToRegister.forEach((action) => actionRegistry.register(action));
2866
- 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
+ }
2867
2998
  const dryRunner = createDryRunner({
2868
2999
  actionRegistry,
2869
3000
  integrations,
@@ -2977,6 +3108,7 @@ async function createRouter(options) {
2977
3108
  name: (_b2 = step.name) != null ? _b2 : step.action
2978
3109
  };
2979
3110
  }),
3111
+ EXPERIMENTAL_recovery: template.spec.EXPERIMENTAL_recovery,
2980
3112
  output: (_b = template.spec.output) != null ? _b : {},
2981
3113
  parameters: values,
2982
3114
  user: {
@@ -3208,4 +3340,4 @@ exports.createRouter = createRouter;
3208
3340
  exports.createWaitAction = createWaitAction;
3209
3341
  exports.scaffolderActionRules = scaffolderActionRules;
3210
3342
  exports.scaffolderTemplateRules = scaffolderTemplateRules;
3211
- //# sourceMappingURL=router-03a1f408.cjs.js.map
3343
+ //# sourceMappingURL=router-842a762b.cjs.js.map