Merge remote-tracking branch 'origin/master' into mob/stages-refactor

This commit is contained in:
Johan Haals
2021-01-28 12:00:31 +01:00
53 changed files with 877 additions and 250 deletions
+20
View File
@@ -0,0 +1,20 @@
---
'@backstage/backend-common': patch
'@backstage/plugin-scaffolder-backend': patch
---
Honor the branch ref in the url when cloning.
This fixes a bug in the scaffolder prepare stage where a non-default branch
was specified in the scaffolder URL but the default branch was cloned.
For example, even though the `other` branch is specified in this example, the
`master` branch was actually cloned:
```yaml
catalog:
locations:
- type: url
target: https://github.com/backstage/backstage/blob/other/plugins/scaffolder-backend/sample-templates/docs-template/template.yaml
```
This also fixes a 404 in the prepare stage for GitLab URLs.
@@ -0,0 +1,6 @@
---
'@backstage/plugin-cost-insights': minor
---
Add support for additional breakdowns of daily cost data.
This changes the type of Cost.groupedCosts returned by CostInsightsApi.getGroupDailyCost.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Fix default branch API url for custom hosted Bitbucket server
+12
View File
@@ -0,0 +1,12 @@
---
'@backstage/backend-common': patch
'@backstage/integration': patch
'@backstage/techdocs-common': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-backend': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-scaffolder': patch
'@backstage/plugin-scaffolder-backend': patch
---
Upgrade [git-url-parse](https://www.npmjs.com/package/git-url-parse) to [v11.4.4](https://github.com/IonicaBizau/git-url-parse/pull/125) which fixes parsing an Azure DevOps branch ref.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Fixed serialization issue with caching of public keys in AWS ALB auth provider
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Support supplying a custom catalog descriptor file parser
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Fix AWS ALB issuer check
+110
View File
@@ -0,0 +1,110 @@
name: Tugboat E2E Tests
on: deployment_status
jobs:
set-pending:
if: github.event.deployment_status.state != 'success' && github.event.deployment_status.state != 'failed'
name: Set pending waiting for Tugboat
runs-on: ubuntu-latest
steps:
# Set an initial commit status message to indicate that the tests are
# running.
- name: set pending status
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
debug: true
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'pending',
context: 'Backstage Tugboat E2E Tests',
description: 'Waiting for Tugboat to complete deployment',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
run-tests:
# Only run after a successful Tugboat deployment.
if: github.event.deployment_status.state == 'success'
name: Run tests against Tugboat deployment
runs-on: ubuntu-latest
steps:
# Set an initial commit status message to indicate that the tests are
# running.
- name: set pending status
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
debug: true
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: 'pending',
context: 'Backstage Tugboat E2E Tests',
description: 'Running against tugboat preview',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '14'
# This is required because the environment_url param that Tugboat uses
# to tell us where the preview is located isn't supported unless you
# specify the custom Accept header when getting the deployment_status,
# and GitHub actions doesn't do that by default. So instead we have to
# load the status object manually and get the data we need.
# https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
- name: get deployment status
id: get-status-env
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
result-encoding: string
script: |
const result = await github.repos.getDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: context.payload.deployment.id,
status_id: context.payload.deployment_status.id,
headers: {
'Accept': 'application/vnd.github.ant-man-preview+json'
},
});
console.log(result);
return result.data.environment_url;
- name: echo tugboat preview url
run: |
curl ${{steps.get-status-env.outputs.result}}
- name: set status
if: ${{ failure() }}
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: "error",
context: 'Backstage Tugboat E2E Tests',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
- name: set status
if: ${{ success() }}
uses: actions/github-script@v3
with:
github-token: ${{secrets.GH_SERVICE_ACCOUNT_TOKEN}}
script: |
return github.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: context.sha,
state: "success",
context: 'Backstage Tugboat E2E Tests',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
+15
View File
@@ -0,0 +1,15 @@
services:
backstage:
image: tugboatqa/node:lts
expose: 7000
default: true
commands:
init:
- mkdir -p /etc/service/node
- echo "#!/bin/sh" > /etc/service/node/run
- echo "yarn --cwd ${TUGBOAT_ROOT} start-backend --config ${TUGBOAT_ROOT}/app-config.yaml --config ${TUGBOAT_ROOT}/.tugboat/tugboat.app-config.production.yaml" >> /etc/service/node/run
- chmod +x /etc/service/node/run
build:
- yarn workspace example-app build
update:
- yarn install
@@ -0,0 +1,13 @@
app:
title: Backstage Tugboat Preview
baseUrl:
$env: TUGBOAT_DEFAULT_SERVICE_URL
backend:
baseUrl:
$env: TUGBOAT_DEFAULT_SERVICE_URL
cors:
origin:
$env: TUGBOAT_DEFAULT_SERVICE_URL
methods: [GET, POST, PUT, DELETE]
credentials: true
@@ -0,0 +1,74 @@
---
id: adrs-adr011
title: ADR011: Plugin Package Structure
description: Architecture Decision Record (ADR) for Plugin Package Structure
---
## Context
A core feature of Backstage is the extensibility via plugins. The Backstage
repository is open for contributions of plugins. Even most of the core features
are implemented as plugins. A plugin consists of one or multiple packages in the
`plugins/` directory. Up till now, we have a simple conventions for naming
plugin packages: Plugins are named `x`, with the option of having a related
backend plugin called `x-backend` (where `x` is the plugin name, like `catalog`
or `techdocs`). There is a need for sharing code between the frontend and
backend of a plugin, between backend plugins, or components and hooks between
different frontend plugins
([some examples](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746)).
This results in emerging plugin packages with shared code, like
`packages/catalog-client` or `packages/techdocs-common`.
> There is a common phrase in software development:
> [Naming things is hard](https://martinfowler.com/bliki/TwoHardThings.html)
To keep the contributed plugins consistent, this Architecture Decision Record
provides rules for naming plugin packages.
## Decision
We will place all plugin related code in the `plugins/` directory. The
`packages/` directory is reserved for core package of Backstage.
We follow this structure for plugin packages (where `x` is the plugin name, for
example `catalog` or `techdocs`):
- `x`: Contains the main frontend code of the plugin.
- `x-backend`: Contains the main backend code of the plugin.
- `x-react`: Contains shared widgets, hooks and similar that both the plugin
itself (`x`) and third-party frontend plugins can depend on.
- `x-node`: Contains utilities for backends that both the plugin backend itself
(`x-backend`) and third-party backend plugins can depend on.
- `x-common`: An isomorphic package with platform agnostic models, clients, and
utilities that all packages above or any third-party plugin package can depend
on.
We prefix the package names with `@backstage/plugin-`.
This structure is based on a
[suggestion in issue #3655](https://github.com/backstage/backstage/issues/3655#issuecomment-758166746).
## Consequences
We will actively migrate existing packages that are part of a plugin to the
`plugins/` folder. This affects packages like:
- `packages/techdocs-common` which should be moved to `plugins/techdocs-node`
and named `@backstage/plugin-techdocs-node`.
- `packages/catalog-client` which will be part of a future
`plugins/catalog-common` and named `@backstage/plugin-catalog-common`.
- While the new location of `packages/catalog-model` should be
`plugins/catalog-common` we might want to do an exception here, as it's a very
central package.
The limited set of rules might not be sufficient in the future. If additional
packages are required, we will revisit this decision and extend the pattern.
If possible, we will add tools, such as lint rules, to help enforce the package
names and dependencies between them or CLI commands to generate these packages.
The distinction between core packages and plugins helps us to setup
[CODEOWNERS](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners)
in the repository. We can set the code owners for the `packages/` folder to the
core team and create additional rules (like `plugins/x*`) for plugin
maintainers.
@@ -173,7 +173,12 @@ and access to a running Docker daemon. You can create a GitHub access token
docs on creating private GitHub access tokens is available
[here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
Note that the need for private GitHub access tokens will be replaced with GitHub
Apps integration further down the line.
Apps integration further down the line by using the existing `integrations`
config.
> Note: Some of this configuration may already be set up as part of your
> `app-config.yaml`. We're moving away from the duplicated config for
> authentication in the `scaffolder` section and using `integrations` instead.
#### GitHub
@@ -187,10 +192,14 @@ by specifying `visibility` option. Valid options are `public`, `private` and
public within the enterprise.
```yaml
integrations:
github:
- host: github.com
token:
$env: GITHUB_TOKEN
scaffolder:
github:
token:
$env: GITHUB_TOKEN
visibility: public # or 'internal' or 'private'
```
@@ -201,10 +210,9 @@ allows to configure the private access token and the base URL of a GitLab
instance:
```yaml
scaffolder:
integrations:
gitlab:
api:
baseUrl: https://gitlab.com
- host: gitlab.com
token:
$env: GITLAB_TOKEN
```
@@ -218,10 +226,9 @@ will hopefully support on-prem installations as well but that has not been
verified.
```yaml
scaffolder:
integrations:
azure:
baseUrl: https://dev.azure.com/{your-organization}
api:
- host: dev.azure.com
token:
$env: AZURE_TOKEN
```
+86 -8
View File
@@ -4,19 +4,97 @@ title: Other
description: Documentation on different ways of Deployment
---
## Deploying Locally
## Docker
### Try on Docker
Here we have an example Dockerfile that you can use to build everything together
in one container. This Dockerfile uses multi-stage builds, and a
`backend:bundle` command from the CLI.
Run the following commands if you have Docker environment
It also provides caching on the `yarn install`'s so that you don't have to do it
unless absolutely necessary.
```bash
$ yarn install
$ yarn docker-build
$ docker run --rm -it -p 7000:7000 -e NODE_ENV=development example-backend:latest
> Note: This Dockerfile assumes that you're running SQLite, or your
> configuration is setup to connect to an external PostgreSQL Database.
```Dockerfile
# Stage 1 - Create yarn install skeleton layer
FROM node:14-buster AS packages
WORKDIR /app
COPY package.json yarn.lock ./
COPY packages packages
# Uncomment this line if you have a local plugins folder
# COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf
# Stage 2 - Install dependencies and build packages
FROM node:14-buster AS build
WORKDIR /app
COPY --from=packages /app .
RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)"
COPY . .
RUN yarn tsc
RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies
# Stage 3 - Build the actual backend image and install production dependencies
FROM node:14-buster
WORKDIR /app
# Copy from build stage
COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
COPY --from=build /app/packages/backend/dist/bundle.tar.gz .
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
COPY app-config.yaml app-config.production.yaml ./
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
```
Then open http://localhost:7000 on your browser.
Before building you should also include a `.dockerignore`. This will greatly
improve the context boot up time of Docker as we are no longer sending all of
the `node_modules` into the context. It also helps us avoid some limitations and
errors that may occur when trying to share the `node_modules` folder to inside
the build.
You can add the following contents to the root of your repository at
`.dockerignore` and it might look something like the following:
```dockerignore
.git
node_modules
packages/*/node_modules
plugins/*/node_modules
plugins/*/dist
```
Once you have added both the `Dockerfile` and `.dockerignore` to the root of
your project, and run the following to build the container under a specified
tag.
```sh
$ docker build -t example-deployment .
```
To run the image locally you can run:
```sh
$ docker run -p -it 7000:7000 example-deployment
```
You should then start to get logs in your terminal, and then you can open your
browser at `http://localhost:7000`
## Heroku
+2 -1
View File
@@ -188,7 +188,8 @@
"architecture-decisions/adrs-adr007",
"architecture-decisions/adrs-adr008",
"architecture-decisions/adrs-adr009",
"architecture-decisions/adrs-adr010"
"architecture-decisions/adrs-adr010",
"architecture-decisions/adrs-adr011"
],
"Contribute": ["../CONTRIBUTING"],
"Support": ["support/support", "support/project-structure"],
+1
View File
@@ -119,6 +119,7 @@ nav:
- ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md'
- ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md'
- ADR010 - Luxon Date Library: 'architecture-decisions/adr010-luxon-date-library.md'
- ADR011 - Plugin Package Structure: 'architecture-decisions/adr011-plugin-package-structure.md'
- Contribute: '../CONTRIBUTING.md'
- Support:
- 'support/support.md'
+1 -1
View File
@@ -43,7 +43,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"knex": "^0.21.6",
@@ -126,12 +126,12 @@ describe('BitbucketUrlReader', () => {
),
),
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/repositories/backstage/mock/commits/some-branch',
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
}),
),
),
@@ -161,13 +161,18 @@ export class BitbucketUrlReader implements UrlReader {
}
private async getLastCommitShortHash(url: string): Promise<string> {
const { name: repoName, owner: project, ref } = parseGitUrl(url);
const { resource, name: repoName, owner: project, ref } = parseGitUrl(url);
let branch = ref;
if (!branch) {
branch = await getBitbucketDefaultBranch(url, this.config);
}
const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`;
const isHosted = resource === 'bitbucket.org';
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
const commitsApiUrl = isHosted
? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
: `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
const commitsResponse = await fetch(
commitsApiUrl,
@@ -182,14 +187,26 @@ export class BitbucketUrlReader implements UrlReader {
}
const commits = await commitsResponse.json();
if (
commits &&
commits.values &&
commits.values.length > 0 &&
commits.values[0].hash
) {
return commits.values[0].hash.substring(0, 12);
if (isHosted) {
if (
commits &&
commits.values &&
commits.values.length > 0 &&
commits.values[0].hash
) {
return commits.values[0].hash.substring(0, 12);
}
} else {
if (
commits &&
commits.values &&
commits.values.length > 0 &&
commits.values[0].id
) {
return commits.values[0].id.substring(0, 12);
}
}
throw new Error(`Failed to read response from ${commitsApiUrl}`);
}
}
+10 -1
View File
@@ -86,13 +86,22 @@ export class Git {
return git.commit({ fs, dir, message, author, committer });
}
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
async clone({
url,
dir,
ref,
}: {
url: string;
dir: string;
ref?: string;
}): Promise<void> {
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
return git.clone({
fs,
http,
url,
dir,
ref,
singleBranch: true,
depth: 1,
onProgress: this.onProgressHandler(),
+1 -1
View File
@@ -31,7 +31,7 @@
"dependencies": {
"@backstage/config": "^0.1.2",
"cross-fetch": "^3.0.6",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"@octokit/rest": "^18.0.12",
"@octokit/auth-app": "^2.10.5",
"luxon": "^1.25.0"
@@ -116,7 +116,7 @@ describe('bitbucket core', () => {
};
worker.use(
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
(_, res, ctx) =>
res(
ctx.status(200),
@@ -144,7 +144,7 @@ describe('bitbucket core', () => {
};
worker.use(
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
(_, res, ctx) =>
res(
ctx.status(200),
@@ -231,7 +231,7 @@ describe('bitbucket core', () => {
};
worker.use(
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch',
(_, res, ctx) =>
res(
ctx.status(200),
+6 -2
View File
@@ -31,9 +31,10 @@ export async function getBitbucketDefaultBranch(
const { name: repoName, owner: project, resource } = parseGitUrl(url);
const isHosted = resource === 'bitbucket.org';
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184
const branchUrl = isHosted
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`;
const response = await fetch(branchUrl, getBitbucketRequestOptions(config));
if (!response.ok) {
@@ -50,7 +51,10 @@ export async function getBitbucketDefaultBranch(
defaultBranch = displayId;
}
if (!defaultBranch) {
throw new Error(`Failed to read default branch from ${branchUrl}`);
throw new Error(
`Failed to read default branch from ${branchUrl}. ` +
`Response ${response.status} ${response.json()}`,
);
}
return defaultBranch;
}
+1 -1
View File
@@ -48,7 +48,7 @@
"dockerode": "^3.2.1",
"express": "^4.17.1",
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"js-yaml": "^4.0.0",
"json5": "^2.1.3",
"mime-types": "^2.1.27",
+4
View File
@@ -72,6 +72,10 @@ export interface Config {
onelogin?: {
development: { [key: string]: string };
};
awsalb?: {
issuer?: string;
region: string;
};
};
};
}
@@ -80,7 +80,7 @@ describe('AwsALBAuthProvider', () => {
const mockResponseSend = jest.fn();
const mockRequest = ({
header: jest.fn(() => {
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzc3VlciI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.zUkMYAuMwC1T0tyHMpxXrkbFDa4aGhB8d9um_tI2hsI';
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImZvbyIsImlzcyI6ImZvbyJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.T2BNS4G-6RoiFnXc8Q8TiwdWzTpNitY8jcsGM3N3-Yo';
}),
} as unknown) as express.Request;
const mockRequestWithoutJwt = ({
@@ -34,7 +34,7 @@ const ALB_JWT_HEADER = 'x-amzn-oidc-data';
*/
type AwsAlbAuthProviderOptions = {
region: string;
issuer: string;
issuer?: string;
identityResolutionCallback: ExperimentalIdentityResolver;
};
export const getJWTHeaders = (input: string) => {
@@ -70,10 +70,7 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
const key = await this.getKey(headers.kid);
const payload = JWT.verify(jwt, key);
if (
this.options.issuer !== '' &&
headers.issuer !== this.options.issuer
) {
if (this.options.issuer && headers.iss !== this.options.issuer) {
throw new Error('issuer mismatch on JWT');
}
@@ -98,13 +95,13 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers {
async getKey(keyId: string): Promise<KeyObject> {
const optionalCacheKey = this.keyCache.get<KeyObject>(keyId);
if (optionalCacheKey) {
return optionalCacheKey;
return crypto.createPublicKey(optionalCacheKey);
}
const keyText: string = await fetch(
`https://public-keys.auth.elb.${this.options.region}.amazonaws.com/${keyId}`,
).then(response => response.text());
const keyValue = crypto.createPublicKey(keyText);
this.keyCache.set(keyId, keyValue);
this.keyCache.set(keyId, keyValue.export({ format: 'pem', type: 'spki' }));
return keyValue;
}
}
@@ -116,7 +113,7 @@ export const createAwsAlbProvider = ({
identityResolver,
}: AuthProviderFactoryOptions) => {
const region = config.getString('region');
const issuer = config.getString('iss');
const issuer = config.getOptionalString('iss');
if (identityResolver !== undefined) {
return new AwsAlbAuthProvider(logger, catalogApi, {
region,
+1 -1
View File
@@ -43,7 +43,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"knex": "^0.21.6",
"ldapjs": "^2.2.0",
"lodash": "^4.17.15",
@@ -32,6 +32,7 @@ import {
CatalogProcessorEntityResult,
CatalogProcessorErrorResult,
CatalogProcessorLocationResult,
CatalogProcessorParser,
CatalogProcessorResult,
} from './processors/types';
import { LocationReader, ReadLocationResult } from './types';
@@ -41,6 +42,7 @@ const MAX_DEPTH = 10;
type Options = {
reader: UrlReader;
parser: CatalogProcessorParser;
logger: Logger;
config: Config;
processors: CatalogProcessor[];
@@ -137,7 +139,6 @@ export class LocationReaders implements LocationReader {
if (emitResult.type === 'relation') {
throw new Error('readLocation may not emit entity relations');
}
emit(emitResult);
};
@@ -149,6 +150,7 @@ export class LocationReaders implements LocationReader {
item.location,
item.optional,
validatedEmit,
this.options.parser,
)
) {
return;
@@ -25,6 +25,7 @@ import {
CatalogProcessorErrorResult,
CatalogProcessorResult,
} from './types';
import { defaultEntityDataParser } from './util/parse';
describe('UrlReaderProcessor', () => {
const mockApiOrigin = 'http://localhost';
@@ -52,7 +53,7 @@ describe('UrlReaderProcessor', () => {
);
const generated = (await new Promise<CatalogProcessorResult>(emit =>
processor.readLocation(spec, false, emit),
processor.readLocation(spec, false, emit, defaultEntityDataParser),
)) as CatalogProcessorEntityResult;
expect(generated.type).toBe('entity');
@@ -81,7 +82,7 @@ describe('UrlReaderProcessor', () => {
);
const generated = (await new Promise<CatalogProcessorResult>(emit =>
processor.readLocation(spec, false, emit),
processor.readLocation(spec, false, emit, defaultEntityDataParser),
)) as CatalogProcessorErrorResult;
expect(generated.type).toBe('error');
@@ -18,8 +18,11 @@ import { UrlReader } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { Logger } from 'winston';
import * as result from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
import { parseEntityYaml } from './util/parse';
import {
CatalogProcessor,
CatalogProcessorEmit,
CatalogProcessorParser,
} from './types';
// TODO(Rugvip): Added for backwards compatibility when moving to UrlReader, this
// can be removed in a bit
@@ -43,6 +46,7 @@ export class UrlReaderProcessor implements CatalogProcessor {
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
): Promise<boolean> {
if (deprecatedTypes.includes(location.type)) {
// TODO(Rugvip): Remove this warning a month or two into 2021, and remove support for the deprecated types.
@@ -57,7 +61,7 @@ export class UrlReaderProcessor implements CatalogProcessor {
try {
const data = await this.options.reader.read(location.target);
for (const parseResult of parseEntityYaml(data, location)) {
for await (const parseResult of parser({ data, location })) {
emit(parseResult);
}
} catch (error) {
@@ -27,12 +27,15 @@ export type CatalogProcessor = {
* @param location The location to read
* @param optional Whether a missing target should trigger an error
* @param emit A sink for items resulting from the read
* @param parser A parser, that is able to take the raw catalog descriptor
* data and turn it into the actual result pieces.
* @returns True if handled by this processor, false otherwise
*/
readLocation?(
location: LocationSpec,
optional: boolean,
emit: CatalogProcessorEmit,
parser: CatalogProcessorParser,
): Promise<boolean>;
/**
@@ -100,6 +103,16 @@ export type CatalogProcessor = {
): Promise<void>;
};
/**
* A parser, that is able to take the raw catalog descriptor data and turn it
* into the actual result pieces. The default implementation performs a YAML
* document parsing.
*/
export type CatalogProcessorParser = (options: {
data: Buffer;
location: LocationSpec;
}) => AsyncIterable<CatalogProcessorResult>;
export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void;
export type CatalogProcessorLocationResult = {
@@ -18,7 +18,7 @@ import { Entity, LocationSpec } from '@backstage/catalog-model';
import lodash from 'lodash';
import yaml from 'yaml';
import * as result from '../results';
import { CatalogProcessorResult } from '../types';
import { CatalogProcessorParser, CatalogProcessorResult } from '../types';
export function* parseEntityYaml(
data: Buffer,
@@ -50,3 +50,12 @@ export function* parseEntityYaml(
}
}
}
export const defaultEntityDataParser: CatalogProcessorParser = async function* defaultEntityDataParser({
data,
location,
}) {
for (const e of parseEntityYaml(data, location)) {
yield e;
}
};
@@ -20,6 +20,7 @@ import { ConfigReader } from '@backstage/config';
import Knex from 'knex';
import yaml from 'yaml';
import { DatabaseManager } from '../database';
import { CatalogProcessorParser } from '../ingestion';
import * as result from '../ingestion/processors/results';
import { CatalogBuilder, CatalogEnvironment } from './CatalogBuilder';
@@ -209,4 +210,26 @@ describe('CatalogBuilder', () => {
}),
]);
});
it('setEntityDataParser works', async () => {
const mockParser: CatalogProcessorParser = jest
.fn()
.mockImplementation(() => {});
const builder = new CatalogBuilder(env)
.setEntityDataParser(mockParser)
.replaceProcessors([
{
async readLocation(_location, _optional, _emit, parser) {
expect(parser).toBe(mockParser);
return true;
},
},
]);
const { higherOrderOperation } = await builder.build();
await higherOrderOperation.addLocation({ type: 'x', target: 'y' });
expect.assertions(1);
});
});
@@ -39,6 +39,7 @@ import {
AnnotateLocationEntityProcessor,
BuiltinKindsEntityProcessor,
CatalogProcessor,
CatalogProcessorParser,
CodeOwnersProcessor,
FileReaderProcessor,
GithubOrgReaderProcessor,
@@ -60,6 +61,7 @@ import {
textPlaceholderResolver,
yamlPlaceholderResolver,
} from '../ingestion/processors/PlaceholderProcessor';
import { defaultEntityDataParser } from '../ingestion/processors/util/parse';
import { LocationAnalyzer } from '../ingestion/types';
export type CatalogEnvironment = {
@@ -96,6 +98,7 @@ export class CatalogBuilder {
private fieldFormatValidators: Partial<Validators>;
private processors: CatalogProcessor[];
private processorsReplace: boolean;
private parser: CatalogProcessorParser | undefined;
constructor(env: CatalogEnvironment) {
this.env = env;
@@ -105,6 +108,7 @@ export class CatalogBuilder {
this.fieldFormatValidators = {};
this.processors = [];
this.processorsReplace = false;
this.parser = undefined;
}
/**
@@ -197,6 +201,20 @@ export class CatalogBuilder {
return this;
}
/**
* Sets up the catalog to use a custom parser for entity data.
*
* This is the function that gets called immediately after some raw entity
* specification data has been read from a remote source, and needs to be
* parsed and emitted as structured data.
*
* @param parser The custom parser
*/
setEntityDataParser(parser: CatalogProcessorParser): CatalogBuilder {
this.parser = parser;
return this;
}
/**
* Wires up and returns all of the component parts of the catalog
*/
@@ -211,9 +229,11 @@ export class CatalogBuilder {
const policy = this.buildEntityPolicy();
const processors = this.buildProcessors();
const rulesEnforcer = CatalogRulesEnforcer.fromConfig(config);
const parser = this.parser || defaultEntityDataParser;
const locationReader = new LocationReaders({
...this.env,
parser,
processors,
rulesEnforcer,
policy,
+1 -1
View File
@@ -39,7 +39,7 @@
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.0.12",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-hook-form": "^6.6.0",
+1 -1
View File
@@ -40,7 +40,7 @@
"@material-ui/lab": "4.0.0-alpha.45",
"@types/react": "^16.9",
"classnames": "^2.2.6",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"moment": "^2.26.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
+14 -8
View File
@@ -33,11 +33,12 @@ import {
UnlabeledDataflowAlert,
} from '../src/utils/alerts';
import {
trendlineOf,
aggregationFor,
changeOf,
entityOf,
getGroupedProducts,
aggregationFor,
getGroupedProjects,
trendlineOf,
} from './utils/mockData';
export class ExampleCostInsightsClient implements CostInsightsApi {
@@ -99,9 +100,12 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
// Optional field on Cost which needs to be supplied in order to see
// the product breakdown view in the top panel.
groupedCosts: getGroupedProducts(intervals),
// Optional field providing cost groupings / breakdowns keyed by the type. In this example,
// daily cost grouped by cloud product OR by project / billing account.
groupedCosts: {
product: getGroupedProducts(intervals),
project: getGroupedProjects(intervals),
},
},
);
@@ -117,9 +121,11 @@ export class ExampleCostInsightsClient implements CostInsightsApi {
aggregation: aggregation,
change: changeOf(aggregation),
trendline: trendlineOf(aggregation),
// Optional field on Cost which needs to be supplied in order to see
// the product breakdown view in the top panel.
groupedCosts: getGroupedProducts(intervals),
// Optional field providing cost groupings / breakdowns keyed by the type. In this example,
// daily project cost grouped by cloud product.
groupedCosts: {
product: getGroupedProducts(intervals),
},
},
);
@@ -56,26 +56,26 @@ import { BarChartLegendOptions } from '../BarChart/BarChartLegend';
dayjs.extend(utc);
export type CostOverviewByProductChartProps = {
costsByProduct: Cost[];
export type CostOverviewBreakdownChartProps = {
costBreakdown: Cost[];
};
const LOW_COST_THRESHOLD = 0.1;
export const CostOverviewByProductChart = ({
costsByProduct,
}: CostOverviewByProductChartProps) => {
export const CostOverviewBreakdownChart = ({
costBreakdown,
}: CostOverviewBreakdownChartProps) => {
const theme = useTheme<CostInsightsTheme>();
const classes = useStyles(theme);
const lastCompleteBillingDate = useLastCompleteBillingDate();
const { duration } = useFilters(mapFiltersToProps);
const [isExpanded, setExpanded] = useState(false);
if (!costsByProduct) {
if (!costBreakdown) {
return null;
}
const flattenedAggregation = costsByProduct
const flattenedAggregation = costBreakdown
.map(cost => cost.aggregation)
.flat();
@@ -87,44 +87,46 @@ export const CostOverviewByProductChart = ({
lastCompleteBillingDate,
);
const currentPeriodTotal = totalCost - previousPeriodTotal;
const otherProducts: string[] = [];
const canExpand = costBreakdown.length >= 8;
const otherCategoryIds: string[] = [];
const productsByDate = costsByProduct.reduce((prodByDate, product) => {
const productTotal = aggregationSum(product.aggregation);
// Group products with less than 10% of the total cost into "Other" category
// when we have >= 8 products.
const isOtherProduct =
costsByProduct.length >= 8 &&
productTotal < totalCost * LOW_COST_THRESHOLD;
const breakdownsByDate = costBreakdown.reduce(
(breakdownByDate, breakdown) => {
const breakdownTotal = aggregationSum(breakdown.aggregation);
// Group breakdown items with less than 10% of the total cost into "Other" category if needed
const isOtherCategory =
canExpand && breakdownTotal < totalCost * LOW_COST_THRESHOLD;
const updatedProdByDate = { ...prodByDate };
if (isOtherProduct) {
otherProducts.push(product.id);
}
product.aggregation.forEach(curAggregation => {
const productCostsForDate = updatedProdByDate[curAggregation.date] || {};
const updatedBreakdownByDate = { ...breakdownByDate };
if (isOtherCategory) {
otherCategoryIds.push(breakdown.id);
}
breakdown.aggregation.forEach(curAggregation => {
const costsForDate = updatedBreakdownByDate[curAggregation.date] || {};
updatedProdByDate[curAggregation.date] = {
...productCostsForDate,
[product.id]:
(productCostsForDate[product.id] || 0) + curAggregation.amount,
};
});
updatedBreakdownByDate[curAggregation.date] = {
...costsForDate,
[breakdown.id]:
(costsForDate[breakdown.id] || 0) + curAggregation.amount,
};
});
return updatedProdByDate;
}, {} as Record<string, Record<string, number>>);
return updatedBreakdownByDate;
},
{} as Record<string, Record<string, number>>,
);
const chartData: Record<string, number>[] = Object.keys(productsByDate).map(
const chartData: Record<string, number>[] = Object.keys(breakdownsByDate).map(
date => {
const costsForDate = Object.keys(productsByDate[date]).reduce(
(dateCosts, product) => {
// Group costs for products that belong to 'Other' in the chart.
const cost = productsByDate[date][product];
const productCost =
!isExpanded && otherProducts.includes(product)
const costsForDate = Object.keys(breakdownsByDate[date]).reduce(
(dateCosts, breakdown) => {
// Group costs for items that belong to 'Other' in the chart.
const cost = breakdownsByDate[date][breakdown];
const breakdownCost =
!isExpanded && otherCategoryIds.includes(breakdown)
? { Other: (dateCosts.Other || 0) + cost }
: { [product]: cost };
return { ...dateCosts, ...productCost };
: { [breakdown]: cost };
return { ...dateCosts, ...breakdownCost };
},
{} as Record<string, number>,
);
@@ -135,40 +137,41 @@ export const CostOverviewByProductChart = ({
},
);
const sortedProducts = costsByProduct.sort(
const sortedBreakdowns = costBreakdown.sort(
(a, b) => aggregationSum(a.aggregation) - aggregationSum(b.aggregation),
);
const renderAreas = () => {
const separatedProducts = sortedProducts
// Check that product is a separate group and hasn't been added to 'Other'
const separatedBreakdowns = sortedBreakdowns
// Check that the breakdown is a separate group and hasn't been added to 'Other'
.filter(
product =>
product.id !== 'Other' && !otherProducts.includes(product.id),
breakdown =>
breakdown.id !== 'Other' && !otherCategoryIds.includes(breakdown.id),
)
.map(product => product.id);
.map(breakdown => breakdown.id);
// Keep 'Other' category at the bottom of the stack
const productsToDisplay = isExpanded
? sortedProducts.map(product => product.id)
: ['Other', ...separatedProducts];
const breakdownsToDisplay = isExpanded
? sortedBreakdowns.map(breakdown => breakdown.id)
: ['Other', ...separatedBreakdowns];
return productsToDisplay.map((product, i) => {
// Logic to handle case where there are more products than data viz colors.
const productColor =
return breakdownsToDisplay.map((breakdown, i) => {
// Logic to handle case where there are more items than data viz colors.
const color =
theme.palette.dataViz[
(productsToDisplay.length - 1 - i) %
(breakdownsToDisplay.length - 1 - i) %
(theme.palette.dataViz.length - 1)
];
return (
<Area
dataKey={product}
key={breakdown}
dataKey={breakdown}
isAnimationActive={false}
stackId="1"
stroke={productColor}
fill={productColor}
stroke={color}
fill={color}
onClick={() => setExpanded(true)}
style={{
cursor: product === 'Other' && !isExpanded ? 'pointer' : null,
cursor: breakdown === 'Other' && !isExpanded ? 'pointer' : null,
}}
/>
);
@@ -206,7 +209,7 @@ export const CostOverviewByProductChart = ({
{items.reverse().map((item, index) => (
<TooltipItem key={`${item.label}-${index}`} item={item} />
))}
{!isExpanded ? expandText : null}
{canExpand && !isExpanded ? expandText : null}
</Tooltip>
);
};
@@ -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 { fireEvent } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import { CostOverviewCard } from './CostOverviewCard';
import { Cost } from '../../types';
import {
changeOf,
getGroupedProducts,
getGroupedProjects,
MockAggregatedDailyCosts,
trendlineOf,
} from '../../utils/mockData';
import {
MockBillingDateProvider,
MockConfigProvider,
MockFilterProvider,
MockScrollProvider,
} from '../../utils/tests';
import { CostInsightsThemeProvider } from '../CostInsightsPage/CostInsightsThemeProvider';
const mockGroupDailyCost: Cost = {
id: 'test-group',
aggregation: MockAggregatedDailyCosts,
change: changeOf(MockAggregatedDailyCosts),
trendline: trendlineOf(MockAggregatedDailyCosts),
};
function renderInContext(children: JSX.Element) {
return renderInTestApp(
<CostInsightsThemeProvider>
<MockConfigProvider>
<MockFilterProvider>
<MockBillingDateProvider>
<MockScrollProvider>{children}</MockScrollProvider>
</MockBillingDateProvider>
</MockFilterProvider>
</MockConfigProvider>
</CostInsightsThemeProvider>,
);
}
describe('<CostOverviewCard/>', () => {
it('Renders without exploding', async () => {
const { getByText } = await renderInContext(
<CostOverviewCard dailyCostData={mockGroupDailyCost} metricData={null} />,
);
expect(getByText('Cloud Cost')).toBeInTheDocument();
});
it('Shows breakdown tabs if provided', async () => {
const mockDailyCostWithBreakdowns = {
...mockGroupDailyCost,
groupedCosts: {
product: getGroupedProducts('R2/P90D/2021-01-01'),
project: getGroupedProjects('R2/P90D/2021-01-01'),
},
};
const { getByText } = await renderInContext(
<CostOverviewCard
dailyCostData={mockDailyCostWithBreakdowns}
metricData={null}
/>,
);
expect(getByText('Cloud Cost')).toBeInTheDocument();
expect(getByText('Breakdown by product')).toBeInTheDocument();
expect(getByText('Breakdown by project')).toBeInTheDocument();
fireEvent.click(getByText('Breakdown by product'));
expect(getByText('Cloud Cost By Product')).toBeInTheDocument();
fireEvent.click(getByText('Breakdown by project'));
expect(getByText('Cloud Cost By Project')).toBeInTheDocument();
});
});
@@ -14,22 +14,23 @@
* limitations under the License.
*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import {
Box,
capitalize,
Card,
CardContent,
Divider,
useTheme,
Tab,
Tabs,
useTheme,
} from '@material-ui/core';
import { CostOverviewChart } from './CostOverviewChart';
import { CostOverviewByProductChart } from './CostOverviewByProductChart';
import { CostOverviewBreakdownChart } from './CostOverviewBreakdownChart';
import { CostOverviewHeader } from './CostOverviewHeader';
import { MetricSelect } from '../MetricSelect';
import { PeriodSelect } from '../PeriodSelect';
import { useScroll, useFilters, useConfig } from '../../hooks';
import { useConfig, useFilters, useScroll } from '../../hooks';
import { mapFiltersToProps } from './selector';
import { DefaultNavigation } from '../../utils/navigation';
import { findAlways } from '../../utils/assert';
@@ -49,6 +50,15 @@ export const CostOverviewCard = ({
const config = useConfig();
const [tabIndex, setTabIndex] = useState(0);
// Reset tabIndex if breakdowns available change
useEffect(() => {
// Intentionally off-by-one to account for the overview tab
const lastIndex = Object.keys(dailyCostData.groupedCosts ?? {}).length;
if (tabIndex > lastIndex) {
setTabIndex(0);
}
}, [dailyCostData, tabIndex, setTabIndex]);
const { ScrollAnchor } = useScroll(DefaultNavigation.CostOverviewCard);
const { setDuration, setProject, setMetric, ...filters } = useFilters(
mapFiltersToProps,
@@ -59,14 +69,18 @@ export const CostOverviewCard = ({
: null;
const styles = useOverviewTabsStyles(theme);
const breakdownTabs = Object.keys(dailyCostData.groupedCosts ?? {}).map(
key => ({
id: key,
label: `Breakdown by ${key}`,
title: `Cloud Cost By ${capitalize(key)}`,
}),
);
const tabs = [
{ id: 'overview', label: 'Total cost', title: 'Cloud Cost' },
{
id: 'breakdown',
label: 'Breakdown by product',
title: 'Cloud Cost By Product',
},
];
].concat(breakdownTabs);
// tabIndex can temporarily be invalid while the useEffect above processes
const safeTabIndex = tabIndex > tabs.length - 1 ? 0 : tabIndex;
const OverviewTabs = () => {
return (
@@ -74,7 +88,7 @@ export const CostOverviewCard = ({
<Tabs
indicatorColor="primary"
onChange={(_, index) => setTabIndex(index)}
value={tabIndex}
value={safeTabIndex}
>
{tabs.map((tab, index) => (
<Tab
@@ -91,27 +105,27 @@ export const CostOverviewCard = ({
};
// Metrics can only be selected on the total cost graph
const showMetricSelect = config.metrics.length && tabIndex === 0;
const showMetricSelect = config.metrics.length && safeTabIndex === 0;
return (
<Card style={{ position: 'relative' }}>
<ScrollAnchor behavior="smooth" top={-20} />
<CardContent>
{dailyCostData.groupedCosts && <OverviewTabs />}
<CostOverviewHeader title={tabs[tabIndex].title}>
<CostOverviewHeader title={tabs[safeTabIndex].title}>
<PeriodSelect duration={filters.duration} onSelect={setDuration} />
</CostOverviewHeader>
<Divider />
<Box ml={2} my={1} display="flex" flexDirection="column">
{tabIndex === 0 ? (
{safeTabIndex === 0 ? (
<CostOverviewChart
dailyCostData={dailyCostData}
metric={metric}
metricData={metricData}
/>
) : (
<CostOverviewByProductChart
costsByProduct={dailyCostData.groupedCosts!}
<CostOverviewBreakdownChart
costBreakdown={dailyCostData.groupedCosts![tabs[safeTabIndex].id]}
/>
)}
</Box>
+1 -1
View File
@@ -23,5 +23,5 @@ export interface Cost {
aggregation: DateAggregation[];
change?: ChangeStatistic;
trendline?: Trendline;
groupedCosts?: Cost[];
groupedCosts?: Record<string, Cost[]>;
}
@@ -1064,3 +1064,18 @@ export const getGroupedProducts = (intervals: string) => [
aggregation: aggregationFor(intervals, 250),
},
];
export const getGroupedProjects = (intervals: string) => [
{
id: 'project-a',
aggregation: aggregationFor(intervals, 1_700),
},
{
id: 'project-b',
aggregation: aggregationFor(intervals, 350),
},
{
id: 'project-c',
aggregation: aggregationFor(intervals, 1_300),
},
];
+1 -1
View File
@@ -48,7 +48,7 @@
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"fs-extra": "^9.0.0",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"globby": "^11.0.0",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
@@ -67,6 +67,22 @@ describe('AzurePreparer', () => {
});
});
it('calls the clone command with the correct arguments for a repository with a specified branch', async () => {
await preparer.prepare({
url:
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo?path=%2Ftemplate.yaml&version=GBmaster',
logger,
workspacePath,
});
expect(mockGitClient.clone).toHaveBeenCalledWith({
url:
'https://dev.azure.com/backstage-org/backstage-project/_git/template-repo',
dir: checkoutPath,
ref: 'master',
});
});
it('calls the clone command with the correct arguments for a repository when no path is provided', async () => {
await preparer.prepare({
url:
@@ -48,6 +48,7 @@ export class AzurePreparer implements PreparerBase {
await git.clone({
url: parsedGitUrl.toString('https'),
ref: parsedGitUrl.ref,
dir: checkoutPath,
});
@@ -55,6 +55,7 @@ describe('BitbucketPreparer', () => {
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://bitbucket.org/backstage-project/backstage-repo',
dir: checkoutPath,
ref: expect.any(String),
});
expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath);
expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git'));
@@ -80,6 +81,7 @@ describe('BitbucketPreparer', () => {
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://bitbucket.org/backstage-project/backstage-repo',
dir: checkoutPath,
ref: expect.any(String),
});
});
@@ -51,6 +51,7 @@ export class BitbucketPreparer implements PreparerBase {
await git.clone({
url: parsedGitUrl.toString('https'),
dir: checkoutPath,
ref: parsedGitUrl.ref,
});
await fs.move(fullPathToTemplate, targetPath);
@@ -54,6 +54,7 @@ describe('GitHubPreparer', () => {
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://github.com/benjdlambert/backstage-graphql-template',
dir: checkoutPath,
ref: expect.any(String),
});
expect(fs.move).toHaveBeenCalledWith(
resolve(checkoutPath, 'templates', 'graphql-starter', 'template'),
@@ -73,6 +74,7 @@ describe('GitHubPreparer', () => {
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://github.com/benjdlambert/backstage-graphql-template',
dir: checkoutPath,
ref: 'master',
});
expect(fs.move).toHaveBeenCalledWith(checkoutPath, templatePath);
expect(fs.rmdir).toHaveBeenCalledWith(resolve(templatePath, '.git'));
@@ -47,6 +47,7 @@ export class GithubPreparer implements PreparerBase {
await git.clone({
url: parsedGitUrl.toString('https'),
dir: checkoutPath,
ref: parsedGitUrl.ref,
});
await fs.move(fullPathToTemplate, targetPath);
@@ -50,8 +50,9 @@ describe('GitLabPreparer', () => {
});
expect(mockGitClient.clone).toHaveBeenCalledWith({
url: 'https://gitlab.com/benjdlambert/backstage-graphql-template',
url: 'https://gitlab.com/benjdlambert/backstage-graphql-template.git',
dir: checkoutPath,
ref: expect.any(String),
});
expect(Git.fromAuth).toHaveBeenCalledWith({
@@ -35,6 +35,7 @@ export class GitlabPreparer implements PreparerBase {
checkoutPath,
parsedGitUrl.filepath,
);
parsedGitUrl.git_suffix = true;
const git = this.config.token
? Git.fromAuth({
@@ -47,6 +48,7 @@ export class GitlabPreparer implements PreparerBase {
await git.clone({
url: parsedGitUrl.toString('https'),
dir: checkoutPath,
ref: parsedGitUrl.ref,
});
await fs.move(fullPathToTemplate, targetPath);
+1 -1
View File
@@ -40,7 +40,7 @@
"@rjsf/core": "^2.4.0",
"@rjsf/material-ui": "^2.4.0",
"classnames": "^2.2.6",
"git-url-parse": "^11.4.3",
"git-url-parse": "^11.4.4",
"moment": "^2.26.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
+123 -109
View File
@@ -5301,17 +5301,17 @@
global "^4.3.2"
regenerator-runtime "^0.13.7"
"@storybook/addons@6.1.14", "@storybook/addons@^6.1.11":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.14.tgz#2b81304bbe696923df95cdcf85cfc592d10f4065"
integrity sha512-HlpmV7aejp/MeW8bo/WKME3i71gi0men9qcwoovjDjnSF6jXoNLT336a5udKXdHqYSZgzdyURlgLtilCWkWaJQ==
"@storybook/addons@6.1.15", "@storybook/addons@^6.1.11":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.15.tgz#09eb8d962f58bd20b4ac2f83b515831c83226352"
integrity sha512-ENyHapLFOG93VaoQXPX8O3IWjLRyVBox9C9P20LMruKX/SfXAXx20qsoAWKKPGssopyOin17aoQX9pj+lFmCZQ==
dependencies:
"@storybook/api" "6.1.14"
"@storybook/channels" "6.1.14"
"@storybook/client-logger" "6.1.14"
"@storybook/core-events" "6.1.14"
"@storybook/router" "6.1.14"
"@storybook/theming" "6.1.14"
"@storybook/api" "6.1.15"
"@storybook/channels" "6.1.15"
"@storybook/client-logger" "6.1.15"
"@storybook/core-events" "6.1.15"
"@storybook/router" "6.1.15"
"@storybook/theming" "6.1.15"
core-js "^3.0.1"
global "^4.3.2"
regenerator-runtime "^0.13.7"
@@ -5341,20 +5341,20 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/api@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.14.tgz#20035dd336aba1c5a0f8c83c8c14a2edaf4db891"
integrity sha512-gWcC/xEW8HL5DsocLujHBUdoNsl4YW1Zx1Y4SBbLCyrhj8v4JudJpylwJpOUBDe/GESXq1zqvNKvUPtI8DQNyw==
"@storybook/api@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.15.tgz#285ba42f7a8efcd3bd0e586a5e978487d826fbb4"
integrity sha512-C4D08e2ZbSe62nNKtmh9YBraoWb2j6Chw8VCkuj91kuKHh3YDNc1gjj5Fi+KYZwIcy0EllzW3RFQs+YR1/Vg1g==
dependencies:
"@reach/router" "^1.3.3"
"@storybook/channels" "6.1.14"
"@storybook/client-logger" "6.1.14"
"@storybook/core-events" "6.1.14"
"@storybook/channels" "6.1.15"
"@storybook/client-logger" "6.1.15"
"@storybook/core-events" "6.1.15"
"@storybook/csf" "0.0.1"
"@storybook/router" "6.1.14"
"@storybook/router" "6.1.15"
"@storybook/semver" "^7.3.2"
"@storybook/theming" "6.1.14"
"@types/reach__router" "^1.3.5"
"@storybook/theming" "6.1.15"
"@types/reach__router" "^1.3.7"
core-js "^3.0.1"
fast-deep-equal "^3.1.1"
global "^4.3.2"
@@ -5379,14 +5379,14 @@
qs "^6.6.0"
telejson "^5.0.2"
"@storybook/channel-postmessage@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.14.tgz#41f3115895010dad9fb30f4ac381e4f904b1e50c"
integrity sha512-If83dXXW9mKIRuvuWhWa/zkEw/F0FDgikp33x8436J3rWCh3recp27kffFRrKG0YDMpFSk/Ci5G47E9zn9SCjw==
"@storybook/channel-postmessage@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.15.tgz#80ea2346d18496f9710dd7f87fd2a9eca46ef36f"
integrity sha512-Es4B5zpLrW28KSbY8FhGVEDgUnKspJ7wPuJyKExUpZ5L9w52RkTD6lRnVPzLUfoQ4luPsExy5fiuo878/Wc9ag==
dependencies:
"@storybook/channels" "6.1.14"
"@storybook/client-logger" "6.1.14"
"@storybook/core-events" "6.1.14"
"@storybook/channels" "6.1.15"
"@storybook/client-logger" "6.1.15"
"@storybook/core-events" "6.1.15"
core-js "^3.0.1"
global "^4.3.2"
qs "^6.6.0"
@@ -5401,10 +5401,10 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/channels@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.14.tgz#c479190ebb853a603f3ed90fc470534a02eb46eb"
integrity sha512-vP19IB2FXj8SiFbQ9ETljEBienL+KRMLgMzz3Ta3nZj/OfjJJbIuj42ZfexQGV4mS0Bo+OW+qT7VMIY6fulnFw==
"@storybook/channels@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.15.tgz#22bb06a671a5ae09d2537bcf63aaf90d7f6b9f6b"
integrity sha512-HIKHDeL/0BDk9a7xc2PLiFFoHjUMKUd2djhUGdeKgdKqoWejp4JJ60fI68+2QuSRbkB8k+rAwmuWJzV7EfB5fg==
dependencies:
core-js "^3.0.1"
ts-dedent "^2.0.0"
@@ -5434,16 +5434,16 @@
ts-dedent "^2.0.0"
util-deprecate "^1.0.2"
"@storybook/client-api@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.14.tgz#6daf56743cc72e13f05fff3d2ac554897cc9f9fd"
integrity sha512-pIDSlS48bhJdtgNg7sXV1NmLJtB0ebRHJI9htIiqtL7EGQenb4+Bbwflhj1j51OEkuM+bQsAAZxq5deiUQEGVw==
"@storybook/client-api@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.15.tgz#8f8ead111459b94621571bdb2276f8a0aace17b1"
integrity sha512-iwuDlgNdB6Y4OidlhWPob3tEIax9taymdKEe9by4rLJ3nfXu7viHcvCAjN24oI4NFW3NZsmtqJotgftRYk0r1Q==
dependencies:
"@storybook/addons" "6.1.14"
"@storybook/channel-postmessage" "6.1.14"
"@storybook/channels" "6.1.14"
"@storybook/client-logger" "6.1.14"
"@storybook/core-events" "6.1.14"
"@storybook/addons" "6.1.15"
"@storybook/channel-postmessage" "6.1.15"
"@storybook/channels" "6.1.15"
"@storybook/client-logger" "6.1.15"
"@storybook/core-events" "6.1.15"
"@storybook/csf" "0.0.1"
"@types/qs" "^6.9.0"
"@types/webpack-env" "^1.15.3"
@@ -5466,10 +5466,10 @@
core-js "^3.0.1"
global "^4.3.2"
"@storybook/client-logger@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.14.tgz#216b9c1332ffa3a3473dad837780a3b14f686bae"
integrity sha512-NSO8nVsp6o0eoQ1Drlu66KXpl6DPuq02Kj8AhttGzvqSYB50SV4CV+wceBcg77tIVu5QmQ+71hAEVXhx7sjRHA==
"@storybook/client-logger@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.15.tgz#b558d6ecbee82c038d684717d8c598eaa4a9324d"
integrity sha512-lUpatG8SxzrUapWMsIPWiR+5qRVT5ebn8tGHQeBeRHXbdmEqyq5DOlrotLUemkA5nNTCs1pMFNvKSpCHznG+fg==
dependencies:
core-js "^3.0.1"
global "^4.3.2"
@@ -5500,15 +5500,15 @@
react-textarea-autosize "^8.1.1"
ts-dedent "^2.0.0"
"@storybook/components@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.14.tgz#4ea47edfa0a3e4a26882aa5a1eb90c1ec86e6f71"
integrity sha512-Nxsp/9o1tqfY8s6RBWNHyM03A5D9k56Kr/4VNa++CbDrz1+TIxpYlDgS4sllUlXyvICLfk3sUtg3KS5CPl2iZA==
"@storybook/components@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.15.tgz#b4a2af23ee6b9cba4c255191eae3d3463e29bfb7"
integrity sha512-lPbA/zyBfctdlpDhRTcRFLWlZPJ3PB4+wI0FUvYs69iG3/bNbQPYu8vRmNhCZOsaGt+b+dik4Tfcth8Bu+eQug==
dependencies:
"@popperjs/core" "^2.5.4"
"@storybook/client-logger" "6.1.14"
"@storybook/client-logger" "6.1.15"
"@storybook/csf" "0.0.1"
"@storybook/theming" "6.1.14"
"@storybook/theming" "6.1.15"
"@types/overlayscrollbars" "^1.9.0"
"@types/react-color" "^3.0.1"
"@types/react-syntax-highlighter" "11.0.4"
@@ -5533,17 +5533,17 @@
dependencies:
core-js "^3.0.1"
"@storybook/core-events@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.14.tgz#a3165e32cefd6be7326bbad4b8140653bdfa0426"
integrity sha512-tpM3VDvzqgRY7S17CRglgt1625rxNoyEwrLQiNcZkUPyO0rpaacPqVEbPCtcTmUeboI1bLdnSQIjT9B0/Y2Pww==
"@storybook/core-events@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.15.tgz#f66e30cbed8afdb8df2254d2aa47fe139e641c60"
integrity sha512-2sz02hdGZshanoq83jaB+goAcapVEWrxe+RJZn/gu2OymlEioWNjPPtOVGgi5DNIiJFnYvc66adayNwX39+tDA==
dependencies:
core-js "^3.0.1"
"@storybook/core@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.14.tgz#17e724a5b94d6e1bb557e213b8176660d2d14762"
integrity sha512-lHKZmfLAo2VGtF/yrZkkWMYgmFRNKbzIDxYJGp8USyUQyTfEpz2qqJlBdoD6rxr1hFPM2954tIKwh8iPhT2PFQ==
"@storybook/core@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/core/-/core-6.1.15.tgz#7ff8c314d3857497bf2e26c69a1fa93ef37301aa"
integrity sha512-mQeKAXcowUwF+pOdWZEFwb5M6sz4yv5cOv1vTci3/1pMmB8QpYlH+P61p4lsRO17Vlak70h18TworPka/4+mhA==
dependencies:
"@babel/core" "^7.12.3"
"@babel/plugin-proposal-class-properties" "^7.12.1"
@@ -5567,20 +5567,20 @@
"@babel/preset-react" "^7.12.1"
"@babel/preset-typescript" "^7.12.1"
"@babel/register" "^7.12.1"
"@storybook/addons" "6.1.14"
"@storybook/api" "6.1.14"
"@storybook/channel-postmessage" "6.1.14"
"@storybook/channels" "6.1.14"
"@storybook/client-api" "6.1.14"
"@storybook/client-logger" "6.1.14"
"@storybook/components" "6.1.14"
"@storybook/core-events" "6.1.14"
"@storybook/addons" "6.1.15"
"@storybook/api" "6.1.15"
"@storybook/channel-postmessage" "6.1.15"
"@storybook/channels" "6.1.15"
"@storybook/client-api" "6.1.15"
"@storybook/client-logger" "6.1.15"
"@storybook/components" "6.1.15"
"@storybook/core-events" "6.1.15"
"@storybook/csf" "0.0.1"
"@storybook/node-logger" "6.1.14"
"@storybook/router" "6.1.14"
"@storybook/node-logger" "6.1.15"
"@storybook/router" "6.1.15"
"@storybook/semver" "^7.3.2"
"@storybook/theming" "6.1.14"
"@storybook/ui" "6.1.14"
"@storybook/theming" "6.1.15"
"@storybook/ui" "6.1.15"
"@types/glob-base" "^0.3.0"
"@types/micromatch" "^4.0.1"
"@types/node-fetch" "^2.5.4"
@@ -5654,10 +5654,10 @@
dependencies:
lodash "^4.17.15"
"@storybook/node-logger@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.1.14.tgz#e5294f986e3ec5c67b2738895b9d16c9a2b667fa"
integrity sha512-3jrw7coAwFXZu4qK1vm54bCPhNRvxjG+7jISbhhocDoNIv0nLWL3+tJyrC5/k/XHQiUlLkhEzpMaASADmkttNw==
"@storybook/node-logger@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/node-logger/-/node-logger-6.1.15.tgz#fcf786d3a323feb6821e40e26f98a513a60d1a79"
integrity sha512-lrO0ei3W7BRci2iUkWTr/rXgHkzxwZTrlkx0iBzbQQRy7K1AJ9bjzhurCH9B8C9XGLmn60LXT81RWD3iCLZjcw==
dependencies:
"@types/npmlog" "^4.1.2"
chalk "^4.0.0"
@@ -5666,16 +5666,16 @@
pretty-hrtime "^1.0.3"
"@storybook/react@^6.1.11":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/react/-/react-6.1.14.tgz#436e9b90096b1d7c83f7f073b5baf47212b2e425"
integrity sha512-M99wHjc/5z+Wz1FdFaScVs6dyAi/6PdcIx5Fyip6Qd8aKwm1XyYoOMql5Vu3Cf560feDYCKS4phzyEZ7EJy+EQ==
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/react/-/react-6.1.15.tgz#e17d00b05b8980ad381ba701805309ed46d1fdcd"
integrity sha512-7WoYLOZuAlzgQsL9oy4JCr9NcB4NBCuxslPSncN5l/7ewGXgfVXTAOMOfw+EVNrtUeVJU2fC8gFiHVl0SJpTZw==
dependencies:
"@babel/preset-flow" "^7.12.1"
"@babel/preset-react" "^7.12.1"
"@pmmmwh/react-refresh-webpack-plugin" "^0.4.2"
"@storybook/addons" "6.1.14"
"@storybook/core" "6.1.14"
"@storybook/node-logger" "6.1.14"
"@storybook/addons" "6.1.15"
"@storybook/core" "6.1.15"
"@storybook/node-logger" "6.1.15"
"@storybook/semver" "^7.3.2"
"@types/webpack-env" "^1.15.3"
babel-plugin-add-react-displayname "^0.0.5"
@@ -5704,13 +5704,13 @@
memoizerific "^1.11.3"
qs "^6.6.0"
"@storybook/router@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.14.tgz#f6aef8c9dabf19bf06dddd80907e66369261fdde"
integrity sha512-rMaUCYzgfVLwFWo3A1Q/weSv8FBqCLmHY+3+t6ao7OV6NYjR0XgLKRzHrXq1uYdbMxWeIKhN2tIt/LR43bmDjQ==
"@storybook/router@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.15.tgz#e0cd7440a2ddc9b265e506b1cb590d3eeab56476"
integrity sha512-HlxDkGpiTSxXCJuqRoZ9Viq6Y/h/7efI8LPhhopr50qWRBTh/PEQzDqWBXG3sj8ISmi9GyUaTSAuqRwdA3lJQQ==
dependencies:
"@reach/router" "^1.3.3"
"@types/reach__router" "^1.3.5"
"@types/reach__router" "^1.3.7"
core-js "^3.0.1"
global "^4.3.2"
memoizerific "^1.11.3"
@@ -5759,15 +5759,15 @@
resolve-from "^5.0.0"
ts-dedent "^2.0.0"
"@storybook/theming@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.14.tgz#fecb66cab22d3b3218b4a98a9c210eb8a7be91e8"
integrity sha512-S+t30y4FqBTXWoVr+dtxVJ/ywiQGHBclBd9aUunbdCV4mMFra5InNo2CWn+RJlNEauLZ93gRIEzSFchIbzLk1A==
"@storybook/theming@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.15.tgz#01083ab89904dd959429b0b3fd1c76bd0ecc59ef"
integrity sha512-88IdYaPzp4NMKf/GKBrPggxD6/d/lkdQ4SNowXxN9g9eONd9M7HtTbjuJGRCbGMJ52xGcbpj2exEnAqKQ2iodA==
dependencies:
"@emotion/core" "^10.1.1"
"@emotion/is-prop-valid" "^0.8.6"
"@emotion/styled" "^10.0.23"
"@storybook/client-logger" "6.1.14"
"@storybook/client-logger" "6.1.15"
core-js "^3.0.1"
deep-object-diff "^1.1.0"
emotion-theming "^10.0.19"
@@ -5777,21 +5777,21 @@
resolve-from "^5.0.0"
ts-dedent "^2.0.0"
"@storybook/ui@6.1.14":
version "6.1.14"
resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.14.tgz#766d696480ee6f6a5a0454ccb2f101c38a0eb9d2"
integrity sha512-DTW2TM05jTMKxh8LzUGk3g5a528PgJxrtgODFU6zzwSg2+LwdmSDtd1HAxopt2vpfTyQyX+6WN2H+lMNwfQTAQ==
"@storybook/ui@6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@storybook/ui/-/ui-6.1.15.tgz#a0f6c49fcf81cf172cd2de4c8dba2be1296891f6"
integrity sha512-quyhJWlOxhk95he7s5/TSYM3eEsaz3s4+98kUZE6r3ssME8u6zDvqa/qa6EWs5/nvZ2V3+12efIzCNbiiT3v3g==
dependencies:
"@emotion/core" "^10.1.1"
"@storybook/addons" "6.1.14"
"@storybook/api" "6.1.14"
"@storybook/channels" "6.1.14"
"@storybook/client-logger" "6.1.14"
"@storybook/components" "6.1.14"
"@storybook/core-events" "6.1.14"
"@storybook/router" "6.1.14"
"@storybook/addons" "6.1.15"
"@storybook/api" "6.1.15"
"@storybook/channels" "6.1.15"
"@storybook/client-logger" "6.1.15"
"@storybook/components" "6.1.15"
"@storybook/core-events" "6.1.15"
"@storybook/router" "6.1.15"
"@storybook/semver" "^7.3.2"
"@storybook/theming" "6.1.14"
"@storybook/theming" "6.1.15"
"@types/markdown-to-jsx" "^6.11.0"
copy-to-clipboard "^3.0.8"
core-js "^3.0.1"
@@ -7093,6 +7093,13 @@
"@types/history" "*"
"@types/react" "*"
"@types/reach__router@^1.3.7":
version "1.3.7"
resolved "https://registry.npmjs.org/@types/reach__router/-/reach__router-1.3.7.tgz#de8ab374259ae7f7499fc1373b9697a5f3cd6428"
integrity sha512-cyBEb8Ef3SJNH5NYEIDGPoMMmYUxROatuxbICusVRQIqZUB85UCt6R2Ok60tKS/TABJsJYaHyNTW3kqbpxlMjg==
dependencies:
"@types/react" "*"
"@types/react-color@^3.0.1":
version "3.0.4"
resolved "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz#c63daf012ad067ac0127bdd86725f079d02082bd"
@@ -10780,9 +10787,9 @@ core-js-pure@^3.0.0, core-js-pure@^3.0.1:
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
core-js@3, core-js@^3.0.1, core-js@^3.0.4, core-js@^3.5.0, core-js@^3.6.0, core-js@^3.6.5:
version "3.6.5"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==
version "3.8.3"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.8.3.tgz#c21906e1f14f3689f93abcc6e26883550dd92dd0"
integrity sha512-KPYXeVZYemC2TkNEkX/01I+7yd+nX3KddKwZ1Ww7SKWdI2wQprSgLmrTddT8nw92AjEklTsPBoSdQBhbI1bQ6Q==
core-js@^2.4.0, core-js@^2.5.7, core-js@^2.6.10, core-js@^2.6.5:
version "2.6.11"
@@ -14173,10 +14180,17 @@ git-url-parse@^11.1.2:
dependencies:
git-up "^4.0.0"
git-url-parse@^11.4.3:
version "11.4.3"
resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.3.tgz#1610284edf1f14964180f5b3399ec68b692cfd87"
integrity sha512-LZTTk0nqJnKN48YRtOpR8H5SEfp1oM2tls90NuZmBxN95PnCvmuXGzqQ4QmVirBgKx2KPYfPGteX3/raWjKenQ==
git-url-parse@^11.4.4:
version "11.4.4"
resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77"
integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw==
dependencies:
git-up "^4.0.0"
git-url-parse@^11.4.4:
version "11.4.4"
resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77"
integrity sha512-Y4o9o7vQngQDIU9IjyCmRJBin5iYjI5u9ZITnddRZpD7dcCFQj2sL2XuMNbLRE4b4B/4ENPsp2Q8P44fjAZ0Pw==
dependencies:
git-up "^4.0.0"