Merge branch 'master' into multi-cluster
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Modifying import functionality to register existing catalog-info.yaml if one exists in given GitHub repository
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Due to a package name change from `@kyma-project/asyncapi-react` to
|
||||
`@asyncapi/react-component` the jest configuration in the root `package.json`
|
||||
has to be updated:
|
||||
|
||||
```diff
|
||||
"jest": {
|
||||
"transformModules": [
|
||||
- "@kyma-project/asyncapi-react
|
||||
+ "@asyncapi/react-component"
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Change AWS Account type from Component to Resource
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
URL Reader: Use API response headers for archive filename in readTree. Fixes bug for users with hosted Bitbucket.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': patch
|
||||
---
|
||||
|
||||
Update `@asyncapi/react-component` to 0.18.2
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
* @backstage/maintainers
|
||||
/docs/features/techdocs @backstage/techdocs-core
|
||||
/docs/features/search @backstage/techdocs-core
|
||||
/plugins/cost-insights @backstage/silver-lining
|
||||
/plugins/cloudbuild @trivago/ebarrios
|
||||
/plugins/search @backstage/techdocs-core
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
name: FOSSA
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2.3.4
|
||||
|
||||
# We use this to modify the generated .fossa.yml
|
||||
- name: Install yq
|
||||
run: sudo snap install yq
|
||||
|
||||
- name: Install Fossa
|
||||
run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
|
||||
|
||||
- name: Configure Fossa
|
||||
# The --option flag for fossa init does not work yet, see https://github.com/fossas/fossa-cli/issues/614
|
||||
run: |
|
||||
fossa init
|
||||
yq eval -i '.analyze.modules[].options.strategy = "yarn-list"' .fossa.yml
|
||||
|
||||
# This deletes entries for template and example packages found within packages and plugins
|
||||
# Seems like yq has a bug that causes only a subset of all matches to be deleted each run
|
||||
yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
|
||||
yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
|
||||
yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
|
||||
yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
|
||||
yq eval -i 'del(.analyze.modules[] | select(.path == "*/*/**"))' .fossa.yml
|
||||
|
||||
- name: Show config
|
||||
run: cat .fossa.yml
|
||||
|
||||
- name: Fossa Analyze
|
||||
env:
|
||||
# FOSSA Push-Only API Token
|
||||
FOSSA_API_KEY: 9ee7e8893660832a7387dcc32377fb61
|
||||
run: fossa analyze --branch "$GITHUB_REF"
|
||||
+31
-18
@@ -185,33 +185,46 @@ catalog:
|
||||
# groupFilter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified')
|
||||
|
||||
locations:
|
||||
# Add a location here to ingest it, for example from an URL:
|
||||
#
|
||||
# - type: url
|
||||
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
|
||||
#
|
||||
# For local development you can use a file location instead:
|
||||
#
|
||||
# - type: file
|
||||
# target: ../catalog-model/examples/all-components.yaml
|
||||
#
|
||||
# File locations are relative to the current working directory of the
|
||||
# backend, for example packages/backend/.
|
||||
|
||||
# Backstage example components
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-components.yaml
|
||||
- type: file
|
||||
target: ../catalog-model/examples/all-components.yaml
|
||||
# Example component for github-actions
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/plugins/github-actions/examples/sample.yaml
|
||||
- type: file
|
||||
target: ../../plugins/github-actions/examples/sample.yaml
|
||||
# Example component for TechDocs
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
|
||||
- type: file
|
||||
target: ../../plugins/techdocs-backend/examples/documented-component/catalog-info.yaml
|
||||
# Backstage example APIs
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml
|
||||
- type: file
|
||||
target: ../catalog-model/examples/all-apis.yaml
|
||||
# Backstage example resources
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-resources.yaml
|
||||
- type: file
|
||||
target: ../catalog-model/examples/all-resources.yaml
|
||||
# Backstage example systems
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-systems.yaml
|
||||
- type: file
|
||||
target: ../catalog-model/examples/all-systems.yaml
|
||||
# Backstage example domains
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all-domains.yaml
|
||||
- type: file
|
||||
target: ../catalog-model/examples/all-domains.yaml
|
||||
# Backstage example templates
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml
|
||||
- type: file
|
||||
target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml
|
||||
# Backstage example groups and users
|
||||
- type: url
|
||||
target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
|
||||
- type: file
|
||||
target: ../catalog-model/examples/acme-corp.yaml
|
||||
|
||||
scaffolder:
|
||||
github:
|
||||
|
||||
@@ -43,6 +43,15 @@ const GoodComponent = ({ text, children }: GoodProps) => (
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
/* Or as a shorthand, if no specifc child type is required */
|
||||
type GoodProps = PropsWithChildren<{ text: string }>;
|
||||
const GoodComponent = ({ text, children }: GoodProps) => (
|
||||
<div>
|
||||
<div>{text}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"jest": {
|
||||
"transformModules": [
|
||||
"@kyma-project/asyncapi-react"
|
||||
"@asyncapi/react-component"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,8 @@ export class AzureUrlReader implements UrlReader {
|
||||
url: string,
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
// TODO: Support filepath based reading tree feature like other providers
|
||||
|
||||
// Get latest commit SHA
|
||||
|
||||
const commitsAzureResponse = await fetch(
|
||||
|
||||
@@ -95,6 +95,10 @@ describe('BitbucketUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-12ab34cd56ef.zip',
|
||||
),
|
||||
ctx.body(repoBuffer),
|
||||
),
|
||||
),
|
||||
@@ -114,6 +118,10 @@ describe('BitbucketUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock.zip',
|
||||
),
|
||||
ctx.body(privateBitbucketRepoBuffer),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -101,17 +101,13 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
url: string,
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
const { name: repoName, owner: project, resource, filepath } = parseGitUrl(
|
||||
url,
|
||||
);
|
||||
const { filepath } = parseGitUrl(url);
|
||||
|
||||
const lastCommitShortHash = await this.getLastCommitShortHash(url);
|
||||
if (options?.etag && options.etag === lastCommitShortHash) {
|
||||
throw new NotModifiedError();
|
||||
}
|
||||
|
||||
const isHosted = resource === 'bitbucket.org';
|
||||
|
||||
const downloadUrl = await getBitbucketDownloadUrl(url, this.config);
|
||||
const archiveBitbucketResponse = await fetch(
|
||||
downloadUrl,
|
||||
@@ -125,14 +121,31 @@ export class BitbucketUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
let folderPath = `${project}-${repoName}`;
|
||||
if (isHosted) {
|
||||
folderPath = `${project}-${repoName}-${lastCommitShortHash}`;
|
||||
// Get the filename of archive from the header of the response
|
||||
const contentDispositionHeader = archiveBitbucketResponse.headers.get(
|
||||
'content-disposition',
|
||||
) as string;
|
||||
if (!contentDispositionHeader) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. ` +
|
||||
'Bitbucket API response for downloading archive does not contain content-disposition header ',
|
||||
);
|
||||
}
|
||||
const fileNameRegEx = new RegExp(
|
||||
/^attachment; filename=(?<fileName>.*).zip$/,
|
||||
);
|
||||
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
|
||||
?.groups?.fileName;
|
||||
if (!archiveFileName) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. Bitbucket API response for downloading archive has an unexpected ` +
|
||||
`format of content-disposition header ${contentDispositionHeader} `,
|
||||
);
|
||||
}
|
||||
|
||||
return await this.treeResponseFactory.fromZipArchive({
|
||||
stream: (archiveBitbucketResponse.body as unknown) as Readable,
|
||||
path: `${folderPath}/${filepath}`,
|
||||
path: `${archiveFileName}/${filepath}`,
|
||||
etag: lastCommitShortHash,
|
||||
filter: options?.filter,
|
||||
});
|
||||
|
||||
@@ -165,6 +165,10 @@ describe('GithubUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/x-gzip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-etag123.tar.gz',
|
||||
),
|
||||
ctx.body(repoBuffer),
|
||||
),
|
||||
),
|
||||
@@ -178,6 +182,10 @@ describe('GithubUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/x-gzip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-etag123.tar.gz',
|
||||
),
|
||||
ctx.body(repoBuffer),
|
||||
),
|
||||
),
|
||||
@@ -244,6 +252,10 @@ describe('GithubUrlReader', () => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/x-gzip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename=backstage-mock-etag123.tar.gz',
|
||||
),
|
||||
ctx.body(repoBuffer),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -166,18 +166,31 @@ export class GithubUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
// Note that repoResponseJson.full_name must be used over full_name because the path
|
||||
// is case sensitive and full_name may not be inq the correct case.
|
||||
// TODO(OrkoHunter): The directory name inside the tarball should be retrieved from the tar
|
||||
// instead of being constructed here. Same goes for GitLab, Bitbucket and Azure.
|
||||
const extractedDirName = `${repoResponseJson.full_name.replace(
|
||||
'/',
|
||||
'-',
|
||||
)}-${commitSha.substr(0, 7)}`;
|
||||
// Get the filename of archive from the header of the response
|
||||
const contentDispositionHeader = archive.headers.get(
|
||||
'content-disposition',
|
||||
) as string;
|
||||
if (!contentDispositionHeader) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. ` +
|
||||
'GitHub API response for downloading archive does not contain content-disposition header ',
|
||||
);
|
||||
}
|
||||
const fileNameRegEx = new RegExp(
|
||||
/^attachment; filename=(?<fileName>.*).tar.gz$/,
|
||||
);
|
||||
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
|
||||
?.groups?.fileName;
|
||||
if (!archiveFileName) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. GitHub API response for downloading archive has an unexpected ` +
|
||||
`format of content-disposition header ${contentDispositionHeader} `,
|
||||
);
|
||||
}
|
||||
|
||||
// The path includes the name of the directory inside the tarball and a sub path
|
||||
// if requested in readTree.
|
||||
const path = `${extractedDirName}/${filepath}`;
|
||||
const path = `${archiveFileName}/${filepath}`;
|
||||
|
||||
return await this.deps.treeResponseFactory.fromTarArchive({
|
||||
// TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want
|
||||
|
||||
@@ -176,6 +176,10 @@ describe('GitlabUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename="mock-main-sha123abc.zip"',
|
||||
),
|
||||
ctx.body(archiveBuffer),
|
||||
),
|
||||
),
|
||||
@@ -225,6 +229,10 @@ describe('GitlabUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename="mock-main-sha123abc.zip"',
|
||||
),
|
||||
ctx.body(archiveBuffer),
|
||||
),
|
||||
),
|
||||
@@ -254,6 +262,10 @@ describe('GitlabUrlReader', () => {
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.set('Content-Type', 'application/zip'),
|
||||
ctx.set(
|
||||
'content-disposition',
|
||||
'attachment; filename="mock-main-sha123abc.zip"',
|
||||
),
|
||||
ctx.body(archiveBuffer),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -78,7 +78,7 @@ export class GitlabUrlReader implements UrlReader {
|
||||
url: string,
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
const { name: repoName, ref, full_name, filepath } = parseGitUrl(url);
|
||||
const { ref, full_name, filepath } = parseGitUrl(url);
|
||||
|
||||
// Use GitLab API to get the default branch
|
||||
// encodeURIComponent is required for GitLab API
|
||||
@@ -140,9 +140,29 @@ export class GitlabUrlReader implements UrlReader {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const path = filepath
|
||||
? `${repoName}-${branch}-${commitSha}/${filepath}/`
|
||||
: '';
|
||||
// Get the filename of archive from the header of the response
|
||||
const contentDispositionHeader = archiveGitLabResponse.headers.get(
|
||||
'content-disposition',
|
||||
) as string;
|
||||
if (!contentDispositionHeader) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. ` +
|
||||
'GitLab API response for downloading archive does not contain content-disposition header ',
|
||||
);
|
||||
}
|
||||
const fileNameRegEx = new RegExp(
|
||||
/^attachment; filename="(?<fileName>.*).zip"$/,
|
||||
);
|
||||
const archiveFileName = contentDispositionHeader.match(fileNameRegEx)
|
||||
?.groups?.fileName;
|
||||
if (!archiveFileName) {
|
||||
throw new Error(
|
||||
`Failed to read tree from ${url}. GitLab API response for downloading archive has an unexpected ` +
|
||||
`format of content-disposition header ${contentDispositionHeader} `,
|
||||
);
|
||||
}
|
||||
|
||||
const path = filepath ? `${archiveFileName}/${filepath}/` : '';
|
||||
|
||||
return await this.treeResponseFactory.fromZipArchive({
|
||||
stream: (archiveGitLabResponse.body as unknown) as Readable,
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"jest": {
|
||||
"transformModules": [
|
||||
"@kyma-project/asyncapi-react"
|
||||
"@asyncapi/react-component"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@asyncapi/react-component": "^0.18.2",
|
||||
"@backstage/catalog-model": "^0.6.0",
|
||||
"@backstage/core": "^0.4.4",
|
||||
"@backstage/plugin-catalog": "^0.2.9",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@kyma-project/asyncapi-react": "^0.14.2",
|
||||
"@material-icons/font": "^1.0.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
+3
-3
@@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import AsyncApi from '@kyma-project/asyncapi-react';
|
||||
import AsyncApi from '@asyncapi/react-component';
|
||||
import '@asyncapi/react-component/lib/styles/fiori.css';
|
||||
import { fade, makeStyles } from '@material-ui/core/styles';
|
||||
import React from 'react';
|
||||
import { makeStyles, fade } from '@material-ui/core/styles';
|
||||
import '@kyma-project/asyncapi-react/lib/styles/fiori.css';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
|
||||
+2
-4
@@ -45,7 +45,7 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
|
||||
location,
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
kind: 'Resource',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'amazonaws.com/arn':
|
||||
@@ -58,7 +58,6 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
|
||||
},
|
||||
spec: {
|
||||
type: 'cloud-account',
|
||||
lifecycle: 'unknown',
|
||||
owner: 'unknown',
|
||||
},
|
||||
},
|
||||
@@ -91,7 +90,7 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
|
||||
location,
|
||||
entity: {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
kind: 'Resource',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'amazonaws.com/arn':
|
||||
@@ -104,7 +103,6 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
|
||||
},
|
||||
spec: {
|
||||
type: 'cloud-account',
|
||||
lifecycle: 'unknown',
|
||||
owner: 'unknown',
|
||||
},
|
||||
},
|
||||
|
||||
+4
-8
@@ -13,10 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
ComponentEntityV1alpha1,
|
||||
LocationSpec,
|
||||
} from '@backstage/catalog-model';
|
||||
import { LocationSpec, ResourceEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import {
|
||||
Account,
|
||||
Organizations,
|
||||
@@ -82,13 +79,13 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
|
||||
return awsAccounts;
|
||||
}
|
||||
|
||||
mapAccountToComponent(account: Account): ComponentEntityV1alpha1 {
|
||||
mapAccountToComponent(account: Account): ResourceEntityV1alpha1 {
|
||||
const { accountId, organizationId } = this.extractInformationFromArn(
|
||||
account.Arn as string,
|
||||
);
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
kind: 'Resource',
|
||||
metadata: {
|
||||
annotations: {
|
||||
[ACCOUNTID_ANNOTATION]: accountId,
|
||||
@@ -100,7 +97,6 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
|
||||
},
|
||||
spec: {
|
||||
type: 'cloud-account',
|
||||
lifecycle: 'unknown',
|
||||
owner: 'unknown',
|
||||
},
|
||||
};
|
||||
@@ -129,7 +125,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.forEach((entity: ComponentEntityV1alpha1) => {
|
||||
.forEach((entity: ResourceEntityV1alpha1) => {
|
||||
emit(results.entity(location, entity));
|
||||
});
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"dependencies": {
|
||||
"@backstage/catalog-model": "^0.6.1",
|
||||
"@backstage/core": "^0.4.4",
|
||||
"@backstage/plugin-catalog": "^0.2.11",
|
||||
"@backstage/integration": "^0.2.0",
|
||||
"@backstage/plugin-catalog": "^0.2.11",
|
||||
"@backstage/theme": "^0.2.2",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
|
||||
@@ -30,6 +30,11 @@ export interface CatalogImportApi {
|
||||
fileContent: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ link: string; location: string }>;
|
||||
checkForExistingCatalogInfo(options: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ exists: boolean; url?: string }>;
|
||||
createRepositoryLocation(options: { location: string }): Promise<void>;
|
||||
generateEntityDefinitions(options: {
|
||||
repo: string;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { CatalogImportClient } from './CatalogImportClient';
|
||||
|
||||
jest.mock('@octokit/rest', () => ({
|
||||
Octokit: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
repos: {
|
||||
get: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
default_branch: 'main',
|
||||
},
|
||||
}),
|
||||
},
|
||||
search: {
|
||||
code: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
total_count: 2,
|
||||
items: [
|
||||
{ path: 'simple/path/catalog-info.yaml' },
|
||||
{ path: 'co/mple/x/path/catalog-info.yaml' },
|
||||
{ path: 'catalog-info.yaml' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('CatalogImportClient', () => {
|
||||
describe('checkForExistingCatalogInfo', () => {
|
||||
const cic = new CatalogImportClient({
|
||||
discoveryApi: { getBaseUrl: () => Promise.resolve('base') },
|
||||
githubAuthApi: { getAccessToken: (_, __) => Promise.resolve('token') },
|
||||
configApi: {} as any,
|
||||
});
|
||||
it('should return the closest-to-root catalog-info from multiple responses', async () => {
|
||||
const respo = await cic.checkForExistingCatalogInfo({
|
||||
owner: 'test-user',
|
||||
repo: 'rest-repo',
|
||||
githubIntegrationConfig: { host: 'https://github.com' },
|
||||
});
|
||||
expect(respo.exists).toBe(true);
|
||||
expect(respo.url).toBe('blob/main/catalog-info.yaml');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -90,6 +90,52 @@ export class CatalogImportClient implements CatalogImportApi {
|
||||
}
|
||||
}
|
||||
|
||||
async checkForExistingCatalogInfo({
|
||||
owner,
|
||||
repo,
|
||||
githubIntegrationConfig,
|
||||
}: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
}): Promise<{ exists: boolean; url?: string }> {
|
||||
const token = await this.githubAuthApi.getAccessToken(['repo']);
|
||||
const octo = new Octokit({
|
||||
auth: token,
|
||||
baseUrl: githubIntegrationConfig.apiBaseUrl,
|
||||
});
|
||||
const catalogFileName = 'catalog-info.yaml';
|
||||
const query = `repo:${owner}/${repo}+filename:${catalogFileName}`;
|
||||
|
||||
const searchResult = await octo.search.code({ q: query }).catch(e => {
|
||||
throw new Error(
|
||||
formatHttpErrorMessage(
|
||||
"Couldn't search repository for metadata file.",
|
||||
e,
|
||||
),
|
||||
);
|
||||
});
|
||||
const exists = searchResult.data.total_count > 0;
|
||||
if (exists) {
|
||||
const repoInformation = await octo.repos.get({ owner, repo }).catch(e => {
|
||||
throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e));
|
||||
});
|
||||
const defaultBranch = repoInformation.data.default_branch;
|
||||
|
||||
// Github search sorts returned values with 'best match' using 'multiple factors to boost the most relevant item',
|
||||
// aka magic.
|
||||
// Sorting to use the shortest item, closest to the repository root.
|
||||
const catalogInfoItem = searchResult.data.items
|
||||
.map(it => it.path)
|
||||
.sort((a, b) => a.length - b.length)[0];
|
||||
return {
|
||||
url: `blob/${defaultBranch}/${catalogInfoItem}`,
|
||||
exists,
|
||||
};
|
||||
}
|
||||
return { exists };
|
||||
}
|
||||
|
||||
async submitPrToRepo({
|
||||
owner,
|
||||
repo,
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { RegisterComponentForm } from './ImportComponentForm';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
DiscoveryApi,
|
||||
errorApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
import { catalogImportApiRef, CatalogImportClient } from '../api';
|
||||
import { fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
|
||||
describe('<RegisterComponentForm />', () => {
|
||||
let apis: ApiRegistry;
|
||||
|
||||
const mockErrorApi: jest.Mocked<typeof errorApiRef.T> = {
|
||||
post: jest.fn(),
|
||||
error$: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
apis = ApiRegistry.from([
|
||||
[catalogApiRef, new CatalogClient({ discoveryApi: {} as DiscoveryApi })],
|
||||
[
|
||||
catalogImportApiRef,
|
||||
new CatalogImportClient({
|
||||
discoveryApi: { getBaseUrl: () => Promise.resolve('base') },
|
||||
githubAuthApi: {
|
||||
getAccessToken: (_, __) => Promise.resolve('token'),
|
||||
},
|
||||
configApi: {} as any,
|
||||
}),
|
||||
],
|
||||
[errorApiRef, mockErrorApi],
|
||||
]);
|
||||
});
|
||||
|
||||
async function renderSUT(
|
||||
nextStep: () => void = () => {},
|
||||
saveConfig: () => void = () => {},
|
||||
) {
|
||||
return await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<RegisterComponentForm
|
||||
nextStep={nextStep}
|
||||
saveConfig={saveConfig}
|
||||
repository="GitHub"
|
||||
/>
|
||||
</ApiProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
it('Renders without exploding', async () => {
|
||||
await renderSUT();
|
||||
expect(
|
||||
screen.getByPlaceholderText('https://github.com/backstage/backstage'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should have basic URL validation for input', async () => {
|
||||
await renderSUT();
|
||||
await waitFor(() => {
|
||||
fireEvent.input(
|
||||
screen.getByPlaceholderText('https://github.com/backstage/backstage'),
|
||||
{ target: { value: 'not a url' } },
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
});
|
||||
expect(screen.getByText('Must start with https://.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -64,27 +64,45 @@ export const RegisterComponentForm = ({
|
||||
|
||||
const isMounted = useMountedState();
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const { generateEntityDefinitions } = useGithubRepos();
|
||||
const {
|
||||
generateEntityDefinitions,
|
||||
checkForExistingCatalogInfo,
|
||||
} = useGithubRepos();
|
||||
|
||||
const onSubmit = async (formData: Record<string, string>) => {
|
||||
const { componentLocation: target } = formData;
|
||||
try {
|
||||
if (!isMounted()) return;
|
||||
const type = urlType(target);
|
||||
async function saveCatalogFileConfig(target: string) {
|
||||
const data = await catalogApi.addLocation({ target });
|
||||
saveConfig({
|
||||
type: 'file',
|
||||
location: data.location.target,
|
||||
config: data.entities,
|
||||
});
|
||||
}
|
||||
|
||||
if (type === 'tree') {
|
||||
async function trySaveRepositoryConfig(target: string) {
|
||||
const existingCatalog = await checkForExistingCatalogInfo(target);
|
||||
if (existingCatalog.exists) {
|
||||
const targetUrl = target.endsWith('/')
|
||||
? `${target}${existingCatalog.url}`
|
||||
: `${target}/${existingCatalog.url}`;
|
||||
await saveCatalogFileConfig(targetUrl);
|
||||
} else {
|
||||
saveConfig({
|
||||
type,
|
||||
type: 'tree',
|
||||
location: target,
|
||||
config: await generateEntityDefinitions(target),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isMounted()) return;
|
||||
const type = urlType(target);
|
||||
if (type === 'tree') {
|
||||
await trySaveRepositoryConfig(target);
|
||||
} else {
|
||||
const data = await catalogApi.addLocation({ target });
|
||||
saveConfig({
|
||||
type,
|
||||
location: data.location.target,
|
||||
config: data.entities,
|
||||
});
|
||||
await saveCatalogFileConfig(target);
|
||||
}
|
||||
nextStep();
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { msw, renderInTestApp } from '@backstage/test-utils';
|
||||
import { ImportComponentPage } from './ImportComponentPage';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
configApiRef,
|
||||
errorApiRef,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog';
|
||||
import { catalogImportApiRef, CatalogImportClient } from '../api';
|
||||
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
|
||||
let codeSearchMockResponse: () => Promise<{
|
||||
data: {
|
||||
total_count: number;
|
||||
items: Array<{ path: string }>;
|
||||
};
|
||||
}>;
|
||||
|
||||
let findGithubConfigMockResponse = () => ({
|
||||
host: 'test.localhost',
|
||||
owner: 'someuser',
|
||||
});
|
||||
|
||||
jest.mock('@backstage/integration', () => ({
|
||||
readGitHubIntegrationConfigs: () => ({
|
||||
find: findGithubConfigMockResponse,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@octokit/rest', () => ({
|
||||
Octokit: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
repos: {
|
||||
get: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
default_branch: 'main',
|
||||
},
|
||||
}),
|
||||
},
|
||||
search: {
|
||||
code: codeSearchMockResponse,
|
||||
},
|
||||
};
|
||||
}),
|
||||
}));
|
||||
|
||||
const OUR_GITHUB_TEST_REPO = 'https://github.com/someuser/somerepo';
|
||||
describe('<ImportComponentPage />', () => {
|
||||
const server = setupServer();
|
||||
msw.setupDefaultHandlers(server);
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
rest.post('https://backend.localhost/locations', (_, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(201),
|
||||
ctx.json(require('../mocks/locations-POST-response.json')),
|
||||
);
|
||||
}),
|
||||
rest.post('https://backend.localhost/analyze-location', (_, res, ctx) => {
|
||||
return res(
|
||||
ctx.json(require('../mocks/analyze-location-POST-response.json')),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
let apis: ApiRegistry;
|
||||
|
||||
const mockErrorApi: jest.Mocked<typeof errorApiRef.T> = {
|
||||
post: jest.fn(),
|
||||
error$: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const getBaseUrl = () => Promise.resolve('https://backend.localhost');
|
||||
apis = ApiRegistry.from([
|
||||
[
|
||||
catalogApiRef,
|
||||
new CatalogClient({
|
||||
discoveryApi: { getBaseUrl },
|
||||
}),
|
||||
],
|
||||
[
|
||||
catalogImportApiRef,
|
||||
new CatalogImportClient({
|
||||
discoveryApi: { getBaseUrl },
|
||||
githubAuthApi: {
|
||||
getAccessToken: (_, __) => Promise.resolve('token'),
|
||||
},
|
||||
configApi: {} as any,
|
||||
}),
|
||||
],
|
||||
[
|
||||
configApiRef,
|
||||
{
|
||||
getOptional: () => 'Title',
|
||||
getOptionalConfigArray: () => [],
|
||||
has: () => true,
|
||||
getConfig: () => ({
|
||||
has: () => true,
|
||||
}),
|
||||
},
|
||||
],
|
||||
[errorApiRef, mockErrorApi],
|
||||
]);
|
||||
});
|
||||
|
||||
async function renderSUT() {
|
||||
return await renderInTestApp(
|
||||
<ApiProvider apis={apis}>
|
||||
<ImportComponentPage catalogRouteRef={{ path: 'path', title: 'ttl' }} />
|
||||
</ApiProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
it('Should not explode on non-Github URLs', async () => {
|
||||
findGithubConfigMockResponse = () => undefined!!;
|
||||
await renderSUT();
|
||||
await waitFor(() => {
|
||||
fireEvent.input(
|
||||
screen.getByPlaceholderText('https://github.com/backstage/backstage'),
|
||||
{
|
||||
target: {
|
||||
value: 'https://test-git-provider.localhost/someuser/somerepo',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
await waitFor(() => {
|
||||
const firstStepInput = screen.queryByPlaceholderText(
|
||||
'https://github.com/backstage/backstage',
|
||||
);
|
||||
expect(firstStepInput).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should offer direct file import from non-Github URLs', async () => {
|
||||
findGithubConfigMockResponse = () => undefined!!;
|
||||
await renderSUT();
|
||||
await waitFor(() => {
|
||||
fireEvent.input(
|
||||
screen.getByPlaceholderText('https://github.com/backstage/backstage'),
|
||||
{
|
||||
target: {
|
||||
value:
|
||||
'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
await waitFor(() => {
|
||||
const firstStepInput = screen.queryByPlaceholderText(
|
||||
'https://github.com/backstage/backstage',
|
||||
);
|
||||
expect(firstStepInput).not.toBeInTheDocument();
|
||||
});
|
||||
expect(
|
||||
screen.getByText(
|
||||
'https://test-git-provider.localhost/someuser/somerepo/catalog-info.yaml',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should use found yaml file directly and not create a pull request if GitHub api returns one', async () => {
|
||||
findGithubConfigMockResponse = () => ({
|
||||
host: 'test.localhost',
|
||||
owner: 'someuser',
|
||||
});
|
||||
codeSearchMockResponse = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
total_count: 3,
|
||||
items: [
|
||||
{ path: 'simple/path/catalog-info.yaml' },
|
||||
{ path: 'co/mple/x/path/catalog-info.yaml' },
|
||||
{ path: 'catalog-info.yaml' },
|
||||
],
|
||||
},
|
||||
});
|
||||
await renderSUT();
|
||||
await waitFor(() => {
|
||||
fireEvent.input(
|
||||
screen.getByPlaceholderText('https://github.com/backstage/backstage'),
|
||||
{ target: { value: OUR_GITHUB_TEST_REPO } },
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const pullReqText = screen.queryByText('pull request');
|
||||
expect(pullReqText).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Should indicate a pull request creation when no yaml file found in the repo', async () => {
|
||||
findGithubConfigMockResponse = () => ({
|
||||
host: 'test.localhost',
|
||||
owner: 'someuser',
|
||||
});
|
||||
codeSearchMockResponse = () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
total_count: 0,
|
||||
items: [],
|
||||
},
|
||||
});
|
||||
const { container } = await renderSUT();
|
||||
await waitFor(() => {
|
||||
fireEvent.input(
|
||||
screen.getByPlaceholderText('https://github.com/backstage/backstage'),
|
||||
{ target: { value: OUR_GITHUB_TEST_REPO } },
|
||||
);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText('Next'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(OUR_GITHUB_TEST_REPO)).toBeInTheDocument();
|
||||
});
|
||||
const textNode = container
|
||||
.querySelector('a[href="https://github.com/someuser/somerepo"]')
|
||||
?.closest('p');
|
||||
expect(textNode?.innerHTML).toContain(
|
||||
'Following config object will be submitted in a pull request to the repository',
|
||||
);
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -109,10 +109,11 @@ export const ImportComponentPage = ({
|
||||
<React.Fragment>
|
||||
<Typography variant="h6">GitHub Repo</Typography>
|
||||
<Typography variant="body2" paragraph>
|
||||
If you already have code in a GitHub repository, enter the
|
||||
full URL to your repo and a new pull request with a sample
|
||||
Backstage metadata Entity File (
|
||||
<code>catalog-info.yaml</code>) will be opened for you.
|
||||
If you already have code in a GitHub repository without
|
||||
Backstage metadata file set up for it, enter the full URL to
|
||||
your repo and a new pull request with a sample Backstage
|
||||
metadata Entity File (<code>catalog-info.yaml</code>) will
|
||||
be opened for you.
|
||||
</Typography>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"existingEntityFiles": [],
|
||||
"generateEntities": [
|
||||
{
|
||||
"entity": {
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Component",
|
||||
"metadata": {
|
||||
"name": "somerepo",
|
||||
"annotations": {
|
||||
"github.com/project-slug": "someuser/somerepo"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"type": "other",
|
||||
"lifecycle": "unknown"
|
||||
}
|
||||
},
|
||||
"fields": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"location": {
|
||||
"id": "d4a64359-a709-4c91-a9de-0905a033bf22",
|
||||
"type": "url",
|
||||
"target": "https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml"
|
||||
},
|
||||
"entities": [
|
||||
{
|
||||
"metadata": {
|
||||
"namespace": "default",
|
||||
"annotations": {
|
||||
"backstage.io/managed-by-location": "url:https://github.com/someusername/somerepo/blob/master/src/catalog-info.yaml",
|
||||
"github.com/project-slug": "someusername/somerepo"
|
||||
},
|
||||
"name": "somerepo",
|
||||
"uid": "e992d5ee-7c70-4316-90cf-325f1a0a5146",
|
||||
"etag": "YWE2M2Q5MzgtNjdkNi00N2QwLWJkZjYtNDM0MTMzMDI4Y2I0",
|
||||
"generation": 1
|
||||
},
|
||||
"apiVersion": "backstage.io/v1alpha1",
|
||||
"kind": "Component",
|
||||
"spec": {
|
||||
"type": "other",
|
||||
"lifecycle": "unknown",
|
||||
"owner": "unknown"
|
||||
},
|
||||
"relations": [
|
||||
{
|
||||
"target": {
|
||||
"kind": "group",
|
||||
"namespace": "default",
|
||||
"name": "unknown"
|
||||
},
|
||||
"type": "ownedBy"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import 'cross-fetch/polyfill';
|
||||
|
||||
@@ -22,18 +22,21 @@ import parseGitUrl from 'git-url-parse';
|
||||
|
||||
// TODO: (O5ten) Refactor into a core API instead of direct usage like this
|
||||
// https://github.com/backstage/backstage/pull/3613#issuecomment-7408929430
|
||||
import { readGitHubIntegrationConfigs } from '@backstage/integration';
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
readGitHubIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
|
||||
export function useGithubRepos() {
|
||||
const api = useApi(catalogImportApiRef);
|
||||
const config = useApi(configApiRef);
|
||||
|
||||
const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
|
||||
const getGithubIntegrationConfig = (location: string) => {
|
||||
const {
|
||||
name: repoName,
|
||||
owner: ownerName,
|
||||
resource: hostname,
|
||||
} = parseGitUrl(selectedRepo.location);
|
||||
} = parseGitUrl(location);
|
||||
|
||||
const configs = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
@@ -41,9 +44,22 @@ export function useGithubRepos() {
|
||||
const githubIntegrationConfig = configs.find(v => v.host === hostname);
|
||||
if (!githubIntegrationConfig) {
|
||||
throw new Error(
|
||||
`Unable to locate github-integration for repo-location: ${selectedRepo.location}`,
|
||||
`Unable to locate github-integration for repo-location, ${location}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
repoName,
|
||||
ownerName,
|
||||
githubIntegrationConfig,
|
||||
};
|
||||
};
|
||||
|
||||
const submitPrToRepo = async (selectedRepo: ConfigSpec) => {
|
||||
const {
|
||||
repoName,
|
||||
ownerName,
|
||||
githubIntegrationConfig,
|
||||
} = getGithubIntegrationConfig(selectedRepo.location);
|
||||
const submitPRResponse = await api
|
||||
.submitPrToRepo({
|
||||
owner: ownerName,
|
||||
@@ -54,7 +70,7 @@ export function useGithubRepos() {
|
||||
githubIntegrationConfig,
|
||||
})
|
||||
.catch(e => {
|
||||
throw new Error(`Failed to submit PR to repo:\n${e.message}`);
|
||||
throw new Error(`Failed to submit PR to repo, ${e.message}`);
|
||||
});
|
||||
|
||||
await api
|
||||
@@ -62,14 +78,41 @@ export function useGithubRepos() {
|
||||
location: submitPRResponse.location,
|
||||
})
|
||||
.catch(e => {
|
||||
throw new Error(`Failed to create repository location:\n${e.message}`);
|
||||
throw new Error(`Failed to create repository location, ${e.message}`);
|
||||
});
|
||||
|
||||
return submitPRResponse;
|
||||
};
|
||||
|
||||
const checkForExistingCatalogInfo = async (
|
||||
location: string,
|
||||
): Promise<{ exists: boolean; url?: string }> => {
|
||||
let githubConfig: {
|
||||
repoName: string;
|
||||
ownerName: string;
|
||||
githubIntegrationConfig: GitHubIntegrationConfig;
|
||||
};
|
||||
try {
|
||||
githubConfig = getGithubIntegrationConfig(location);
|
||||
} catch (e) {
|
||||
return { exists: false };
|
||||
}
|
||||
return await api
|
||||
.checkForExistingCatalogInfo({
|
||||
owner: githubConfig.ownerName,
|
||||
repo: githubConfig.repoName,
|
||||
githubIntegrationConfig: githubConfig.githubIntegrationConfig,
|
||||
})
|
||||
.catch(e => {
|
||||
throw new Error(
|
||||
`Failed to inspect repository for existing catalog-info.yaml, ${e.message}`,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
submitPrToRepo,
|
||||
checkForExistingCatalogInfo,
|
||||
generateEntityDefinitions: (repo: string) =>
|
||||
api.generateEntityDefinitions({ repo }),
|
||||
addLocation: (location: string) =>
|
||||
|
||||
@@ -84,6 +84,5 @@ YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user a
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only works with projects that use the Git SCM
|
||||
- It requires jobs to be organised into folders
|
||||
- No pagination support currently - don't run this on a Jenkins with lots of builds
|
||||
- Only works with organization folder jobs backed by GitHub
|
||||
- No pagination support currently, limited to 50 jobs - don't run this on a Jenkins with lots of builds
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 455 KiB After Width: | Height: | Size: 256 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="282" height="173" fill="none" viewBox="0 0 282 173"><path fill="#000" fill-opacity=".05" fill-rule="evenodd" d="M16.4571 45.1637C11.0514 46.1711 7.48574 51.3699 8.49306 56.7756C9.50039 62.1814 14.6992 65.747 20.105 64.7397L27.5528 63.3518C25.4791 65.5835 24.4525 68.7347 25.0535 71.9596C26.0608 77.3653 31.2596 80.931 36.6654 79.9236L89.691 70.0427C89.7016 70.1067 89.7129 70.1708 89.7249 70.2349C90.3258 73.4598 92.4185 76.0298 95.1569 77.3647L91.9031 77.971C86.4974 78.9784 82.9318 84.1772 83.9391 89.583C84.9464 94.9887 90.1452 98.5543 95.551 97.547L250.098 68.7482C255.504 67.7409 259.069 62.5421 258.062 57.1363C257.461 53.9114 255.368 51.3414 252.63 50.0065L257.835 49.0366C263.241 48.0292 266.807 42.8304 265.799 37.4247C264.792 32.0189 259.593 28.4533 254.187 29.4606L161.492 46.7338C161.481 46.6697 161.47 46.6056 161.458 46.5415C160.857 43.3166 158.764 40.7466 156.026 39.4117L165.025 37.7347C170.431 36.7274 173.997 31.5286 172.989 26.1228C171.982 20.7171 166.783 17.1514 161.378 18.1588L16.4571 45.1637ZM24.3031 122.54C23.2958 117.134 26.8614 111.936 32.2672 110.928L190.856 81.3762C196.262 80.3688 201.461 83.9345 202.468 89.3402C203.476 94.746 199.91 99.9448 194.504 100.952L189.963 101.798C190.493 102.057 190.999 102.362 191.474 102.708L246.43 92.4677C251.835 91.4604 257.034 95.026 258.041 100.432C258.642 103.657 257.616 106.808 255.542 109.04L256.649 108.833C262.055 107.826 267.253 111.392 268.261 116.797C269.268 122.203 265.702 127.402 260.297 128.409L95.5591 159.107C90.1534 160.114 84.9545 156.549 83.9472 151.143C82.9399 145.737 86.5055 140.538 91.9113 139.531L103.94 137.29C103.41 137.031 102.904 136.726 102.429 136.38L29.1002 150.044C23.6944 151.051 18.4956 147.486 17.4882 142.08C16.4809 136.674 20.0465 131.475 25.4523 130.468L29.7352 129.67C26.9967 128.335 24.904 125.765 24.3031 122.54Z" clip-rule="evenodd"/><g filter="url(#filter0_d)"><path fill="#EEE" d="M232.896 31.2403H51.2975C49.1452 31.2403 47.4005 32.983 47.4005 35.1327V46.8101C47.4005 48.9598 49.1452 50.7025 51.2975 50.7025H232.896C235.048 50.7025 236.793 48.9598 236.793 46.8101V35.1327C236.793 32.983 235.048 31.2403 232.896 31.2403Z"/><mask id="mask0" width="190" height="114" x="47" y="31" mask-type="alpha" maskUnits="userSpaceOnUse"><path fill="#404040" d="M232.896 31.2403H51.2975C49.1452 31.2403 47.4005 32.983 47.4005 35.1327V141.007C47.4005 143.157 49.1452 144.9 51.2975 144.9H232.896C235.048 144.9 236.793 143.157 236.793 141.007V35.1327C236.793 32.983 235.048 31.2403 232.896 31.2403Z"/></mask><g mask="url(#mask0)"><path fill="#EEE" d="M239.91 42.1391H47.4005V150.349H239.91V42.1391Z"/></g></g><circle cx="188" cy="55" r="6" fill="#69DDC7"/><circle cx="91" cy="92" r="6" fill="#69DDC7"/><path fill="#69DDC7" d="M121 114L95.5 88L86.5 96L121 130L192.5 59L183.5 51L121 114Z"/><defs><filter id="filter0_d" width="229.392" height="153.66" x="29.401" y="15.24" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dx="2" dy="4"/><feGaussianBlur stdDeviation="10"/><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" mode="normal" result="shape"/></filter></defs></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="282" height="173" fill="none" viewBox="0 0 282 173"><path fill="#000" fill-opacity=".05" fill-rule="evenodd" d="M16.4571 45.1637C11.0514 46.1711 7.48574 51.3699 8.49306 56.7756C9.50039 62.1814 14.6992 65.747 20.105 64.7397L27.5528 63.3518C25.4791 65.5835 24.4525 68.7347 25.0535 71.9596C26.0608 77.3653 31.2596 80.931 36.6654 79.9236L89.691 70.0427C89.7016 70.1067 89.7129 70.1708 89.7249 70.2349C90.3258 73.4598 92.4185 76.0298 95.1569 77.3647L91.9031 77.971C86.4974 78.9784 82.9318 84.1772 83.9391 89.583C84.9464 94.9887 90.1452 98.5543 95.551 97.547L250.098 68.7482C255.504 67.7409 259.069 62.5421 258.062 57.1363C257.461 53.9114 255.368 51.3414 252.63 50.0065L257.835 49.0366C263.241 48.0292 266.807 42.8304 265.799 37.4247C264.792 32.0189 259.593 28.4533 254.187 29.4606L161.492 46.7338C161.481 46.6697 161.47 46.6056 161.458 46.5415C160.857 43.3166 158.764 40.7466 156.026 39.4117L165.025 37.7347C170.431 36.7274 173.997 31.5286 172.989 26.1228C171.982 20.7171 166.783 17.1514 161.378 18.1588L16.4571 45.1637ZM24.3031 122.54C23.2958 117.134 26.8614 111.936 32.2672 110.928L190.856 81.3762C196.262 80.3688 201.461 83.9345 202.468 89.3402C203.476 94.746 199.91 99.9448 194.504 100.952L189.963 101.798C190.493 102.057 190.999 102.362 191.474 102.708L246.43 92.4677C251.835 91.4604 257.034 95.026 258.041 100.432C258.642 103.657 257.616 106.808 255.542 109.04L256.649 108.833C262.055 107.826 267.253 111.392 268.261 116.797C269.268 122.203 265.702 127.402 260.297 128.409L95.5591 159.107C90.1534 160.114 84.9545 156.549 83.9472 151.143C82.9399 145.737 86.5055 140.538 91.9113 139.531L103.94 137.29C103.41 137.031 102.904 136.726 102.429 136.38L29.1002 150.044C23.6944 151.051 18.4956 147.486 17.4882 142.08C16.4809 136.674 20.0465 131.475 25.4523 130.468L29.7352 129.67C26.9967 128.335 24.904 125.765 24.3031 122.54Z" clip-rule="evenodd"/><g filter="url(#filter0_d)"><path fill="#EEE" d="M232.896 31.2403H51.2975C49.1452 31.2403 47.4005 32.983 47.4005 35.1327V46.8101C47.4005 48.9598 49.1452 50.7025 51.2975 50.7025H232.896C235.048 50.7025 236.793 48.9598 236.793 46.8101V35.1327C236.793 32.983 235.048 31.2403 232.896 31.2403Z"/><mask id="mask0" width="190" height="114" x="47" y="31" mask-type="alpha" maskUnits="userSpaceOnUse"><path fill="#404040" d="M232.896 31.2403H51.2975C49.1452 31.2403 47.4005 32.983 47.4005 35.1327V141.007C47.4005 143.157 49.1452 144.9 51.2975 144.9H232.896C235.048 144.9 236.793 143.157 236.793 141.007V35.1327C236.793 32.983 235.048 31.2403 232.896 31.2403Z"/></mask><g mask="url(#mask0)"><path fill="#EEE" d="M239.91 42.1391H47.4005V150.349H239.91V42.1391Z"/></g></g><circle cx="188" cy="55" r="6" fill="#69DDC7"/><circle cx="91" cy="92" r="6" fill="#69DDC7"/><path fill="#69DDC7" d="M121 114L95.5 88L86.5 96L121 130L192.5 59L183.5 51L121 114Z"/><defs><filter id="filter0_d" width="229.392" height="153.66" x="29.401" y="15.24" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dx="2" dy="4"/><feGaussianBlur stdDeviation="10"/><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/><feBlend in2="BackgroundImageFix" mode="normal" result="effect1_dropShadow"/><feBlend in="SourceGraphic" in2="effect1_dropShadow" mode="normal" result="shape"/></filter></defs></svg>
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
@@ -68,10 +68,10 @@
|
||||
dependencies:
|
||||
"@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0"
|
||||
|
||||
"@asyncapi/parser@^1.0.1":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.1.0.tgz#b366c85a6322e182e040d60f36400a0b5c26393c"
|
||||
integrity sha512-0+NeTiW5sPNYaFf4P2VCcy7Z7MLMP7H969cgOp3gvVTKroI5idkYnWo/C16tKAxs+2oOQ9WnPolsNgc+jb/qtg==
|
||||
"@asyncapi/parser@^1.3.2":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.3.2.tgz#6ff22680ff0e9aaf732121040e01e1f5836a305d"
|
||||
integrity sha512-YDGdxoVA3+gQ3jvWDp+J3IV5XsvAe+nSHSytPg9m5hfJ48R+mBPMnO7Bnh//gR17LjbmyebPj27h4Ceyaa4d3Q==
|
||||
dependencies:
|
||||
"@apidevtools/json-schema-ref-parser" "^9.0.6"
|
||||
"@asyncapi/specs" "^2.7.5"
|
||||
@@ -82,6 +82,21 @@
|
||||
node-fetch "^2.6.0"
|
||||
tiny-merge-patch "^0.1.2"
|
||||
|
||||
"@asyncapi/react-component@^0.18.2":
|
||||
version "0.18.2"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.18.2.tgz#74882b4c2a5ecd00c156898f38d0a5c9e8c4384d"
|
||||
integrity sha512-vsD3acSM0hTxxSsTJoU79kpI9FXVA64OqNCu8xrawfaU4wdsSQWWn0y7SD8pUfwe4gohwmms0xNcAaMzYIhGLw==
|
||||
dependencies:
|
||||
"@asyncapi/avro-schema-parser" "^0.2.0"
|
||||
"@asyncapi/openapi-schema-parser" "^2.0.0"
|
||||
"@asyncapi/parser" "^1.3.2"
|
||||
constate "^1.2.0"
|
||||
dompurify "^2.1.1"
|
||||
markdown-it "^11.0.1"
|
||||
merge "^2.1.0"
|
||||
openapi-sampler "^1.0.0-beta.15"
|
||||
react-use "^12.2.0"
|
||||
|
||||
"@asyncapi/specs@^2.7.5":
|
||||
version "2.7.5"
|
||||
resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d"
|
||||
@@ -3770,21 +3785,6 @@
|
||||
dependencies:
|
||||
stream "^0.0.2"
|
||||
|
||||
"@kyma-project/asyncapi-react@^0.14.2":
|
||||
version "0.14.2"
|
||||
resolved "https://registry.npmjs.org/@kyma-project/asyncapi-react/-/asyncapi-react-0.14.2.tgz#6d99ef878f0481b05db0f3be7c94f4534176675f"
|
||||
integrity sha512-lf3zcIuCTaVEIkCpF7Vwulrucgdfp1zXUIvkqjYPteOJjOuT6BAvnYwgvFUYibitmD/ADkvK8ybIeKdNJcyaPw==
|
||||
dependencies:
|
||||
"@asyncapi/avro-schema-parser" "^0.2.0"
|
||||
"@asyncapi/openapi-schema-parser" "^2.0.0"
|
||||
"@asyncapi/parser" "^1.0.1"
|
||||
constate "^1.2.0"
|
||||
dompurify "^2.1.1"
|
||||
markdown-it "^11.0.1"
|
||||
merge "^1.2.1"
|
||||
openapi-sampler "^1.0.0-beta.15"
|
||||
react-use "^12.2.0"
|
||||
|
||||
"@lerna/add@3.21.0":
|
||||
version "3.21.0"
|
||||
resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b"
|
||||
@@ -18348,10 +18348,10 @@ merge2@^1.2.3, merge2@^1.3.0:
|
||||
resolved "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81"
|
||||
integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==
|
||||
|
||||
merge@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145"
|
||||
integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==
|
||||
merge@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/merge/-/merge-2.1.0.tgz#91fff62458ba2eca378dd395fa85f1690bf87f60"
|
||||
integrity sha512-TcuhVDV+e6X457MQAm7xIb19rWhZuEDEho7RrwxMpQ/3GhD5sDlnP188gjQQuweXHy9igdke5oUtVOXX1X8Sxg==
|
||||
|
||||
methods@^1.0.0, methods@^1.1.1, methods@^1.1.2, methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
|
||||
Reference in New Issue
Block a user