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