diff --git a/.changeset/smart-turkeys-bathe.md b/.changeset/smart-turkeys-bathe.md new file mode 100644 index 0000000000..5f429b5b37 --- /dev/null +++ b/.changeset/smart-turkeys-bathe.md @@ -0,0 +1,28 @@ +--- +'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-import': minor +'@backstage/catalog-model': patch +'@backstage/plugin-scaffolder': patch +--- + +Add Analyze location endpoint to catalog backend. Add catalog-import plugin and replace import-component with it. To start using Analyze location endpoint, you have add it to the `createRouter` function options in the `\backstage\packages\backend\src\plugins\catalog.ts` file: + +```ts +export default async function createPlugin(env: PluginEnvironment) { + const builder = new CatalogBuilder(env); + const { + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + locationAnalyzer, //<-- + } = await builder.build(); + + return await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + locationAnalyzer, //<-- + logger: env.logger, + }); +} +``` diff --git a/packages/app/package.json b/packages/app/package.json index bde325ed46..7469674626 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -9,6 +9,7 @@ "@backstage/core": "^0.3.2", "@backstage/plugin-api-docs": "^0.3.0", "@backstage/plugin-catalog": "^0.2.4", + "@backstage/plugin-catalog-import": "^0.2.0", "@backstage/plugin-circleci": "^0.2.2", "@backstage/plugin-cloudbuild": "^0.2.2", "@backstage/plugin-cost-insights": "^0.4.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 81d08f58a9..28aebeeb79 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -34,6 +34,7 @@ import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component'; import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; +import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; import { Route, Routes, Navigate } from 'react-router'; import { EntityPage } from './components/catalog/EntityPage'; @@ -67,6 +68,10 @@ const catalogRouteRef = createRouteRef({ const AppRoutes = () => ( + } + /> } diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 87b4c0ceea..a02365d17d 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -37,6 +37,7 @@ export { plugin as Kubernetes } from '@backstage/plugin-kubernetes'; export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; export { plugin as CostInsights } from '@backstage/plugin-cost-insights'; export { plugin as GitHubInsights } from '@roadiehq/backstage-plugin-github-insights'; +export { plugin as CatalogImport } from '@backstage/plugin-catalog-import'; export { plugin as UserSettings } from '@backstage/plugin-user-settings'; export { plugin as Buildkite } from '@roadiehq/backstage-plugin-buildkite'; export { plugin as Search } from '@backstage/plugin-search'; diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 3c205a3800..1338fab440 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -28,6 +28,7 @@ export default async function createPlugin(env: PluginEnvironment) { entitiesCatalog, locationsCatalog, higherOrderOperation, + locationAnalyzer, } = await builder.build(); useHotCleanup( @@ -39,6 +40,7 @@ export default async function createPlugin(env: PluginEnvironment) { entitiesCatalog, locationsCatalog, higherOrderOperation, + locationAnalyzer, logger: env.logger, }); } diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index ff696524ab..ce64b988a6 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -15,5 +15,9 @@ */ export type { Location, LocationSpec } from './types'; -export { locationSchema, locationSpecSchema } from './validation'; +export { + locationSchema, + locationSpecSchema, + analyzeLocationSchema, +} from './validation'; export { LOCATION_ANNOTATION } from './annotation'; diff --git a/packages/catalog-model/src/location/validation.ts b/packages/catalog-model/src/location/validation.ts index 27d74b9a82..4d2e602862 100644 --- a/packages/catalog-model/src/location/validation.ts +++ b/packages/catalog-model/src/location/validation.ts @@ -34,3 +34,10 @@ export const locationSchema = yup }) .noUnknown() .required(); + +export const analyzeLocationSchema = yup + .object<{ location: LocationSpec }>({ + location: locationSpecSchema, + }) + .noUnknown() + .required(); diff --git a/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts b/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts new file mode 100644 index 0000000000..97df326fc5 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/LocationAnalyzer.ts @@ -0,0 +1,52 @@ +/* + * 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 { Logger } from 'winston'; +import parseGitUri from 'git-url-parse'; +import { + AnalyzeLocationRequest, + AnalyzeLocationResponse, + LocationAnalyzer, +} from './types'; + +export class RepoLocationAnalyzer implements LocationAnalyzer { + private readonly logger: Logger; + + constructor(logger: Logger) { + this.logger = logger; + } + async analyzeLocation( + request: AnalyzeLocationRequest, + ): Promise { + const { owner, name, source } = parseGitUri(request.location.target); + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: name, + // Probably won't handle properly self-hosted git providers with custom url + annotations: { [`${source}/project-slug`]: `${owner}/${name}` }, + }, + spec: { type: 'other', lifecycle: 'unknown' }, + }; + + this.logger.debug(`entity created for ${request.location.target}`); + return { + existingEntityFiles: [], + generateEntities: [{ entity, fields: [] }], + }; + } +} diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index 4ebb2edc79..fad4ae34f7 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -16,12 +16,5 @@ export { HigherOrderOperations } from './HigherOrderOperations'; export { LocationReaders } from './LocationReaders'; -export type { - AddLocationResult, - HigherOrderOperation, - LocationReader, - ReadLocationEntity, - ReadLocationError, - ReadLocationResult, -} from './types'; +export * from './types'; export * from './processors'; diff --git a/plugins/catalog-backend/src/ingestion/types.ts b/plugins/catalog-backend/src/ingestion/types.ts index a83b4adecc..c8d642b6b5 100644 --- a/plugins/catalog-backend/src/ingestion/types.ts +++ b/plugins/catalog-backend/src/ingestion/types.ts @@ -14,12 +14,13 @@ * limitations under the License. */ -import type { +import { Entity, EntityRelationSpec, Location, LocationSpec, } from '@backstage/catalog-model'; +import { RecursivePartial } from '../util/RecursivePartial'; // // HigherOrderOperation @@ -68,3 +69,75 @@ export type ReadLocationError = { location: LocationSpec; error: Error; }; + +// +// LocationAnalyzer +// + +export type LocationAnalyzer = { + /** + * Generates an entity configuration for given git repository. It's used for + * importing new component to the backstage app. + * + * @param location Git repository to analyze and generate config for. + */ + analyzeLocation( + location: AnalyzeLocationRequest, + ): Promise; +}; + +export type AnalyzeLocationRequest = { + location: LocationSpec; +}; + +export type AnalyzeLocationResponse = { + existingEntityFiles: AnalyzeLocationExistingEntity[]; + generateEntities: AnalyzeLocationGenerateEntity[]; +}; + +// If the folder pointed to already contained catalog info yaml files, they are +// read and emitted like this so that the frontend can inform the user that it +// located them and can make sure to register them as well if they weren't +// already +type AnalyzeLocationExistingEntity = { + location: LocationSpec; + isRegistered: boolean; + entity: Entity; +}; + +// This is some form of representation of what the analyzer could deduce. +// We should probably have a chat about how this can best be conveyed to +// the frontend. It'll probably contain a (possibly incomplete) entity, plus +// enough info for the frontend to know what form data to show to the user +// for overriding/completing the info. +type AnalyzeLocationGenerateEntity = { + // Some form of partial representation of the entity + entity: RecursivePartial; + // Lists the suggestions that the user may want to override + fields: AnalyzeLocationEntityField[]; +}; + +// This is where I get really vague. Something like this perhaps? Or it could be +// something like a json-schema that contains enough info for the frontend to +// be able to present a form and explanations +type AnalyzeLocationEntityField = { + // e.g. "spec.owner"? The frontend needs to know how to "inject" the field into the + // entity again if the user wants to change it + field: string; + + // The outcome of the analysis for this particular field + state: + | 'analysisSuggestedValue' + | 'analysisSuggestedNoValue' + | 'needsUserInput'; + + // If the analysis did suggest a value, this is where it would be. Not sure if we want + // to limit this to strings or if we want it to be any JsonValue + value: string | null; + + // A text to show to the user to inform about the choices made. Like, it could say + // "Found a CODEOWNERS file that covers this target, so we suggest leaving this + // field empty; which would currently make it owned by X" where X is taken from the + // codeowners file. + description: string; +}; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 4be0f1799b..856d5e54f2 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -54,11 +54,13 @@ import { UrlReaderProcessor, } from '../ingestion'; import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; +import { RepoLocationAnalyzer } from '../ingestion/LocationAnalyzer'; import { jsonPlaceholderResolver, textPlaceholderResolver, yamlPlaceholderResolver, } from '../ingestion/processors/PlaceholderProcessor'; +import { LocationAnalyzer } from '../ingestion/types'; export type CatalogEnvironment = { logger: Logger; @@ -202,6 +204,7 @@ export class CatalogBuilder { entitiesCatalog: EntitiesCatalog; locationsCatalog: LocationsCatalog; higherOrderOperation: HigherOrderOperation; + locationAnalyzer: LocationAnalyzer; }> { const { config, database, logger } = this.env; @@ -229,11 +232,13 @@ export class CatalogBuilder { locationReader, logger, ); + const locationAnalyzer = new RepoLocationAnalyzer(logger); return { entitiesCatalog, locationsCatalog, higherOrderOperation, + locationAnalyzer, }; } diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index ebad544bef..43c2967233 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -15,29 +15,38 @@ */ import { errorHandler } from '@backstage/backend-common'; +import { + locationSpecSchema, + analyzeLocationSchema, +} from '@backstage/catalog-model'; import type { Entity } from '@backstage/catalog-model'; -import { locationSpecSchema } from '@backstage/catalog-model'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import yn from 'yn'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; -import { HigherOrderOperation } from '../ingestion/types'; -import { EntityFilters } from './EntityFilters'; +import { LocationAnalyzer, HigherOrderOperation } from '../ingestion/types'; import { translateQueryToFieldMapper } from './filterQuery'; +import { EntityFilters } from './EntityFilters'; import { requireRequestBody, validateRequestBody } from './util'; export interface RouterOptions { entitiesCatalog?: EntitiesCatalog; locationsCatalog?: LocationsCatalog; higherOrderOperation?: HigherOrderOperation; + locationAnalyzer?: LocationAnalyzer; logger: Logger; } export async function createRouter( options: RouterOptions, ): Promise { - const { entitiesCatalog, locationsCatalog, higherOrderOperation } = options; + const { + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + locationAnalyzer, + } = options; const router = Router(); router.use(express.json()); @@ -127,6 +136,14 @@ export async function createRouter( }); } + if (locationAnalyzer) { + router.post('/analyze-location', async (req, res) => { + const input = await validateRequestBody(req, analyzeLocationSchema); + const output = await locationAnalyzer.analyzeLocation(input); + res.status(200).send(output); + }); + } + router.use(errorHandler()); return router; } diff --git a/plugins/catalog-import/.eslintrc.js b/plugins/catalog-import/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/catalog-import/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/catalog-import/README.md b/plugins/catalog-import/README.md new file mode 100644 index 0000000000..1dc0d7cf89 --- /dev/null +++ b/plugins/catalog-import/README.md @@ -0,0 +1,21 @@ +# Catalog import plugin + +Welcome to the catalog-import plugin! + +This plugin allows you to bootstrap a component-config YAML file for your repository and open a pull request to add it. + +When installed it is accessible on [localhost:3000/catalog-import](localhost:3000/catalog-import). + + + +## Running + +Just run the backstage. + +``` +yarn start && yarn --cwd packages/backend start +``` + +## Usage + +Pretty straightforward, navigate to [localhost:3000/catalog-import](localhost:3000/catalog-import) and enter your repo's URL. diff --git a/plugins/catalog-import/dev/index.tsx b/plugins/catalog-import/dev/index.tsx new file mode 100644 index 0000000000..812a5585d4 --- /dev/null +++ b/plugins/catalog-import/dev/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json new file mode 100644 index 0000000000..11562b741e --- /dev/null +++ b/plugins/catalog-import/package.json @@ -0,0 +1,57 @@ +{ + "name": "@backstage/plugin-catalog-import", + "version": "0.2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.3.0", + "@backstage/core": "^0.3.2", + "@backstage/plugin-catalog": "^0.2.0", + "@backstage/plugin-catalog-backend": "^0.2.2", + "@backstage/theme": "^0.2.1", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@octokit/rest": "^18.0.6", + "git-url-parse": "^11.4.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-hook-form": "^6.6.0", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3", + "yaml": "^1.10.0" + }, + "devDependencies": { + "@backstage/cli": "^0.3.2", + "@backstage/dev-utils": "^0.1.4", + "@backstage/test-utils": "^0.1.3", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts new file mode 100644 index 0000000000..49012764e2 --- /dev/null +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -0,0 +1,35 @@ +/* + * 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 { createApiRef } from '@backstage/core'; +import { PartialEntity } from '../util/types'; + +export const catalogImportApiRef = createApiRef({ + id: 'plugin.catalogimport.service', + description: 'Used by the catalog import plugin to make requests', +}); + +export interface CatalogImportApi { + submitPrToRepo(options: { + owner: string; + repo: string; + fileContent: string; + }): Promise<{ link: string; location: string }>; + createRepositoryLocation(options: { location: string }): Promise; + generateEntityDefinitions(options: { + repo: string; + }): Promise; +} diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts new file mode 100644 index 0000000000..94ba260d55 --- /dev/null +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -0,0 +1,192 @@ +/* + * 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 { Octokit } from '@octokit/rest'; +import { DiscoveryApi, OAuthApi } from '@backstage/core'; +import { CatalogImportApi } from './CatalogImportApi'; +import { AnalyzeLocationResponse } from '@backstage/plugin-catalog-backend'; +import { PartialEntity } from '../util/types'; + +export class CatalogImportClient implements CatalogImportApi { + private readonly discoveryApi: DiscoveryApi; + private readonly githubAuthApi: OAuthApi; + + constructor(options: { + discoveryApi: DiscoveryApi; + githubAuthApi: OAuthApi; + }) { + this.discoveryApi = options.discoveryApi; + this.githubAuthApi = options.githubAuthApi; + } + + async generateEntityDefinitions({ + repo, + }: { + repo: string; + }): Promise { + const response = await fetch( + `${await this.discoveryApi.getBaseUrl('catalog')}/analyze-location`, + { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + location: { type: 'github', target: repo }, + }), + }, + ).catch(e => { + throw new Error(`Failed to generate entity definitions, ${e.message}`); + }); + if (!response.ok) { + throw new Error( + `Failed to generate entity definitions. Received http response ${response.status}: ${response.statusText}`, + ); + } + + const payload = (await response.json()) as AnalyzeLocationResponse; + return payload.generateEntities.map(x => x.entity); + } + + async createRepositoryLocation({ + location, + }: { + location: string; + }): Promise { + const response = await fetch( + `${await this.discoveryApi.getBaseUrl('catalog')}/locations`, + { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + type: 'github', + target: location, + presence: 'optional', + }), + }, + ); + if (!response.ok) { + throw new Error( + `Received http response ${response.status}: ${response.statusText}`, + ); + } + } + + async submitPrToRepo({ + owner, + repo, + fileContent, + }: { + owner: string; + repo: string; + fileContent: string; + }): Promise<{ link: string; location: string }> { + const token = await this.githubAuthApi.getAccessToken(['repo']); + + const octo = new Octokit({ + auth: token, + }); + + const branchName = 'backstage-integration'; + const fileName = 'catalog-info.yaml'; + + const repoData = await octo.repos + .get({ + owner, + repo, + }) + .catch(e => { + throw new Error(formatHttpErrorMessage("Couldn't fetch repo data", e)); + }); + + const parentRef = await octo.git + .getRef({ + owner, + repo, + ref: `heads/${repoData.data.default_branch}`, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage("Couldn't fetch default branch data", e), + ); + }); + + await octo.git + .createRef({ + owner, + repo, + ref: `refs/heads/${branchName}`, + sha: parentRef.data.object.sha, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage( + `Couldn't create a new branch with name '${branchName}'`, + e, + ), + ); + }); + + await octo.repos + .createOrUpdateFileContents({ + owner, + repo, + path: fileName, + message: `Add ${fileName} config file`, + content: btoa(fileContent), + branch: branchName, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage( + `Couldn't create a commit with ${fileName} file added`, + e, + ), + ); + }); + + const pullRequestRespone = await octo.pulls + .create({ + owner, + repo, + title: `Add ${fileName} config file`, + head: branchName, + base: repoData.data.default_branch, + }) + .catch(e => { + throw new Error( + formatHttpErrorMessage( + `Couldn't create a pull request for ${branchName} branch`, + e, + ), + ); + }); + + return { + link: pullRequestRespone.data.html_url, + location: `https://github.com/${owner}/${repo}/blob/${repoData.data.default_branch}/${fileName}`, + }; + } +} + +function formatHttpErrorMessage( + message: string, + error: { status: number; message: string }, +) { + return `${message}, received http response status code ${error.status}: ${error.message}`; +} diff --git a/plugins/catalog-import/src/api/index.ts b/plugins/catalog-import/src/api/index.ts new file mode 100644 index 0000000000..078342d284 --- /dev/null +++ b/plugins/catalog-import/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export * from './CatalogImportApi'; +export * from './CatalogImportClient'; diff --git a/plugins/catalog-import/src/assets/catalog-import-screenshot.png b/plugins/catalog-import/src/assets/catalog-import-screenshot.png new file mode 100644 index 0000000000..7ba8ae9eb5 Binary files /dev/null and b/plugins/catalog-import/src/assets/catalog-import-screenshot.png differ diff --git a/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx new file mode 100644 index 0000000000..f3dda35003 --- /dev/null +++ b/plugins/catalog-import/src/components/ComponentConfigDisplay.tsx @@ -0,0 +1,197 @@ +/* + * 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 React, { useCallback, useState } from 'react'; +import { + Button, + CircularProgress, + Grid, + Link, + List, + ListItem, + Typography, + Divider, +} from '@material-ui/core'; +import { useGithubRepos } from '../util/useGithubRepos'; +import { ConfigSpec } from './ImportComponentPage'; +import { + errorApiRef, + RouteRef, + StructuredMetadataTable, + useApi, +} from '@backstage/core'; +import parseGitUri from 'git-url-parse'; +import { PartialEntity } from '../util/types'; +import { generatePath, resolvePath } from 'react-router'; +import { entityRoute, entityRouteParams } from '@backstage/plugin-catalog'; +import { Entity } from '@backstage/catalog-model'; +import { Link as RouterLink } from 'react-router-dom'; +import * as YAML from 'yaml'; + +const getEntityCatalogPath = ({ + entity, + catalogRouteRef, +}: { + entity: PartialEntity; + catalogRouteRef: RouteRef; +}) => { + const relativeEntityPathInsideCatalog = generatePath( + entityRoute.path, + entityRouteParams(entity as Entity), + ); + + const resolvedAbsolutePath = resolvePath( + relativeEntityPathInsideCatalog, + catalogRouteRef.path, + )?.pathname; + + return resolvedAbsolutePath; +}; + +type Props = { + nextStep: (options?: { reset: boolean }) => void; + configFile: ConfigSpec; + savePRLink: (PRLink: string) => void; + catalogRouteRef: RouteRef; +}; + +const ComponentConfigDisplay = ({ + nextStep, + configFile, + savePRLink, + catalogRouteRef, +}: Props) => { + const [errorOccured, setErrorOccured] = useState(false); + const [submitting, setSubmitting] = useState(false); + const errorApi = useApi(errorApiRef); + const { submitPrToRepo, addLocation } = useGithubRepos(); + const onNext = useCallback(async () => { + try { + setSubmitting(true); + if (!parseGitUri(configFile.location).filepathtype) { + const result = await submitPrToRepo(configFile); + savePRLink(result.link); + setSubmitting(false); + nextStep(); + } else { + addLocation(configFile.location); + setSubmitting(false); + nextStep(); + } + } catch (e) { + setErrorOccured(true); + setSubmitting(false); + errorApi.post(e); + } + }, [submitPrToRepo, configFile, nextStep, savePRLink, errorApi, addLocation]); + + return ( + + {!parseGitUri(configFile.location).filepathtype ? ( + + Following config object will be submitted in a pull request to the + repository{' '} + + {configFile.location} + {' '} + and added as a new location to the backend + + ) : ( + + Following config object will be added as a new location to the backend{' '} + + {configFile.location} + + + )} + + + {!parseGitUri(configFile.location).filepathtype ? ( +
{YAML.stringify(configFile.config)}
+ ) : ( + + {configFile.config.map((entity: any, index: number) => { + const entityPath = getEntityCatalogPath({ + entity, + catalogRouteRef, + }); + return ( + + + + {entityPath} + + ), + }} + /> + + {index < configFile.config.length - 1 && ( + + )} + + ); + })} + + )} +
+ + {submitting ? ( + + + + ) : null} + + + {errorOccured ? ( + + ) : null} + + +
+ ); +}; + +export default ComponentConfigDisplay; diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx new file mode 100644 index 0000000000..9a4fd38a06 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -0,0 +1,131 @@ +/* + * 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 { errorApiRef, useApi } from '@backstage/core'; +import { BackstageTheme } from '@backstage/theme'; +import { + Button, + FormControl, + FormHelperText, + TextField, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { useMountedState } from 'react-use'; +import parseGitUri from 'git-url-parse'; +import { ComponentIdValidators } from '../util/validate'; +import { useGithubRepos } from '../util/useGithubRepos'; +import { ConfigSpec } from './ImportComponentPage'; +import { catalogApiRef } from '@backstage/plugin-catalog'; + +const useStyles = makeStyles(theme => ({ + form: { + alignItems: 'flex-start', + display: 'flex', + flexFlow: 'column nowrap', + }, + submit: { + marginTop: theme.spacing(1), + }, +})); + +type Props = { + nextStep: () => void; + saveConfig: (configFile: ConfigSpec) => void; +}; + +export const RegisterComponentForm = ({ nextStep, saveConfig }: Props) => { + const { register, handleSubmit, errors, formState } = useForm({ + mode: 'onChange', + }); + const classes = useStyles(); + const hasErrors = !!errors.componentLocation; + const dirty = formState?.isDirty; + const catalogApi = useApi(catalogApiRef); + + const isMounted = useMountedState(); + const errorApi = useApi(errorApiRef); + const { generateEntityDefinitions } = useGithubRepos(); + + const onSubmit = async (formData: Record) => { + const { componentLocation: target } = formData; + try { + if (!isMounted()) return; + const type = !parseGitUri(target).filepathtype ? 'repo' : 'file'; + + if (type === 'repo') { + saveConfig({ + type, + location: target, + config: await generateEntityDefinitions(target), + }); + } else { + const data = await catalogApi.addLocation({ target }); + saveConfig({ + type, + location: data.location.target, + config: data.entities, + }); + } + nextStep(); + } catch (e) { + errorApi.post(e); + } + }; + + return ( +
+ + + + {errors.componentLocation && ( + + {errors.componentLocation.message} + + )} + + + +
+ ); +}; diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx new file mode 100644 index 0000000000..c2eb935e54 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -0,0 +1,111 @@ +/* + * 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 React, { useState } from 'react'; +import { Grid } from '@material-ui/core'; +import { + InfoCard, + Page, + Content, + Header, + SupportButton, + ContentHeader, + RouteRef, +} from '@backstage/core'; +import { RegisterComponentForm } from './ImportComponentForm'; +import ImportStepper from './ImportStepper'; +import ComponentConfigDisplay from './ComponentConfigDisplay'; +import { ImportFinished } from './ImportFinished'; +import { PartialEntity } from '../util/types'; + +export type ConfigSpec = { + type: 'repo' | 'file'; + location: string; + config: PartialEntity[]; +}; + +export const ImportComponentPage = ({ + catalogRouteRef, +}: { + catalogRouteRef: RouteRef; +}) => { + const [activeStep, setActiveStep] = useState(0); + const [configFile, setConfigFile] = useState({ + type: 'repo', + location: '', + config: [], + }); + const [endLink, setEndLink] = useState(''); + const nextStep = (options?: { reset: boolean }) => { + setActiveStep(step => (options?.reset ? 0 : step + 1)); + }; + + return ( + +
+ + + + Start tracking your component in Backstage. TODO: Add more + information about what this is. + + + + + + + ), + }, + { + step: 'Review', + content: ( + + ), + }, + { + step: 'Finish', + content: ( + + ), + }, + ]} + activeStep={activeStep} + nextStep={nextStep} + /> + + + + + + ); +}; diff --git a/plugins/catalog-import/src/components/ImportFinished.tsx b/plugins/catalog-import/src/components/ImportFinished.tsx new file mode 100644 index 0000000000..caba678999 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportFinished.tsx @@ -0,0 +1,58 @@ +/* + * 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 React from 'react'; +import { Alert } from '@material-ui/lab'; +import { Button, Grid, Link } from '@material-ui/core'; + +type Props = { + type: 'repo' | 'file'; + nextStep: (options?: { reset: boolean }) => void; + PRLink: string; +}; + +export const ImportFinished = ({ nextStep, PRLink, type }: Props) => { + return ( + + + + {type === 'repo' + ? 'Pull requests have been successfully opened. You can start again to import more repositories' + : 'Entity added to catalog successfully'} + + + + {type === 'repo' ? ( + + View pull request on GitHub + + ) : null} + + + + ); +}; diff --git a/plugins/catalog-import/src/components/ImportStepper.tsx b/plugins/catalog-import/src/components/ImportStepper.tsx new file mode 100644 index 0000000000..03752fa7d4 --- /dev/null +++ b/plugins/catalog-import/src/components/ImportStepper.tsx @@ -0,0 +1,42 @@ +/* + * 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 React from 'react'; +import Stepper from '@material-ui/core/Stepper'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import { StepContent } from '@material-ui/core'; + +type Props = { + steps: { step: string; content: React.ReactNode }[]; + activeStep: number; + nextStep: (options?: { reset: boolean }) => void; +}; + +export default function ImportStepper({ steps, activeStep }: Props) { + return ( + + {steps.map(({ step }) => { + const stepProps: { completed?: boolean } = {}; + return ( + + {step} + {steps[activeStep].content} + + ); + })} + + ); +} diff --git a/plugins/catalog-import/src/components/Router.tsx b/plugins/catalog-import/src/components/Router.tsx new file mode 100644 index 0000000000..5175f8f077 --- /dev/null +++ b/plugins/catalog-import/src/components/Router.tsx @@ -0,0 +1,28 @@ +/* + * 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 { RouteRef } from '@backstage/core'; +import React from 'react'; +import { Route, Routes } from 'react-router-dom'; +import { ImportComponentPage } from './ImportComponentPage'; + +export const Router = ({ catalogRouteRef }: { catalogRouteRef: RouteRef }) => ( + + } + /> + +); diff --git a/plugins/catalog-import/src/index.ts b/plugins/catalog-import/src/index.ts new file mode 100644 index 0000000000..ff7857cacd --- /dev/null +++ b/plugins/catalog-import/src/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { plugin } from './plugin'; +export { Router } from './components/Router'; diff --git a/plugins/catalog-import/src/plugin.test.ts b/plugins/catalog-import/src/plugin.test.ts new file mode 100644 index 0000000000..2f5964946c --- /dev/null +++ b/plugins/catalog-import/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * 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 { plugin } from './plugin'; + +describe('catalog-import', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts new file mode 100644 index 0000000000..3ce4918553 --- /dev/null +++ b/plugins/catalog-import/src/plugin.ts @@ -0,0 +1,42 @@ +/* + * 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 { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, + githubAuthApiRef, +} from '@backstage/core'; +import { catalogImportApiRef } from './api/CatalogImportApi'; +import { CatalogImportClient } from './api/CatalogImportClient'; + +export const rootRouteRef = createRouteRef({ + path: '', + title: 'catalog-import', +}); + +export const plugin = createPlugin({ + id: 'catalog-import', + apis: [ + createApiFactory({ + api: catalogImportApiRef, + deps: { discoveryApi: discoveryApiRef, githubAuthApi: githubAuthApiRef }, + factory: ({ discoveryApi, githubAuthApi }) => + new CatalogImportClient({ discoveryApi, githubAuthApi }), + }), + ], +}); diff --git a/plugins/catalog-import/src/setupTests.ts b/plugins/catalog-import/src/setupTests.ts new file mode 100644 index 0000000000..825bcd4115 --- /dev/null +++ b/plugins/catalog-import/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; diff --git a/plugins/catalog-import/src/util/types.ts b/plugins/catalog-import/src/util/types.ts new file mode 100644 index 0000000000..f23649a2a9 --- /dev/null +++ b/plugins/catalog-import/src/util/types.ts @@ -0,0 +1,27 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; + +export type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object + ? RecursivePartial + : T[P]; +}; + +export type PartialEntity = RecursivePartial; diff --git a/plugins/catalog-import/src/util/useGithubRepos.ts b/plugins/catalog-import/src/util/useGithubRepos.ts new file mode 100644 index 0000000000..a18edc2777 --- /dev/null +++ b/plugins/catalog-import/src/util/useGithubRepos.ts @@ -0,0 +1,57 @@ +/* + * 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 * as YAML from 'yaml'; +import { useApi } from '@backstage/core'; +import { catalogImportApiRef } from '../api/CatalogImportApi'; +import { ConfigSpec } from '../components/ImportComponentPage'; + +export function useGithubRepos() { + const api = useApi(catalogImportApiRef); + + const submitPrToRepo = async (selectedRepo: ConfigSpec) => { + const [ownerName, repoName] = selectedRepo.location.split('/').slice(-2); + const submitPRResponse = await api + .submitPrToRepo({ + owner: ownerName, + repo: repoName, + fileContent: selectedRepo.config + .map(entity => `---\n${YAML.stringify(entity)}`) + .join('\n'), + }) + .catch(e => { + throw new Error(`Failed to submit PR to repo:\n${e.message}`); + }); + + await api + .createRepositoryLocation({ + location: submitPRResponse.location, + }) + .catch(e => { + throw new Error(`Failed to create repository location:\n${e.message}`); + }); + + return submitPRResponse; + }; + + return { + submitPrToRepo, + generateEntityDefinitions: (repo: string) => + api.generateEntityDefinitions({ repo }), + addLocation: (location: string) => + api.createRepositoryLocation({ location }), + }; +} diff --git a/plugins/catalog-import/src/util/validate.test.ts b/plugins/catalog-import/src/util/validate.test.ts new file mode 100644 index 0000000000..139c7fb0f8 --- /dev/null +++ b/plugins/catalog-import/src/util/validate.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { ComponentIdValidators } from './validate'; + +describe('ComponentIdValidators', () => { + describe('httpsValidator validator', () => { + const errorMessage = 'Must start with https://.'; + test.each([ + [true, 'https://example.com'], + [errorMessage, 'http://example.com'], + [errorMessage, 'example.com'], + [errorMessage, 'www.example.com'], + [errorMessage, ''], + [errorMessage, undefined], + ])('should return %p for %s', (expected: string | boolean, arg: any) => { + expect(ComponentIdValidators.httpsValidator(arg)).toBe(expected); + }); + }); +}); diff --git a/plugins/catalog-import/src/util/validate.ts b/plugins/catalog-import/src/util/validate.ts new file mode 100644 index 0000000000..056131aaae --- /dev/null +++ b/plugins/catalog-import/src/util/validate.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export const ComponentIdValidators = { + httpsValidator: (value: any) => + (typeof value === 'string' && value.match(/^https:\/\//) !== null) || + 'Must start with https://.', +}; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index abdb411ad9..39999f938f 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -83,7 +83,7 @@ export const ScaffolderPage = () => { variant="contained" color="primary" component={RouterLink} - to="/register-component" + to="/catalog-import" > Register Existing Component diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 3c05b406da..8e3266a8fa 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -46,7 +46,7 @@ const getFilesToLint = () => { .toString() .split('\n') .filter(function (el) { - return el != ''; + return el !== ''; }); }; diff --git a/yarn.lock b/yarn.lock index d67d0730c0..ddf679e8d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3515,6 +3515,14 @@ "@octokit/types" "^5.4.1" deprecation "^2.3.1" +"@octokit/plugin-rest-endpoint-methods@4.2.0": + version "4.2.0" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz#c5a0691b3aba5d8b4ef5dffd6af3649608f167ba" + integrity sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw== + dependencies: + "@octokit/types" "^5.5.0" + deprecation "^2.3.1" + "@octokit/request-error@^1.0.2": version "1.2.1" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" @@ -3579,6 +3587,16 @@ "@octokit/plugin-request-log" "^1.0.0" "@octokit/plugin-rest-endpoint-methods" "4.1.4" +"@octokit/rest@^18.0.6": + version "18.0.6" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.6.tgz#76c274f1a68f40741a131768ef483f041e7b98b6" + integrity sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w== + dependencies: + "@octokit/core" "^3.0.0" + "@octokit/plugin-paginate-rest" "^2.2.0" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "4.2.0" + "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.5.0" resolved "https://registry.npmjs.org/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773" @@ -3600,6 +3618,13 @@ dependencies: "@types/node" ">= 8" +"@octokit/types@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" + integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== + dependencies: + "@types/node" ">= 8" + "@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca"