@backstage/plugin-catalog-import 0.12.0-next.2 → 0.12.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 +43 -0
- package/alpha/package.json +1 -1
- package/dist/api/AzureDevops.esm.js +67 -0
- package/dist/api/AzureDevops.esm.js.map +1 -0
- package/dist/api/AzureRepoApiClient.esm.js +146 -0
- package/dist/api/AzureRepoApiClient.esm.js.map +1 -0
- package/dist/api/CatalogImportClient.esm.js +43 -113
- package/dist/api/CatalogImportClient.esm.js.map +1 -1
- package/dist/api/GitHub.esm.js +90 -11
- package/dist/api/GitHub.esm.js.map +1 -1
- package/dist/components/DefaultImportPage/DefaultImportPage.esm.js +2 -2
- package/dist/components/ImportInfoCard/ImportInfoCard.esm.js +2 -2
- package/dist/components/ImportStepper/ImportStepper.esm.js +2 -2
- package/dist/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.esm.js +2 -2
- package/dist/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.esm.js +1 -1
- package/dist/index.d.ts +0 -1
- package/package.json +19 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# @backstage/plugin-catalog-import
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 4f92394: Migrate from identityApi to fetchApi in frontend plugins.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- d44a20a: Added additional plugin metadata to `package.json`.
|
|
12
|
+
- 3daad61: Integrated Azure DevOps as a catalog import source. This enables Backstage to create Pull Requests to Azure DevOps repositories as it does with GitHub repositories
|
|
13
|
+
- Updated dependencies
|
|
14
|
+
- @backstage/core-components@0.14.8
|
|
15
|
+
- @backstage/core-compat-api@0.2.6
|
|
16
|
+
- @backstage/integration@1.12.0
|
|
17
|
+
- @backstage/core-plugin-api@1.9.3
|
|
18
|
+
- @backstage/plugin-catalog-common@1.0.24
|
|
19
|
+
- @backstage/plugin-catalog-react@1.12.1
|
|
20
|
+
- @backstage/integration-react@1.1.28
|
|
21
|
+
- @backstage/frontend-plugin-api@0.6.6
|
|
22
|
+
- @backstage/catalog-client@1.6.5
|
|
23
|
+
- @backstage/catalog-model@1.5.0
|
|
24
|
+
- @backstage/config@1.2.0
|
|
25
|
+
- @backstage/errors@1.2.4
|
|
26
|
+
|
|
27
|
+
## 0.12.0-next.3
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- d44a20a: Added additional plugin metadata to `package.json`.
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
- @backstage/core-components@0.14.8-next.2
|
|
34
|
+
- @backstage/integration@1.12.0-next.1
|
|
35
|
+
- @backstage/plugin-catalog-common@1.0.24-next.0
|
|
36
|
+
- @backstage/plugin-catalog-react@1.12.1-next.2
|
|
37
|
+
- @backstage/integration-react@1.1.28-next.1
|
|
38
|
+
- @backstage/frontend-plugin-api@0.6.6-next.2
|
|
39
|
+
- @backstage/core-compat-api@0.2.6-next.2
|
|
40
|
+
- @backstage/catalog-client@1.6.5
|
|
41
|
+
- @backstage/catalog-model@1.5.0
|
|
42
|
+
- @backstage/config@1.2.0
|
|
43
|
+
- @backstage/core-plugin-api@1.9.3-next.0
|
|
44
|
+
- @backstage/errors@1.2.4
|
|
45
|
+
|
|
3
46
|
## 0.12.0-next.2
|
|
4
47
|
|
|
5
48
|
### Patch Changes
|
package/alpha/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getBranchName, getCatalogFilename } from '../components/helpers.esm.js';
|
|
2
|
+
import { createAzurePullRequest } from './AzureRepoApiClient.esm.js';
|
|
3
|
+
|
|
4
|
+
function parseAzureUrl(repoUrl) {
|
|
5
|
+
const { org, repo, project, host } = parseRepoUrl(repoUrl);
|
|
6
|
+
if (!org || !repo || !project) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Invalid AzureDevops Repository. Please use a valid repository url and try again "
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
const tenantUrl = `https://${host}/${org}`;
|
|
12
|
+
return { tenantUrl, repoName: repo, project };
|
|
13
|
+
}
|
|
14
|
+
async function submitAzurePrToRepo(options, scmAuthApi, configApi) {
|
|
15
|
+
const { repositoryUrl, fileContent, title, body } = options;
|
|
16
|
+
const branchName = getBranchName(configApi);
|
|
17
|
+
const fileName = getCatalogFilename(configApi);
|
|
18
|
+
const { token } = await scmAuthApi.getCredentials({
|
|
19
|
+
url: repositoryUrl,
|
|
20
|
+
additionalScope: {
|
|
21
|
+
repoWrite: true
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const { tenantUrl, repoName, project } = parseAzureUrl(repositoryUrl);
|
|
25
|
+
const result = await createAzurePullRequest({
|
|
26
|
+
token,
|
|
27
|
+
fileContent,
|
|
28
|
+
title,
|
|
29
|
+
description: body,
|
|
30
|
+
project,
|
|
31
|
+
repository: repoName,
|
|
32
|
+
branchName,
|
|
33
|
+
tenantUrl,
|
|
34
|
+
fileName
|
|
35
|
+
});
|
|
36
|
+
const catalogLocation = `${result.repository.webUrl}?path=/${fileName}`;
|
|
37
|
+
const prLocation = `${result.repository.webUrl}/pullrequest/${result.pullRequestId}`;
|
|
38
|
+
return {
|
|
39
|
+
link: prLocation,
|
|
40
|
+
location: catalogLocation
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function parseRepoUrl(sourceUrl) {
|
|
44
|
+
const url = new URL(sourceUrl);
|
|
45
|
+
let host = url.host;
|
|
46
|
+
let org;
|
|
47
|
+
let project;
|
|
48
|
+
let repo;
|
|
49
|
+
const parts = url.pathname.split("/").map((part) => decodeURIComponent(part));
|
|
50
|
+
if (parts[2] === "_git") {
|
|
51
|
+
org = parts[1];
|
|
52
|
+
project = repo = parts[3];
|
|
53
|
+
} else if (parts[3] === "_git") {
|
|
54
|
+
org = parts[1];
|
|
55
|
+
project = parts[2];
|
|
56
|
+
repo = parts[4];
|
|
57
|
+
} else if (parts[4] === "_git") {
|
|
58
|
+
host = `${host}/${parts[1]}`;
|
|
59
|
+
org = parts[2];
|
|
60
|
+
project = parts[3];
|
|
61
|
+
repo = parts[5];
|
|
62
|
+
}
|
|
63
|
+
return { host, org, project, repo };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { parseAzureUrl, parseRepoUrl, submitAzurePrToRepo };
|
|
67
|
+
//# sourceMappingURL=AzureDevops.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AzureDevops.esm.js","sources":["../../src/api/AzureDevops.ts"],"sourcesContent":["/*\n * Copyright 2024 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { ScmAuthApi } from '@backstage/integration-react';\nimport { ConfigApi } from '@backstage/core-plugin-api';\nimport { getBranchName, getCatalogFilename } from '../components/helpers';\nimport { createAzurePullRequest } from './AzureRepoApiClient';\n\nexport interface AzureRepoParts {\n tenantUrl: string;\n repoName: string;\n project: string;\n}\n\nexport function parseAzureUrl(repoUrl: string): AzureRepoParts {\n const { org, repo, project, host } = parseRepoUrl(repoUrl);\n if (!org || !repo || !project) {\n throw new Error(\n 'Invalid AzureDevops Repository. Please use a valid repository url and try again ',\n );\n }\n const tenantUrl = `https://${host}/${org}`;\n\n return { tenantUrl, repoName: repo, project: project };\n}\n\nexport async function submitAzurePrToRepo(\n options: {\n title: string;\n body: string;\n fileContent: string;\n repositoryUrl: string;\n },\n scmAuthApi: ScmAuthApi,\n configApi: ConfigApi,\n) {\n const { repositoryUrl, fileContent, title, body } = options;\n\n const branchName = getBranchName(configApi);\n const fileName = getCatalogFilename(configApi);\n\n const { token } = await scmAuthApi.getCredentials({\n url: repositoryUrl,\n additionalScope: {\n repoWrite: true,\n },\n });\n const { tenantUrl, repoName, project } = parseAzureUrl(repositoryUrl);\n const result = await createAzurePullRequest({\n token,\n fileContent,\n title,\n description: body,\n project,\n repository: repoName,\n branchName,\n tenantUrl,\n fileName,\n });\n const catalogLocation = `${result.repository.webUrl}?path=/${fileName}`;\n const prLocation = `${result.repository.webUrl}/pullrequest/${result.pullRequestId}`;\n return {\n link: prLocation!,\n location: catalogLocation,\n };\n}\n\nexport function parseRepoUrl(sourceUrl: string) {\n const url = new URL(sourceUrl);\n\n let host = url.host;\n let org;\n let project;\n let repo;\n\n const parts = url.pathname.split('/').map(part => decodeURIComponent(part));\n if (parts[2] === '_git') {\n org = parts[1];\n project = repo = parts[3];\n } else if (parts[3] === '_git') {\n org = parts[1];\n project = parts[2];\n repo = parts[4];\n } else if (parts[4] === '_git') {\n host = `${host}/${parts[1]}`;\n org = parts[2];\n project = parts[3];\n repo = parts[5];\n }\n\n return { host, org, project, repo };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const apiVersions = "6.0";
|
|
2
|
+
class RepoApiClient {
|
|
3
|
+
constructor(_options) {
|
|
4
|
+
this._options = _options;
|
|
5
|
+
}
|
|
6
|
+
createEndpoint = (path, version, queryParams = void 0) => {
|
|
7
|
+
const url = new URL(
|
|
8
|
+
`${this._options.tenantUrl}/${this._options.project}/_apis/git/repositories`
|
|
9
|
+
);
|
|
10
|
+
url.pathname += path;
|
|
11
|
+
url.searchParams.set("api-version", version);
|
|
12
|
+
Object.entries(queryParams ?? {}).forEach(
|
|
13
|
+
([key, value]) => url.searchParams.set(key, value)
|
|
14
|
+
);
|
|
15
|
+
return url.toString();
|
|
16
|
+
};
|
|
17
|
+
async get(path, version, queryParams = void 0, token) {
|
|
18
|
+
const endpoint = this.createEndpoint(path, version, queryParams);
|
|
19
|
+
const result = await fetch(endpoint, {
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bearer ${token}`
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
if (!result.ok) {
|
|
25
|
+
return result.json().then((it) => Promise.reject(new Error(it.message)));
|
|
26
|
+
}
|
|
27
|
+
return await result.json();
|
|
28
|
+
}
|
|
29
|
+
async post(path, version, payload, token) {
|
|
30
|
+
const endpoint = this.createEndpoint(path, version);
|
|
31
|
+
const result = await fetch(endpoint, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: `Bearer ${token}`,
|
|
35
|
+
"Content-Type": "application/json"
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(payload)
|
|
38
|
+
});
|
|
39
|
+
if (!result.ok) {
|
|
40
|
+
return result.json().then((it) => Promise.reject(new Error(it.message)));
|
|
41
|
+
}
|
|
42
|
+
return await result.json();
|
|
43
|
+
}
|
|
44
|
+
async getRepository(repositoryName, token) {
|
|
45
|
+
return this.get(`/${repositoryName}`, apiVersions, void 0, token);
|
|
46
|
+
}
|
|
47
|
+
async getDefaultBranch(repo, token) {
|
|
48
|
+
const filter = repo.defaultBranch.replace("refs/", "");
|
|
49
|
+
const result = await this.get(
|
|
50
|
+
`/${repo.name}/refs`,
|
|
51
|
+
apiVersions,
|
|
52
|
+
{ filter },
|
|
53
|
+
token
|
|
54
|
+
);
|
|
55
|
+
if (!result.value?.length) {
|
|
56
|
+
return Promise.reject(
|
|
57
|
+
new Error(`The requested ref '${filter}' was not found`)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return result.value[0];
|
|
61
|
+
}
|
|
62
|
+
async pushNewBranch(options, token) {
|
|
63
|
+
const { sourceBranch, repoName } = options;
|
|
64
|
+
const push = {
|
|
65
|
+
refUpdates: [
|
|
66
|
+
{
|
|
67
|
+
name: `refs/heads/${options.branchName}`,
|
|
68
|
+
oldObjectId: sourceBranch.objectId
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
commits: [
|
|
72
|
+
{
|
|
73
|
+
comment: options.title,
|
|
74
|
+
changes: [
|
|
75
|
+
{
|
|
76
|
+
changeType: "add",
|
|
77
|
+
item: {
|
|
78
|
+
path: `/${options.fileName}`
|
|
79
|
+
},
|
|
80
|
+
newContent: {
|
|
81
|
+
content: options.fileContent,
|
|
82
|
+
contentType: "rawtext"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
const result = await this.post(
|
|
90
|
+
`/${repoName}/pushes`,
|
|
91
|
+
apiVersions,
|
|
92
|
+
push,
|
|
93
|
+
token
|
|
94
|
+
);
|
|
95
|
+
return result.refUpdates[0];
|
|
96
|
+
}
|
|
97
|
+
async createPullRequest(options, token) {
|
|
98
|
+
const { repoName, sourceName, targetName } = options;
|
|
99
|
+
const payload = {
|
|
100
|
+
title: options.title,
|
|
101
|
+
description: options.description,
|
|
102
|
+
sourceRefName: sourceName,
|
|
103
|
+
targetRefName: targetName
|
|
104
|
+
};
|
|
105
|
+
return await this.post(
|
|
106
|
+
`/${repoName}/pullrequests`,
|
|
107
|
+
apiVersions,
|
|
108
|
+
payload,
|
|
109
|
+
token
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function createAzurePullRequest(options, client = void 0) {
|
|
114
|
+
const {
|
|
115
|
+
title,
|
|
116
|
+
repository,
|
|
117
|
+
token,
|
|
118
|
+
fileContent,
|
|
119
|
+
fileName,
|
|
120
|
+
branchName,
|
|
121
|
+
description
|
|
122
|
+
} = options;
|
|
123
|
+
const actualClient = client ?? new RepoApiClient(options);
|
|
124
|
+
const repo = await actualClient.getRepository(repository, token);
|
|
125
|
+
const defaultBranch = await actualClient.getDefaultBranch(repo, token);
|
|
126
|
+
const branchOptions = {
|
|
127
|
+
title,
|
|
128
|
+
repoName: repo.name,
|
|
129
|
+
sourceBranch: defaultBranch,
|
|
130
|
+
branchName,
|
|
131
|
+
fileContent,
|
|
132
|
+
fileName
|
|
133
|
+
};
|
|
134
|
+
const refUpdate = await actualClient.pushNewBranch(branchOptions, token);
|
|
135
|
+
const prOptions = {
|
|
136
|
+
title,
|
|
137
|
+
description,
|
|
138
|
+
repoName: repo.name,
|
|
139
|
+
sourceName: refUpdate.name,
|
|
140
|
+
targetName: defaultBranch.name
|
|
141
|
+
};
|
|
142
|
+
return actualClient.createPullRequest(prOptions, token);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { RepoApiClient, createAzurePullRequest };
|
|
146
|
+
//# sourceMappingURL=AzureRepoApiClient.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AzureRepoApiClient.esm.js","sources":["../../src/api/AzureRepoApiClient.ts"],"sourcesContent":["/*\n * Copyright 2024 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\nexport interface AzurePrOptions {\n tenantUrl: string;\n title: string;\n project: string;\n branchName: string;\n repository: string;\n token: string;\n fileContent: string;\n fileName: string;\n description: string;\n}\n\nexport interface CreateAzurePr {\n sourceRefName: string;\n targetRefName: string;\n title: string;\n description: string;\n}\n\nexport interface AzureRepo {\n id: string;\n name: string;\n defaultBranch: string;\n}\n\nexport interface AzureRef {\n name: string;\n objectId: string;\n}\n\ninterface RefQueryResult {\n value: AzureRef[];\n}\n\nexport interface AzureCommit {\n comment: string;\n\n changes: {\n changeType: string;\n item: {\n path: string;\n };\n newContent: {\n content: string;\n contentType: 'rawtext';\n };\n }[];\n}\n\nexport interface AzureRefUpdate {\n repositoryId: string;\n name: string;\n oldObjectId: string;\n newObjectId: string;\n}\n\nexport interface AzurePushResult {\n refUpdates: AzureRefUpdate[];\n}\n\nexport interface AzurePush {\n refUpdates: {\n name: string;\n oldObjectId: string;\n }[];\n commits: AzureCommit[];\n}\n\nexport interface AzurePrResult {\n pullRequestId: string;\n repository: {\n name: string;\n webUrl: string;\n };\n}\n\nconst apiVersions = '6.0';\n\nexport interface RepoApiClientOptions {\n project: string;\n tenantUrl: string;\n}\n\nexport interface NewBranchOptions {\n fileContent: string;\n fileName: string;\n title: string;\n branchName: string;\n repoName: string;\n sourceBranch: AzureRef;\n}\n\nexport interface CreatePrOptions {\n repoName: string;\n sourceName: string;\n targetName: string;\n description: string;\n title: string;\n}\n\nexport class RepoApiClient {\n private createEndpoint = (\n path: string,\n version: string,\n queryParams: Record<string, string> | undefined = undefined,\n ) => {\n const url = new URL(\n `${this._options.tenantUrl}/${this._options.project}/_apis/git/repositories`,\n );\n url.pathname += path;\n\n url.searchParams.set('api-version', version);\n Object.entries(queryParams ?? {}).forEach(([key, value]) =>\n url.searchParams.set(key, value),\n );\n return url.toString();\n };\n\n constructor(private _options: RepoApiClientOptions) {}\n\n private async get<T>(\n path: string,\n version: string,\n queryParams: Record<string, string> | undefined = undefined,\n token: string,\n ): Promise<T> {\n const endpoint = this.createEndpoint(path, version, queryParams);\n const result = await fetch(endpoint, {\n headers: {\n Authorization: `Bearer ${token}`,\n },\n });\n if (!result.ok) {\n return result.json().then(it => Promise.reject(new Error(it.message)));\n }\n return await result.json();\n }\n\n private async post<T>(\n path: string,\n version: string,\n payload: unknown,\n token: string,\n ): Promise<T> {\n const endpoint = this.createEndpoint(path, version);\n const result = await fetch(endpoint, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(payload),\n });\n if (!result.ok) {\n return result.json().then(it => Promise.reject(new Error(it.message)));\n }\n return await result.json();\n }\n\n async getRepository(\n repositoryName: string,\n token: string,\n ): Promise<AzureRepo> {\n return this.get(`/${repositoryName}`, apiVersions, undefined, token);\n }\n\n async getDefaultBranch(repo: AzureRepo, token: string): Promise<AzureRef> {\n const filter = repo.defaultBranch.replace('refs/', '');\n const result: RefQueryResult = await this.get(\n `/${repo.name}/refs`,\n apiVersions,\n { filter },\n token,\n );\n if (!result.value?.length) {\n return Promise.reject(\n new Error(`The requested ref '${filter}' was not found`),\n );\n }\n return result.value[0];\n }\n\n async pushNewBranch(\n options: NewBranchOptions,\n token: string,\n ): Promise<AzureRefUpdate> {\n const { sourceBranch, repoName } = options;\n const push: AzurePush = {\n refUpdates: [\n {\n name: `refs/heads/${options.branchName}`,\n oldObjectId: sourceBranch.objectId,\n },\n ],\n commits: [\n {\n comment: options.title,\n changes: [\n {\n changeType: 'add',\n item: {\n path: `/${options.fileName}`,\n },\n newContent: {\n content: options.fileContent,\n contentType: 'rawtext',\n },\n },\n ],\n },\n ],\n };\n const result = await this.post<AzurePushResult>(\n `/${repoName}/pushes`,\n apiVersions,\n push,\n token,\n );\n return result.refUpdates[0];\n }\n\n async createPullRequest(\n options: CreatePrOptions,\n token: string,\n ): Promise<AzurePrResult> {\n const { repoName, sourceName, targetName } = options;\n const payload: CreateAzurePr = {\n title: options.title,\n description: options.description,\n sourceRefName: sourceName,\n targetRefName: targetName,\n };\n\n return await this.post<AzurePrResult>(\n `/${repoName}/pullrequests`,\n apiVersions,\n payload,\n token,\n );\n }\n}\n\nexport async function createAzurePullRequest(\n options: AzurePrOptions,\n client: RepoApiClient | undefined = undefined,\n): Promise<AzurePrResult> {\n const {\n title,\n repository,\n token,\n fileContent,\n fileName,\n branchName,\n description,\n } = options;\n const actualClient = client ?? new RepoApiClient(options);\n const repo = await actualClient.getRepository(repository, token);\n const defaultBranch = await actualClient.getDefaultBranch(repo, token);\n const branchOptions: NewBranchOptions = {\n title: title,\n repoName: repo.name,\n sourceBranch: defaultBranch,\n branchName: branchName,\n fileContent: fileContent,\n fileName: fileName,\n };\n const refUpdate = await actualClient.pushNewBranch(branchOptions, token);\n const prOptions: CreatePrOptions = {\n title,\n description,\n repoName: repo.name,\n sourceName: refUpdate.name,\n targetName: defaultBranch.name,\n };\n return actualClient.createPullRequest(prOptions, token);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Octokit } from '@octokit/rest';
|
|
2
|
-
import { Base64 } from 'js-base64';
|
|
3
1
|
import YAML from 'yaml';
|
|
4
|
-
import {
|
|
5
|
-
import { getCatalogFilename
|
|
2
|
+
import { submitGitHubPrToRepo } from './GitHub.esm.js';
|
|
3
|
+
import { getCatalogFilename } from '../components/helpers.esm.js';
|
|
4
|
+
import parseGitUrl from 'git-url-parse';
|
|
5
|
+
import { submitAzurePrToRepo } from './AzureDevops.esm.js';
|
|
6
6
|
|
|
7
7
|
class CatalogImportClient {
|
|
8
8
|
discoveryApi;
|
|
@@ -41,17 +41,18 @@ class CatalogImportClient {
|
|
|
41
41
|
]
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
const supportedIntegrations = ["github", "azure"];
|
|
45
|
+
const foundIntegration = this.scmIntegrationsApi.byUrl(url);
|
|
46
|
+
const iSupported = !!foundIntegration && supportedIntegrations.find((it) => it === foundIntegration.type);
|
|
47
|
+
if (!iSupported) {
|
|
47
48
|
const catalogFilename = getCatalogFilename(this.configApi);
|
|
48
|
-
if (
|
|
49
|
+
if (foundIntegration) {
|
|
49
50
|
throw new Error(
|
|
50
|
-
`The ${
|
|
51
|
+
`The ${foundIntegration.title} integration only supports full URLs to ${catalogFilename} files. Did you try to pass in the URL of a directory instead?`
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
throw new Error(
|
|
54
|
-
`This URL was not recognized as a valid
|
|
55
|
+
`This URL was not recognized as a valid git URL because there was no configured integration that matched the given host name. Currently GitHub and Azure DevOps are supported. You could try to paste the full URL to a ${catalogFilename} file instead.`
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
58
|
const analyzation = await this.analyzeLocation({
|
|
@@ -80,7 +81,7 @@ class CatalogImportClient {
|
|
|
80
81
|
}
|
|
81
82
|
return {
|
|
82
83
|
type: "repository",
|
|
83
|
-
integrationType:
|
|
84
|
+
integrationType: foundIntegration.type,
|
|
84
85
|
url,
|
|
85
86
|
generatedEntities: analyzation.generateEntities.map((x) => x.entity)
|
|
86
87
|
};
|
|
@@ -108,20 +109,38 @@ For more information, read an [overview of the Backstage software catalog](https
|
|
|
108
109
|
if (!validationResponse.valid) {
|
|
109
110
|
throw new Error(validationResponse.errors[0].message);
|
|
110
111
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
const provider = this.scmIntegrationsApi.byUrl(repositoryUrl);
|
|
113
|
+
switch (provider?.type) {
|
|
114
|
+
case "github": {
|
|
115
|
+
const { config } = provider;
|
|
116
|
+
const { name, owner } = parseGitUrl(repositoryUrl);
|
|
117
|
+
const options2 = {
|
|
118
|
+
githubIntegrationConfig: config,
|
|
119
|
+
repo: name,
|
|
120
|
+
owner,
|
|
121
|
+
repositoryUrl,
|
|
122
|
+
fileContent,
|
|
123
|
+
title,
|
|
124
|
+
body
|
|
125
|
+
};
|
|
126
|
+
return submitGitHubPrToRepo(options2, this.scmAuthApi, this.configApi);
|
|
127
|
+
}
|
|
128
|
+
case "azure": {
|
|
129
|
+
return submitAzurePrToRepo(
|
|
130
|
+
{
|
|
131
|
+
repositoryUrl,
|
|
132
|
+
fileContent,
|
|
133
|
+
title,
|
|
134
|
+
body
|
|
135
|
+
},
|
|
136
|
+
this.scmAuthApi,
|
|
137
|
+
this.configApi
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
default: {
|
|
141
|
+
throw new Error("unimplemented!");
|
|
142
|
+
}
|
|
123
143
|
}
|
|
124
|
-
throw new Error("unimplemented!");
|
|
125
144
|
}
|
|
126
145
|
// TODO: this could be part of the catalog api
|
|
127
146
|
async analyzeLocation(options) {
|
|
@@ -154,95 +173,6 @@ For more information, read an [overview of the Backstage software catalog](https
|
|
|
154
173
|
const payload = await response.json();
|
|
155
174
|
return payload;
|
|
156
175
|
}
|
|
157
|
-
// TODO: extract this function and implement for non-github
|
|
158
|
-
async submitGitHubPrToRepo(options) {
|
|
159
|
-
const {
|
|
160
|
-
owner,
|
|
161
|
-
repo,
|
|
162
|
-
title,
|
|
163
|
-
body,
|
|
164
|
-
fileContent,
|
|
165
|
-
repositoryUrl,
|
|
166
|
-
githubIntegrationConfig
|
|
167
|
-
} = options;
|
|
168
|
-
const { token } = await this.scmAuthApi.getCredentials({
|
|
169
|
-
url: repositoryUrl,
|
|
170
|
-
additionalScope: {
|
|
171
|
-
repoWrite: true
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
const octo = new Octokit({
|
|
175
|
-
auth: token,
|
|
176
|
-
baseUrl: githubIntegrationConfig.apiBaseUrl
|
|
177
|
-
});
|
|
178
|
-
const branchName = getBranchName(this.configApi);
|
|
179
|
-
const fileName = getCatalogFilename(this.configApi);
|
|
180
|
-
const repoData = await octo.repos.get({
|
|
181
|
-
owner,
|
|
182
|
-
repo
|
|
183
|
-
}).catch((e) => {
|
|
184
|
-
throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e));
|
|
185
|
-
});
|
|
186
|
-
const parentRef = await octo.git.getRef({
|
|
187
|
-
owner,
|
|
188
|
-
repo,
|
|
189
|
-
ref: `heads/${repoData.data.default_branch}`
|
|
190
|
-
}).catch((e) => {
|
|
191
|
-
throw new Error(
|
|
192
|
-
formatHttpErrorMessage("Couldn't fetch default branch data", e)
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
await octo.git.createRef({
|
|
196
|
-
owner,
|
|
197
|
-
repo,
|
|
198
|
-
ref: `refs/heads/${branchName}`,
|
|
199
|
-
sha: parentRef.data.object.sha
|
|
200
|
-
}).catch((e) => {
|
|
201
|
-
throw new Error(
|
|
202
|
-
formatHttpErrorMessage(
|
|
203
|
-
`Couldn't create a new branch with name '${branchName}'`,
|
|
204
|
-
e
|
|
205
|
-
)
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
await octo.repos.createOrUpdateFileContents({
|
|
209
|
-
owner,
|
|
210
|
-
repo,
|
|
211
|
-
path: fileName,
|
|
212
|
-
message: title,
|
|
213
|
-
content: Base64.encode(fileContent),
|
|
214
|
-
branch: branchName
|
|
215
|
-
}).catch((e) => {
|
|
216
|
-
throw new Error(
|
|
217
|
-
formatHttpErrorMessage(
|
|
218
|
-
`Couldn't create a commit with ${fileName} file added`,
|
|
219
|
-
e
|
|
220
|
-
)
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
const pullRequestResponse = await octo.pulls.create({
|
|
224
|
-
owner,
|
|
225
|
-
repo,
|
|
226
|
-
title,
|
|
227
|
-
head: branchName,
|
|
228
|
-
body,
|
|
229
|
-
base: repoData.data.default_branch
|
|
230
|
-
}).catch((e) => {
|
|
231
|
-
throw new Error(
|
|
232
|
-
formatHttpErrorMessage(
|
|
233
|
-
`Couldn't create a pull request for ${branchName} branch`,
|
|
234
|
-
e
|
|
235
|
-
)
|
|
236
|
-
);
|
|
237
|
-
});
|
|
238
|
-
return {
|
|
239
|
-
link: pullRequestResponse.data.html_url,
|
|
240
|
-
location: `https://${githubIntegrationConfig.host}/${owner}/${repo}/blob/${repoData.data.default_branch}/${fileName}`
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
function formatHttpErrorMessage(message, error) {
|
|
245
|
-
return `${message}, received http response status code ${error.status}: ${error.message}`;
|
|
246
176
|
}
|
|
247
177
|
|
|
248
178
|
export { CatalogImportClient };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CatalogImportClient.esm.js","sources":["../../src/api/CatalogImportClient.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { CatalogApi } from '@backstage/catalog-client';\nimport { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';\nimport {\n GithubIntegrationConfig,\n ScmIntegrationRegistry,\n} from '@backstage/integration';\nimport { ScmAuthApi } from '@backstage/integration-react';\nimport { Octokit } from '@octokit/rest';\nimport { Base64 } from 'js-base64';\nimport { AnalyzeResult, CatalogImportApi } from './CatalogImportApi';\nimport YAML from 'yaml';\nimport { getGithubIntegrationConfig } from './GitHub';\nimport { getBranchName, getCatalogFilename } from '../components/helpers';\nimport { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common';\nimport { CompoundEntityRef } from '@backstage/catalog-model';\n\n/**\n * The default implementation of the {@link CatalogImportApi}.\n *\n * @public\n */\nexport class CatalogImportClient implements CatalogImportApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n private readonly scmAuthApi: ScmAuthApi;\n private readonly scmIntegrationsApi: ScmIntegrationRegistry;\n private readonly catalogApi: CatalogApi;\n private readonly configApi: ConfigApi;\n\n constructor(options: {\n discoveryApi: DiscoveryApi;\n scmAuthApi: ScmAuthApi;\n fetchApi: FetchApi;\n scmIntegrationsApi: ScmIntegrationRegistry;\n catalogApi: CatalogApi;\n configApi: ConfigApi;\n }) {\n this.discoveryApi = options.discoveryApi;\n this.scmAuthApi = options.scmAuthApi;\n this.fetchApi = options.fetchApi;\n this.scmIntegrationsApi = options.scmIntegrationsApi;\n this.catalogApi = options.catalogApi;\n this.configApi = options.configApi;\n }\n\n async analyzeUrl(url: string): Promise<AnalyzeResult> {\n if (\n new URL(url).pathname.match(/\\.ya?ml$/) ||\n new URL(url).searchParams.get('path')?.match(/.ya?ml$/)\n ) {\n const location = await this.catalogApi.addLocation({\n type: 'url',\n target: url,\n dryRun: true,\n });\n\n return {\n type: 'locations',\n locations: [\n {\n exists: location.exists,\n target: location.location.target,\n entities: location.entities.map(e => ({\n kind: e.kind,\n namespace: e.metadata.namespace ?? 'default',\n name: e.metadata.name,\n })),\n },\n ],\n };\n }\n\n const ghConfig = getGithubIntegrationConfig(this.scmIntegrationsApi, url);\n if (!ghConfig) {\n const other = this.scmIntegrationsApi.byUrl(url);\n const catalogFilename = getCatalogFilename(this.configApi);\n\n if (other) {\n throw new Error(\n `The ${other.title} integration only supports full URLs to ${catalogFilename} files. Did you try to pass in the URL of a directory instead?`,\n );\n }\n throw new Error(\n `This URL was not recognized as a valid GitHub URL because there was no configured integration that matched the given host name. You could try to paste the full URL to a ${catalogFilename} file instead.`,\n );\n }\n\n const analyzation = await this.analyzeLocation({\n repo: url,\n });\n\n if (analyzation.existingEntityFiles.length > 0) {\n const locations = analyzation.existingEntityFiles.reduce<\n Record<\n string,\n {\n target: string;\n exists?: boolean;\n entities: CompoundEntityRef[];\n }\n >\n >((state, curr) => {\n state[curr.location.target] = {\n target: curr.location.target,\n exists: curr.isRegistered,\n entities: [\n ...(curr.location.target in state\n ? state[curr.location.target].entities\n : []),\n {\n name: curr.entity.metadata.name,\n namespace: curr.entity.metadata.namespace ?? 'default',\n kind: curr.entity.kind,\n },\n ],\n };\n return state;\n }, {});\n return {\n type: 'locations',\n locations: Object.values(locations),\n };\n }\n\n return {\n type: 'repository',\n integrationType: 'github',\n url: url,\n generatedEntities: analyzation.generateEntities.map(x => x.entity),\n };\n }\n\n async preparePullRequest(): Promise<{\n title: string;\n body: string;\n }> {\n const appTitle =\n this.configApi.getOptionalString('app.title') ?? 'Backstage';\n const appBaseUrl = this.configApi.getString('app.baseUrl');\n const catalogFilename = getCatalogFilename(this.configApi);\n\n return {\n title: `Add ${catalogFilename} config file`,\n body: `This pull request adds a **Backstage entity metadata file** \\\nto this repository so that the component can be added to the \\\n[${appTitle} software catalog](${appBaseUrl}).\\n\\nAfter this pull request is merged, \\\nthe component will become available.\\n\\nFor more information, read an \\\n[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/).`,\n };\n }\n\n async submitPullRequest(options: {\n repositoryUrl: string;\n fileContent: string;\n title: string;\n body: string;\n }): Promise<{ link: string; location: string }> {\n const { repositoryUrl, fileContent, title, body } = options;\n const parseData = YAML.parse(fileContent);\n\n const validationResponse = await this.catalogApi.validateEntity(\n parseData,\n `url:${repositoryUrl}`,\n );\n if (!validationResponse.valid) {\n throw new Error(validationResponse.errors[0].message);\n }\n const ghConfig = getGithubIntegrationConfig(\n this.scmIntegrationsApi,\n repositoryUrl,\n );\n\n if (ghConfig) {\n return await this.submitGitHubPrToRepo({\n ...ghConfig,\n repositoryUrl,\n fileContent,\n title,\n body,\n });\n }\n throw new Error('unimplemented!');\n }\n\n // TODO: this could be part of the catalog api\n private async analyzeLocation(options: {\n repo: string;\n }): Promise<AnalyzeLocationResponse> {\n const response = await this.fetchApi\n .fetch(\n `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,\n {\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n body: JSON.stringify({\n location: { type: 'url', target: options.repo },\n ...(this.configApi.getOptionalString(\n 'catalog.import.entityFilename',\n ) && {\n catalogFilename: this.configApi.getOptionalString(\n 'catalog.import.entityFilename',\n ),\n }),\n }),\n },\n )\n .catch(e => {\n throw new Error(`Failed to generate entity definitions, ${e.message}`);\n });\n if (!response.ok) {\n throw new Error(\n `Failed to generate entity definitions. Received http response ${response.status}: ${response.statusText}`,\n );\n }\n\n const payload = await response.json();\n return payload;\n }\n\n // TODO: extract this function and implement for non-github\n private async submitGitHubPrToRepo(options: {\n owner: string;\n repo: string;\n title: string;\n body: string;\n fileContent: string;\n repositoryUrl: string;\n githubIntegrationConfig: GithubIntegrationConfig;\n }): Promise<{ link: string; location: string }> {\n const {\n owner,\n repo,\n title,\n body,\n fileContent,\n repositoryUrl,\n githubIntegrationConfig,\n } = options;\n\n const { token } = await this.scmAuthApi.getCredentials({\n url: repositoryUrl,\n additionalScope: {\n repoWrite: true,\n },\n });\n\n const octo = new Octokit({\n auth: token,\n baseUrl: githubIntegrationConfig.apiBaseUrl,\n });\n\n const branchName = getBranchName(this.configApi);\n const fileName = getCatalogFilename(this.configApi);\n\n const repoData = await octo.repos\n .get({\n owner,\n repo,\n })\n .catch(e => {\n throw new Error(formatHttpErrorMessage(\"Couldn't fetch repo data\", e));\n });\n\n const parentRef = await octo.git\n .getRef({\n owner,\n repo,\n ref: `heads/${repoData.data.default_branch}`,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\"Couldn't fetch default branch data\", e),\n );\n });\n\n await octo.git\n .createRef({\n owner,\n repo,\n ref: `refs/heads/${branchName}`,\n sha: parentRef.data.object.sha,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\n `Couldn't create a new branch with name '${branchName}'`,\n e,\n ),\n );\n });\n\n await octo.repos\n .createOrUpdateFileContents({\n owner,\n repo,\n path: fileName,\n message: title,\n content: Base64.encode(fileContent),\n branch: branchName,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\n `Couldn't create a commit with ${fileName} file added`,\n e,\n ),\n );\n });\n\n const pullRequestResponse = await octo.pulls\n .create({\n owner,\n repo,\n title,\n head: branchName,\n body,\n base: repoData.data.default_branch,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\n `Couldn't create a pull request for ${branchName} branch`,\n e,\n ),\n );\n });\n\n return {\n link: pullRequestResponse.data.html_url,\n location: `https://${githubIntegrationConfig.host}/${owner}/${repo}/blob/${repoData.data.default_branch}/${fileName}`,\n };\n }\n}\n\nfunction formatHttpErrorMessage(\n message: string,\n error: { status: number; message: string },\n) {\n return `${message}, received http response status code ${error.status}: ${error.message}`;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CatalogImportClient.esm.js","sources":["../../src/api/CatalogImportClient.ts"],"sourcesContent":["/*\n * Copyright 2020 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 { CatalogApi } from '@backstage/catalog-client';\nimport { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';\nimport {\n GithubIntegration,\n ScmIntegrationRegistry,\n} from '@backstage/integration';\nimport { ScmAuthApi } from '@backstage/integration-react';\nimport { AnalyzeResult, CatalogImportApi } from './CatalogImportApi';\nimport YAML from 'yaml';\nimport { GitHubOptions, submitGitHubPrToRepo } from './GitHub';\nimport { getCatalogFilename } from '../components/helpers';\nimport { AnalyzeLocationResponse } from '@backstage/plugin-catalog-common';\nimport { CompoundEntityRef } from '@backstage/catalog-model';\nimport parseGitUrl from 'git-url-parse';\nimport { submitAzurePrToRepo } from './AzureDevops';\n\n/**\n * The default implementation of the {@link CatalogImportApi}.\n *\n * @public\n */\nexport class CatalogImportClient implements CatalogImportApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n private readonly scmAuthApi: ScmAuthApi;\n private readonly scmIntegrationsApi: ScmIntegrationRegistry;\n private readonly catalogApi: CatalogApi;\n private readonly configApi: ConfigApi;\n\n constructor(options: {\n discoveryApi: DiscoveryApi;\n scmAuthApi: ScmAuthApi;\n fetchApi: FetchApi;\n scmIntegrationsApi: ScmIntegrationRegistry;\n catalogApi: CatalogApi;\n configApi: ConfigApi;\n }) {\n this.discoveryApi = options.discoveryApi;\n this.scmAuthApi = options.scmAuthApi;\n this.fetchApi = options.fetchApi;\n this.scmIntegrationsApi = options.scmIntegrationsApi;\n this.catalogApi = options.catalogApi;\n this.configApi = options.configApi;\n }\n\n async analyzeUrl(url: string): Promise<AnalyzeResult> {\n if (\n new URL(url).pathname.match(/\\.ya?ml$/) ||\n new URL(url).searchParams.get('path')?.match(/.ya?ml$/)\n ) {\n const location = await this.catalogApi.addLocation({\n type: 'url',\n target: url,\n dryRun: true,\n });\n\n return {\n type: 'locations',\n locations: [\n {\n exists: location.exists,\n target: location.location.target,\n entities: location.entities.map(e => ({\n kind: e.kind,\n namespace: e.metadata.namespace ?? 'default',\n name: e.metadata.name,\n })),\n },\n ],\n };\n }\n const supportedIntegrations = ['github', 'azure'];\n const foundIntegration = this.scmIntegrationsApi.byUrl(url);\n const iSupported =\n !!foundIntegration &&\n supportedIntegrations.find(it => it === foundIntegration.type);\n if (!iSupported) {\n const catalogFilename = getCatalogFilename(this.configApi);\n\n if (foundIntegration) {\n throw new Error(\n `The ${foundIntegration.title} integration only supports full URLs to ${catalogFilename} files. Did you try to pass in the URL of a directory instead?`,\n );\n }\n throw new Error(\n `This URL was not recognized as a valid git URL because there was no configured integration that matched the given host name. Currently GitHub and Azure DevOps are supported. You could try to paste the full URL to a ${catalogFilename} file instead.`,\n );\n }\n\n const analyzation = await this.analyzeLocation({\n repo: url,\n });\n\n if (analyzation.existingEntityFiles.length > 0) {\n const locations = analyzation.existingEntityFiles.reduce<\n Record<\n string,\n {\n target: string;\n exists?: boolean;\n entities: CompoundEntityRef[];\n }\n >\n >((state, curr) => {\n state[curr.location.target] = {\n target: curr.location.target,\n exists: curr.isRegistered,\n entities: [\n ...(curr.location.target in state\n ? state[curr.location.target].entities\n : []),\n {\n name: curr.entity.metadata.name,\n namespace: curr.entity.metadata.namespace ?? 'default',\n kind: curr.entity.kind,\n },\n ],\n };\n return state;\n }, {});\n return {\n type: 'locations',\n locations: Object.values(locations),\n };\n }\n\n return {\n type: 'repository',\n integrationType: foundIntegration.type,\n url: url,\n generatedEntities: analyzation.generateEntities.map(x => x.entity),\n };\n }\n\n async preparePullRequest(): Promise<{\n title: string;\n body: string;\n }> {\n const appTitle =\n this.configApi.getOptionalString('app.title') ?? 'Backstage';\n const appBaseUrl = this.configApi.getString('app.baseUrl');\n const catalogFilename = getCatalogFilename(this.configApi);\n\n return {\n title: `Add ${catalogFilename} config file`,\n body: `This pull request adds a **Backstage entity metadata file** \\\nto this repository so that the component can be added to the \\\n[${appTitle} software catalog](${appBaseUrl}).\\n\\nAfter this pull request is merged, \\\nthe component will become available.\\n\\nFor more information, read an \\\n[overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/).`,\n };\n }\n\n async submitPullRequest(options: {\n repositoryUrl: string;\n fileContent: string;\n title: string;\n body: string;\n }): Promise<{ link: string; location: string }> {\n const { repositoryUrl, fileContent, title, body } = options;\n const parseData = YAML.parse(fileContent);\n\n const validationResponse = await this.catalogApi.validateEntity(\n parseData,\n `url:${repositoryUrl}`,\n );\n if (!validationResponse.valid) {\n throw new Error(validationResponse.errors[0].message);\n }\n\n const provider = this.scmIntegrationsApi.byUrl(repositoryUrl);\n\n switch (provider?.type) {\n case 'github': {\n const { config } = provider as GithubIntegration;\n const { name, owner } = parseGitUrl(repositoryUrl);\n const options2: GitHubOptions = {\n githubIntegrationConfig: config,\n repo: name,\n owner: owner,\n repositoryUrl,\n fileContent,\n title,\n body,\n };\n return submitGitHubPrToRepo(options2, this.scmAuthApi, this.configApi);\n }\n case 'azure': {\n return submitAzurePrToRepo(\n {\n repositoryUrl,\n fileContent,\n title,\n body,\n },\n this.scmAuthApi,\n this.configApi,\n );\n }\n default: {\n throw new Error('unimplemented!');\n }\n }\n }\n\n // TODO: this could be part of the catalog api\n private async analyzeLocation(options: {\n repo: string;\n }): Promise<AnalyzeLocationResponse> {\n const response = await this.fetchApi\n .fetch(\n `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`,\n {\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n body: JSON.stringify({\n location: { type: 'url', target: options.repo },\n ...(this.configApi.getOptionalString(\n 'catalog.import.entityFilename',\n ) && {\n catalogFilename: this.configApi.getOptionalString(\n 'catalog.import.entityFilename',\n ),\n }),\n }),\n },\n )\n .catch(e => {\n throw new Error(`Failed to generate entity definitions, ${e.message}`);\n });\n if (!response.ok) {\n throw new Error(\n `Failed to generate entity definitions. Received http response ${response.status}: ${response.statusText}`,\n );\n }\n\n const payload = await response.json();\n return payload;\n }\n}\n"],"names":[],"mappings":";;;;;;AAqCO,MAAM,mBAAgD,CAAA;AAAA,EAC1C,YAAA,CAAA;AAAA,EACA,QAAA,CAAA;AAAA,EACA,UAAA,CAAA;AAAA,EACA,kBAAA,CAAA;AAAA,EACA,UAAA,CAAA;AAAA,EACA,SAAA,CAAA;AAAA,EAEjB,YAAY,OAOT,EAAA;AACD,IAAA,IAAA,CAAK,eAAe,OAAQ,CAAA,YAAA,CAAA;AAC5B,IAAA,IAAA,CAAK,aAAa,OAAQ,CAAA,UAAA,CAAA;AAC1B,IAAA,IAAA,CAAK,WAAW,OAAQ,CAAA,QAAA,CAAA;AACxB,IAAA,IAAA,CAAK,qBAAqB,OAAQ,CAAA,kBAAA,CAAA;AAClC,IAAA,IAAA,CAAK,aAAa,OAAQ,CAAA,UAAA,CAAA;AAC1B,IAAA,IAAA,CAAK,YAAY,OAAQ,CAAA,SAAA,CAAA;AAAA,GAC3B;AAAA,EAEA,MAAM,WAAW,GAAqC,EAAA;AACpD,IAAA,IACE,IAAI,GAAI,CAAA,GAAG,EAAE,QAAS,CAAA,KAAA,CAAM,UAAU,CACtC,IAAA,IAAI,GAAI,CAAA,GAAG,EAAE,YAAa,CAAA,GAAA,CAAI,MAAM,CAAG,EAAA,KAAA,CAAM,SAAS,CACtD,EAAA;AACA,MAAA,MAAM,QAAW,GAAA,MAAM,IAAK,CAAA,UAAA,CAAW,WAAY,CAAA;AAAA,QACjD,IAAM,EAAA,KAAA;AAAA,QACN,MAAQ,EAAA,GAAA;AAAA,QACR,MAAQ,EAAA,IAAA;AAAA,OACT,CAAA,CAAA;AAED,MAAO,OAAA;AAAA,QACL,IAAM,EAAA,WAAA;AAAA,QACN,SAAW,EAAA;AAAA,UACT;AAAA,YACE,QAAQ,QAAS,CAAA,MAAA;AAAA,YACjB,MAAA,EAAQ,SAAS,QAAS,CAAA,MAAA;AAAA,YAC1B,QAAU,EAAA,QAAA,CAAS,QAAS,CAAA,GAAA,CAAI,CAAM,CAAA,MAAA;AAAA,cACpC,MAAM,CAAE,CAAA,IAAA;AAAA,cACR,SAAA,EAAW,CAAE,CAAA,QAAA,CAAS,SAAa,IAAA,SAAA;AAAA,cACnC,IAAA,EAAM,EAAE,QAAS,CAAA,IAAA;AAAA,aACjB,CAAA,CAAA;AAAA,WACJ;AAAA,SACF;AAAA,OACF,CAAA;AAAA,KACF;AACA,IAAM,MAAA,qBAAA,GAAwB,CAAC,QAAA,EAAU,OAAO,CAAA,CAAA;AAChD,IAAA,MAAM,gBAAmB,GAAA,IAAA,CAAK,kBAAmB,CAAA,KAAA,CAAM,GAAG,CAAA,CAAA;AAC1D,IAAM,MAAA,UAAA,GACJ,CAAC,CAAC,gBAAA,IACF,sBAAsB,IAAK,CAAA,CAAA,EAAA,KAAM,EAAO,KAAA,gBAAA,CAAiB,IAAI,CAAA,CAAA;AAC/D,IAAA,IAAI,CAAC,UAAY,EAAA;AACf,MAAM,MAAA,eAAA,GAAkB,kBAAmB,CAAA,IAAA,CAAK,SAAS,CAAA,CAAA;AAEzD,MAAA,IAAI,gBAAkB,EAAA;AACpB,QAAA,MAAM,IAAI,KAAA;AAAA,UACR,CAAO,IAAA,EAAA,gBAAA,CAAiB,KAAK,CAAA,wCAAA,EAA2C,eAAe,CAAA,8DAAA,CAAA;AAAA,SACzF,CAAA;AAAA,OACF;AACA,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,0NAA0N,eAAe,CAAA,cAAA,CAAA;AAAA,OAC3O,CAAA;AAAA,KACF;AAEA,IAAM,MAAA,WAAA,GAAc,MAAM,IAAA,CAAK,eAAgB,CAAA;AAAA,MAC7C,IAAM,EAAA,GAAA;AAAA,KACP,CAAA,CAAA;AAED,IAAI,IAAA,WAAA,CAAY,mBAAoB,CAAA,MAAA,GAAS,CAAG,EAAA;AAC9C,MAAA,MAAM,YAAY,WAAY,CAAA,mBAAA,CAAoB,MAShD,CAAA,CAAC,OAAO,IAAS,KAAA;AACjB,QAAM,KAAA,CAAA,IAAA,CAAK,QAAS,CAAA,MAAM,CAAI,GAAA;AAAA,UAC5B,MAAA,EAAQ,KAAK,QAAS,CAAA,MAAA;AAAA,UACtB,QAAQ,IAAK,CAAA,YAAA;AAAA,UACb,QAAU,EAAA;AAAA,YACR,GAAI,IAAK,CAAA,QAAA,CAAS,MAAU,IAAA,KAAA,GACxB,KAAM,CAAA,IAAA,CAAK,QAAS,CAAA,MAAM,CAAE,CAAA,QAAA,GAC5B,EAAC;AAAA,YACL;AAAA,cACE,IAAA,EAAM,IAAK,CAAA,MAAA,CAAO,QAAS,CAAA,IAAA;AAAA,cAC3B,SAAW,EAAA,IAAA,CAAK,MAAO,CAAA,QAAA,CAAS,SAAa,IAAA,SAAA;AAAA,cAC7C,IAAA,EAAM,KAAK,MAAO,CAAA,IAAA;AAAA,aACpB;AAAA,WACF;AAAA,SACF,CAAA;AACA,QAAO,OAAA,KAAA,CAAA;AAAA,OACT,EAAG,EAAE,CAAA,CAAA;AACL,MAAO,OAAA;AAAA,QACL,IAAM,EAAA,WAAA;AAAA,QACN,SAAA,EAAW,MAAO,CAAA,MAAA,CAAO,SAAS,CAAA;AAAA,OACpC,CAAA;AAAA,KACF;AAEA,IAAO,OAAA;AAAA,MACL,IAAM,EAAA,YAAA;AAAA,MACN,iBAAiB,gBAAiB,CAAA,IAAA;AAAA,MAClC,GAAA;AAAA,MACA,mBAAmB,WAAY,CAAA,gBAAA,CAAiB,GAAI,CAAA,CAAA,CAAA,KAAK,EAAE,MAAM,CAAA;AAAA,KACnE,CAAA;AAAA,GACF;AAAA,EAEA,MAAM,kBAGH,GAAA;AACD,IAAA,MAAM,QACJ,GAAA,IAAA,CAAK,SAAU,CAAA,iBAAA,CAAkB,WAAW,CAAK,IAAA,WAAA,CAAA;AACnD,IAAA,MAAM,UAAa,GAAA,IAAA,CAAK,SAAU,CAAA,SAAA,CAAU,aAAa,CAAA,CAAA;AACzD,IAAM,MAAA,eAAA,GAAkB,kBAAmB,CAAA,IAAA,CAAK,SAAS,CAAA,CAAA;AAEzD,IAAO,OAAA;AAAA,MACL,KAAA,EAAO,OAAO,eAAe,CAAA,YAAA,CAAA;AAAA,MAC7B,IAAM,EAAA,CAAA,0HAAA,EAET,QAAQ,CAAA,mBAAA,EAAsB,UAAU,CAAA;AAAA;AAAA;AAAA;AAAA,iIAAA,CAAA;AAAA,KAGvC,CAAA;AAAA,GACF;AAAA,EAEA,MAAM,kBAAkB,OAKwB,EAAA;AAC9C,IAAA,MAAM,EAAE,aAAA,EAAe,WAAa,EAAA,KAAA,EAAO,MAAS,GAAA,OAAA,CAAA;AACpD,IAAM,MAAA,SAAA,GAAY,IAAK,CAAA,KAAA,CAAM,WAAW,CAAA,CAAA;AAExC,IAAM,MAAA,kBAAA,GAAqB,MAAM,IAAA,CAAK,UAAW,CAAA,cAAA;AAAA,MAC/C,SAAA;AAAA,MACA,OAAO,aAAa,CAAA,CAAA;AAAA,KACtB,CAAA;AACA,IAAI,IAAA,CAAC,mBAAmB,KAAO,EAAA;AAC7B,MAAA,MAAM,IAAI,KAAM,CAAA,kBAAA,CAAmB,MAAO,CAAA,CAAC,EAAE,OAAO,CAAA,CAAA;AAAA,KACtD;AAEA,IAAA,MAAM,QAAW,GAAA,IAAA,CAAK,kBAAmB,CAAA,KAAA,CAAM,aAAa,CAAA,CAAA;AAE5D,IAAA,QAAQ,UAAU,IAAM;AAAA,MACtB,KAAK,QAAU,EAAA;AACb,QAAM,MAAA,EAAE,QAAW,GAAA,QAAA,CAAA;AACnB,QAAA,MAAM,EAAE,IAAA,EAAM,KAAM,EAAA,GAAI,YAAY,aAAa,CAAA,CAAA;AACjD,QAAA,MAAM,QAA0B,GAAA;AAAA,UAC9B,uBAAyB,EAAA,MAAA;AAAA,UACzB,IAAM,EAAA,IAAA;AAAA,UACN,KAAA;AAAA,UACA,aAAA;AAAA,UACA,WAAA;AAAA,UACA,KAAA;AAAA,UACA,IAAA;AAAA,SACF,CAAA;AACA,QAAA,OAAO,oBAAqB,CAAA,QAAA,EAAU,IAAK,CAAA,UAAA,EAAY,KAAK,SAAS,CAAA,CAAA;AAAA,OACvE;AAAA,MACA,KAAK,OAAS,EAAA;AACZ,QAAO,OAAA,mBAAA;AAAA,UACL;AAAA,YACE,aAAA;AAAA,YACA,WAAA;AAAA,YACA,KAAA;AAAA,YACA,IAAA;AAAA,WACF;AAAA,UACA,IAAK,CAAA,UAAA;AAAA,UACL,IAAK,CAAA,SAAA;AAAA,SACP,CAAA;AAAA,OACF;AAAA,MACA,SAAS;AACP,QAAM,MAAA,IAAI,MAAM,gBAAgB,CAAA,CAAA;AAAA,OAClC;AAAA,KACF;AAAA,GACF;AAAA;AAAA,EAGA,MAAc,gBAAgB,OAEO,EAAA;AACnC,IAAM,MAAA,QAAA,GAAW,MAAM,IAAA,CAAK,QACzB,CAAA,KAAA;AAAA,MACC,GAAG,MAAM,IAAA,CAAK,YAAa,CAAA,UAAA,CAAW,SAAS,CAAC,CAAA,iBAAA,CAAA;AAAA,MAChD;AAAA,QACE,OAAS,EAAA;AAAA,UACP,cAAgB,EAAA,kBAAA;AAAA,SAClB;AAAA,QACA,MAAQ,EAAA,MAAA;AAAA,QACR,IAAA,EAAM,KAAK,SAAU,CAAA;AAAA,UACnB,UAAU,EAAE,IAAA,EAAM,KAAO,EAAA,MAAA,EAAQ,QAAQ,IAAK,EAAA;AAAA,UAC9C,GAAI,KAAK,SAAU,CAAA,iBAAA;AAAA,YACjB,+BAAA;AAAA,WACG,IAAA;AAAA,YACH,eAAA,EAAiB,KAAK,SAAU,CAAA,iBAAA;AAAA,cAC9B,+BAAA;AAAA,aACF;AAAA,WACF;AAAA,SACD,CAAA;AAAA,OACH;AAAA,KACF,CACC,MAAM,CAAK,CAAA,KAAA;AACV,MAAA,MAAM,IAAI,KAAA,CAAM,CAA0C,uCAAA,EAAA,CAAA,CAAE,OAAO,CAAE,CAAA,CAAA,CAAA;AAAA,KACtE,CAAA,CAAA;AACH,IAAI,IAAA,CAAC,SAAS,EAAI,EAAA;AAChB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAiE,8DAAA,EAAA,QAAA,CAAS,MAAM,CAAA,EAAA,EAAK,SAAS,UAAU,CAAA,CAAA;AAAA,OAC1G,CAAA;AAAA,KACF;AAEA,IAAM,MAAA,OAAA,GAAU,MAAM,QAAA,CAAS,IAAK,EAAA,CAAA;AACpC,IAAO,OAAA,OAAA,CAAA;AAAA,GACT;AACF;;;;"}
|
package/dist/api/GitHub.esm.js
CHANGED
|
@@ -1,17 +1,96 @@
|
|
|
1
|
-
import
|
|
1
|
+
import 'git-url-parse';
|
|
2
|
+
import { Octokit } from '@octokit/rest';
|
|
3
|
+
import { getBranchName, getCatalogFilename } from '../components/helpers.esm.js';
|
|
4
|
+
import { Base64 } from 'js-base64';
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
async function submitGitHubPrToRepo(options, scmAuthApi, configApi) {
|
|
7
|
+
const {
|
|
8
|
+
owner,
|
|
9
|
+
repo,
|
|
10
|
+
title,
|
|
11
|
+
body,
|
|
12
|
+
fileContent,
|
|
13
|
+
repositoryUrl,
|
|
14
|
+
githubIntegrationConfig
|
|
15
|
+
} = options;
|
|
16
|
+
const { token } = await scmAuthApi.getCredentials({
|
|
17
|
+
url: repositoryUrl,
|
|
18
|
+
additionalScope: {
|
|
19
|
+
repoWrite: true
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const octo = new Octokit({
|
|
23
|
+
auth: token,
|
|
24
|
+
baseUrl: githubIntegrationConfig.apiBaseUrl
|
|
25
|
+
});
|
|
26
|
+
const branchName = getBranchName(configApi);
|
|
27
|
+
const fileName = getCatalogFilename(configApi);
|
|
28
|
+
const repoData = await octo.repos.get({
|
|
29
|
+
owner,
|
|
30
|
+
repo
|
|
31
|
+
}).catch((e) => {
|
|
32
|
+
throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e));
|
|
33
|
+
});
|
|
34
|
+
const parentRef = await octo.git.getRef({
|
|
35
|
+
owner,
|
|
10
36
|
repo,
|
|
37
|
+
ref: `heads/${repoData.data.default_branch}`
|
|
38
|
+
}).catch((e) => {
|
|
39
|
+
throw new Error(
|
|
40
|
+
formatHttpErrorMessage("Couldn't fetch default branch data", e)
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
await octo.git.createRef({
|
|
11
44
|
owner,
|
|
12
|
-
|
|
45
|
+
repo,
|
|
46
|
+
ref: `refs/heads/${branchName}`,
|
|
47
|
+
sha: parentRef.data.object.sha
|
|
48
|
+
}).catch((e) => {
|
|
49
|
+
throw new Error(
|
|
50
|
+
formatHttpErrorMessage(
|
|
51
|
+
`Couldn't create a new branch with name '${branchName}'`,
|
|
52
|
+
e
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
await octo.repos.createOrUpdateFileContents({
|
|
57
|
+
owner,
|
|
58
|
+
repo,
|
|
59
|
+
path: fileName,
|
|
60
|
+
message: title,
|
|
61
|
+
content: Base64.encode(fileContent),
|
|
62
|
+
branch: branchName
|
|
63
|
+
}).catch((e) => {
|
|
64
|
+
throw new Error(
|
|
65
|
+
formatHttpErrorMessage(
|
|
66
|
+
`Couldn't create a commit with ${fileName} file added`,
|
|
67
|
+
e
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
const pullRequestResponse = await octo.pulls.create({
|
|
72
|
+
owner,
|
|
73
|
+
repo,
|
|
74
|
+
title,
|
|
75
|
+
head: branchName,
|
|
76
|
+
body,
|
|
77
|
+
base: repoData.data.default_branch
|
|
78
|
+
}).catch((e) => {
|
|
79
|
+
throw new Error(
|
|
80
|
+
formatHttpErrorMessage(
|
|
81
|
+
`Couldn't create a pull request for ${branchName} branch`,
|
|
82
|
+
e
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
link: pullRequestResponse.data.html_url,
|
|
88
|
+
location: `https://${githubIntegrationConfig.host}/${owner}/${repo}/blob/${repoData.data.default_branch}/${fileName}`
|
|
13
89
|
};
|
|
14
|
-
}
|
|
90
|
+
}
|
|
91
|
+
function formatHttpErrorMessage(message, error) {
|
|
92
|
+
return `${message}, received http response status code ${error.status}: ${error.message}`;
|
|
93
|
+
}
|
|
15
94
|
|
|
16
|
-
export {
|
|
95
|
+
export { submitGitHubPrToRepo };
|
|
17
96
|
//# sourceMappingURL=GitHub.esm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GitHub.esm.js","sources":["../../src/api/GitHub.ts"],"sourcesContent":["/*\n * Copyright 2020 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 {
|
|
1
|
+
{"version":3,"file":"GitHub.esm.js","sources":["../../src/api/GitHub.ts"],"sourcesContent":["/*\n * Copyright 2020 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 GithubIntegrationConfig,\n ScmIntegrationRegistry,\n} from '@backstage/integration';\nimport parseGitUrl from 'git-url-parse';\nimport { ScmAuthApi } from '@backstage/integration-react';\nimport { Octokit } from '@octokit/rest';\nimport { getBranchName, getCatalogFilename } from '../components/helpers';\nimport { ConfigApi } from '@backstage/core-plugin-api';\nimport { Base64 } from 'js-base64';\n\nexport interface GitHubOptions {\n owner: string;\n repo: string;\n title: string;\n body: string;\n fileContent: string;\n repositoryUrl: string;\n githubIntegrationConfig: GithubIntegrationConfig;\n}\nexport const getGithubIntegrationConfig = (\n scmIntegrationsApi: ScmIntegrationRegistry,\n location: string,\n) => {\n const integration = scmIntegrationsApi.github.byUrl(location);\n if (!integration) {\n return undefined;\n }\n\n const { name: repo, owner } = parseGitUrl(location);\n return {\n repo,\n owner,\n githubIntegrationConfig: integration.config,\n };\n};\n\nexport async function submitGitHubPrToRepo(\n options: GitHubOptions,\n scmAuthApi: ScmAuthApi,\n configApi: ConfigApi,\n): Promise<{ link: string; location: string }> {\n const {\n owner,\n repo,\n title,\n body,\n fileContent,\n repositoryUrl,\n githubIntegrationConfig,\n } = options;\n\n const { token } = await scmAuthApi.getCredentials({\n url: repositoryUrl,\n additionalScope: {\n repoWrite: true,\n },\n });\n\n const octo = new Octokit({\n auth: token,\n baseUrl: githubIntegrationConfig.apiBaseUrl,\n });\n\n const branchName = getBranchName(configApi);\n const fileName = getCatalogFilename(configApi);\n\n const repoData = await octo.repos\n .get({\n owner,\n repo,\n })\n .catch(e => {\n throw new Error(formatHttpErrorMessage(\"Couldn't fetch repo data\", e));\n });\n\n const parentRef = await octo.git\n .getRef({\n owner,\n repo,\n ref: `heads/${repoData.data.default_branch}`,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\"Couldn't fetch default branch data\", e),\n );\n });\n\n await octo.git\n .createRef({\n owner,\n repo,\n ref: `refs/heads/${branchName}`,\n sha: parentRef.data.object.sha,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\n `Couldn't create a new branch with name '${branchName}'`,\n e,\n ),\n );\n });\n\n await octo.repos\n .createOrUpdateFileContents({\n owner,\n repo,\n path: fileName,\n message: title,\n content: Base64.encode(fileContent),\n branch: branchName,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\n `Couldn't create a commit with ${fileName} file added`,\n e,\n ),\n );\n });\n\n const pullRequestResponse = await octo.pulls\n .create({\n owner,\n repo,\n title,\n head: branchName,\n body,\n base: repoData.data.default_branch,\n })\n .catch(e => {\n throw new Error(\n formatHttpErrorMessage(\n `Couldn't create a pull request for ${branchName} branch`,\n e,\n ),\n );\n });\n\n return {\n link: pullRequestResponse.data.html_url,\n location: `https://${githubIntegrationConfig.host}/${owner}/${repo}/blob/${repoData.data.default_branch}/${fileName}`,\n };\n}\n\nfunction formatHttpErrorMessage(\n message: string,\n error: { status: number; message: string },\n) {\n return `${message}, received http response status code ${error.status}: ${error.message}`;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -21,10 +21,10 @@ import '@backstage/catalog-model';
|
|
|
21
21
|
import '@backstage/plugin-catalog-react';
|
|
22
22
|
import 'react-hook-form';
|
|
23
23
|
import '../../api/CatalogImportApi.esm.js';
|
|
24
|
-
import '@octokit/rest';
|
|
25
|
-
import 'js-base64';
|
|
26
24
|
import 'yaml';
|
|
27
25
|
import 'git-url-parse';
|
|
26
|
+
import '@octokit/rest';
|
|
27
|
+
import 'js-base64';
|
|
28
28
|
import '@material-ui/core/CircularProgress';
|
|
29
29
|
import '@material-ui/lab/Autocomplete';
|
|
30
30
|
import '@material-ui/core/Card';
|
|
@@ -4,10 +4,10 @@ import Chip from '@material-ui/core/Chip';
|
|
|
4
4
|
import Typography from '@material-ui/core/Typography';
|
|
5
5
|
import React from 'react';
|
|
6
6
|
import { catalogImportApiRef } from '../../api/CatalogImportApi.esm.js';
|
|
7
|
-
import '@octokit/rest';
|
|
8
|
-
import 'js-base64';
|
|
9
7
|
import 'yaml';
|
|
10
8
|
import 'git-url-parse';
|
|
9
|
+
import '@octokit/rest';
|
|
10
|
+
import 'js-base64';
|
|
11
11
|
import { useCatalogFilename } from '../../hooks/useCatalogFilename.esm.js';
|
|
12
12
|
|
|
13
13
|
const ImportInfoCard = (props) => {
|
|
@@ -6,10 +6,10 @@ import Stepper from '@material-ui/core/Stepper';
|
|
|
6
6
|
import { makeStyles } from '@material-ui/core/styles';
|
|
7
7
|
import React, { useMemo } from 'react';
|
|
8
8
|
import { catalogImportApiRef } from '../../api/CatalogImportApi.esm.js';
|
|
9
|
-
import '@octokit/rest';
|
|
10
|
-
import 'js-base64';
|
|
11
9
|
import 'yaml';
|
|
12
10
|
import 'git-url-parse';
|
|
11
|
+
import '@octokit/rest';
|
|
12
|
+
import 'js-base64';
|
|
13
13
|
import { useImportState } from '../useImportState.esm.js';
|
|
14
14
|
import { defaultStepper, defaultGenerateStepper } from './defaults.esm.js';
|
|
15
15
|
|
|
@@ -5,11 +5,11 @@ import TextField from '@material-ui/core/TextField';
|
|
|
5
5
|
import React, { useState, useCallback } from 'react';
|
|
6
6
|
import { useForm } from 'react-hook-form';
|
|
7
7
|
import { catalogImportApiRef } from '../../api/CatalogImportApi.esm.js';
|
|
8
|
-
import '@octokit/rest';
|
|
9
|
-
import 'js-base64';
|
|
10
8
|
import 'yaml';
|
|
11
9
|
import 'git-url-parse';
|
|
10
|
+
import '@octokit/rest';
|
|
12
11
|
import { asInputRef } from '../helpers.esm.js';
|
|
12
|
+
import 'js-base64';
|
|
13
13
|
import { NextButton } from '../Buttons/index.esm.js';
|
|
14
14
|
|
|
15
15
|
const StepInitAnalyzeUrl = (props) => {
|
|
@@ -10,9 +10,9 @@ import React, { useState, useEffect, useCallback } from 'react';
|
|
|
10
10
|
import useAsync from 'react-use/esm/useAsync';
|
|
11
11
|
import YAML from 'yaml';
|
|
12
12
|
import { catalogImportApiRef } from '../../api/CatalogImportApi.esm.js';
|
|
13
|
+
import 'git-url-parse';
|
|
13
14
|
import '@octokit/rest';
|
|
14
15
|
import 'js-base64';
|
|
15
|
-
import 'git-url-parse';
|
|
16
16
|
import { useCatalogFilename } from '../../hooks/useCatalogFilename.esm.js';
|
|
17
17
|
import { BackButton, NextButton } from '../Buttons/index.esm.js';
|
|
18
18
|
import { PreparePullRequestForm } from './PreparePullRequestForm.esm.js';
|
package/dist/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage/plugin-catalog-import",
|
|
3
|
-
"version": "0.12.0
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A Backstage plugin the helps you import entities into your catalog",
|
|
5
5
|
"backstage": {
|
|
6
|
-
"role": "frontend-plugin"
|
|
6
|
+
"role": "frontend-plugin",
|
|
7
|
+
"pluginId": "catalog-import",
|
|
8
|
+
"pluginPackages": [
|
|
9
|
+
"@backstage/plugin-catalog-import"
|
|
10
|
+
]
|
|
7
11
|
},
|
|
8
12
|
"publishConfig": {
|
|
9
13
|
"access": "public"
|
|
@@ -52,15 +56,15 @@
|
|
|
52
56
|
"@backstage/catalog-client": "^1.6.5",
|
|
53
57
|
"@backstage/catalog-model": "^1.5.0",
|
|
54
58
|
"@backstage/config": "^1.2.0",
|
|
55
|
-
"@backstage/core-compat-api": "^0.2.6
|
|
56
|
-
"@backstage/core-components": "^0.14.8
|
|
57
|
-
"@backstage/core-plugin-api": "^1.9.3
|
|
59
|
+
"@backstage/core-compat-api": "^0.2.6",
|
|
60
|
+
"@backstage/core-components": "^0.14.8",
|
|
61
|
+
"@backstage/core-plugin-api": "^1.9.3",
|
|
58
62
|
"@backstage/errors": "^1.2.4",
|
|
59
|
-
"@backstage/frontend-plugin-api": "^0.6.6
|
|
60
|
-
"@backstage/integration": "^1.12.0
|
|
61
|
-
"@backstage/integration-react": "^1.1.28
|
|
62
|
-
"@backstage/plugin-catalog-common": "^1.0.
|
|
63
|
-
"@backstage/plugin-catalog-react": "^1.12.1
|
|
63
|
+
"@backstage/frontend-plugin-api": "^0.6.6",
|
|
64
|
+
"@backstage/integration": "^1.12.0",
|
|
65
|
+
"@backstage/integration-react": "^1.1.28",
|
|
66
|
+
"@backstage/plugin-catalog-common": "^1.0.24",
|
|
67
|
+
"@backstage/plugin-catalog-react": "^1.12.1",
|
|
64
68
|
"@material-ui/core": "^4.12.2",
|
|
65
69
|
"@material-ui/icons": "^4.9.1",
|
|
66
70
|
"@material-ui/lab": "4.0.0-alpha.61",
|
|
@@ -74,11 +78,11 @@
|
|
|
74
78
|
"yaml": "^2.0.0"
|
|
75
79
|
},
|
|
76
80
|
"devDependencies": {
|
|
77
|
-
"@backstage/cli": "^0.26.7
|
|
78
|
-
"@backstage/core-app-api": "^1.12.6
|
|
79
|
-
"@backstage/dev-utils": "^1.0.33
|
|
80
|
-
"@backstage/plugin-catalog": "^1.21.0
|
|
81
|
-
"@backstage/test-utils": "^1.5.6
|
|
81
|
+
"@backstage/cli": "^0.26.7",
|
|
82
|
+
"@backstage/core-app-api": "^1.12.6",
|
|
83
|
+
"@backstage/dev-utils": "^1.0.33",
|
|
84
|
+
"@backstage/plugin-catalog": "^1.21.0",
|
|
85
|
+
"@backstage/test-utils": "^1.5.6",
|
|
82
86
|
"@testing-library/dom": "^10.0.0",
|
|
83
87
|
"@testing-library/jest-dom": "^6.0.0",
|
|
84
88
|
"@testing-library/react": "^15.0.0",
|