diff --git a/.changeset/angry-bobcats-sit.md b/.changeset/angry-bobcats-sit.md new file mode 100644 index 0000000000..ad02ff68da --- /dev/null +++ b/.changeset/angry-bobcats-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +The `subscribe` method on the `Config` returned by `loadBackendConfig` is now forwarded through `getConfig` and `getOptionalConfig`. diff --git a/.changeset/brave-shoes-push.md b/.changeset/brave-shoes-push.md new file mode 100644 index 0000000000..3c6a8d7e0e --- /dev/null +++ b/.changeset/brave-shoes-push.md @@ -0,0 +1,40 @@ +--- +'@backstage/create-app': patch +--- + +Added the default `ScmAuth` implementation to the app. + +To apply this change to an existing app, head to `packages/app/apis.ts`, import `ScmAuth` from `@backstage/integration-react`, and add a `ScmAuth.createDefaultApiFactory()` to your list of APIs: + +```diff + import { + ScmIntegrationsApi, + scmIntegrationsApiRef, ++ ScmAuth, + } from '@backstage/integration-react'; + + export const apis: AnyApiFactory[] = [ +... ++ ScmAuth.createDefaultApiFactory(), +... + ]; +``` + +If you have integrations towards SCM providers other than the default ones (github.com, gitlab.com, etc.), you will want to create a custom `ScmAuth` factory instead, for example like this: + +```ts +createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: gheAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghe.example.com', + }), + ), +}); +``` diff --git a/.changeset/curly-months-count.md b/.changeset/curly-months-count.md new file mode 100644 index 0000000000..8541c8c5c2 --- /dev/null +++ b/.changeset/curly-months-count.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed a bug where internal references within the catalog were broken when new entities where added through entity providers, such as registering a new location or adding one in configuration. These broken references then caused some entities to be incorrectly marked as orphaned and prevented refresh from working properly. diff --git a/.changeset/few-penguins-watch.md b/.changeset/few-penguins-watch.md deleted file mode 100644 index 3d7d9fb457..0000000000 --- a/.changeset/few-penguins-watch.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -'@backstage/plugin-catalog-import': minor ---- - -Add initial support for customizing the catalog import page. - -It is now possible to pass a custom layout to the import page, as it's already -supported by the search page. If no custom layout is passed, the default layout -is used. - -```typescript -}> - -
- - - - Start tracking your component in Backstage by adding it to the - software catalog. - - - - - - Hello World - - - - - - - - - -``` - -Previously it was possible to disable and customize the automatic pull request -feature by passing options to `` (`pullRequest.disable` and -`pullRequest.preparePullRequest`). This functionality is moved to the -`CatalogImportApi` which now provides an optional `preparePullRequest()` -function. The function can either be overridden to generate a different content -for the pull request, or removed to disable this feature. - -The export of the long term deprecated legacy `` is removed, migrate to -`` instead. diff --git a/.changeset/great-rabbits-juggle.md b/.changeset/great-rabbits-juggle.md new file mode 100644 index 0000000000..649af14aeb --- /dev/null +++ b/.changeset/great-rabbits-juggle.md @@ -0,0 +1,36 @@ +--- +'@backstage/plugin-catalog-import': minor +--- + +Switched to using the `ScmAuthApi` for authentication rather than GitHub auth. If you are instantiating your `CatalogImportClient` manually you now need to pass in an instance of `ScmAuthApi` instead. + +Also be sure to register the `scmAuthApiRef` from the `@backstage/integration-react` in your app: + +```ts +import { ScmAuth } from '@backstage/integration-react'; + +// in packages/app/apis.ts + +const apis = [ +// ... other APIs + +ScmAuth.createDefaultApiFactory(); + +// OR + +createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: gheAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghe.example.com', + }), + ), +}); +] +``` diff --git a/.changeset/heavy-rabbits-melt.md b/.changeset/heavy-rabbits-melt.md new file mode 100644 index 0000000000..cb836588f6 --- /dev/null +++ b/.changeset/heavy-rabbits-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The `create-plugin` command now prefers dependency versions ranges that are already in the lockfile. diff --git a/.changeset/khaki-hotels-doubt.md b/.changeset/khaki-hotels-doubt.md new file mode 100644 index 0000000000..8bab499f3e --- /dev/null +++ b/.changeset/khaki-hotels-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Support selective GitHub app installation for GHE diff --git a/.changeset/lovely-cycles-shout.md b/.changeset/lovely-cycles-shout.md deleted file mode 100644 index f65e2520bf..0000000000 --- a/.changeset/lovely-cycles-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-client': patch ---- - -Update `AddLocationResponse` to optionally return `exists` to signal that the location already exists, this is only returned when calling `addLocation` in dryRun. diff --git a/.changeset/lovely-keys-occur.md b/.changeset/lovely-keys-occur.md deleted file mode 100644 index b01ccd3bf5..0000000000 --- a/.changeset/lovely-keys-occur.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Update `createLocation` to optionally return `exists` to signal that the location already exists, this is only returned for dry runs. diff --git a/.changeset/mighty-needles-know.md b/.changeset/mighty-needles-know.md deleted file mode 100644 index 7e69a53678..0000000000 --- a/.changeset/mighty-needles-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Disabled ECMAScript transforms in app and backend builds in order to reduce bundle size and runtime performance. For the rationale and a full list of syntax that is no longer transformed, see https://github.com/alangpierce/sucrase#transforms. This also enables TypeScripts `useDefineForClassFields` flag by default, which in rare occasions could cause build failures. For instructions on how to mitigate issues due to the flag, see the [TypeScript documentation](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier). diff --git a/.changeset/mighty-students-itch.md b/.changeset/mighty-students-itch.md new file mode 100644 index 0000000000..7471c56d7e --- /dev/null +++ b/.changeset/mighty-students-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Fix an issue where filtering in search doesn't work correctly for Bitbucket. diff --git a/.changeset/neat-coats-sell.md b/.changeset/neat-coats-sell.md deleted file mode 100644 index 80c9ba111c..0000000000 --- a/.changeset/neat-coats-sell.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -This change adds an API endpoint for requesting a catalog refresh at `/refresh`, which is activated if a `RefreshService` is passed to `createRouter`. -The creation of the router has been abstracted behind the `CatalogBuilder` to simplify usage and future changes. The following **changes are required** to your `catalog.ts` for the refresh endpoint to function. - -```diff -- import { -- CatalogBuilder, -- createRouter, -- } from '@backstage/plugin-catalog-backend'; -+ import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; - - export default async function createPlugin( - env: PluginEnvironment, - ): Promise { - const builder = await CatalogBuilder.create(env); -- const { -- entitiesCatalog, -- locationAnalyzer, -- processingEngine, -- locationService, -- } = await builder.build(); -+ const { processingEngine, router } = await builder.build(); - await processingEngine.start(); - -- return await createRouter({ -- entitiesCatalog, -- locationAnalyzer, -- locationService, -- logger: env.logger, -- config: env.config, -- }); -+ return router; - } -``` diff --git a/.changeset/neat-pigs-lay.md b/.changeset/neat-pigs-lay.md deleted file mode 100644 index 48fee7a703..0000000000 --- a/.changeset/neat-pigs-lay.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Switched required engine from Node.js 12 or 14, to 14 or 16. - -To apply these changes to an existing app, switch out the following in the root `package.json`: - -```diff - "engines": { -- "node": "12 || 14" -+ "node": "14 || 16" - }, -``` - -Also get rid of the entire `engines` object in `packages/backend/package.json`, as it is redundant: - -```diff -- "engines": { -- "node": "12 || 14" -- }, -``` diff --git a/.changeset/nice-lions-hug.md b/.changeset/nice-lions-hug.md deleted file mode 100644 index 69dd33fceb..0000000000 --- a/.changeset/nice-lions-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Updates the `AboutCard` with a refresh button that allows the entity to be scheduled for refresh. diff --git a/.changeset/pink-windows-suffer.md b/.changeset/pink-windows-suffer.md new file mode 100644 index 0000000000..f27af1f4a3 --- /dev/null +++ b/.changeset/pink-windows-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Added a check for the TechDocs annotation on the entity diff --git a/.changeset/plenty-feet-pay.md b/.changeset/plenty-feet-pay.md new file mode 100644 index 0000000000..43fab61c79 --- /dev/null +++ b/.changeset/plenty-feet-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Stop forcing `target="_blank"` in the `SupportButton` but instead use the default logic of the `Link` component, that opens external targets in a new window and relative targets in the same window. diff --git a/.changeset/proud-adults-bathe.md b/.changeset/proud-adults-bathe.md deleted file mode 100644 index a8f15b18a3..0000000000 --- a/.changeset/proud-adults-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend-module-ldap': patch ---- - -Alters LDAP processor to handle one SearchEntry at a time diff --git a/.changeset/proud-carrots-prove.md b/.changeset/proud-carrots-prove.md new file mode 100644 index 0000000000..8d18b27282 --- /dev/null +++ b/.changeset/proud-carrots-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix duplication checks to stop looking for the old core packages, and to allow some explicitly diff --git a/.changeset/purple-scissors-allow.md b/.changeset/purple-scissors-allow.md new file mode 100644 index 0000000000..39d70d4c2a --- /dev/null +++ b/.changeset/purple-scissors-allow.md @@ -0,0 +1,89 @@ +--- +'@backstage/integration-react': patch +--- + +Added `ScmAuthApi` along with the implementation `ScmAuth`. The `ScmAuthApi` provides methods for client-side authentication towards multiple different source code management services simultaneously. + +When requesting credentials you supply a URL along with the same options as the other `OAuthApi`s, and optionally a request for additional high-level scopes. + +For example like this: + +```ts +const { token } = await scmAuthApi.getCredentials({ + url: 'https://ghe.example.com/backstage/backstage', + additionalScope: { + repoWrite: true, + }, +}); +``` + +The instantiation of the API can either be done with a default factory that adds support for the public providers (github.com, gitlab.com, etc.): + +```ts +// in packages/app/apis.ts +ScmAuth.createDefaultApiFactory(); +``` + +Or with a more custom setup that can add support for additional providers, for example like this: + +```ts +createApiFactory({ + api: scmAuthApiRef, + deps: { + gheAuthApi: gheAuthApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ githubAuthApi, gheAuthApi }) => + ScmAuth.merge( + ScmAuth.forGithub(githubAuthApi), + ScmAuth.forGithub(gheAuthApi, { + host: 'ghe.example.com', + }), + ), +}); +``` + +The additional `gheAuthApiRef` utility API can be defined either inside the app itself if it's only used for this purpose, for inside an internal common package for APIs, such as `@internal/apis`: + +```ts +const gheAuthApiRef: ApiRef = + createApiRef({ + id: 'internal.auth.ghe', + }); +``` + +And then implemented using the `GithubAuth` class from `@backstage/core-app-api`: + +```ts +createApiFactory({ + api: githubAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + GithubAuth.create({ + provider: { + id: 'ghe', + icon: ..., + title: 'GHE' + }, + discoveryApi, + oauthRequestApi, + defaultScopes: ['read:user'], + environment: configApi.getOptionalString('auth.environment'), + }), +}) +``` + +Finally you also need to add and configure another GitHub provider to the `auth-backend` using the provider ID `ghe`: + +```ts +// Add the following options to `createRouter` in packages/backend/src/plugins/auth.ts +providerFactories: { + ghe: createGithubProvider(), +}, +``` + +Other providers follow the same steps, but you will want to use the appropriate auth API implementation in the frontend, such as for example `GitlabAuth`. diff --git a/.changeset/red-lizards-accept.md b/.changeset/red-lizards-accept.md deleted file mode 100644 index 74c3a76db5..0000000000 --- a/.changeset/red-lizards-accept.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': minor ---- - -Add API endpoint for requesting a catalog refresh at `/refresh`, which is activated if a `RefreshService` is passed to `createRouter`. - -The new method is used to trigger a refresh of an entity in an as localized was as possible, usually by refreshing the parent location. diff --git a/.changeset/seven-bulldogs-grin.md b/.changeset/seven-bulldogs-grin.md new file mode 100644 index 0000000000..2d6f0c14c1 --- /dev/null +++ b/.changeset/seven-bulldogs-grin.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Introduced a new `CatalogProcessorCache` that is available to catalog processors. It allows arbitrary values to be saved that will then be visible during the next run. The cache is scoped to each individual processor and entity, but is shared across processing steps in a single processor. + +The cache is available as a new argument to each of the processing steps, except for `validateEntityKind` and `handleError`. + +This also introduces an optional `getProcessorName` to the `CatalogProcessor` interface, which is used to provide a stable identifier for the processor. While it is currently optional it will move to be required in the future. + +The breaking part of this change is the modification of the `state` field in the `EntityProcessingRequest` and `EntityProcessingResult` types. This is unlikely to have any impact as the `state` field was previously unused, but could require some minor updates. diff --git a/.changeset/short-pens-stare.md b/.changeset/short-pens-stare.md deleted file mode 100644 index f92ff12941..0000000000 --- a/.changeset/short-pens-stare.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/create-app': patch ---- - -Bumped the default `@spotify/prettier-config` dependency to `^11.0.0`. - -This is an optional upgrade, but you may be interested in doing the same, to get the most modern lint rules out there. diff --git a/.changeset/silent-years-tickle.md b/.changeset/silent-years-tickle.md deleted file mode 100644 index 87c73705cc..0000000000 --- a/.changeset/silent-years-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-circleci': patch ---- - -Swapped over to using Luxon as opposed to DayJS in the CircleCI plugin as part of the single Date library improvements. diff --git a/.changeset/slimy-impalas-admire.md b/.changeset/slimy-impalas-admire.md deleted file mode 100644 index 853d3eee65..0000000000 --- a/.changeset/slimy-impalas-admire.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/catalog-client': minor -'@backstage/plugin-catalog-react': minor ---- - -Extends the `CatalogClient` interface with a `refreshEntity` method. diff --git a/.changeset/slow-starfishes-retire.md b/.changeset/slow-starfishes-retire.md deleted file mode 100644 index 35fc64fa8b..0000000000 --- a/.changeset/slow-starfishes-retire.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Restrict imports on the form `../../plugins/x/src` diff --git a/.changeset/smart-cooks-drop.md b/.changeset/smart-cooks-drop.md new file mode 100644 index 0000000000..5ce59f2837 --- /dev/null +++ b/.changeset/smart-cooks-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Added a check for the Kubernetes annotation on the entity diff --git a/.changeset/smooth-experts-hunt.md b/.changeset/smooth-experts-hunt.md deleted file mode 100644 index 8fc4be206c..0000000000 --- a/.changeset/smooth-experts-hunt.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog-react': patch ---- - -This makes Type and Lifecycle columns consistent for all table cases and adds a new line in Description column for better readability diff --git a/.changeset/spicy-oranges-tan.md b/.changeset/spicy-oranges-tan.md deleted file mode 100644 index f835bcffb3..0000000000 --- a/.changeset/spicy-oranges-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-git-release-manager': patch ---- - -Remove 'refresh' icon from success dialog's OK-CTA. User feedback deemed it confusing. diff --git a/.changeset/spicy-yaks-notice.md b/.changeset/spicy-yaks-notice.md deleted file mode 100644 index ae5c3ba1a5..0000000000 --- a/.changeset/spicy-yaks-notice.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/catalog-model': patch -'@backstage/cli': patch -'@backstage/config': patch -'@backstage/core-components': patch -'@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-backend-module-ldap': patch -'@backstage/plugin-catalog-backend-module-msgraph': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-kafka-backend': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-rollbar-backend': patch -'@backstage/plugin-search-backend-module-pg': patch ---- - -Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability diff --git a/.changeset/techdocs-shy-beans-prove.md b/.changeset/techdocs-shy-beans-prove.md deleted file mode 100644 index 5aa9db1da7..0000000000 --- a/.changeset/techdocs-shy-beans-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-techdocs': patch ---- - -Make techdocs context search bar width adjust on smaller screens. diff --git a/.changeset/thick-rabbits-double.md b/.changeset/thick-rabbits-double.md new file mode 100644 index 0000000000..4ff38bf860 --- /dev/null +++ b/.changeset/thick-rabbits-double.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Update OAuth refresh handler to pass updated refresh token to ensure cookie is updated with new value. diff --git a/.changeset/tidy-windows-compare.md b/.changeset/tidy-windows-compare.md deleted file mode 100644 index 27005542ae..0000000000 --- a/.changeset/tidy-windows-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Switched the Jest YAML transform from `yaml-jest` to `jest-transform-yaml`, which works with newer versions of Node.js. diff --git a/.changeset/tiny-berries-battle.md b/.changeset/tiny-berries-battle.md deleted file mode 100644 index fdcb612d61..0000000000 --- a/.changeset/tiny-berries-battle.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-kubernetes': patch -'@backstage/plugin-kubernetes-backend': patch -'@backstage/plugin-kubernetes-common': patch ---- - -Provide access to the Kubernetes dashboard when viewing a specific resource diff --git a/.changeset/tricky-pens-camp.md b/.changeset/tricky-pens-camp.md new file mode 100644 index 0000000000..dc47274351 --- /dev/null +++ b/.changeset/tricky-pens-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Update AboutCard to only render refresh button if the entity is managed by an url location. diff --git a/.changeset/unlucky-laws-divide.md b/.changeset/unlucky-laws-divide.md deleted file mode 100644 index 0a1c69c73a..0000000000 --- a/.changeset/unlucky-laws-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-import': patch ---- - -The import form is now aware of locations that already exist. It lists them separately and shows a button for triggering a refresh. diff --git a/ADOPTERS.md b/ADOPTERS.md index 430ed36dbb..5e46f4b0ea 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,49 +1,54 @@ -| Organization | Contact | Description of Use | -| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | -| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | -| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | -| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | -| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | -| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | -| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | -| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | -| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | -| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | -| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | -| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | -| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | -| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | -| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | -| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | -| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | -| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | -| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | -| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | -| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | -| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | -| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | -| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | -| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | -| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | -| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | -| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | -| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process πŸŒ•πŸš€πŸ§‘β€πŸš€ | -| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | -| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | -| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | -| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | -| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | -| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | -| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | -| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | -| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | -| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | -| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | -| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | -| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | -| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | -| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | -| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| Organization | Contact | Description of Use | +| ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Spotify](https://www.spotify.com) | [@leemills83](https://github.com/leemills83) | Main interface towards all of Spotify's infrastructure and technical documentation. | +| [bol.com](https://www.bol.com) | [@sagacity](https://github.com/sagacity) | Initial work being done to unify platform tooling. | +| [DFDS](https://www.dfds.com) | [@carlsendk](https://github.com/carlsendk) | V2 self-service platform. | +| [Roadie](https://roadie.io) | [@dtuite](https://github.com/dtuite) | Hosted, managed Backstage with easy set-up | +| [Roku](https://www.roku.com) | [@timurista](https://github.com/timurista) | Initial work on Cloud engineering service platform. | +| [SDA SE](https://sda.se) | [@Fox32](https://github.com/Fox32) | Central place for developing and sharing services in our insurance ecosystem. | +| [H-E-B](https://www.heb.com) | [@german-j-rodriguez](https://github.com/german-j-rodriguez) | Initial work on Engineering Portal service platform. | +| [American Airlines](https://www.aa.com) | [@paulpach](https://github.com/paulpach) | Central place for developers to develop and maintain applications | +| [Kiwi.com](https://kiwi.com) | [@aexvir](https://github.com/aexvir) | Replacing the frontend of [The Zoo](https://github.com/kiwicom/the-zoo), their service registry. | +| [Voi](https://www.voiscooters.com/) | [@K-Phoen](https://github.com/K-Phoen) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [Talkdesk](https://www.talkdesk.com) | [@jaime-talkdesk](https://github.com/jaime-talkdesk) | Initial work for Engineering Portal and Self Provisioning to R&D | +| [Wealthsimple](https://www.wealthsimple.com) | [@andrewthauer](https://github.com/andrewthauer) | Developer portal, service catalog, documentation and tooling | +| [Grab](https://www.grab.com) | [@althafh](https://github.com/althafh) | Initial work as a unified interface for all of Grab's internal tooling | +| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks | +| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. | +| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. | +| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | +| [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo), [Erik Lindgren](https://github.com/lindgren) | EG Common Developer Toolkit | +| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | +| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | +| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | +| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | +| [Oriflame](https://www.oriflame.com/) | [Oriflame](https://github.com/oriflame) | Internal developer portal for services, single page apps and packages overview, API documentation, technical guides, tech-radar and more. | +| [Booz Allen Hamilton](https://www.boozallen.com/) | [Jason Miller](https://github.com/JasonMiller-BAH) | Developer portal for a full-stack software development ecosystem that accelerates consistent and repeatable Modern Software Development practices for internal innovation and investments. | +| [Netflix](https://www.netflix.com/) | [bleathem](https://github.com/bleathem) | Our Backstage implementation will be the front door to a unified experience connecting our internal platform products across important workflows with integrated knowledge and support. | +| [b.well](https://www.icanbwell.com/) | [Jacob Rosales](https://github.com/jrosales) | Foundation for our engineering portal and cloud insights. | +| [PagerDuty](https://www.pagerduty.com/) | [Mark Shaw](https://github.com/markshawtoronto) | Developer portal, initially focused on software templates and tech-docs. | +| [MoonShiner](https://moonshiner.at) | [Fabian Hippmann](https://github.com/FabianHippmann) | Developer portal - helps us keep track of our customer projects, onboard new developers & improve our development process πŸŒ•πŸš€πŸ§‘β€πŸš€ | +| [FundApps](https://www.fundapps.co/) | [Elliot Greenwood](https://github.com/egnwd) | Developer Portal - A place for us to keep track of our projects and documentation for all services and processes | +| [DAZN](https://dazn.com/) | [Lou Bichard](https://twitter.com/loujaybee), [Marco Crivellaro](https://github.com/crivetechie), [Alex Hollerith](mailto:alex.hollerith@dazn.com) | Ingesting all of DAZN's repos for the catalog, migrating our internal platform apps (pull request boards, release information, inner source marketplace etc) to Backstage plugins (where applicable). | +| [HelloFresh](https://www.hellofresh.de/) | [@iammuho](https://github.com/iammuho), [@ElenaForester](https://github.com/ElenaForester), [@diegomarangoni](https://github.com/diegomarangoni) | Our developer portal at HelloFresh - Spread across an organisation of 500+ engineers globally. | +| [FactSet](https://www.factset.com/) | [@kuangp](https://github.com/kuangp) | Developer portal to provide discoverability to all internal components, APIs, documentation, and scaffold templates with integrations to our internal infrastructure tools. | +| [Workrise](https://www.workrise.com/) | [Michael Rode](https://github.com/michaelrode) | Developer portal, main gateway to our infrastructure, documentation and internal tooling. | +| [RedVentures](https://www.redventures.com/) | [Chris Diaz](https://github.com/codingdiaz) | Developer portal that brings everything an engineer needs to provide value into a single pane of glass. | +| [MavTek](https://www.mavtek.com/) | [@fgascon](https://github.com/fgascon) | Developer portal focused on standardizing practices, centralizing documentation and streamlining developer practices. | +| [QuintoAndar](https://www.quintoandar.com.br/) | [@quintoandar](https://github.com/quintoandar) | Developer portal, services catalog and centralization of service metrics. | +| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. | +| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. | +| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration | +| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. | +| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar | +| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. | +| [Busuu](https://busuu.com/) | [Adam Tester](https://github.com/adamtester) | Developer portal with service catalog, API docs, Event docs, service templating, and cost insights. | +| [Loadsmart](https://loadsmart.com/) | [Loadsmart](https://github.com/loadsmart) | Improve services visibility and operations for service owners and developers. | +| [Monzo](https://monzo.com/) | [@WillSewell](https://github.com/WillSewell), [@joechrisellis](https://github.com/joechrisellis) | Developer portal showing metadata and docs for over 2000 microservices. We have built a number of plugins such as a UI for our system to measure [software excellence](https://monzo.com/blog/2021/09/15/how-we-measure-software-excellence), and a UI to show deployment and config change events. | +| [Vaimo](https://www.vaimo.com) | [@vaimo-magnus](https://github.com/vaimo-magnus) | Developer Portal for our developers at Vaimo, currently docs and self-service towards our internal PaaS based on k8s. Plans to extend the catalog into Projects, Environments etc | +| [Wayfair](https://www.wayfair.com) | [@fransan](https://github.com/fransan), [@errskipower](https://github.com/errskipower), [@hrrs](https://github.com/hrrs) | Developer portal for service catalog, technical documentation, and APIs. | +| [CircleHD](https://www.circlehd.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | +| [CastDesk](https://castdesk.com) | [@circlehddev](https://github.com/circlehddev) | Developer Portal for internal dev team across the globe | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 466c77c575..5242309eee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -102,6 +102,18 @@ Signed-off-by: Jane Smith Note: If you have already pushed you branch to a remote, you might have to force push: `git push -f` after the rebase. +### Using GitHub Desktop? + +If you are using the GitHub Desktop client, you need to manually add the `Signed-off-by` line to the Description field on the Changes tab before committing: + +``` +Awesome description (commit message) + +Signed-off-by: Jane Smith +``` + +In case you forgot to add the line to your most recent commit, you can amend the commit message from the History tab before pushing your branch (GitHub Desktop 2.9 or later). + ## Creating Changesets We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. They help us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy, it helps when contributors include changesets with their pull requests. diff --git a/contrib/docker/frontend-with-nginx/docker/inject-config.sh b/contrib/docker/frontend-with-nginx/docker/inject-config.sh index a0cea96ff3..98797b65d3 100755 --- a/contrib/docker/frontend-with-nginx/docker/inject-config.sh +++ b/contrib/docker/frontend-with-nginx/docker/inject-config.sh @@ -23,7 +23,7 @@ function inject_config() { # escape ' and " twice, for both sed and json local config_escaped_1 - config_escaped_1="$(echo "$config" | jq -cM . | sed -e 's/[\\"\x27]/\\&/g')" # \x27 = ' + config_escaped_1="$(echo "$config" | jq -cM . | sed -e 's/[\\"'\'']/\\&/g')" # escape / and & for sed local config_escaped_2 config_escaped_2="$(echo "$config_escaped_1" | sed -e 's/[\/&]/\\&/g')" diff --git a/contrib/docs/tutorials/authenticate-api-requests.md b/contrib/docs/tutorials/authenticate-api-requests.md index 6fc39811b5..9f651ddd1a 100644 --- a/contrib/docs/tutorials/authenticate-api-requests.md +++ b/contrib/docs/tutorials/authenticate-api-requests.md @@ -188,9 +188,9 @@ Assuming you follow the common plugin structure, the changes to your front-end m ```diff // plugins/internal-plugin/src/api.ts -- import {createApiRef} from '@backstage/core'; -+ import {createApiRef, IdentityApi} from '@backstage/core'; -import {Config} from '@backstage/config'; +- import { createApiRef } from '@backstage/core-plugin-api'; ++ import { createApiRef, IdentityApi } from '@backstage/core-plugin-api'; +import { Config } from '@backstage/config'; // ... type MyApiOptions = { @@ -237,14 +237,14 @@ import { createApiFactory, createPlugin, + identityApiRef, -} from '@backstage/core'; -import {mypluginPageRouteRef} from './routeRefs'; -import {MyApi, myApiRef} from './api'; +} from '@backstage/core-plugin-api'; +import { myPluginPageRouteRef } from './routeRefs'; +import { MyApi, myApiRef } from './api'; export const plugin = createPlugin({ id: 'my-plugin', routes: { - mainPage: mypluginPageRouteRef, + mainPage: myPluginPageRouteRef, }, apis: [ createApiFactory({ @@ -253,9 +253,9 @@ export const plugin = createPlugin({ configApi: configApiRef, + identityApi: identityApiRef, }, -- factory: ({configApi}) => +- factory: ({ configApi }) => - new MyApi({ configApi }), -+ factory: ({configApi, identityApi}) => ++ factory: ({ configApi, identityApi }) => + new MyApi({ configApi, identityApi }), }), ], diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index b546285c94..05553c3fd3 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -5,8 +5,8 @@ sidebar_label: Auth0 description: Adding Auth0 as an authentication provider in Backstage --- -The Backstage `core-api` package comes with an Auth0 authentication provider -that can authenticate users using OAuth. +The Backstage `core-plugin-api` package comes with an Auth0 authentication +provider that can authenticate users using OAuth. ## Create an Auth0 Application diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 81b2e98f2c..05726e8a4e 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -5,8 +5,8 @@ sidebar_label: GitHub description: Adding GitHub OAuth as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a GitHub authentication provider -that can authenticate users using GitHub or GitHub Enterprise OAuth. +The Backstage `core-plugin-api` package comes with a GitHub authentication +provider that can authenticate users using GitHub or GitHub Enterprise OAuth. ## Create an OAuth App on GitHub diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index c7ac2cedc9..fd64ddac14 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -5,8 +5,8 @@ sidebar_label: GitLab description: Adding GitLab OAuth as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a GitLab authentication provider -that can authenticate users using GitLab OAuth. +The Backstage `core-plugin-api` package comes with a GitLab authentication +provider that can authenticate users using GitLab OAuth. ## Create an OAuth App on GitLab diff --git a/docs/auth/google/provider.md b/docs/auth/google/provider.md index 457224b2db..6216b21704 100644 --- a/docs/auth/google/provider.md +++ b/docs/auth/google/provider.md @@ -5,8 +5,8 @@ sidebar_label: Google description: Adding Google OAuth as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a Google authentication provider -that can authenticate users using Google OAuth. +The Backstage `core-plugin-api` package comes with a Google authentication +provider that can authenticate users using Google OAuth. ## Create OAuth Credentials diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index 52f463bab4..1b81ff76f1 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -5,8 +5,8 @@ sidebar_label: Azure description: Adding Microsoft Azure as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a Microsoft authentication provider -that can authenticate users using Azure OAuth. +The Backstage `core-plugin-api` package comes with a Microsoft authentication +provider that can authenticate users using Azure OAuth. ## Create an App Registration on Azure diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index bbd23a1b2a..b5aaabe4f1 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -5,8 +5,8 @@ sidebar_label: Okta description: Adding Okta OAuth as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a Okta authentication provider that -can authenticate users using Okta OpenID Connect. +The Backstage `core-plugin-api` package comes with a Okta authentication +provider that can authenticate users using Okta OpenID Connect. ## Create an Application on Okta diff --git a/docs/auth/onelogin/provider.md b/docs/auth/onelogin/provider.md index 62c1657fea..a11304ae84 100644 --- a/docs/auth/onelogin/provider.md +++ b/docs/auth/onelogin/provider.md @@ -5,8 +5,8 @@ sidebar_label: OneLogin description: Adding OneLogin OIDC as an authentication provider in Backstage --- -The Backstage `core-api` package comes with a OneLogin authentication provider -that can authenticate users using OpenID Connect. +The Backstage `core-plugin-api` package comes with a OneLogin authentication +provider that can authenticate users using OpenID Connect. ## Create an Application on OneLogin diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 320b82d0ad..849823a59e 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -23,7 +23,7 @@ Locations are added to the catalog under the `catalog.locations` key: catalog: locations: - type: url - target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml + target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/components/artist-lookup-component.yaml ``` The `url` type locations are handled by a standard processor included with the diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 326ac02969..db093caa39 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -217,3 +217,56 @@ techdocs: [beta-migrate-bug]: https://github.com/backstage/backstage/issues/new?assignees=&labels=bug&template=bug_template.md&title=[TechDocs]%20Unable%20to%20run%20beta%20migration [using-cloud-storage]: ./using-cloud-storage.md + +## How to implement your own TechDocs APIs + +The TechDocs plugin provides implementations of two primary APIs by default: the +[TechDocsStorageApi](https://github.com/backstage/backstage/blob/55114cfeb7045e3e5eeeaf67546b58964f4adcc7/plugins/techdocs/src/api.ts#L33), +which is responsible for talking to TechDocs storage to fetch files to render, +and +[TechDocsApi](https://github.com/backstage/backstage/blob/55114cfeb7045e3e5eeeaf67546b58964f4adcc7/plugins/techdocs/src/api.ts#L49), +which is responsible for talking to techdocs-backend. + +There may be occasions where you need to implement these two APIs yourself, to +customize them to your own needs. The purpose of this guide is to walk you +through how to do that in two steps. + +1. Implement the `TechDocsStorageApi` and `TechDocsApi` interfaces according to + your needs. + +```typescript +export class TechDocsCustomStorageApi implements TechDocsStorageApi { + // your implementation +} + +export class TechDocsCustomApiClient implements TechDocsApi { + // your implementation +} +``` + +2. Override the API refs `techdocsStorageApiRef` and `techdocsApiRef` with your + new implemented APIs in the `App.tsx` using `ApiFactories`. + [Read more about App APIs](https://backstage.io/docs/api/utility-apis#app-apis). + +```typescript +const app = createApp({ + apis: [ + // TechDocsStorageApi + createApiFactory({ + api: techdocsStorageApiRef, + deps: { discoveryApi: discoveryApiRef, configApi: configApiRef }, + factory({ discoveryApi, configApi }) { + return new TechDocsCustomStorageApi({ discoveryApi, configApi }); + }, + }), + // TechDocsApi + createApiFactory({ + api: techdocsApiRef, + deps: { discoveryApi: discoveryApiRef }, + factory({ discoveryApi }) { + return new TechDocsCustomApiClient({ discoveryApi }); + }, + }), + ], +}); +``` diff --git a/docs/getting-started/project-structure.md b/docs/getting-started/project-structure.md index bf2cc93ea5..687743f99d 100644 --- a/docs/getting-started/project-structure.md +++ b/docs/getting-started/project-structure.md @@ -166,12 +166,12 @@ are separated out into their own folder, see further down. plugin and [techdocs-cli](https://github.com/backstage/techdocs-cli). - [`test-utils/`](https://github.com/backstage/backstage/tree/master/packages/test-utils) - - This package contains specific testing facilities used when testing - `core-api`. + This package contains more general purpose testing facilities for testing a + Backstage App or its plugins. - [`test-utils-core/`](https://github.com/backstage/backstage/tree/master/packages/test-utils-core) - - This package contains more general purpose testing facilities for testing a - Backstage App. + This package contains specific testing facilities used when testing Backstage + core internals. - [`theme/`](https://github.com/backstage/backstage/tree/master/packages/theme) - Holds the Backstage Theme. diff --git a/docs/integrations/bitbucket/discovery.md b/docs/integrations/bitbucket/discovery.md index 708acd8db2..b08386f3c7 100644 --- a/docs/integrations/bitbucket/discovery.md +++ b/docs/integrations/bitbucket/discovery.md @@ -11,12 +11,12 @@ catalog entities located in Bitbucket. The processor will crawl your Bitbucket account and register entities matching the configured path. This can be useful as an alternative to static locations or manually adding things to the catalog. -> Note: The Bitbucket Discovery Processor currently only supports a self-hosted -> Bitbucket Server, and not the hosted Bitbucket Cloud product. +## Self-hosted Bitbucket Server -To use the discovery processor, you'll need a Bitbucket integration -[set up](locations.md) with a `BITBUCKET_TOKEN` and a `BITBUCKET_API_BASE_URL`. -Then you can add a location target to the catalog configuration: +To use the discovery processor with a self-hosted Bitbucket Server, you'll need +a Bitbucket integration [set up](locations.md) with a `BITBUCKET_TOKEN` and a +`BITBUCKET_API_BASE_URL`. Then you can add a location target to the catalog +configuration: ```yaml catalog: @@ -44,6 +44,58 @@ The target is composed of four parts: will result in: `https://bitbucket.mycompany.com/projects/my-project/repos/service-a/catalog-info.yaml`. +## Bitbucket Cloud + +To use the discovery processor with Bitbucket Cloud, you'll need a Bitbucket +integration [set up](locations.md) with a `username` and an `appPassword`. Then +you can add a location target to the catalog configuration: + +```yaml +catalog: + locations: + - type: bitbucket-discovery + target: https://bitbucket.org/workspaces/my-workspace +``` + +Note the `bitbucket-discovery` type, as this is not a regular `url` processor. + +The target is composed of the following parts: + +- The base URL for Bitbucket, `https://bitbucket.org` +- The workspace name to scan (following the `workspaces/` path part), which must + match a workspace accessible with the username of your integration. +- (Optional) The project key to scan (following the `projects/` path part), + which accepts \* wildcard tokens. If omitted, repositories from all projects + in the workspace are included. +- (Optional) The repository blob to scan (following the `repos/` path part), + which accepts \* wildcard tokens. If omitted, all repositories in the + workspace are included. +- (Optional) The `catalogPath` query argument to specify the location within + each repository to find the catalog YAML file. This will usually be + `/catalog-info.yaml` or a similar variation for catalog files stored in the + root directory of each repository. If omitted, the default value + `catalog-info.yaml` will be used. +- (Optional) The `q` query argument to be passed through to Bitbucket for + filtering results via the API. This is the most flexible option and will + reduce the amount of API calls if you have a large workspace. + [See here for the specification](https://developer.atlassian.com/bitbucket/api/2/reference/meta/filtering) + for the query argument (will be passed as the `q` query parameter). + +Examples: + +- `https://bitbucket.org/workspaces/my-workspace/projects/my-project` will find + all repositories in the `my-project` project in the `my-workspace` workspace. +- `https://bitbucket.org/workspaces/my-workspace/repos/service-*` will find all + repositories starting with `service-` in the `my-workspace` workspace. +- `https://bitbucket.org/workspaces/my-workspace/projects/apis-*/repos/service-*` + will find all repositories starting with `service-`, in all projects starting + with `apis-` in the `my-workspace` workspace. +- `https://bitbucket.org/workspaces/my-workspace?q=project.key ~ "my-project"` + will find all repositories in a project containing `my-project` in its key. +- `https://bitbucket.org/workspaces/my-workspace?catalogPath=my/nested/path/catalog.yaml` + will find all repositories in the `my-workspace` workspace and use the catalog + file at `my/nested/path/catalog.yaml`. + ## Custom repository processing The Bitbucket Discovery Processor will by default emit a location for each diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 71b04c0a4a..467b29c759 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -49,17 +49,28 @@ When using a custom pattern, the target is composed of three parts: ## GitHub API Rate Limits -GitHub -[rate limits](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) -API requests to 5,000 per hour (or more for Enterprise accounts). The default -Backstage catalog backend refreshes data every 100 seconds, which issues an API -request for each discovered location. +GitHub [rate limits] API requests to 5,000 per hour (or more for Enterprise +accounts). The default Backstage catalog backend refreshes data every 100 +seconds, which issues an API request for each discovered location. This means if you have more than ~140 catalog entities, you may get throttled by rate limiting. This will soon be resolved once catalog refreshes make use of ETags; to work around this in the meantime, you can change the refresh rate of -the catalog in your `packages/backend/src/plugins/catalog.ts` file, or configure -Backstage to use the [github-apps plugin](../../plugins/github-apps.md). +the catalog in your `packages/backend/src/plugins/catalog.ts` file: + +```typescript +const builder = await CatalogBuilder.create(env); + +// For example, to refresh every 5 minutes (300 seconds). +builder.setRefreshIntervalSeconds(300); +``` + +Alternatively, or additionally, you can configure [github-apps] authentication +which carries a much higher rate limit at GitHub. This is true for any method of adding GitHub entities to the catalog, but especially easy to hit with automatic discovery. + +[rate limits]: + https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting +[github-apps]: ../../plugins/github-apps.md diff --git a/docs/integrations/github/locations.md b/docs/integrations/github/locations.md index 5b972f332c..330b03eab9 100644 --- a/docs/integrations/github/locations.md +++ b/docs/integrations/github/locations.md @@ -60,5 +60,7 @@ preferred. ## Authentication with GitHub Apps Alternatively, Backstage can use GitHub Apps for backend authentication. This -has higher rate limits, and a clearer authorization model. See the -[github-apps plugin](../../plugins/github-apps.md) for how to set this up. +has higher rate limits, and a clearer authorization model. See [github-apps] for +how to set this up. + +[github-apps]: ../../plugins/github-apps.md diff --git a/microsite/.yarnrc b/microsite/.yarnrc new file mode 100644 index 0000000000..a2c82b2833 --- /dev/null +++ b/microsite/.yarnrc @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +registry "https://registry.npmjs.org/" +network-timeout 300000 diff --git a/microsite/data/plugins/bugsnag.yaml b/microsite/data/plugins/bugsnag.yaml new file mode 100644 index 0000000000..8f36153382 --- /dev/null +++ b/microsite/data/plugins/bugsnag.yaml @@ -0,0 +1,9 @@ +--- +title: Bugsnag +author: roadie.io +authorUrl: https://roadie.io/?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=bugsnag +category: Monitoring +description: View and monitor Bugsnag errors. +documentation: https://roadie.io/backstage/plugins/bugsnag/?utm_source=backstage.io&utm_medium=marketplace&utm_campaign=bugsnag +iconUrl: https://roadie.io/images/logos/bugsnag.png +npmPackageName: '@roadiehq/backstage-plugin-bugsnag' diff --git a/microsite/package.json b/microsite/package.json index dafab893e2..a2a2fd98de 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -16,7 +16,7 @@ "lock:check": "yarn-lock-check" }, "devDependencies": { - "@spotify/prettier-config": "^11.0.0", + "@spotify/prettier-config": "^12.0.0", "docusaurus": "^2.0.0-alpha.70", "js-yaml": "^4.1.0", "prettier": "^2.4.1", diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 6dfdb022fb..f3834ac734 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -909,10 +909,10 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== -"@spotify/prettier-config@^11.0.0": - version "11.0.0" - resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-11.0.0.tgz#d91e0546a8c1c0f7299e2edc7e44306e9be210f6" - integrity sha512-dOI13j1uHMZkRxhZuge/ugOE7Aqcg7Nxki932lDZuXyY4G8CGxkc/66PeQ8pR4PCzThHORXo7Ptvau6bh101lQ== +"@spotify/prettier-config@^12.0.0": + version "12.0.0" + resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-12.0.0.tgz#936ca5e977cfccbccd1731ab98b1f2bf65852b5d" + integrity sha512-64WWqE40U/WwWV8iIQBseTU+b2t+SdJSyQoCLdVPCKM9uf7KOjRivVwXe4KlWoV3y7duNSGuB2UgWhkXzscVmQ== "@types/cheerio@^0.22.8": version "0.22.23" diff --git a/package.json b/package.json index 2620ffd32e..4f360b8f71 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "fs-extra": "9.1.0", "husky": "^6.0.0", "lerna": "^4.0.0", - "lint-staged": "^10.1.0", + "lint-staged": "^11.1.2", "prettier": "^2.2.1", "recursive-readdir": "^2.2.2", "shx": "^0.3.2", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 3ce0c206c7..4b6a39a792 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,47 @@ # example-app +## 0.2.47 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-explore@0.3.17 + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-import@0.6.0 + - @backstage/plugin-catalog-graph@0.1.1 + - @backstage/cli@0.7.13 + - @backstage/plugin-catalog@0.6.16 + - @backstage/plugin-user-settings@0.3.6 + - @backstage/plugin-circleci@0.2.24 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/plugin-api-docs@0.6.9 + - @backstage/catalog-model@0.9.3 + - @backstage/plugin-rollbar@0.3.15 + - @backstage/plugin-techdocs@0.11.3 + - @backstage/plugin-kubernetes@0.4.14 + - @backstage/core-app-api@0.1.14 + - @backstage/integration-react@0.1.10 + - @backstage/plugin-badges@0.2.10 + - @backstage/plugin-cloudbuild@0.2.24 + - @backstage/plugin-code-coverage@0.1.12 + - @backstage/plugin-cost-insights@0.11.7 + - @backstage/plugin-gcp-projects@0.3.5 + - @backstage/plugin-github-actions@0.4.19 + - @backstage/plugin-graphiql@0.2.17 + - @backstage/plugin-home@0.4.1 + - @backstage/plugin-jenkins@0.5.7 + - @backstage/plugin-kafka@0.2.16 + - @backstage/plugin-lighthouse@0.2.26 + - @backstage/plugin-newrelic@0.3.5 + - @backstage/plugin-org@0.3.24 + - @backstage/plugin-pagerduty@0.3.14 + - @backstage/plugin-scaffolder@0.11.5 + - @backstage/plugin-search@0.4.12 + - @backstage/plugin-sentry@0.3.22 + - @backstage/plugin-shortcuts@0.1.9 + - @backstage/plugin-tech-radar@0.4.8 + - @backstage/plugin-todo@0.1.11 + ## 0.2.46 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index b997313485..dedce0bf85 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,46 +1,46 @@ { "name": "example-app", - "version": "0.2.46", + "version": "0.2.47", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.9.2", - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/integration-react": "^0.1.9", - "@backstage/plugin-api-docs": "^0.6.8", - "@backstage/plugin-badges": "^0.2.9", - "@backstage/plugin-catalog": "^0.6.15", - "@backstage/plugin-catalog-graph": "^0.1.0", - "@backstage/plugin-catalog-import": "^0.5.21", - "@backstage/plugin-catalog-react": "^0.4.6", - "@backstage/plugin-circleci": "^0.2.23", - "@backstage/plugin-cloudbuild": "^0.2.23", - "@backstage/plugin-code-coverage": "^0.1.11", - "@backstage/plugin-cost-insights": "^0.11.6", - "@backstage/plugin-explore": "^0.3.16", - "@backstage/plugin-gcp-projects": "^0.3.4", - "@backstage/plugin-github-actions": "^0.4.18", - "@backstage/plugin-graphiql": "^0.2.16", - "@backstage/plugin-home": "^0.4.0", - "@backstage/plugin-jenkins": "^0.5.6", - "@backstage/plugin-kafka": "^0.2.15", - "@backstage/plugin-kubernetes": "^0.4.13", - "@backstage/plugin-lighthouse": "^0.2.25", - "@backstage/plugin-newrelic": "^0.3.4", - "@backstage/plugin-org": "^0.3.23", - "@backstage/plugin-pagerduty": "0.3.13", - "@backstage/plugin-rollbar": "^0.3.14", - "@backstage/plugin-scaffolder": "^0.11.4", - "@backstage/plugin-search": "^0.4.11", - "@backstage/plugin-sentry": "^0.3.21", - "@backstage/plugin-shortcuts": "^0.1.8", - "@backstage/plugin-tech-radar": "^0.4.7", - "@backstage/plugin-techdocs": "^0.11.2", - "@backstage/plugin-todo": "^0.1.10", - "@backstage/plugin-user-settings": "^0.3.5", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-api-docs": "^0.6.9", + "@backstage/plugin-badges": "^0.2.10", + "@backstage/plugin-catalog": "^0.6.16", + "@backstage/plugin-catalog-graph": "^0.1.1", + "@backstage/plugin-catalog-import": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.5.0", + "@backstage/plugin-circleci": "^0.2.24", + "@backstage/plugin-cloudbuild": "^0.2.24", + "@backstage/plugin-code-coverage": "^0.1.12", + "@backstage/plugin-cost-insights": "^0.11.7", + "@backstage/plugin-explore": "^0.3.17", + "@backstage/plugin-gcp-projects": "^0.3.5", + "@backstage/plugin-github-actions": "^0.4.19", + "@backstage/plugin-graphiql": "^0.2.17", + "@backstage/plugin-home": "^0.4.1", + "@backstage/plugin-jenkins": "^0.5.7", + "@backstage/plugin-kafka": "^0.2.16", + "@backstage/plugin-kubernetes": "^0.4.14", + "@backstage/plugin-lighthouse": "^0.2.26", + "@backstage/plugin-newrelic": "^0.3.5", + "@backstage/plugin-org": "^0.3.24", + "@backstage/plugin-pagerduty": "0.3.14", + "@backstage/plugin-rollbar": "^0.3.15", + "@backstage/plugin-scaffolder": "^0.11.5", + "@backstage/plugin-search": "^0.4.12", + "@backstage/plugin-sentry": "^0.3.22", + "@backstage/plugin-shortcuts": "^0.1.9", + "@backstage/plugin-tech-radar": "^0.4.8", + "@backstage/plugin-techdocs": "^0.11.3", + "@backstage/plugin-todo": "^0.1.11", + "@backstage/plugin-user-settings": "^0.3.6", "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 08ff781319..d587a88402 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -17,6 +17,7 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, + ScmAuth, } from '@backstage/integration-react'; import { costInsightsApiRef, @@ -41,6 +42,8 @@ export const apis: AnyApiFactory[] = [ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), + ScmAuth.createDefaultApiFactory(), + createApiFactory({ api: graphQlBrowseApiRef, deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index b661b759af..28f092886e 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-common +## 0.9.4 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/integration@0.6.5 + - @backstage/config@0.1.10 + ## 0.9.3 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 4967fac228..67a2c1a853 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.3", + "version": "0.9.4", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,10 +30,10 @@ }, "dependencies": { "@backstage/cli-common": "^0.1.3", - "@backstage/config": "^0.1.9", + "@backstage/config": "^0.1.10", "@backstage/config-loader": "^0.6.8", "@backstage/errors": "^0.1.2", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@google-cloud/storage": "^5.8.0", "@octokit/rest": "^18.5.3", "@types/cors": "^2.8.6", @@ -77,7 +77,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.17", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend-common/src/config.test.ts b/packages/backend-common/src/config.test.ts new file mode 100644 index 0000000000..a60b28ac15 --- /dev/null +++ b/packages/backend-common/src/config.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { ConfigReader } from '@backstage/config'; +import { ObservableConfigProxy } from './config'; + +describe('ObservableConfigProxy', () => { + const errLogger = { + error: (message: string) => { + throw new Error(message); + }, + } as unknown as Logger; + + it('should notify subscribers', () => { + const config = new ObservableConfigProxy(errLogger); + + const fn = jest.fn(); + const sub = config.subscribe(fn); + expect(config.getOptionalNumber('x')).toBe(undefined); + + config.setConfig(new ConfigReader({})); + expect(fn).toHaveBeenCalledTimes(1); + expect(config.getOptionalNumber('x')).toBe(undefined); + + config.setConfig(new ConfigReader({ x: 1 })); + expect(fn).toHaveBeenCalledTimes(2); + expect(config.getOptionalNumber('x')).toBe(1); + + config.setConfig(new ConfigReader({ x: 3 })); + expect(fn).toHaveBeenCalledTimes(3); + sub.unsubscribe(); + expect(config.getOptionalNumber('x')).toBe(3); + + config.setConfig(new ConfigReader({ x: 5 })); + expect(fn).toHaveBeenCalledTimes(3); + expect(config.getOptionalNumber('x')).toBe(5); + }); + + it('should forward subscriptions', () => { + const config1 = new ObservableConfigProxy(errLogger); + + const fn1 = jest.fn(); + const fn2 = jest.fn(); + const fn3 = jest.fn(); + const config2 = config1.getConfig('a'); + const config3 = config2.getConfig('b'); + const sub1 = config1.subscribe(fn1); + const sub2 = config2.subscribe!(fn2); + const sub3 = config3.subscribe!(fn3); + expect(config1.getOptionalNumber('x')).toBe(undefined); + expect(config2.getOptionalNumber('x')).toBe(undefined); + expect(config3.getOptionalNumber('x')).toBe(undefined); + + config1.setConfig(new ConfigReader({})); + expect(fn1).toHaveBeenCalledTimes(1); + expect(fn2).toHaveBeenCalledTimes(1); + expect(fn3).toHaveBeenCalledTimes(1); + expect(config1.getOptionalNumber('x')).toBe(undefined); + expect(config2.getOptionalNumber('x')).toBe(undefined); + expect(config3.getOptionalNumber('x')).toBe(undefined); + + config1.setConfig(new ConfigReader({ x: 1, a: { x: 2, b: { x: 3 } } })); + expect(fn1).toHaveBeenCalledTimes(2); + expect(fn2).toHaveBeenCalledTimes(2); + expect(fn3).toHaveBeenCalledTimes(2); + expect(config1.getNumber('x')).toBe(1); + expect(config2.getNumber('x')).toBe(2); + expect(config3.getNumber('x')).toBe(3); + + sub1.unsubscribe(); + sub2.unsubscribe(); + sub3.unsubscribe(); + + config1.setConfig(new ConfigReader({ x: 4, a: { x: 5, b: { x: 6 } } })); + expect(fn1).toHaveBeenCalledTimes(2); + expect(fn2).toHaveBeenCalledTimes(2); + expect(fn3).toHaveBeenCalledTimes(2); + expect(config1.getNumber('x')).toBe(4); + expect(config2.getNumber('x')).toBe(5); + expect(config3.getNumber('x')).toBe(6); + + config1.setConfig(new ConfigReader({})); + expect(() => config1.getNumber('x')).toThrow( + "Missing required config value at 'x'", + ); + expect(() => config2.getNumber('x')).toThrow( + "Missing required config value at 'a'", + ); + expect(() => config3.getNumber('x')).toThrow( + "Missing required config value at 'a'", + ); + + config1.setConfig( + new ConfigReader({ x: 's', a: { x: 's', b: { x: 's' } } }), + ); + expect(() => config1.getNumber('x')).toThrow( + "Unable to convert config value for key 'x' in 'mock-config' to a number", + ); + expect(() => config2.getNumber('x')).toThrow( + "Unable to convert config value for key 'a.x' in 'mock-config' to a number", + ); + expect(() => config3.getNumber('x')).toThrow( + "Unable to convert config value for key 'a.b.x' in 'mock-config' to a number", + ); + }); + + it('should make sub configs available as expected', () => { + const config = new ObservableConfigProxy(errLogger); + + config.setConfig(new ConfigReader({ a: { x: 1 } })); + + expect(config.getConfig('a')).toBeDefined(); + expect(config.getConfig('a').getNumber('x')).toBe(1); + expect(config.getConfig('a').getOptionalNumber('x')).toBe(1); + expect(config.getOptionalConfig('a')?.getNumber('x')).toBe(1); + expect(config.getOptionalConfig('a')?.getOptionalNumber('x')).toBe(1); + expect(config.getOptionalConfig('b')).toBeUndefined(); + expect(() => config.getConfig('b')).toBeDefined(); + expect(() => config.getConfig('b').get()).toThrow(); + }); +}); diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 96907870cb..04cf1ac2bb 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -21,14 +21,25 @@ import { findPaths } from '@backstage/cli-common'; import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; -class ObservableConfigProxy implements Config { +export class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); private readonly subscribers: (() => void)[] = []; - constructor(private readonly logger: Logger) {} + constructor( + private readonly logger: Logger, + private readonly parent?: ObservableConfigProxy, + private parentKey?: string, + ) { + if (parent && !parentKey) { + throw new Error('parentKey is required if parent is set'); + } + } setConfig(config: Config) { + if (this.parent) { + throw new Error('immutable'); + } this.config = config; for (const subscriber of this.subscribers) { try { @@ -40,6 +51,10 @@ class ObservableConfigProxy implements Config { } subscribe(onChange: () => void): { unsubscribe: () => void } { + if (this.parent) { + return this.parent.subscribe(onChange); + } + this.subscribers.push(onChange); return { unsubscribe: () => { @@ -51,53 +66,69 @@ class ObservableConfigProxy implements Config { }; } + private select(required: true): Config; + private select(required: false): Config | undefined; + private select(required: boolean): Config | undefined { + if (this.parent && this.parentKey) { + if (required) { + return this.parent.select(true).getConfig(this.parentKey); + } + return this.parent.select(false)?.getOptionalConfig(this.parentKey); + } + + return this.config; + } + has(key: string): boolean { - return this.config.has(key); + return this.select(false)?.has(key) ?? false; } keys(): string[] { - return this.config.keys(); + return this.select(false)?.keys() ?? []; } get(key?: string): T { - return this.config.get(key); + return this.select(true).get(key); } getOptional(key?: string): T | undefined { - return this.config.getOptional(key); + return this.select(false)?.getOptional(key); } getConfig(key: string): Config { - return this.config.getConfig(key); + return new ObservableConfigProxy(this.logger, this, key); } getOptionalConfig(key: string): Config | undefined { - return this.config.getOptionalConfig(key); + if (this.select(false)?.has(key)) { + return new ObservableConfigProxy(this.logger, this, key); + } + return undefined; } getConfigArray(key: string): Config[] { - return this.config.getConfigArray(key); + return this.select(true).getConfigArray(key); } getOptionalConfigArray(key: string): Config[] | undefined { - return this.config.getOptionalConfigArray(key); + return this.select(false)?.getOptionalConfigArray(key); } getNumber(key: string): number { - return this.config.getNumber(key); + return this.select(true).getNumber(key); } getOptionalNumber(key: string): number | undefined { - return this.config.getOptionalNumber(key); + return this.select(false)?.getOptionalNumber(key); } getBoolean(key: string): boolean { - return this.config.getBoolean(key); + return this.select(true).getBoolean(key); } getOptionalBoolean(key: string): boolean | undefined { - return this.config.getOptionalBoolean(key); + return this.select(false)?.getOptionalBoolean(key); } getString(key: string): string { - return this.config.getString(key); + return this.select(true).getString(key); } getOptionalString(key: string): string | undefined { - return this.config.getOptionalString(key); + return this.select(false)?.getOptionalString(key); } getStringArray(key: string): string[] { - return this.config.getStringArray(key); + return this.select(true).getStringArray(key); } getOptionalStringArray(key: string): string[] | undefined { - return this.config.getOptionalStringArray(key); + return this.select(false)?.getOptionalStringArray(key); } } diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 4609459faa..c214961a2e 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -21,7 +21,7 @@ */ export * from './cache'; -export * from './config'; +export { loadBackendConfig } from './config'; export * from './database'; export * from './discovery'; export * from './hot'; diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index b032322627..ef61452163 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -360,6 +360,20 @@ describe('BitbucketUrlReader', () => { ); }); + it('works in nested folders', async () => { + const result = await bitbucketProcessor.search( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.*', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + it('throws NotModifiedError when same etag', async () => { await expect( bitbucketProcessor.search( @@ -422,6 +436,20 @@ describe('BitbucketUrlReader', () => { ); }); + it('works in nested folders', async () => { + const result = await hostedBitbucketProcessor.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.*?at=master', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + it('throws NotModifiedError when same etag', async () => { await expect( hostedBitbucketProcessor.search( diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index 4965d5a370..611e94f0db 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { BitbucketIntegration, getBitbucketDefaultBranch, @@ -26,18 +27,16 @@ import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; -import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { stripFirstDirectoryFromPath } from './tree/util'; import { - ReadTreeResponseFactory, ReaderFactory, ReadTreeOptions, ReadTreeResponse, + ReadTreeResponseFactory, + ReadUrlOptions, + ReadUrlResponse, SearchOptions, SearchResponse, UrlReader, - ReadUrlResponse, - ReadUrlOptions, } from './types'; /** @@ -154,7 +153,7 @@ export class BitbucketUrlReader implements UrlReader { const tree = await this.readTree(treeUrl, { etag: options?.etag, - filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + filter: path => matcher.match(path), }); const files = await tree.files(); diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 78d0d029e5..6c4fe32dca 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,29 @@ # example-backend +## 0.2.47 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@0.14.0 + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + - @backstage/plugin-kafka-backend@0.2.10 + - @backstage/plugin-kubernetes-backend@0.3.16 + - @backstage/plugin-rollbar-backend@0.1.15 + - @backstage/plugin-search-backend-module-pg@0.2.1 + - example-app@0.2.47 + - @backstage/plugin-auth-backend@0.4.1 + - @backstage/plugin-badges-backend@0.1.10 + - @backstage/plugin-code-coverage-backend@0.1.11 + - @backstage/plugin-jenkins-backend@0.1.5 + - @backstage/plugin-scaffolder-backend@0.15.6 + - @backstage/plugin-techdocs-backend@0.10.3 + - @backstage/plugin-todo-backend@0.1.12 + ## 0.2.46 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index f564dfeeee..5346466ba1 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.46", + "version": "0.2.47", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,35 +24,35 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/catalog-client": "^0.3.17", - "@backstage/catalog-model": "^0.9.1", - "@backstage/config": "^0.1.8", - "@backstage/integration": "^0.6.4", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/integration": "^0.6.5", "@backstage/plugin-app-backend": "^0.3.16", - "@backstage/plugin-auth-backend": "^0.4.0", - "@backstage/plugin-badges-backend": "^0.1.9", - "@backstage/plugin-catalog-backend": "^0.13.8", - "@backstage/plugin-code-coverage-backend": "^0.1.10", + "@backstage/plugin-auth-backend": "^0.4.1", + "@backstage/plugin-badges-backend": "^0.1.10", + "@backstage/plugin-catalog-backend": "^0.14.0", + "@backstage/plugin-code-coverage-backend": "^0.1.11", "@backstage/plugin-graphql-backend": "^0.1.9", - "@backstage/plugin-jenkins-backend": "^0.1.4", - "@backstage/plugin-kubernetes-backend": "^0.3.15", - "@backstage/plugin-kafka-backend": "^0.2.9", + "@backstage/plugin-jenkins-backend": "^0.1.5", + "@backstage/plugin-kubernetes-backend": "^0.3.16", + "@backstage/plugin-kafka-backend": "^0.2.10", "@backstage/plugin-proxy-backend": "^0.2.12", - "@backstage/plugin-rollbar-backend": "^0.1.14", - "@backstage/plugin-scaffolder-backend": "^0.15.5", + "@backstage/plugin-rollbar-backend": "^0.1.15", + "@backstage/plugin-scaffolder-backend": "^0.15.6", "@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5", "@backstage/plugin-search-backend": "^0.2.6", "@backstage/plugin-search-backend-node": "^0.4.2", "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.4", - "@backstage/plugin-search-backend-module-pg": "^0.2.0", - "@backstage/plugin-techdocs-backend": "^0.10.2", - "@backstage/plugin-todo-backend": "^0.1.11", + "@backstage/plugin-search-backend-module-pg": "^0.2.1", + "@backstage/plugin-techdocs-backend": "^0.10.3", + "@backstage/plugin-todo-backend": "^0.1.12", "@gitbeaker/node": "^30.2.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.2.1", - "example-app": "^0.2.46", + "example-app": "^0.2.47", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -64,7 +64,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index b32007a3e2..ffb629b5bc 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/catalog-client +## 0.4.0 + +### Minor Changes + +- dbcaa6387a: Extends the `CatalogClient` interface with a `refreshEntity` method. + +### Patch Changes + +- 9ef2987a83: Update `AddLocationResponse` to optionally return `exists` to signal that the location already exists, this is only returned when calling `addLocation` in dryRun. +- Updated dependencies + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + ## 0.3.19 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index c6922d5c1f..8520164f91 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "0.3.19", + "version": "0.4.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,13 +30,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.2", - "@backstage/config": "^0.1.9", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.2", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.7.13", "@types/jest": "^26.0.7", "msw": "^0.29.0" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 6e6a7193c1..cce24390b1 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/catalog-model +## 0.9.3 + +### Patch Changes + +- d42566c5c9: Loosen constraints on what's a valid catalog entity tag name (include + and #) +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/config@0.1.10 + ## 0.9.2 ### Patch Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 7d734339b3..3f92a671fa 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -50,6 +50,7 @@ export class CommonValidatorFunctions { isValidSuffix: (value: string) => boolean, ): boolean; static isValidString(value: unknown): boolean; + static isValidTag(value: unknown): boolean; static isValidUrl(value: unknown): boolean; } diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index a2fc76c83a..0bea531816 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.2", + "version": "0.9.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.9", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.2", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", @@ -41,7 +41,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.7.13", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts index 8655116ec6..b99901b89b 100644 --- a/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts +++ b/packages/catalog-model/src/entity/policies/FieldFormatEntityPolicy.ts @@ -78,6 +78,10 @@ export class FieldFormatEntityPolicy implements EntityPolicy { expectation = 'a string that is sequences of [a-z0-9] separated by [-], at most 63 characters in total'; break; + case 'isValidTag': + expectation = + 'a string that is sequences of [a-z0-9+#] separated by [-], at most 63 characters in total'; + break; case 'isValidAnnotationValue': expectation = 'a string'; break; diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index b15ac385b1..88e3f9b002 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -162,6 +162,31 @@ describe('CommonValidatorFunctions', () => { expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result); }); + it.each([ + // These are identical to isValidDnsLabel + [null, false], + [7, false], + ['', false], + ['a', true], + ['a-b', true], + ['-a-b', false], + ['a-b-', false], + ['a--b', false], + ['a_b', false], + [`${'a'.repeat(63)}`, true], + [`${'a'.repeat(64)}`, false], + // Tags can have other characters though + ['a+b', true], + ['+a+b', true], + ['a+b+', true], + ['a++b', true], + ['c++', true], + ['c#', true], + ['#c++', true], + ])(`isValidTag %p ? %p`, (value, result) => { + expect(CommonValidatorFunctions.isValidTag(value)).toBe(result); + }); + it.each([ [null, false], [7, false], diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 5fcd2ee755..d0357660fc 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -95,6 +95,20 @@ export class CommonValidatorFunctions { ); } + /** + * Checks that the value is a valid tag. + * + * @param value - The value to check + */ + static isValidTag(value: unknown): boolean { + return ( + typeof value === 'string' && + value.length >= 1 && + value.length <= 63 && + /^[a-z0-9+#]+(\-[a-z0-9+#]+)*$/.test(value) + ); + } + /** * Checks that the value is a valid URL. * diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 48eff566f2..bd6dccc419 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -27,7 +27,7 @@ const defaultValidators: Validators = { isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, isValidAnnotationValue: KubernetesValidatorFunctions.isValidAnnotationValue, - isValidTag: CommonValidatorFunctions.isValidDnsLabel, + isValidTag: CommonValidatorFunctions.isValidTag, }; /** @public */ diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7aaac78f1e..96170d6899 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/cli +## 0.7.13 + +### Patch Changes + +- c0c51c9710: Disabled ECMAScript transforms in app and backend builds in order to reduce bundle size and runtime performance. For the rationale and a full list of syntax that is no longer transformed, see https://github.com/alangpierce/sucrase#transforms. This also enables TypeScripts `useDefineForClassFields` flag by default, which in rare occasions could cause build failures. For instructions on how to mitigate issues due to the flag, see the [TypeScript documentation](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#the-usedefineforclassfields-flag-and-the-declare-property-modifier). +- e9f332a51c: Restrict imports on the form `../../plugins/x/src` +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- 050797c5b3: Switched the Jest YAML transform from `yaml-jest` to `jest-transform-yaml`, which works with newer versions of Node.js. +- Updated dependencies + - @backstage/config@0.1.10 + ## 0.7.12 ### Patch Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 3e0a7f4e6f..0dc4dc3cf7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.7.12", + "version": "0.7.13", "private": false, "publishConfig": { "access": "public" @@ -31,7 +31,7 @@ "@babel/core": "^7.4.4", "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@backstage/cli-common": "^0.1.3", - "@backstage/config": "^0.1.9", + "@backstage/config": "^0.1.10", "@backstage/config-loader": "^0.6.8", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", @@ -117,12 +117,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/config": "^0.1.9", - "@backstage/core-components": "^0.4.2", + "@backstage/backend-common": "^0.9.4", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.10", "@types/diff": "^5.0.0", diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index c9784969af..0d47df8c22 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -30,8 +30,9 @@ import { getCodeownersFilePath, } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; -import { packageVersions } from '../../lib/version'; import { Task, templatingTask } from '../../lib/tasks'; +import { Lockfile } from '../../lib/versioning'; +import { createPackageVersionProvider } from '../../lib/version'; const exec = promisify(execCb); @@ -262,6 +263,13 @@ export default async (cmd: Command) => { ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) : { version: '0.1.0' }; + let lockfile: Lockfile | undefined; + try { + lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); + } catch (error) { + console.warn(`No yarn.lock available, ${error}`); + } + Task.log(); Task.log('Creating the plugin...'); @@ -288,7 +296,7 @@ export default async (cmd: Command) => { privatePackage, npmRegistry, }, - packageVersions, + createPackageVersionProvider(lockfile), ); Task.section('Moving to final location'); diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index c542da6dfb..527245d6f5 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -27,13 +27,21 @@ export const includedFilter = (name: string) => // Packages that are not allowed to have any duplicates const FORBID_DUPLICATES = [ - /^@backstage\/core$/, - /^@backstage\/core-api$/, + /^@backstage\/core-app-api$/, /^@backstage\/plugin-/, ]; +// There are some packages that ARE explicitly allowed to have duplicates since +// they handle that appropriately. This takes precedence over FORBID_DUPLICATES +// above. +const ALLOW_DUPLICATES = [ + /^@backstage\/core-plugin-api$/, + /^@backstage\/plugin-catalog-react$/, +]; + export const forbiddenDuplicatesFilter = (name: string) => - FORBID_DUPLICATES.some(pattern => pattern.test(name)); + FORBID_DUPLICATES.some(pattern => pattern.test(name)) && + !ALLOW_DUPLICATES.some(pattern => pattern.test(name)); export default async (cmd: Command) => { const fix = Boolean(cmd.fix); diff --git a/packages/cli/src/lib/diff/read.ts b/packages/cli/src/lib/diff/read.ts index 8388d22196..95b0bed43d 100644 --- a/packages/cli/src/lib/diff/read.ts +++ b/packages/cli/src/lib/diff/read.ts @@ -24,7 +24,7 @@ import handlebars from 'handlebars'; import recursiveReadDir from 'recursive-readdir'; import { paths } from '../paths'; import { FileDiff } from './types'; -import { packageVersions } from '../../lib/version'; +import { createPackageVersionProvider } from '../../lib/version'; export type TemplatedFile = { path: string; @@ -40,14 +40,15 @@ async function readTemplateFile( if (!templateFile.endsWith('.hbs')) { return contents; } + const packageVersionProvider = createPackageVersionProvider(undefined); return handlebars.compile(contents)(templateVars, { helpers: { - version(name: keyof typeof packageVersions) { - if (name in packageVersions) { - return packageVersions[name]; - } - throw new Error(`No version available for package ${name}`); + versionQuery(name: string, hint: string | unknown) { + return packageVersionProvider( + name, + typeof hint === 'string' ? hint : undefined, + ); }, }, }); diff --git a/packages/cli/src/lib/tasks.test.ts b/packages/cli/src/lib/tasks.test.ts index 9ee4e93336..4d9af05c4b 100644 --- a/packages/cli/src/lib/tasks.test.ts +++ b/packages/cli/src/lib/tasks.test.ts @@ -34,7 +34,7 @@ describe('templatingTask', () => { // Files content const testFileContent = 'testing'; const testVersionFileContent = - "version: {{pluginVersion}} {{version 'mock-pkg'}}"; + "version: {{pluginVersion}} {{versionQuery 'mock-pkg'}}"; mockFs({ [tmplDir]: { @@ -52,7 +52,7 @@ describe('templatingTask', () => { { pluginVersion: '0.0.0', }, - { 'mock-pkg': '0.1.2' }, + () => '^0.1.2', ); await expect( @@ -60,6 +60,6 @@ describe('templatingTask', () => { ).resolves.toBe(testFileContent); await expect( fs.readFile(resolvePath(destDir, 'sub/version.txt'), 'utf8'), - ).resolves.toBe('version: 0.0.0 0.1.2'); + ).resolves.toBe('version: 0.0.0 ^0.1.2'); }); }); diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index be6d052365..e17015d6cf 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -69,7 +69,7 @@ export async function templatingTask( templateDir: string, destinationDir: string, context: any, - versions: { [name: string]: string }, + versionProvider: (name: string, versionHint?: string) => string, ) { const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); @@ -90,11 +90,11 @@ export async function templatingTask( { name: basename(destination), ...context }, { helpers: { - version(name: string) { - if (versions[name]) { - return versions[name]; - } - throw new Error(`No version available for package ${name}`); + versionQuery(name: string, versionHint: string | unknown) { + return versionProvider( + name, + typeof versionHint === 'string' ? versionHint : undefined, + ); }, }, }, diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts new file mode 100644 index 0000000000..415ada76d6 --- /dev/null +++ b/packages/cli/src/lib/version.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 mockFs from 'mock-fs'; +import { createPackageVersionProvider } from './version'; +import { Lockfile } from './versioning'; +// eslint-disable-next-line monorepo/no-internal-import +import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; + +describe('createPackageVersionProvider', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should provide package versions', async () => { + mockFs({ + 'yarn.lock': ` +"a@^0.1.0": + version "0.1.5" + +"b@^0.2.0","b@*","b@^0.2.1": + version "0.2.5" + +"c@^0.1.4": + version "0.1.8" + +"c@^0.2.4": + version "0.2.8" + +"c@^0.3.4": + version "0.3.8" + +"@types/t@^1.1.0","@types/t@*","@types/t@^1.2.3": + version "1.4.5" + +"@backstage/cli@*": + version "1.1.5" + `, + }); + + const lockfile = await Lockfile.load('yarn.lock'); + const provider = createPackageVersionProvider(lockfile); + + expect(provider('a', '0.1.5')).toBe('^0.1.0'); + expect(provider('b', '1.0.0')).toBe('*'); + expect(provider('c', '0.1.0')).toBe('^0.1.0'); + expect(provider('c', '0.1.6')).toBe('^0.1.4'); + expect(provider('c', '0.2.0')).toBe('^0.2.0'); + expect(provider('c', '0.2.6')).toBe('^0.2.4'); + expect(provider('c', '0.3.0-rc1')).toBe('0.3.0-rc1'); + expect(provider('c', '0.3.0')).toBe('^0.3.0'); + expect(provider('c', '0.3.6')).toBe('^0.3.4'); + expect(provider('@backstage/cli')).toBe('*'); + expect(provider('@backstage/core-plugin-api')).toBe( + `^${corePluginApiPkg.version}`, + ); + expect(provider('@types/t', '1.4.2')).toBe('*'); + }); +}); diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 61ee4003fc..c69072c64d 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -15,7 +15,9 @@ */ import fs from 'fs-extra'; +import semver from 'semver'; import { paths } from './paths'; +import { Lockfile } from './versioning'; /* eslint-disable import/no-extraneous-dependencies,monorepo/no-internal-import */ /* @@ -41,7 +43,7 @@ import { version as devUtils } from '@backstage/dev-utils/package.json'; import { version as testUtils } from '@backstage/test-utils/package.json'; import { version as theme } from '@backstage/theme/package.json'; -export const packageVersions = { +export const packageVersions: Record = { '@backstage/backend-common': backendCommon, '@backstage/cli': cli, '@backstage/config': config, @@ -60,3 +62,36 @@ export function findVersion() { export const version = findVersion(); export const isDev = fs.pathExistsSync(paths.resolveOwn('src')); + +export function createPackageVersionProvider(lockfile?: Lockfile) { + return (name: string, versionHint?: string) => { + const packageVersion = packageVersions[name]; + const targetVersion = versionHint || packageVersion; + if (!targetVersion) { + throw new Error(`No version available for package ${name}`); + } + + const lockfileEntries = lockfile?.get(name); + if ( + name.startsWith('@types/') && + lockfileEntries?.some(entry => entry.range === '*') + ) { + return '*'; + } + const validRanges = lockfileEntries?.filter(entry => + semver.satisfies(targetVersion, entry.range), + ); + const highestRange = validRanges?.slice(-1)[0]; + + if (highestRange?.range) { + return highestRange?.range; + } + if (packageVersion) { + return `^${packageVersion}`; + } + if (semver.parse(versionHint)?.prerelease.length) { + return versionHint!; + } + return `^${versionHint}`; + }; +} diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 8275223f49..43b9e83fe1 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -23,20 +23,20 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^{{version '@backstage/backend-common'}}", - "@backstage/config": "^{{version '@backstage/config'}}", - "@types/express": "^4.17.6", - "express": "^4.17.1", - "express-promise-router": "^4.1.0", - "winston": "^3.2.1", - "cross-fetch": "^3.0.6", - "yn": "^4.0.0" + "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", + "@backstage/config": "{{versionQuery '@backstage/config'}}", + "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", + "express": "{{versionQuery 'express' '4.17.1'}}", + "express-promise-router": "{{versionQuery 'express-promise-router' '4.1.0'}}", + "winston": "{{versionQuery 'winston' '3.2.1'}}", + "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}", + "yn": "{{versionQuery 'yn' '4.0.0'}}" }, "devDependencies": { - "@backstage/cli": "^{{version '@backstage/cli'}}", - "@types/supertest": "^2.0.8", - "supertest": "^4.0.2", - "msw": "^0.29.0" + "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@types/supertest": "{{versionQuery '@types/supertest' '2.0.8'}}", + "supertest": "{{versionQuery 'supertest' '4.0.2'}}", + "msw": "{{versionQuery 'msw' '0.29.0'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 318830da39..ef56d55343 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -24,28 +24,28 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^{{version '@backstage/core-components'}}", - "@backstage/core-plugin-api": "^{{version '@backstage/core-plugin-api'}}", - "@backstage/theme": "^{{version '@backstage/theme'}}", - "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "4.0.0-alpha.57", - "react": "^16.13.1", - "react-dom": "^16.13.1", - "react-use": "^17.2.4" + "@backstage/core-components": "{{versionQuery '@backstage/core-components'}}", + "@backstage/core-plugin-api": "{{versionQuery '@backstage/core-plugin-api'}}", + "@backstage/theme": "{{versionQuery '@backstage/theme'}}", + "@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}", + "@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}", + "@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.57'}}", + "react": "{{versionQuery 'react' '16.13.1'}}", + "react-dom": "{{versionQuery 'react-dom' '16.13.1'}}", + "react-use": "{{versionQuery 'react-use' '17.2.4'}}" }, "devDependencies": { - "@backstage/cli": "^{{version '@backstage/cli'}}", - "@backstage/core-app-api": "^{{version '@backstage/core-app-api'}}", - "@backstage/dev-utils": "^{{version '@backstage/dev-utils'}}", - "@backstage/test-utils": "^{{version '@backstage/test-utils'}}", - "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react": "^11.2.5", - "@testing-library/user-event": "^13.1.8", - "@types/jest": "^26.0.7", - "@types/node": "^14.14.32", - "msw": "^0.29.0", - "cross-fetch": "^3.0.6" + "@backstage/cli": "{{versionQuery '@backstage/cli'}}", + "@backstage/core-app-api": "{{versionQuery '@backstage/core-app-api'}}", + "@backstage/dev-utils": "{{versionQuery '@backstage/dev-utils'}}", + "@backstage/test-utils": "{{versionQuery '@backstage/test-utils'}}", + "@testing-library/jest-dom": "{{versionQuery '@testing-library/jest-dom' '5.10.1'}}", + "@testing-library/react": "{{versionQuery '@testing-library/react' '11.2.5'}}", + "@testing-library/user-event": "{{versionQuery '@testing-library/user-event' '13.1.8'}}", + "@types/jest": "{{versionQuery '@types/jest' '26.0.7'}}", + "@types/node": "{{versionQuery '@types/node' '14.14.32'}}", + "msw": "{{versionQuery 'msw' '0.29.0'}}", + "cross-fetch": "{{versionQuery 'cross-fetch' '3.0.6'}}" }, "files": [ "dist" diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 704e427797..fa5c9d663b 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/codemods +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/core-app-api@0.1.14 + ## 0.1.14 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 772ac97064..6f07940f3b 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.14", + "version": "0.1.15", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 7691ed2865..9735c330c1 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/config +## 0.1.10 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability + ## 0.1.9 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index d165ceb6f6..ffa2404300 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.9", + "version": "0.1.10", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 0ae4c2ea13..3b3ca169cd 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/core-app-api +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/config@0.1.10 + ## 0.1.13 ### Patch Changes diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index b39fe4bbcd..18d20f4afc 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.1.13", + "version": "0.1.14", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", - "@backstage/config": "^0.1.9", + "@backstage/core-components": "^0.5.0", + "@backstage/config": "^0.1.10", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@backstage/version-bridge": "^0.1.0", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.17", "@backstage/test-utils-core": "^0.1.2", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-app-api/src/app/createApp.tsx b/packages/core-app-api/src/app/createApp.tsx index a2d22c8dd0..e9c37a1420 100644 --- a/packages/core-app-api/src/app/createApp.tsx +++ b/packages/core-app-api/src/app/createApp.tsx @@ -90,11 +90,6 @@ export const defaultConfigLoader: AppConfigLoader = async ( return configs; }; -// createApp is defined in core, and not core-api, since we need access -// to the components inside core to provide defaults. -// The actual implementation of the app class still lives in core-api, -// as it needs to be used by dev- and test-utils. - export function OptionallyWrapInRouter({ children }: PropsWithChildren<{}>) { if (useInRouterContext()) { return <>{children}; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 8ff449a95c..8028af8b96 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/core-components +## 0.5.0 + +### Minor Changes + +- 537bd04005: Fixed a popup-blocking bug affecting iOS Safari in SignInPage.tsx by ensuring that the popup occurs in the same tick as the tap/click + +### Patch Changes + +- c0eb1fb9df: Allow to configure zooming for ``. `zoom` can either be + `enabled`, `disabled`, or `enable-on-click`. The latter requires the user to + click into the diagram to enable zooming. +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/config@0.1.10 + ## 0.4.2 ### Patch Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index b5c98add13..72ce416549 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -162,12 +162,36 @@ type DependencyEdge = T & { label?: string; }; -// Warning: (ae-forgotten-export) The symbol "DependencyGraphProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "DependencyGraph" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function DependencyGraph(props: DependencyGraphProps): JSX.Element; +// Warning: (ae-missing-release-tag) "DependencyGraphProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type DependencyGraphProps = React_2.SVGProps & { + edges: DependencyEdge[]; + nodes: DependencyNode[]; + direction?: Direction; + align?: Alignment; + nodeMargin?: number; + edgeMargin?: number; + rankMargin?: number; + paddingX?: number; + paddingY?: number; + acyclicer?: 'greedy'; + ranker?: Ranker; + labelPosition?: LabelPosition; + labelOffset?: number; + edgeRanks?: number; + edgeWeight?: number; + renderNode?: RenderNodeFunction; + renderLabel?: RenderLabelFunction; + defs?: SVGDefsElement | SVGDefsElement[]; + zoom?: 'enabled' | 'disabled' | 'enable-on-click'; +}; + declare namespace DependencyGraphTypes { export { DependencyEdge, diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 7ffff145f5..48f8dc116b 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.4.2", + "version": "0.5.0", "private": false, "publishConfig": { "access": "public", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.9", + "@backstage/config": "^0.1.10", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.2", "@backstage/theme": "^0.2.10", @@ -70,8 +70,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/core-app-api": "^0.1.13", - "@backstage/cli": "^0.7.12", + "@backstage/core-app-api": "^0.1.14", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx b/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx index 0b97059754..e81988976c 100644 --- a/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx +++ b/packages/core-components/src/components/DependencyGraph/DependencyGraph.stories.tsx @@ -51,6 +51,32 @@ export const Default = () => ( ); +export const ZoomDisabled = () => ( +
+ +
+); + +export const ZoomEnableOnClick = () => ( +
+ +
+); + export const BottomToTop = () => (
& { renderNode?: RenderNodeFunction; renderLabel?: RenderLabelFunction; defs?: SVGDefsElement | SVGDefsElement[]; + zoom?: 'enabled' | 'disabled' | 'enable-on-click'; }; const WORKSPACE_ID = 'workspace'; @@ -80,6 +81,7 @@ export function DependencyGraph(props: DependencyGraphProps) { edgeWeight = 1, renderLabel, defs, + zoom = 'enabled', ...svgProps } = props; const theme: BackstageTheme = useTheme(); @@ -110,28 +112,37 @@ export function DependencyGraph(props: DependencyGraphProps) { // Set up zooming + panning const container = d3Selection.select(node); const workspace = d3Selection.select(node.getElementById(WORKSPACE_ID)); - const zoom = d3Zoom - .zoom() - .scaleExtent([1, 10]) - .on('zoom', event => { - event.transform.x = Math.min( - 0, - Math.max( - event.transform.x, - maxWidth - maxWidth * event.transform.k, - ), - ); - event.transform.y = Math.min( - 0, - Math.max( - event.transform.y, - maxHeight - maxHeight * event.transform.k, - ), - ); - workspace.attr('transform', event.transform); - }); - container.call(zoom); + function enableZoom() { + container.call( + d3Zoom + .zoom() + .scaleExtent([1, 10]) + .on('zoom', event => { + event.transform.x = Math.min( + 0, + Math.max( + event.transform.x, + maxWidth - maxWidth * event.transform.k, + ), + ); + event.transform.y = Math.min( + 0, + Math.max( + event.transform.y, + maxHeight - maxHeight * event.transform.k, + ), + ); + workspace.attr('transform', event.transform); + }), + ); + } + + if (zoom === 'enabled') { + enableZoom(); + } else if (zoom === 'enable-on-click') { + container.on('click', () => enableZoom()); + } const { width: newContainerWidth, height: newContainerHeight } = node.getBoundingClientRect(); @@ -142,7 +153,7 @@ export function DependencyGraph(props: DependencyGraphProps) { setContainerHeight(newContainerHeight); } }, 100), - [containerHeight, containerWidth, maxWidth, maxHeight], + [containerHeight, containerWidth, maxWidth, maxHeight, zoom], ); const setNodesAndEdges = React.useCallback(() => { diff --git a/packages/core-components/src/components/DependencyGraph/index.ts b/packages/core-components/src/components/DependencyGraph/index.ts index 5f5e7a5439..03a5bfd584 100644 --- a/packages/core-components/src/components/DependencyGraph/index.ts +++ b/packages/core-components/src/components/DependencyGraph/index.ts @@ -17,4 +17,5 @@ import * as DependencyGraphTypes from './types'; export { DependencyGraph } from './DependencyGraph'; +export type { DependencyGraphProps } from './DependencyGraph'; export { DependencyGraphTypes }; diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 8076636654..62d753e08b 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -54,9 +54,7 @@ const SupportIcon = ({ icon }: { icon: string | undefined }) => { }; const SupportLink = ({ link }: { link: SupportItemLink }) => ( - - {link.title ?? link.url} - + {link.title ?? link.url} ); const SupportListItem = ({ item }: { item: SupportItem }) => { diff --git a/packages/core-components/src/layout/SignInPage/SignInPage.tsx b/packages/core-components/src/layout/SignInPage/SignInPage.tsx index e0d4f6ed0d..d64a59158d 100644 --- a/packages/core-components/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core-components/src/layout/SignInPage/SignInPage.tsx @@ -15,12 +15,13 @@ */ import { + BackstageIdentity, configApiRef, SignInPageProps, useApi, } from '@backstage/core-plugin-api'; import { Button, Grid, Typography } from '@material-ui/core'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { Progress } from '../../components/Progress'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; @@ -91,62 +92,63 @@ export const SingleSignInPage = ({ const authApi = useApi(provider.apiRef); const configApi = useApi(configApiRef); - const [autoShowPopup, setAutoShowPopup] = useState(auto ?? false); - // Defaults to true so that an initial check for existing user session is made - const [retry, setRetry] = useState<{} | boolean | undefined>(undefined); const [error, setError] = useState(); + const [loginCount, setLoginCount] = useState(0); // The SignIn component takes some time to decide whether the user is logged-in or not. // showLoginPage is used to prevent a glitch-like experience where the sign-in page is // displayed for a split second when the user is already logged-in. const [showLoginPage, setShowLoginPage] = useState(false); - useEffect(() => { - const login = async () => { - try { - let identity; + type LoginOpts = { checkExisting?: boolean; showPopup?: boolean }; + const login = async ({ checkExisting, showPopup }: LoginOpts) => { + setLoginCount(prev => prev + 1); + try { + let identity: BackstageIdentity | undefined; + if (checkExisting) { // Do an initial check if any logged-in session exists identity = await authApi.getBackstageIdentity({ optional: true, }); - - // If no session exists, show the sign-in page - if (!identity && autoShowPopup) { - // Unless auto is set to true, this step should not happen. - // When user intentionally clicks the Sign In button, autoShowPopup is set to true - setShowLoginPage(true); - identity = await authApi.getBackstageIdentity({ - instantPopup: true, - }); - } - - if (!identity) { - setShowLoginPage(true); - return; - } - - const profile = await authApi.getProfile(); - onResult({ - userId: identity!.id, - profile: profile!, - getIdToken: () => { - return authApi - .getBackstageIdentity() - .then(i => i!.token ?? i!.idToken); - }, - signOut: async () => { - await authApi.signOut(); - }, - }); - } catch (err) { - // User closed the sign-in modal - setError(err); - setShowLoginPage(true); } - }; - login(); - }, [onResult, authApi, retry, autoShowPopup]); + // If no session exists, show the sign-in page + if (!identity && (showPopup || auto)) { + // Unless auto is set to true, this step should not happen. + // When user intentionally clicks the Sign In button, autoShowPopup is set to true + setShowLoginPage(true); + identity = await authApi.getBackstageIdentity({ + instantPopup: true, + }); + } + + if (!identity) { + setShowLoginPage(true); + return; + } + + const profile = await authApi.getProfile(); + onResult({ + userId: identity!.id, + profile: profile!, + getIdToken: () => { + return authApi + .getBackstageIdentity() + .then(i => i!.token ?? i!.idToken); + }, + signOut: async () => { + await authApi.signOut(); + }, + }); + } catch (err: any) { + // User closed the sign-in modal + setError(err); + setShowLoginPage(true); + } + }; + if (loginCount === 0) { + login({ checkExisting: true }); + } return showLoginPage ? ( @@ -168,8 +170,7 @@ export const SingleSignInPage = ({ color="primary" variant="outlined" onClick={() => { - setRetry({}); - setAutoShowPopup(true); + login({ showPopup: true }); }} > Sign In diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 746db2e1fb..0a2e7d013d 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/create-app +## 0.3.42 + +### Patch Changes + +- 89fd81a1ab: This change adds an API endpoint for requesting a catalog refresh at `/refresh`, which is activated if a `RefreshService` is passed to `createRouter`. + The creation of the router has been abstracted behind the `CatalogBuilder` to simplify usage and future changes. The following **changes are required** to your `catalog.ts` for the refresh endpoint to function. + + ```diff + - import { + - CatalogBuilder, + - createRouter, + - } from '@backstage/plugin-catalog-backend'; + + import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const builder = await CatalogBuilder.create(env); + - const { + - entitiesCatalog, + - locationAnalyzer, + - processingEngine, + - locationService, + - } = await builder.build(); + + const { processingEngine, router } = await builder.build(); + await processingEngine.start(); + + - return await createRouter({ + - entitiesCatalog, + - locationAnalyzer, + - locationService, + - logger: env.logger, + - config: env.config, + - }); + + return router; + } + ``` + +- d0a47c8605: Switched required engine from Node.js 12 or 14, to 14 or 16. + + To apply these changes to an existing app, switch out the following in the root `package.json`: + + ```diff + "engines": { + - "node": "12 || 14" + + "node": "14 || 16" + }, + ``` + + Also get rid of the entire `engines` object in `packages/backend/package.json`, as it is redundant: + + ```diff + - "engines": { + - "node": "12 || 14" + - }, + ``` + +- df95665e4b: Bumped the default `@spotify/prettier-config` dependency to `^11.0.0`. + + This is an optional upgrade, but you may be interested in doing the same, to get the most modern lint rules out there. + ## 0.3.41 ## 0.3.40 diff --git a/packages/create-app/package.json b/packages/create-app/package.json index a94b108716..eab773d562 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.3.41", + "version": "0.3.42", "private": false, "publishConfig": { "access": "public" diff --git a/packages/create-app/templates/default-app/packages/app/src/apis.ts b/packages/create-app/templates/default-app/packages/app/src/apis.ts index f2cd272c87..c89753aae8 100644 --- a/packages/create-app/templates/default-app/packages/app/src/apis.ts +++ b/packages/create-app/templates/default-app/packages/app/src/apis.ts @@ -1,6 +1,7 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, + ScmAuth, } from '@backstage/integration-react'; import { AnyApiFactory, @@ -14,4 +15,5 @@ export const apis: AnyApiFactory[] = [ deps: { configApi: configApiRef }, factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), + ScmAuth.createDefaultApiFactory(), ]; diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index b15b3437f2..1105ea48ca 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/dev-utils +## 0.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/core-app-api@0.1.14 + - @backstage/integration-react@0.1.10 + ## 0.2.9 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 5d6bbba3d7..d8a28eb9aa 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.2.9", + "version": "0.2.10", "private": false, "publishConfig": { "access": "public", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-app-api": "^0.1.12", - "@backstage/core-components": "^0.4.1", + "@backstage/core-app-api": "^0.1.14", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.7", - "@backstage/catalog-model": "^0.9.2", - "@backstage/integration-react": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.5", + "@backstage/catalog-model": "^0.9.3", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/test-utils": "^0.1.17", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -50,7 +50,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.7.11", + "@backstage/cli": "^0.7.13", "@types/jest": "^26.0.7", "@types/node": "^14.14.32" }, diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index dde93341ef..89a46704c2 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -127,6 +127,16 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { } return pkge.version; }, + versionQuery(name: string, hint: string) { + const pkgData = require(`${name}/package.json`); + if (!pkgData) { + if (typeof hint !== 'string') { + throw new Error(`No version available for package ${name}`); + } + return `^${hint}`; + } + return `^${pkgData.version}`; + }, }, }, ), diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index a1637099b4..e723c266cf 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/integration-react +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/config@0.1.10 + ## 0.1.9 ### Patch Changes diff --git a/packages/integration-react/api-report.md b/packages/integration-react/api-report.md index 88057fda74..b7feb6b6a9 100644 --- a/packages/integration-react/api-report.md +++ b/packages/integration-react/api-report.md @@ -5,9 +5,95 @@ ```ts /// +import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; +import { AuthRequestOptions } from '@backstage/core-plugin-api'; +import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; +import { OAuthApi } from '@backstage/core-plugin-api'; +import { OpenIdConnectApi } from '@backstage/core-plugin-api'; +import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { SessionApi } from '@backstage/core-plugin-api'; + +// @public +export class ScmAuth implements ScmAuthApi { + static createDefaultApiFactory(): ApiFactory< + ScmAuthApi, + ScmAuthApi, + { + github: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi; + gitlab: OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi; + azure: OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi; + } + >; + static forAuthApi( + authApi: OAuthApi, + options: { + host: string; + scopeMapping: { + default: string[]; + repoWrite: string[]; + }; + }, + ): ScmAuth; + static forAzure( + microsoftAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth; + static forBitbucket( + bitbucketAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth; + static forGithub( + githubAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth; + static forGitlab( + gitlabAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth; + // (undocumented) + getCredentials(options: ScmAuthTokenOptions): Promise; + isUrlSupported(url: URL): boolean; + static merge(...providers: ScmAuth[]): ScmAuthApi; +} + +// @public +export interface ScmAuthApi { + getCredentials(options: ScmAuthTokenOptions): Promise; +} + +// @public +export const scmAuthApiRef: ApiRef; + +// @public (undocumented) +export interface ScmAuthTokenOptions extends AuthRequestOptions { + additionalScope?: { + repoWrite?: boolean; + }; + url: string; +} + +// @public (undocumented) +export interface ScmAuthTokenResponse { + headers: { + [name: string]: string; + }; + token: string; +} // Warning: (ae-missing-release-tag) "ScmIntegrationIcon" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 6103cf10f3..508497d9e0 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.4.2", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@backstage/theme": "^0.2.9", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -34,8 +34,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/dev-utils": "^0.2.8", + "@backstage/cli": "^0.7.13", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/integration-react/src/api/ScmAuth.test.ts b/packages/integration-react/src/api/ScmAuth.test.ts new file mode 100644 index 0000000000..2d0ea9b97d --- /dev/null +++ b/packages/integration-react/src/api/ScmAuth.test.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { OAuthApi } from '@backstage/core-plugin-api'; +import { ScmAuth } from './ScmAuth'; + +class MockOAuthApi implements OAuthApi { + constructor(private readonly accessToken: string) {} + + getAccessToken = jest.fn(async () => { + return this.accessToken; + }); +} + +describe('ScmAuth', () => { + it('should provide credentials for GitHub and GHE', async () => { + const mockGithubAuth = new MockOAuthApi('github-access-token'); + const mockGheAuth = new MockOAuthApi('ghe-access-token'); + + const api = ScmAuth.merge( + ScmAuth.forGithub(mockGithubAuth), + ScmAuth.forGithub(mockGheAuth, { + host: 'ghe.example.com', + }), + ); + + await expect( + api.getCredentials({ url: 'https://github.com/backstage/backstage' }), + ).resolves.toEqual({ + token: 'github-access-token', + headers: { + Authorization: 'Bearer github-access-token', + }, + }); + await expect( + api.getCredentials({ + url: 'https://ghe.example.com/backstage/backstage', + additionalScope: { + repoWrite: true, + }, + }), + ).resolves.toEqual({ + token: 'ghe-access-token', + headers: { + Authorization: 'Bearer ghe-access-token', + }, + }); + + expect(mockGithubAuth.getAccessToken).toHaveBeenCalledTimes(1); + expect(mockGithubAuth.getAccessToken).toHaveBeenCalledWith( + ['repo', 'read:org', 'read:user'], + {}, + ); + expect(mockGheAuth.getAccessToken).toHaveBeenCalledTimes(1); + expect(mockGheAuth.getAccessToken).toHaveBeenCalledWith( + ['repo', 'read:org', 'read:user', 'gist'], + {}, + ); + }); + + it('should use correct scopes for each provider', async () => { + const mockAuthApi = { + getAccessToken: async (scopes: string[]) => { + return scopes.join(' '); + }, + }; + + const githubAuth = ScmAuth.forGithub(mockAuthApi); + await expect( + githubAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'repo read:org read:user', + }); + await expect( + githubAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: 'repo read:org read:user gist', + }); + + const gitlabAuth = ScmAuth.forGitlab(mockAuthApi); + await expect( + gitlabAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'read_user read_api read_repository', + }); + await expect( + gitlabAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: 'read_user read_api read_repository write_repository api', + }); + + const azureAuth = ScmAuth.forAzure(mockAuthApi); + await expect( + azureAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'vso.build vso.code vso.graph vso.project vso.profile', + }); + await expect( + azureAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: + 'vso.build vso.code vso.graph vso.project vso.profile vso.code_manage', + }); + + const bitbucketAuth = ScmAuth.forBitbucket(mockAuthApi); + await expect( + bitbucketAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ + token: 'account team pullrequest snippet issue', + }); + await expect( + bitbucketAuth.getCredentials({ + url: 'http://example.com', + additionalScope: { repoWrite: true }, + }), + ).resolves.toMatchObject({ + token: + 'account team pullrequest snippet issue pullrequest:write snippet:write issue:write', + }); + }); + + it('should handle host option', () => { + const mockAuthApi = { + getAccessToken: jest.fn(), + }; + + const expectUrlSupport = (scm: ScmAuth, url: string) => { + expect(scm.isUrlSupported(new URL(url))).toBe(true); + expect(scm.isUrlSupported(new URL('https://not.supported.com'))).toBe( + false, + ); + }; + + expectUrlSupport(ScmAuth.forGithub(mockAuthApi), 'https://github.com'); + expectUrlSupport(ScmAuth.forGitlab(mockAuthApi), 'https://gitlab.com'); + expectUrlSupport( + ScmAuth.forAzure(mockAuthApi, {}), + 'https://dev.azure.com', + ); + expectUrlSupport( + ScmAuth.forBitbucket(mockAuthApi, {}), + 'https://bitbucket.org', + ); + expectUrlSupport( + ScmAuth.forGithub(mockAuthApi, { host: 'example.com' }), + 'https://example.com/abc', + ); + expectUrlSupport( + ScmAuth.forGitlab(mockAuthApi, { host: 'example.com' }), + 'http://example.com', + ); + expectUrlSupport( + ScmAuth.forAzure(mockAuthApi, { host: 'example.com' }), + 'https://example.com', + ); + expectUrlSupport( + ScmAuth.forBitbucket(mockAuthApi, { host: 'example.com:8080' }), + 'https://example.com:8080', + ); + }); + + it('should throw an error for unknown URLs', async () => { + const emptyMux = ScmAuth.merge(); + await expect( + emptyMux.getCredentials({ url: 'http://example.com' }), + ).rejects.toThrow( + "No authentication provider available for access to 'http://example.com'", + ); + + const scmAuth = ScmAuth.merge( + ScmAuth.forAuthApi(new MockOAuthApi('token'), { + host: 'example.com', + scopeMapping: { + default: ['a'], + repoWrite: ['b'], + }, + }), + ); + await expect( + scmAuth.getCredentials({ url: 'http://example.com' }), + ).resolves.toMatchObject({ token: 'token' }); + await expect( + scmAuth.getCredentials({ url: 'http://not.example.com' }), + ).rejects.toThrow( + "No authentication provider available for access to 'http://not.example.com'", + ); + await expect( + scmAuth.getCredentials({ url: 'http://example.com:8080' }), + ).rejects.toThrow( + "No authentication provider available for access to 'http://example.com:8080'", + ); + }); +}); diff --git a/packages/integration-react/src/api/ScmAuth.ts b/packages/integration-react/src/api/ScmAuth.ts new file mode 100644 index 0000000000..c34c4da758 --- /dev/null +++ b/packages/integration-react/src/api/ScmAuth.ts @@ -0,0 +1,275 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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, + githubAuthApiRef, + gitlabAuthApiRef, + microsoftAuthApiRef, + OAuthApi, +} from '@backstage/core-plugin-api'; +import { + ScmAuthApi, + scmAuthApiRef, + ScmAuthTokenOptions, + ScmAuthTokenResponse, +} from './ScmAuthApi'; + +type ScopeMapping = { + /** The base scopes used for all requests */ + default: string[]; + /** Additional scopes added if `repoWrite` is requested */ + repoWrite: string[]; +}; + +class ScmAuthMux implements ScmAuthApi { + #providers: Array; + + constructor(providers: ScmAuth[]) { + this.#providers = providers; + } + + async getCredentials( + options: ScmAuthTokenOptions, + ): Promise { + const url = new URL(options.url); + const provider = this.#providers.find(p => p.isUrlSupported(url)); + if (!provider) { + throw new Error( + `No authentication provider available for access to '${options.url}'`, + ); + } + + return provider.getCredentials(options); + } +} + +/** + * An implementation of the ScmAuthApi that merges together OAuthApi instances + * to form a single instance that can handles authentication for multiple providers. + * + * @public + * + * @example + * ``` + * // Supports authentication towards both public GitHub and GHE: + * createApiFactory({ + * api: scmAuthApiRef, + * deps: { + * gheAuthApi: gheAuthApiRef, + * githubAuthApi: githubAuthApiRef, + * }, + * factory: ({ githubAuthApi, gheAuthApi }) => + * ScmAuth.merge( + * ScmAuth.forGithub(githubAuthApi), + * ScmAuth.forGithub(gheAuthApi, { + * host: 'ghe.example.com', + * }), + * ) + * }) + * ``` + */ +export class ScmAuth implements ScmAuthApi { + /** + * Creates an API factory that enables auth for each of the default SCM providers. + */ + static createDefaultApiFactory() { + return createApiFactory({ + api: scmAuthApiRef, + deps: { + github: githubAuthApiRef, + gitlab: gitlabAuthApiRef, + azure: microsoftAuthApiRef, + }, + factory: ({ github, gitlab, azure }) => + ScmAuth.merge( + ScmAuth.forGithub(github), + ScmAuth.forGitlab(gitlab), + ScmAuth.forAzure(azure), + ), + }); + } + + /** + * Creates a general purpose ScmAuth instance with a custom scope mapping. + */ + static forAuthApi( + authApi: OAuthApi, + options: { + host: string; + scopeMapping: { + default: string[]; + repoWrite: string[]; + }; + }, + ): ScmAuth { + return new ScmAuth(authApi, options.host, options.scopeMapping); + } + + /** + * Creates a new ScmAuth instance that handles authentication towards GitHub. + * + * The host option determines which URLs that are handled by this instance and defaults to `github.com`. + * + * The default scopes are: + * + * `repo read:org read:user` + * + * If the additional `repoWrite` permission is requested, these scopes are added: + * + * `gist` + */ + static forGithub( + githubAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth { + const host = options?.host ?? 'github.com'; + return new ScmAuth(githubAuthApi, host, { + default: ['repo', 'read:org', 'read:user'], + repoWrite: ['gist'], + }); + } + + /** + * Creates a new ScmAuth instance that handles authentication towards GitLab. + * + * The host option determines which URLs that are handled by this instance and defaults to `gitlab.com`. + * + * The default scopes are: + * + * `read_user read_api read_repository` + * + * If the additional `repoWrite` permission is requested, these scopes are added: + * + * `write_repository api` + */ + static forGitlab( + gitlabAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth { + const host = options?.host ?? 'gitlab.com'; + return new ScmAuth(gitlabAuthApi, host, { + default: ['read_user', 'read_api', 'read_repository'], + repoWrite: ['write_repository', 'api'], + }); + } + + /** + * Creates a new ScmAuth instance that handles authentication towards Azure. + * + * The host option determines which URLs that are handled by this instance and defaults to `dev.azure.com`. + * + * The default scopes are: + * + * `vso.build vso.code vso.graph vso.project vso.profile` + * + * If the additional `repoWrite` permission is requested, these scopes are added: + * + * `vso.code_manage` + */ + static forAzure( + microsoftAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth { + const host = options?.host ?? 'dev.azure.com'; + return new ScmAuth(microsoftAuthApi, host, { + default: [ + 'vso.build', + 'vso.code', + 'vso.graph', + 'vso.project', + 'vso.profile', + ], + repoWrite: ['vso.code_manage'], + }); + } + + /** + * Creates a new ScmAuth instance that handles authentication towards Bitbucket. + * + * The host option determines which URLs that are handled by this instance and defaults to `bitbucket.org`. + * + * The default scopes are: + * + * `account team pullrequest snippet issue` + * + * If the additional `repoWrite` permission is requested, these scopes are added: + * + * `pullrequest:write snippet:write issue:write` + */ + static forBitbucket( + bitbucketAuthApi: OAuthApi, + options?: { + host?: string; + }, + ): ScmAuth { + const host = options?.host ?? 'bitbucket.org'; + return new ScmAuth(bitbucketAuthApi, host, { + default: ['account', 'team', 'pullrequest', 'snippet', 'issue'], + repoWrite: ['pullrequest:write', 'snippet:write', 'issue:write'], + }); + } + + /** + * Merges together multiple ScmAuth instances into one that + * routes requests to the correct instance based on the URL. + */ + static merge(...providers: ScmAuth[]): ScmAuthApi { + return new ScmAuthMux(providers); + } + + #api: OAuthApi; + #host: string; + #scopeMapping: ScopeMapping; + + private constructor(api: OAuthApi, host: string, scopeMapping: ScopeMapping) { + this.#api = api; + this.#host = host; + this.#scopeMapping = scopeMapping; + } + + /** + * Checks whether the implementation is able to provide authentication for the given URL. + */ + isUrlSupported(url: URL): boolean { + return url.host === this.#host; + } + + async getCredentials( + options: ScmAuthTokenOptions, + ): Promise { + const { url, additionalScope, ...restOptions } = options; + + const scopes = this.#scopeMapping.default.slice(); + if (additionalScope?.repoWrite) { + scopes.push(...this.#scopeMapping.repoWrite); + } + + const token = await this.#api.getAccessToken(scopes, restOptions); + return { + token, + headers: { + Authorization: `Bearer ${token}`, + }, + }; + } +} diff --git a/packages/integration-react/src/api/ScmAuthApi.ts b/packages/integration-react/src/api/ScmAuthApi.ts new file mode 100644 index 0000000000..d8fc413620 --- /dev/null +++ b/packages/integration-react/src/api/ScmAuthApi.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2020 The Backstage Authors + * + * 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 { + ApiRef, + createApiRef, + AuthRequestOptions, +} from '@backstage/core-plugin-api'; + +/** @public */ +export interface ScmAuthTokenOptions extends AuthRequestOptions { + /** + * The URL of the SCM resource to be accessed. + * + * @example https://github.com/backstage/backstage + */ + url: string; + + /** + * Whether to request additional access scope. + * + * Read access to user, organization, and repositories is always included. + */ + additionalScope?: { + /** + * Requests access to be able to write repository content, including + * the ability to create things like issues and pull requests. + */ + repoWrite?: boolean; + }; +} + +/** @public */ +export interface ScmAuthTokenResponse { + /** + * An authorization token that can be used to authenticate requests. + */ + token: string; + + /** + * The set of HTTP headers that are needed to authenticate requests. + */ + headers: { [name: string]: string }; +} + +/** + * ScmAuthApi provides methods for authenticating towards source code management services. + * + * As opposed to using the GitHub, GitLab and other auth APIs + * directly, this API allows for more generic access to SCM services. + * + * @public + */ +export interface ScmAuthApi { + /** + * Requests credentials for accessing an SCM resource. + */ + getCredentials(options: ScmAuthTokenOptions): Promise; +} + +/** + * The ApiRef for the ScmAuthApi. + * + * @public + */ +export const scmAuthApiRef: ApiRef = createApiRef({ + id: 'core.scmauth', +}); diff --git a/packages/integration-react/src/api/index.ts b/packages/integration-react/src/api/index.ts index 7417bd0c7b..2847804b63 100644 --- a/packages/integration-react/src/api/index.ts +++ b/packages/integration-react/src/api/index.ts @@ -14,6 +14,13 @@ * limitations under the License. */ +export { scmAuthApiRef } from './ScmAuthApi'; +export { ScmAuth } from './ScmAuth'; +export type { + ScmAuthApi, + ScmAuthTokenOptions, + ScmAuthTokenResponse, +} from './ScmAuthApi'; export { ScmIntegrationsApi, scmIntegrationsApiRef, diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 868641b630..8389d87f01 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/integration +## 0.6.5 + +### Patch Changes + +- 8113ba5ebb: Allow file extension `.yml` to be ingested in GitLab processor +- Updated dependencies + - @backstage/config@0.1.10 + ## 0.6.4 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 51ebc34222..1866ef18de 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration", "description": "Helpers for managing integrations towards external systems", - "version": "0.6.4", + "version": "0.6.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.8", + "@backstage/config": "^0.1.10", "cross-fetch": "^3.0.6", "git-url-parse": "^11.6.0", "@octokit/rest": "^18.5.3", @@ -38,11 +38,11 @@ "luxon": "^2.0.2" }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@backstage/config-loader": "^0.6.7", "@backstage/test-utils": "^0.1.17", "@types/jest": "^26.0.7", - "@types/luxon": "^1.25.0", + "@types/luxon": "^2.0.4", "msw": "^0.29.0" }, "files": [ diff --git a/packages/integration/src/github/GithubCredentialsProvider.test.ts b/packages/integration/src/github/GithubCredentialsProvider.test.ts index 9450d96ab7..0f51db85be 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.test.ts @@ -76,7 +76,7 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ data: { - expires_at: DateTime.local().plus({ hour: 1 }).toString(), + expires_at: DateTime.local().plus({ hours: 1 }).toString(), token: 'secret_token', }, } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); @@ -120,7 +120,7 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ data: { - expires_at: DateTime.local().plus({ hour: 1 }).toString(), + expires_at: DateTime.local().plus({ hours: 1 }).toString(), token: 'secret_token', }, } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); @@ -151,7 +151,7 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ data: { - expires_at: DateTime.local().plus({ hour: 1 }).toString(), + expires_at: DateTime.local().plus({ hours: 1 }).toString(), token: 'secret_token', }, } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); @@ -272,7 +272,7 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.createInstallationAccessToken.mockResolvedValueOnce({ data: { - expires_at: DateTime.local().plus({ hour: 1 }).toString(), + expires_at: DateTime.local().plus({ hours: 1 }).toString(), token: 'secret_token', }, } as RestEndpointMethodTypes['apps']['createInstallationAccessToken']['response']); diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index 3156488ba7..62c7cd7a57 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -64,12 +64,14 @@ const HEADERS = { */ class GithubAppManager { private readonly appClient: Octokit; + private readonly baseUrl?: string; private readonly baseAuthConfig: { appId: number; privateKey: string }; private readonly cache = new Cache(); private readonly allowedInstallationOwners: string[] | undefined; // undefined allows all installations constructor(config: GithubAppConfig, baseUrl?: string) { this.allowedInstallationOwners = config.allowedInstallationOwners; + this.baseUrl = baseUrl; this.baseAuthConfig = { appId: config.appId, privateKey: config.privateKey, @@ -108,6 +110,7 @@ class GithubAppManager { }); if (repo && result.data.repository_selection === 'selected') { const installationClient = new Octokit({ + baseUrl: this.baseUrl, auth: result.data.token, }); const repos = await installationClient.paginate( diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index f07d65e02a..4f39fb2fe7 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -47,7 +47,7 @@ describe('gitlab core', () => { baseUrl: '', }; - describe('getGitLabFileFetchUrl', () => { + describe('getGitLabFileFetchUrl with .yaml extension', () => { it.each([ // Project URLs { @@ -91,4 +91,49 @@ describe('gitlab core', () => { await expect(getGitLabFileFetchUrl(url, config)).resolves.toBe(result); }); }); + + describe('getGitLabFileFetchUrl with .yml extension', () => { + it.each([ + // Project URLs + { + config: configWithNoToken, + url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yml/raw?ref=branch', + }, + { + config: configWithNoToken, + // Works with non URI encoded link + url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yml/raw?ref=branch', + }, + { + config: configWithNoToken, + url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yml/raw?ref=branch', + }, + { + config: configWithToken, + url: 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yml', + result: + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yml/raw?ref=branch', + }, + { + config: configWithNoToken, + url: 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yml', // Repo not in subgroup + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yml/raw?ref=branch', + }, + // Raw URLs + { + config: configWithNoToken, + url: 'https://gitlab.example.com/a/b/blob/master/c.yml', + result: 'https://gitlab.example.com/a/b/raw/master/c.yml', + }, + ])('should handle happy path %#', async ({ config, url, result }) => { + await expect(getGitLabFileFetchUrl(url, config)).resolves.toBe(result); + }); + }); }); diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 83ab29c62e..b30d44e2db 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -76,7 +76,7 @@ export function buildRawUrl(target: string): URL { userOrOrg === '' || repoName === '' || blobKeyword !== 'blob' || - !restOfPath.join('/').match(/\.yaml$/) + !restOfPath.join('/').match(/\.(yaml|yml)$/) ) { throw new Error('Wrong GitLab URL'); } diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md index 0f14f55278..9bc2159d40 100644 --- a/plugins/allure/CHANGELOG.md +++ b/plugins/allure/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-allure +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.1.2 ### Patch Changes diff --git a/plugins/allure/package.json b/plugins/allure/package.json index eb808f5b36..d9eee7ab97 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-allure", "description": "A Backstage plugin that integrates with Allure", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,9 +36,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 4e6cd833f4..f544a8ef30 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-api-docs +## 0.6.9 + +### Patch Changes + +- cc464a56b3: This makes Type and Lifecycle columns consistent for all table cases and adds a new line in Description column for better readability +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog@0.6.16 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.6.8 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index e294a1a83f..7ccec47ad2 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.6.8", + "version": "0.6.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ }, "dependencies": { "@asyncapi/react-component": "^0.23.0", - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog": "^0.6.15", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog": "^0.6.16", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.12.2", @@ -53,9 +53,9 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index f2b184eea3..5f29897fd3 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-auth-backend +## 0.4.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.4.0 ### Minor Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index a924d30a3f..9293aec596 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/catalog-client": "^0.3.18", - "@backstage/catalog-model": "^0.9.1", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", "@backstage/test-utils": "^0.1.17", "@types/express": "^4.17.6", @@ -71,7 +71,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts index c3b2f5c6c9..a924c80242 100644 --- a/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts +++ b/plugins/auth-backend/src/lib/flow/authFlowHelpers.ts @@ -58,7 +58,7 @@ export const postMessageResponse = ( (window.opener || window.parent).postMessage(JSON.parse(authResponse), origin); setTimeout(() => { window.close(); - }, 100); // same as the interval of the core-api lib/loginPopup.ts (to address race conditions) + }, 100); // same as the interval of the core-app-api lib/loginPopup.ts (to address race conditions) `; const hash = crypto.createHash('sha256').update(script).digest('base64'); diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 17e3769d21..2bf8fa2520 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -36,7 +36,7 @@ export const readState = (stateString: string): OAuthState => { export const encodeState = (state: OAuthState): string => { const stateString = new URLSearchParams( - pickBy(state, value => value !== undefined), + pickBy(state, value => value !== undefined), ).toString(); return Buffer.from(stateString, 'utf-8').toString('hex'); diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 36e2f6ed99..cf70580664 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -166,6 +166,7 @@ export class OAuth2AuthProvider implements OAuthHandlers { accessToken: result.accessToken, scope: result.params.scope, expiresInSeconds: result.params.expires_in, + refreshToken: result.refreshToken, }, profile, }; diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index 6ff444fa36..bc04826b9c 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-badges-backend +## 0.1.10 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.1.9 ### Patch Changes diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 9f8c2021ad..021038b754 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-client": "^0.3.16", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", "@types/express": "^4.17.6", "badge-maker": "^3.3.0", @@ -46,7 +46,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.1", + "@backstage/cli": "^0.7.13", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index dee12b696a..c08bb283e6 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-badges +## 0.2.10 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.2.9 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index bbc246082e..16e5121132 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,9 +36,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/bazaar-backend/src/service/router.test.ts b/plugins/bazaar-backend/src/service/router.test.ts deleted file mode 100644 index 947ad27aee..0000000000 --- a/plugins/bazaar-backend/src/service/router.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * 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 { getVoidLogger } from '@backstage/backend-common'; -import express from 'express'; -import request from 'supertest'; -import { ConfigReader } from '@backstage/config'; -import { createRouter } from './router'; - -describe('createRouter', () => { - let app: express.Express; - - beforeAll(async () => { - const router = await createRouter({ - logger: getVoidLogger(), - config: new ConfigReader({}), - }); - app = express().use(router); - }); - - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('GET /health', () => { - it('returns ok', async () => { - const response = await request(app).get('/health'); - - expect(response.status).toEqual(200); - expect(response.body).toEqual({ status: 'ok' }); - }); - }); -}); diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 5d4415d570..c0d0ecefe4 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -23,7 +23,7 @@ "dependencies": { "@backstage/catalog-model": "^0.9.0", "@backstage/cli": "^0.7.2", - "@backstage/core-components": "^0.3.0", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.3", "@backstage/plugin-catalog": "^0.6.6", "@backstage/plugin-catalog-react": "^0.4.0", diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index 2ab1d2efdb..58d4d87e89 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-bitrise +## 0.1.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.1.12 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 0394e776bc..263e27e65c 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.12", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,9 +39,9 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index c93ba4ed19..d50af57559 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.3.1 + +### Patch Changes + +- 8b016ce67b: Alters LDAP processor to handle one SearchEntry at a time +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/plugin-catalog-backend@0.14.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + ## 0.3.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index 2787287b62..a51c0ceceb 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend modules that helps integrate towards LDAP", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,16 +29,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.7", - "@backstage/plugin-catalog-backend": "^0.13.3", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/plugin-catalog-backend": "^0.14.0", "@types/ldapjs": "^2.2.0", "ldapjs": "^2.2.0", "lodash": "^4.17.21", "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.16", "@types/lodash": "^4.14.151", "msw": "^0.29.0" diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index b226c749aa..d15def9084 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.2.4 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/plugin-catalog-backend@0.14.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + ## 0.2.3 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 397d4fffb3..3cccc69215 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend modules that helps integrate towards Microsoft Graph", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ }, "dependencies": { "@azure/msal-node": "^1.1.0", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.8", - "@backstage/plugin-catalog-backend": "^0.13.5", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/plugin-catalog-backend": "^0.14.0", "@microsoft/microsoft-graph-types": "^1.25.0", "cross-fetch": "^3.0.6", "lodash": "^4.17.21", @@ -41,8 +41,8 @@ "qs": "^6.9.4" }, "devDependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/cli": "^0.7.9", + "@backstage/backend-common": "^0.9.4", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.14", "@types/lodash": "^4.14.151", "msw": "^0.29.0" diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index b30ba23099..e3e6bbe786 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-catalog-backend +## 0.14.0 + +### Minor Changes + +- d6f90e934d: #### Enforcing catalog rules + + Apply the catalog rules enforcer, based on origin location. + + This is a breaking change, in the sense that this was not properly checked in earlier versions of the new catalog engine. You may see ingestion of certain entities start to be rejected after this update, if the following conditions apply to you: + + - You are using the configuration key `catalog.rules.[].allow`, and + - Your registered locations point (directly or transitively) to entities whose kinds are not listed in `catalog.rules.[].allow` + + and/or + + - You are using the configuration key `catalog.locations.[].rules.[].allow` + - The config locations point (directly or transitively) to entities whose kinds are not listed neither `catalog.rules.[].allow`, nor in the corresponding `.rules.[].allow` of that config location + + This is an example of what the configuration might look like: + + ```yaml + catalog: + # These do not list Template as a valid kind; users are therefore unable to + # manually register entities of the Template kind + rules: + - allow: + - Component + - API + - Resource + - Group + - User + - System + - Domain + - Location + locations: + # This lists Template as valid only for that specific config location + - type: file + target: ../../plugins/scaffolder-backend/sample-templates/all-templates.yaml + rules: + - allow: [Template] + ``` + + If you are not using any of those `rules` section, you should not be affected by this change. + + If you do use any of those `rules` sections, make sure that they are complete and list all of the kinds that are in active use in your Backstage installation. + + #### Other + + Also, the class `CatalogRulesEnforcer` was renamed to `DefaultCatalogRulesEnforcer`, implementing the type `CatalogRulesEnforcer`. + +- 501ce92f9c: Bitbucket Cloud Discovery support +- 89fd81a1ab: Add API endpoint for requesting a catalog refresh at `/refresh`, which is activated if a `RefreshService` is passed to `createRouter`. + + The new method is used to trigger a refresh of an entity in an as localized was as possible, usually by refreshing the parent location. + +### Patch Changes + +- 9ef2987a83: Update `createLocation` to optionally return `exists` to signal that the location already exists, this is only returned for dry runs. +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.13.8 ### Patch Changes diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 89ef2c09d1..133deb96fc 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -302,23 +302,27 @@ export interface CatalogProcessingOrchestrator { // // @public (undocumented) export type CatalogProcessor = { + getProcessorName?(): string; readLocation?( location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, + cache: CatalogProcessorCache, ): Promise; preProcessEntity?( entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit, originLocation: LocationSpec, + cache: CatalogProcessorCache, ): Promise; validateEntityKind?(entity: Entity): Promise; postProcessEntity?( entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit, + cache: CatalogProcessorCache, ): Promise; handleError?( error: Error, @@ -327,6 +331,12 @@ export type CatalogProcessor = { ): Promise; }; +// @public +export interface CatalogProcessorCache { + get(key: string): Promise; + set(key: string, value: ItemType): Promise; +} + // Warning: (ae-missing-release-tag) "CatalogProcessorEmit" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -385,6 +395,22 @@ export type CatalogProcessorResult = | CatalogProcessorRelationResult | CatalogProcessorErrorResult; +// @public +export type CatalogRule = { + allow: Array<{ + kind: string; + }>; + locations?: Array<{ + target?: string; + type: string; + }>; +}; + +// @public +export type CatalogRulesEnforcer = { + isAllowed(entity: Entity, location: LocationSpec): boolean; +}; + // Warning: (ae-missing-release-tag) "CodeOwnersProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -751,11 +777,20 @@ export class DefaultCatalogProcessingOrchestrator logger: Logger_2; parser: CatalogProcessorParser; policy: EntityPolicy; + rulesEnforcer: CatalogRulesEnforcer; }); // (undocumented) process(request: EntityProcessingRequest): Promise; } +// @public +export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { + constructor(rules: CatalogRule[]); + static readonly defaultRules: CatalogRule[]; + static fromConfig(config: Config): DefaultCatalogRulesEnforcer; + isAllowed(entity: Entity, location: LocationSpec): boolean; +} + // Warning: (ae-missing-release-tag) "DeferredEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -843,7 +878,7 @@ export type EntityPagination = { // @public (undocumented) export type EntityProcessingRequest = { entity: Entity; - state: Map; + state?: JsonObject; }; // Warning: (ae-missing-release-tag) "EntityProcessingResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -852,7 +887,7 @@ export type EntityProcessingRequest = { export type EntityProcessingResult = | { ok: true; - state: Map; + state: JsonObject; completedEntity: Entity; deferredEntities: DeferredEntity[]; relations: EntityRelationSpec[]; @@ -1453,11 +1488,14 @@ export class UrlReaderProcessor implements CatalogProcessor { // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts constructor(options: Options_3); // (undocumented) + getProcessorName(): string; + // (undocumented) readLocation( location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, + cache: CatalogProcessorCache, ): Promise; } @@ -1480,21 +1518,6 @@ export class UrlReaderProcessor implements CatalogProcessor { // src/database/types.d.ts:164:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/database/types.d.ts:165:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/ingestion/processors/GithubMultiOrgReaderProcessor.d.ts:23:9 - (ae-forgotten-export) The symbol "GithubMultiOrgConfig" needs to be exported by the entry point index.d.ts -// src/ingestion/processors/types.d.ts:7:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:8:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:9:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:10:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:23:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:24:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:25:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:26:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:36:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:47:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:48:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:49:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:56:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:57:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/ingestion/processors/types.d.ts:58:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/ingestion/types.d.ts:17:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/ingestion/types.d.ts:41:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen ``` diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 16f61c2b9a..b9ccd40862 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.13.8", + "version": "0.14.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/catalog-client": "^0.3.19", - "@backstage/catalog-model": "^0.9.2", - "@backstage/config": "^0.1.9", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.2", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@backstage/plugin-search-backend-node": "^0.4.2", "@backstage/search-common": "^0.2.0", "@octokit/graphql": "^4.5.8", @@ -65,7 +65,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.7", - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.17", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 82a548a7f1..24d6b2fd66 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { LocationSpec, Entity } from '@backstage/catalog-model'; -import { CatalogRulesEnforcer } from './CatalogRules'; +import { Entity, LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; +import { DefaultCatalogRulesEnforcer } from './CatalogRules'; const entity = { user: { @@ -48,9 +48,9 @@ const location: Record = { }, }; -describe('CatalogRulesEnforcer', () => { +describe('DefaultCatalogRulesEnforcer', () => { it('should deny by default', () => { - const enforcer = new CatalogRulesEnforcer([]); + const enforcer = new DefaultCatalogRulesEnforcer([]); expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); @@ -58,7 +58,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should deny all', () => { - const enforcer = new CatalogRulesEnforcer([{ allow: [] }]); + const enforcer = new DefaultCatalogRulesEnforcer([{ allow: [] }]); expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); @@ -66,7 +66,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should allow all', () => { - const enforcer = new CatalogRulesEnforcer([ + const enforcer = new DefaultCatalogRulesEnforcer([ { allow: ['User', 'Group', 'Component', 'Location'].map(kind => ({ kind, @@ -80,7 +80,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should deny groups', () => { - const enforcer = new CatalogRulesEnforcer([ + const enforcer = new DefaultCatalogRulesEnforcer([ { allow: [{ kind: 'User' }, { kind: 'Component' }] }, ]); expect(enforcer.isAllowed(entity.user, location.x)).toBe(true); @@ -91,7 +91,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should deny groups from github', () => { - const enforcer = new CatalogRulesEnforcer([ + const enforcer = new DefaultCatalogRulesEnforcer([ { allow: [{ kind: 'User' }, { kind: 'Component' }] }, { allow: [{ kind: 'Group' }], locations: [{ type: 'file' }] }, ]); @@ -103,7 +103,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should allow groups from files', () => { - const enforcer = new CatalogRulesEnforcer([ + const enforcer = new DefaultCatalogRulesEnforcer([ { allow: [{ kind: 'Group' }], locations: [{ type: 'file' }] }, ]); expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); @@ -114,7 +114,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should not be sensitive to kind case', () => { - const enforcer = new CatalogRulesEnforcer([ + const enforcer = new DefaultCatalogRulesEnforcer([ { allow: [{ kind: 'group' }] }, { allow: [{ kind: 'Component' }] }, ]); @@ -127,7 +127,9 @@ describe('CatalogRulesEnforcer', () => { describe('fromConfig', () => { it('should allow components by default', () => { - const enforcer = CatalogRulesEnforcer.fromConfig(new ConfigReader({})); + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( + new ConfigReader({}), + ); expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); expect(enforcer.isAllowed(entity.group, location.y)).toBe(false); expect(enforcer.isAllowed(entity.component, location.z)).toBe(true); @@ -135,7 +137,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should deny all', () => { - const enforcer = CatalogRulesEnforcer.fromConfig( + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ catalog: { rules: [] } }), ); expect(enforcer.isAllowed(entity.user, location.x)).toBe(false); @@ -145,7 +147,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should allow all', () => { - const enforcer = CatalogRulesEnforcer.fromConfig( + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ catalog: { rules: [{ allow: ['User', 'Group'] }, { allow: ['Component'] }], @@ -158,7 +160,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should deny groups', () => { - const enforcer = CatalogRulesEnforcer.fromConfig( + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ catalog: { rules: [{ allow: ['User'] }, { allow: ['Component'] }] }, }), @@ -172,7 +174,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should allow groups from a specific github location', () => { - const enforcer = CatalogRulesEnforcer.fromConfig( + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ catalog: { rules: [{ allow: ['user'] }], @@ -199,7 +201,7 @@ describe('CatalogRulesEnforcer', () => { }); it('should not care about location configuration in catalog.rules', () => { - const enforcer = CatalogRulesEnforcer.fromConfig( + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( new ConfigReader({ catalog: { rules: [{ allow: ['Group'], locations: [{ type: 'github' }] }], diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 97ee2a942b..a0ef5f3d53 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -16,33 +16,42 @@ import { Config } from '@backstage/config'; import { LocationSpec, Entity } from '@backstage/catalog-model'; +import path from 'path'; /** - * A structure for matching entities to a given rule. - */ -type EntityMatcher = { - kind: string; -}; - -/** - * A structure for matching locations to a given rule. - */ -type LocationMatcher = { - target?: string; - type: string; -}; - -/** - * Rules to apply to catalog entities + * Rules to apply to catalog entities. * - * An undefined list of matchers means match all, an empty list of matchers means match none + * An undefined list of matchers means match all, an empty list of matchers means match none. + * + * @public */ -type CatalogRule = { - allow: EntityMatcher[]; - locations?: LocationMatcher[]; +export type CatalogRule = { + allow: Array<{ + kind: string; + }>; + locations?: Array<{ + target?: string; + type: string; + }>; }; -export class CatalogRulesEnforcer { +/** + * Decides whether an entity from a given location is allowed to enter the + * catalog, according to some rule set. + * + * @public + */ +export type CatalogRulesEnforcer = { + isAllowed(entity: Entity, location: LocationSpec): boolean; +}; + +/** + * Implements the default catalog rule set, consuming the config keys + * `catalog.rules` and `catalog.locations.[].rules`. + * + * @public + */ +export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { /** * Default rules used by the catalog. * @@ -93,7 +102,7 @@ export class CatalogRulesEnforcer { })); rules.push(...globalRules); } else { - rules.push(...CatalogRulesEnforcer.defaultRules); + rules.push(...DefaultCatalogRulesEnforcer.defaultRules); } if (config.has('catalog.locations')) { @@ -104,7 +113,7 @@ export class CatalogRulesEnforcer { return []; } const type = locConf.getString('type'); - const target = locConf.getString('target'); + const target = resolveTarget(type, locConf.getString('target')); return locConf.getConfigArray('rules').map(ruleConf => ({ allow: ruleConf.getStringArray('allow').map(kind => ({ kind })), @@ -115,7 +124,7 @@ export class CatalogRulesEnforcer { rules.push(...locationRules); } - return new CatalogRulesEnforcer(rules); + return new DefaultCatalogRulesEnforcer(rules); } constructor(private readonly rules: CatalogRule[]) {} @@ -140,7 +149,7 @@ export class CatalogRulesEnforcer { private matchLocation( location: LocationSpec, - matchers?: LocationMatcher[], + matchers?: { target?: string; type: string }[], ): boolean { if (!matchers) { return true; @@ -159,7 +168,7 @@ export class CatalogRulesEnforcer { return false; } - private matchEntity(entity: Entity, matchers?: EntityMatcher[]): boolean { + private matchEntity(entity: Entity, matchers?: { kind: string }[]): boolean { if (!matchers) { return true; } @@ -175,3 +184,11 @@ export class CatalogRulesEnforcer { return false; } } + +function resolveTarget(type: string, target: string): string { + if (type !== 'file') { + return target; + } + + return path.resolve(target); +} diff --git a/plugins/catalog-backend/src/ingestion/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/LocationReaders.ts index 624c7aaa61..b8c9125893 100644 --- a/plugins/catalog-backend/src/ingestion/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/LocationReaders.ts @@ -52,6 +52,13 @@ type Options = { policy: EntityPolicy; }; +const noopCache = { + async get() { + return undefined; + }, + async set() {}, +}; + /** * Implements the reading of a location through a series of processor tasks. */ @@ -167,6 +174,7 @@ export class LocationReaders implements LocationReader { item.optional, validatedEmit, this.options.parser, + noopCache, ) ) { return; @@ -215,6 +223,7 @@ export class LocationReaders implements LocationReader { item.location, emit, originLocation, + noopCache, ); } catch (e) { const message = `Processor ${ @@ -285,6 +294,7 @@ export class LocationReaders implements LocationReader { current, item.location, emit, + noopCache, ); } catch (e) { const message = `Processor ${ diff --git a/plugins/catalog-backend/src/ingestion/index.ts b/plugins/catalog-backend/src/ingestion/index.ts index e27a3ef0ac..80fea962b9 100644 --- a/plugins/catalog-backend/src/ingestion/index.ts +++ b/plugins/catalog-backend/src/ingestion/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +export type { CatalogRule, CatalogRulesEnforcer } from './CatalogRules'; +export { DefaultCatalogRulesEnforcer } from './CatalogRules'; export { HigherOrderOperations } from './HigherOrderOperations'; export { LocationReaders } from './LocationReaders'; export type { diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts index c067958093..2a63605b0d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts @@ -17,7 +17,12 @@ import { getVoidLogger } from '@backstage/backend-common'; import { BitbucketDiscoveryProcessor } from './BitbucketDiscoveryProcessor'; import { ConfigReader } from '@backstage/config'; import { LocationSpec } from '@backstage/catalog-model'; -import { BitbucketRepositoryParser, PagedResponse } from './bitbucket'; +import { + BitbucketRepository20, + BitbucketRepositoryParser, + PagedResponse, + PagedResponse20, +} from './bitbucket'; import { results } from './index'; import { RequestHandler, rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -79,6 +84,43 @@ function setupStubs(projects: any[]) { } } +function setupBitbucketCloudStubs( + workspace: string, + repositories: Pick[], +) { + const stubCallerFn = jest.fn(); + function pagedResponse(values: any): PagedResponse20 { + return { + values: values, + page: 1, + } as PagedResponse20; + } + + server.use( + rest.get( + `https://api.bitbucket.org/2.0/repositories/${workspace}`, + (req, res, ctx) => { + stubCallerFn(req); + return res( + ctx.json( + pagedResponse( + repositories.map(r => ({ + ...r, + links: { + html: { + href: `https://bitbucket.org/${workspace}/${r.slug}`, + }, + }, + })), + ), + ), + ); + }, + ), + ); + return stubCallerFn; +} + describe('BitbucketDiscoveryProcessor', () => { beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); @@ -129,7 +171,7 @@ describe('BitbucketDiscoveryProcessor', () => { }); }); - describe('handles repositories', () => { + describe('handles organisation repositories', () => { const processor = BitbucketDiscoveryProcessor.fromConfig( new ConfigReader({ integrations: { @@ -255,6 +297,262 @@ describe('BitbucketDiscoveryProcessor', () => { }); }); + describe('handles cloud repositories', () => { + const processor = BitbucketDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: 'myuser', + appPassword: 'blob', + }, + ], + }, + }), + { logger: getVoidLogger() }, + ); + + it('output all repositories by default', async () => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ]); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: 'https://bitbucket.org/workspaces/myworkspace', + }; + + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + }, + optional: true, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog-info.yaml', + }, + optional: true, + }); + }); + + it('uses provided catalog path', async () => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ]); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace?catalogPath=my/nested/path/catalog.yaml', + }; + + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/my/nested/path/catalog.yaml', + }, + optional: true, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-two/src/master/my/nested/path/catalog.yaml', + }, + optional: true, + }); + }); + + it('output all repositories', async () => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ]); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace/projects/*/repos/*?catalogPath=catalog.yaml', + }; + + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', + }, + optional: true, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog.yaml', + }, + optional: true, + }); + }); + + it('output repositories with wildcards', async () => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-two' }, slug: 'repository-two' }, + ]); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/*?catalogPath=catalog.yaml', + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', + }, + optional: true, + }); + }); + + it('filter unrelated repositories', async () => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + { project: { key: 'prj-one' }, slug: 'repository-two' }, + { project: { key: 'prj-one' }, slug: 'repository-three' }, + ]); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/repository-three?catalogPath=catalog.yaml', + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-three/src/master/catalog.yaml', + }, + optional: true, + }); + }); + + it('submits query', async () => { + const mockCall = setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + ]); + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: + 'https://bitbucket.org/workspaces/myworkspace?q=project.key ~ "prj-one"', + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toBeCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + }, + optional: true, + }); + expect(mockCall).toBeCalledTimes(1); + // it should be possible to do this via an `expect.objectContaining` check but seems to fail with some encoding issue. + expect(mockCall.mock.calls[0][0].url).toMatchInlineSnapshot( + `"https://api.bitbucket.org/2.0/repositories/myworkspace?page=1&pagelen=100&q=project.key+%7E+%22prj-one%22"`, + ); + }); + + it.each` + target + ${'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/*'} + ${'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/*/'} + ${'https://bitbucket.org/workspaces/myworkspace/projects/prj-one/repos/repository-*/'} + `("target '$target' adds default path to catalog", async ({ target }) => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + ]); + + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: target, + }; + + const emitter = jest.fn(); + await processor.readLocation(location, false, emitter); + + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + }, + optional: true, + }); + }); + + it.each` + target + ${'https://bitbucket.org/test'} + `("target '$target' is rejected", async ({ target }) => { + setupBitbucketCloudStubs('myworkspace', [ + { project: { key: 'prj-one' }, slug: 'repository-one' }, + ]); + + const location: LocationSpec = { + type: 'bitbucket-discovery', + target: target, + }; + + const emitter = jest.fn(); + await expect( + processor.readLocation(location, false, emitter), + ).rejects.toThrow(/Failed to parse /); + }); + }); + describe('Custom repository parser', () => { const customRepositoryParser: BitbucketRepositoryParser = async function* customRepositoryParser({}) { diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts index 3b535881d9..6a09694ae8 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.ts @@ -17,6 +17,7 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { + BitbucketIntegration, ScmIntegrationRegistry, ScmIntegrations, } from '@backstage/integration'; @@ -26,10 +27,16 @@ import { BitbucketClient, defaultRepositoryParser, paginated, + paginated20, BitbucketRepository, + BitbucketRepository20, } from './bitbucket'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; +const DEFAULT_BRANCH = 'master'; +const DEFAULT_CATALOG_LOCATION = '/catalog-info.yaml'; +const EMPTY_CATALOG_LOCATION = '/'; + export class BitbucketDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly parser: BitbucketRepositoryParser; @@ -71,10 +78,6 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { throw new Error( `There is no Bitbucket integration that matches ${location.target}. Please add a configuration entry for it under integrations.bitbucket`, ); - } else if (integration.config.host === 'bitbucket.org') { - throw new Error( - `Component discovery for Bitbucket Cloud is not yet supported`, - ); } const client = new BitbucketClient({ @@ -83,38 +86,89 @@ export class BitbucketDiscoveryProcessor implements CatalogProcessor { const startTimestamp = Date.now(); this.logger.info(`Reading Bitbucket repositories from ${location.target}`); - const { catalogPath } = parseUrl(location.target); - const expandedCatalogPath = - catalogPath === '/' ? '/catalog-info.yaml' : catalogPath; + const isBitbucketCloud = integration.config.host === 'bitbucket.org'; - const result = await readBitbucketOrg(client, location.target); + const processOptions: ProcessOptions = { + client, + emit, + integration, + location, + }; + const { scanned, matches } = isBitbucketCloud + ? await this.processCloudRepositories(processOptions) + : await this.processOrganisationRepositories(processOptions); + + const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); + this.logger.debug( + `Read ${scanned} Bitbucket repositories (${matches} matching the pattern) in ${duration} seconds`, + ); + + return true; + } + + private async processCloudRepositories( + options: ProcessOptions, + ): Promise { + const { client, location, integration, emit } = options; + + const { catalogPath: requestedCatalogPath } = parseBitbucketCloudUrl( + location.target, + ); + const catalogPath = + requestedCatalogPath === EMPTY_CATALOG_LOCATION + ? DEFAULT_CATALOG_LOCATION + : requestedCatalogPath; + const result = await readBitbucketCloud(client, location.target); for (const repository of result.matches) { + const mainbranch = repository.mainbranch?.name ?? DEFAULT_BRANCH; for await (const entity of this.parser({ - integration: integration, - target: `${repository.links.self[0].href}${expandedCatalogPath}`, + integration, + target: `${repository.links.html.href}/src/${mainbranch}${catalogPath}`, logger: this.logger, })) { emit(entity); } } + return { + matches: result.matches.length, + scanned: result.scanned, + }; + } - const duration = ((Date.now() - startTimestamp) / 1000).toFixed(1); - this.logger.debug( - `Read ${result.scanned} Bitbucket repositories (${result.matches.length} matching the pattern) in ${duration} seconds`, - ); - - return true; + private async processOrganisationRepositories( + options: ProcessOptions, + ): Promise { + const { client, location, integration, emit } = options; + const { catalogPath: requestedCatalogPath } = parseUrl(location.target); + const catalogPath = + requestedCatalogPath === EMPTY_CATALOG_LOCATION + ? DEFAULT_CATALOG_LOCATION + : requestedCatalogPath; + const result = await readBitbucketOrg(client, location.target); + for (const repository of result.matches) { + for await (const entity of this.parser({ + integration, + target: `${repository.links.self[0].href}${catalogPath}`, + logger: this.logger, + })) { + emit(entity); + } + } + return { + matches: result.matches.length, + scanned: result.scanned, + }; } } export async function readBitbucketOrg( client: BitbucketClient, target: string, -): Promise { +): Promise> { const { projectSearchPath, repoSearchPath } = parseUrl(target); const projects = paginated(options => client.listProjects(options)); - const result: Result = { + const result: Result = { scanned: 0, matches: [], }; @@ -136,6 +190,40 @@ export async function readBitbucketOrg( return result; } +export async function readBitbucketCloud( + client: BitbucketClient, + target: string, +): Promise> { + const { + workspacePath, + queryParam: q, + projectSearchPath, + repoSearchPath, + } = parseBitbucketCloudUrl(target); + + const repositories = paginated20( + options => client.listRepositoriesByWorkspace20(workspacePath, options), + { + q, + }, + ); + const result: Result = { + scanned: 0, + matches: [], + }; + + for await (const repository of repositories) { + result.scanned++; + if ( + (!projectSearchPath || projectSearchPath.test(repository.project.key)) && + (!repoSearchPath || repoSearchPath.test(repository.slug)) + ) { + result.matches.push(repository); + } + } + return result; +} + function parseUrl(urlString: string): { projectSearchPath: RegExp; repoSearchPath: RegExp; @@ -156,11 +244,59 @@ function parseUrl(urlString: string): { throw new Error(`Failed to parse ${urlString}`); } +function readPathParameters(pathParts: string[]): Map { + const vals: Record = {}; + for (let i = 0; i + 1 < pathParts.length; i += 2) { + vals[pathParts[i]] = decodeURIComponent(pathParts[i + 1]); + } + return new Map(Object.entries(vals)); +} + +function parseBitbucketCloudUrl(urlString: string): { + workspacePath: string; + catalogPath: string; + projectSearchPath?: RegExp; + repoSearchPath?: RegExp; + queryParam?: string; +} { + const url = new URL(urlString); + const pathMap = readPathParameters(url.pathname.substr(1).split('/')); + const query = url.searchParams; + + if (!pathMap.has('workspaces')) { + throw new Error(`Failed to parse workspace from ${urlString}`); + } + + return { + workspacePath: pathMap.get('workspaces')!, + projectSearchPath: pathMap.has('projects') + ? escapeRegExp(pathMap.get('projects')!) + : undefined, + repoSearchPath: pathMap.has('repos') + ? escapeRegExp(pathMap.get('repos')!) + : undefined, + catalogPath: `/${query.get('catalogPath') || ''}`, + queryParam: query.get('q') || undefined, + }; +} + function escapeRegExp(str: string): RegExp { return new RegExp(`^${str.replace(/\*/g, '.*')}$`); } -type Result = { - scanned: number; - matches: BitbucketRepository[]; +type ProcessOptions = { + client: BitbucketClient; + integration: BitbucketIntegration; + location: LocationSpec; + emit: CatalogProcessorEmit; +}; + +type Result = { + scanned: number; + matches: T[]; +}; + +type ResultSummary = { + scanned: number; + matches: number; }; diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts index 6521a2b6bf..a63a001082 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.test.ts @@ -24,6 +24,7 @@ import { msw } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { + CatalogProcessorCache, CatalogProcessorEntityResult, CatalogProcessorErrorResult, CatalogProcessorResult, @@ -33,10 +34,17 @@ import { defaultEntityDataParser } from './util/parse'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; - + const mockCache: jest.Mocked = { + get: jest.fn(), + set: jest.fn(), + }; const server = setupServer(); msw.setupDefaultHandlers(server); + beforeEach(() => { + jest.resetAllMocks(); + }); + it('should load from url', async () => { const logger = getVoidLogger(); const reader = UrlReaders.default({ @@ -53,17 +61,72 @@ describe('UrlReaderProcessor', () => { server.use( rest.get(`${mockApiOrigin}/component.yaml`, (_, res, ctx) => - res(ctx.json({ mock: 'entity' })), + res(ctx.set({ ETag: 'my-etag' }), ctx.json({ mock: 'entity' })), ), ); + const emitted = new Array(); + await processor.readLocation( + spec, + false, + result => emitted.push(result), + defaultEntityDataParser, + mockCache, + ); + + expect(emitted.length).toBe(1); + expect(emitted[0]).toEqual({ + type: 'entity', + location: spec, + entity: { mock: 'entity' }, + }); + expect(mockCache.set).toBeCalledWith('v1', { + etag: 'my-etag', + value: [{ type: 'entity', location: spec, entity: { mock: 'entity' } }], + }); + expect(mockCache.set).toBeCalledTimes(1); + }); + + it('should use cached data when available', async () => { + const logger = getVoidLogger(); + const reader = UrlReaders.default({ + logger, + config: new ConfigReader({ + backend: { reading: { allow: [{ host: 'localhost' }] } }, + }), + }); + server.use( + rest.get(`${mockApiOrigin}/component.yaml`, (_, res, ctx) => + res(ctx.status(304)), + ), + ); + const spec = { + type: 'url', + target: `${mockApiOrigin}/component.yaml`, + }; + const cacheItem = { + etag: 'my-etag', + value: [{ type: 'entity', location: spec, entity: { mock: 'entity' } }], + }; + mockCache.get.mockResolvedValue(cacheItem); + const processor = new UrlReaderProcessor({ reader, logger }); + const generated = (await new Promise(emit => - processor.readLocation(spec, false, emit, defaultEntityDataParser), + processor.readLocation( + spec, + false, + emit, + defaultEntityDataParser, + mockCache, + ), )) as CatalogProcessorEntityResult; expect(generated.type).toBe('entity'); expect(generated.location).toEqual(spec); expect(generated.entity).toEqual({ mock: 'entity' }); + expect(mockCache.get).toBeCalledWith('v1'); + expect(mockCache.get).toBeCalledTimes(1); + expect(mockCache.set).toBeCalledTimes(0); }); it('should fail load from url with error', async () => { @@ -87,7 +150,13 @@ describe('UrlReaderProcessor', () => { ); const generated = (await new Promise(emit => - processor.readLocation(spec, false, emit, defaultEntityDataParser), + processor.readLocation( + spec, + false, + emit, + defaultEntityDataParser, + mockCache, + ), )) as CatalogProcessorErrorResult; expect(generated.type).toBe('error'); @@ -116,6 +185,7 @@ describe('UrlReaderProcessor', () => { false, emit, defaultEntityDataParser, + mockCache, ); expect(reader.search).toBeCalledTimes(1); diff --git a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts index e3ec8c0480..cf889eb577 100644 --- a/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/UrlReaderProcessor.ts @@ -15,49 +15,88 @@ */ import { UrlReader } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; +import { Entity, LocationSpec } from '@backstage/catalog-model'; import parseGitUrl from 'git-url-parse'; import limiterFactory from 'p-limit'; import { Logger } from 'winston'; import * as result from './results'; import { CatalogProcessor, + CatalogProcessorCache, CatalogProcessorEmit, + CatalogProcessorEntityResult, CatalogProcessorParser, + CatalogProcessorResult, } from './types'; +const CACHE_KEY = 'v1'; + type Options = { reader: UrlReader; logger: Logger; }; +// WARNING: If you change this type, you likely need to bump the CACHE_KEY as well +type CacheItem = { + etag: string; + value: { + type: 'entity'; + entity: Entity; + location: LocationSpec; + }[]; +}; + export class UrlReaderProcessor implements CatalogProcessor { constructor(private readonly options: Options) {} + getProcessorName() { + return 'url-reader'; + } + async readLocation( location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, + cache: CatalogProcessorCache, ): Promise { if (location.type !== 'url') { return false; } + const cacheItem = await cache.get(CACHE_KEY); + try { - const output = await this.doRead(location.target); - for (const item of output) { + const { response, etag: newEtag } = await this.doRead( + location.target, + cacheItem?.etag, + ); + + const parseResults: CatalogProcessorResult[] = []; + for (const item of response) { for await (const parseResult of parser({ data: item.data, location: { type: location.type, target: item.url }, })) { + parseResults.push(parseResult); emit(parseResult); } } + + const isOnlyEntities = parseResults.every(r => r.type === 'entity'); + if (newEtag && isOnlyEntities) { + await cache.set(CACHE_KEY, { + etag: newEtag, + value: parseResults as CatalogProcessorEntityResult[], + }); + } } catch (error) { const message = `Unable to read ${location.type}, ${error}`; - - if (error.name === 'NotFoundError') { + if (error.name === 'NotModifiedError' && cacheItem) { + for (const parseResult of cacheItem.value) { + emit(parseResult); + } + } else if (error.name === 'NotFoundError') { if (!optional) { emit(result.notFoundError(location, message)); } @@ -71,27 +110,31 @@ export class UrlReaderProcessor implements CatalogProcessor { private async doRead( location: string, - ): Promise<{ data: Buffer; url: string }[]> { + etag?: string, + ): Promise<{ response: { data: Buffer; url: string }[]; etag?: string }> { // Does it contain globs? I.e. does it contain asterisks or question marks // (no curly braces for now) const { filepath } = parseGitUrl(location); if (filepath?.match(/[*?]/)) { const limiter = limiterFactory(5); - const response = await this.options.reader.search(location); + const response = await this.options.reader.search(location, { etag }); const output = response.files.map(async file => ({ url: file.url, data: await limiter(file.content), })); - return Promise.all(output); + return { response: await Promise.all(output), etag: response.etag }; } // Otherwise do a plain read, prioritizing readUrl if available if (this.options.reader.readUrl) { - const data = await this.options.reader.readUrl(location); - return [{ url: location, data: await data.buffer() }]; + const data = await this.options.reader.readUrl(location, { etag }); + return { + response: [{ url: location, data: await data.buffer() }], + etag: data.etag, + }; } const data = await this.options.reader.read(location); - return [{ url: location, data }]; + return { response: [{ url: location, data }] }; } } diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts index 5a1419dc27..b578799229 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/client.ts @@ -19,6 +19,7 @@ import { BitbucketIntegrationConfig, getBitbucketRequestOptions, } from '@backstage/integration'; +import { BitbucketRepository20 } from './types'; export class BitbucketClient { private readonly config: BitbucketIntegrationConfig; @@ -31,12 +32,24 @@ export class BitbucketClient { return this.pagedRequest(`${this.config.apiBaseUrl}/projects`, options); } + async listRepositoriesByWorkspace20( + workspace: string, + options?: ListOptions20, + ): Promise> { + return this.pagedRequest20( + `${this.config.apiBaseUrl}/repositories/${encodeURIComponent(workspace)}`, + options, + ); + } + async listRepositories( projectKey: string, options?: ListOptions, ): Promise> { return this.pagedRequest( - `${this.config.apiBaseUrl}/projects/${projectKey}/repos`, + `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + projectKey, + )}/repos`, options, ); } @@ -63,9 +76,32 @@ export class BitbucketClient { } - ${response.statusText}`, ); } - return response.json().then(repositories => { - return repositories as PagedResponse; - }); + return response.json() as Promise>; + } + + private async pagedRequest20( + endpoint: string, + options?: ListOptions20, + ): Promise> { + const request = new URL(endpoint); + for (const key in options) { + if (options[key]) { + request.searchParams.append(key, options[key]!.toString()); + } + } + + const response = await fetch( + request.toString(), + getBitbucketRequestOptions(this.config), + ); + if (!response.ok) { + throw new Error( + `Unexpected response when fetching ${request.toString()}. Expected 200 but got ${ + response.status + } - ${response.statusText}`, + ); + } + return response.json() as Promise>; } } @@ -84,6 +120,20 @@ export type PagedResponse = { nextPageStart: number; }; +export type ListOptions20 = { + [key: string]: string | number | undefined; + page?: number | undefined; + pagelen?: number | undefined; +}; + +export type PagedResponse20 = { + page: number; + pagelen: number; + size: number; + values: T[]; + next: string; +}; + export async function* paginated( request: (options: ListOptions) => Promise>, options?: ListOptions, @@ -98,3 +148,18 @@ export async function* paginated( } } while (!res.isLastPage); } + +export async function* paginated20( + request: (options: ListOptions20) => Promise>, + options?: ListOptions20, +) { + const opts = { page: 1, pagelen: 100, ...options }; + let res; + do { + res = await request(opts); + opts.page = opts.page + 1; + for (const item of res.values) { + yield item; + } + } while (res.next); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts index 7adab7746f..4b28c7f115 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/index.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { BitbucketClient, paginated } from './client'; +export { BitbucketClient, paginated, paginated20 } from './client'; export { defaultRepositoryParser } from './BitbucketRepositoryParser'; -export type { PagedResponse } from './client'; -export type { BitbucketRepository } from './types'; +export type { PagedResponse, PagedResponse20 } from './client'; +export type { BitbucketRepository, BitbucketRepository20 } from './types'; export type { BitbucketRepositoryParser } from './BitbucketRepositoryParser'; diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts index b273d26874..db8cf69688 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/types.ts @@ -13,11 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export type BitbucketRepository = { + +type BitbucketRepositoryBase = { project: { key: string; }; slug: string; +}; + +export type BitbucketRepository = BitbucketRepositoryBase & { links: Record< string, { @@ -25,3 +29,33 @@ export type BitbucketRepository = { }[] >; }; + +export type BitbucketRepository20 = BitbucketRepositoryBase & { + links: Record< + | 'self' + | 'source' + | 'html' + | 'avatar' + | 'pullrequests' + | 'commits' + | 'forks' + | 'watchers' + | 'downloads' + | 'hooks', + { + href: string; + name?: string; + } + > & + Record< + 'clone', + { + href: string; + name?: string; + }[] + >; + mainbranch?: { + type: string; + name: string; + }; +}; diff --git a/plugins/catalog-backend/src/ingestion/processors/types.ts b/plugins/catalog-backend/src/ingestion/processors/types.ts index 2e2418c8f1..dccc60d42c 100644 --- a/plugins/catalog-backend/src/ingestion/processors/types.ts +++ b/plugins/catalog-backend/src/ingestion/processors/types.ts @@ -19,16 +19,24 @@ import { EntityRelationSpec, LocationSpec, } from '@backstage/catalog-model'; +import { JsonValue } from '@backstage/config'; export type CatalogProcessor = { + /** + * A unique identifier for the Catalog Processor. + * It's strongly recommended to implement getProcessorName as this method will be required in the future. + */ + getProcessorName?(): string; + /** * Reads the contents of a location. * - * @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 + * @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. + * @param cache - A cache for storing values local to this processor and the current entity. * @returns True if handled by this processor, false otherwise */ readLocation?( @@ -36,6 +44,7 @@ export type CatalogProcessor = { optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, + cache: CatalogProcessorCache, ): Promise; /** @@ -46,12 +55,13 @@ export type CatalogProcessor = { * additional data, and the input entity may actually still be incomplete * when the processor is invoked. * - * @param entity The (possibly partial) entity to process - * @param location The location that the entity came from - * @param emit A sink for auxiliary items resulting from the processing - * @param originLocation The location that the entity originally came from. + * @param entity - The (possibly partial) entity to process + * @param location - The location that the entity came from + * @param emit - A sink for auxiliary items resulting from the processing + * @param originLocation - The location that the entity originally came from. * While location resolves to the direct parent location, originLocation * tells which location was used to start the ingestion loop. + * @param cache - A cache for storing values local to this processor and the current entity. * @returns The same entity or a modified version of it */ preProcessEntity?( @@ -59,13 +69,14 @@ export type CatalogProcessor = { location: LocationSpec, emit: CatalogProcessorEmit, originLocation: LocationSpec, + cache: CatalogProcessorCache, ): Promise; /** * Validates the entity as a known entity kind, after it has been pre- * processed and has passed through basic overall validation. * - * @param entity The entity to validate + * @param entity - The entity to validate * @returns Resolves to true, if the entity was of a kind that was known and * handled by this processor, and was found to be valid. Resolves to false, * if the entity was not of a kind that was known by this processor. @@ -77,23 +88,25 @@ export type CatalogProcessor = { /** * Post-processes an emitted entity, after it has been validated. * - * @param entity The entity to process - * @param location The location that the entity came from - * @param emit A sink for auxiliary items resulting from the processing + * @param entity - The entity to process + * @param location - The location that the entity came from + * @param emit - A sink for auxiliary items resulting from the processing + * @param cache - A cache for storing values local to this processor and the current entity. * @returns The same entity or a modified version of it */ postProcessEntity?( entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit, + cache: CatalogProcessorCache, ): Promise; /** * Handles an emitted error. * - * @param error The error - * @param location The location where the error occurred - * @param emit A sink for items resulting from this handling + * @param error - The error + * @param location - The location where the error occurred + * @param emit - A sink for items resulting from this handling * @returns Nothing */ handleError?( @@ -113,6 +126,34 @@ export type CatalogProcessorParser = (options: { location: LocationSpec; }) => AsyncIterable; +/** + * A cache for storing data during processing. + * + * The values stored in the cache are always local to each processor, meaning + * no processor can see cache values from other processors. + * + * The cache instance provided to the CatalogProcessor is also scoped to the + * entity being processed, meaning that each processor run can't see cache + * values from processing runs for other entities. + * + * Values that are set during a processing run will only be visible in the directly + * following run. The cache will be overwritten every run unless no new cache items + * are written, in which case the existing values remain in the cache. + * + * @public + */ +export interface CatalogProcessorCache { + /** + * Retrieve a value from the cache. + */ + get(key: string): Promise; + + /** + * Store a value in the cache. + */ + set(key: string, value: ItemType): Promise; +} + export type CatalogProcessorEmit = (generated: CatalogProcessorResult) => void; export type CatalogProcessorLocationResult = { diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts index f7dede4adb..430d7f251f 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts @@ -28,6 +28,7 @@ describe('DefaultCatalogProcessingEngine', () => { transaction: jest.fn(), getProcessableEntities: jest.fn(), updateProcessedEntity: jest.fn(), + updateEntityCache: jest.fn(), } as unknown as jest.Mocked; const orchestrator: jest.Mocked = { process: jest.fn(), @@ -55,7 +56,7 @@ describe('DefaultCatalogProcessingEngine', () => { relations: [], errors: [], deferredEntities: [], - state: new Map(), + state: {}, }); const engine = new DefaultCatalogProcessingEngine( getVoidLogger(), @@ -84,7 +85,7 @@ describe('DefaultCatalogProcessingEngine', () => { metadata: { name: 'test' }, }, resultHash: '', - state: new Map(), + state: [] as any, nextUpdateAt: DateTime.now(), lastDiscoveryAt: DateTime.now(), }, @@ -100,7 +101,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, - state: expect.anything(), + state: [], // State is forwarded as is, even if it's a bad format }); }); await engine.stop(); @@ -117,7 +118,7 @@ describe('DefaultCatalogProcessingEngine', () => { relations: [], errors: [], deferredEntities: [], - state: new Map(), + state: {}, }); const engine = new DefaultCatalogProcessingEngine( getVoidLogger(), @@ -147,7 +148,7 @@ describe('DefaultCatalogProcessingEngine', () => { metadata: { name: 'test' }, }, resultHash: '', - state: new Map(), + state: { cache: { myProcessor: { myKey: 'myValue' } } }, nextUpdateAt: DateTime.now(), lastDiscoveryAt: DateTime.now(), }, @@ -163,7 +164,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, - state: expect.anything(), + state: { cache: { myProcessor: { myKey: 'myValue' } } }, }); }); await engine.stop(); @@ -181,7 +182,7 @@ describe('DefaultCatalogProcessingEngine', () => { entityRef: '', unprocessedEntity: entity, resultHash: 'the matching hash', - state: new Map(), + state: {}, nextUpdateAt: DateTime.now(), lastDiscoveryAt: DateTime.now(), }; @@ -194,7 +195,7 @@ describe('DefaultCatalogProcessingEngine', () => { relations: [], errors: [], deferredEntities: [], - state: new Map(), + state: {}, }); const engine = new DefaultCatalogProcessingEngine( @@ -221,16 +222,100 @@ describe('DefaultCatalogProcessingEngine', () => { expect(hash.digest).toBeCalledTimes(1); expect(db.updateProcessedEntity).toBeCalledTimes(1); }); + expect(db.updateEntityCache).not.toHaveBeenCalled(); db.getProcessableEntities .mockReset() - .mockResolvedValueOnce({ items: [refreshState] }) + .mockResolvedValueOnce({ + items: [{ ...refreshState, state: { something: 'different' } }], + }) .mockResolvedValue({ items: [] }); await waitForExpect(() => { expect(orchestrator.process).toBeCalledTimes(2); expect(hash.digest).toBeCalledTimes(2); expect(db.updateProcessedEntity).toBeCalledTimes(1); + expect(db.updateEntityCache).toBeCalledTimes(1); + }); + expect(db.updateEntityCache).toHaveBeenCalledWith(expect.anything(), { + id: '', + state: { ttl: 5 }, + }); + await engine.stop(); + }); + + it('should decrease the state ttl if there are errors', async () => { + const entity = { + apiVersion: '1', + kind: 'Location', + metadata: { name: 'test' }, + }; + + const refreshState = { + id: '', + entityRef: '', + unprocessedEntity: entity, + resultHash: 'the matching hash', + state: { some: 'value', ttl: 1 }, + nextUpdateAt: DateTime.now(), + lastDiscoveryAt: DateTime.now(), + }; + + hash.digest.mockReturnValue('the matching hash'); + + orchestrator.process.mockResolvedValue({ + ok: false, + errors: [], + }); + + const engine = new DefaultCatalogProcessingEngine( + getVoidLogger(), + [], + db, + orchestrator, + stitcher, + () => hash, + ); + + db.transaction.mockImplementation(cb => cb((() => {}) as any)); + + await engine.start(); + + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [refreshState], + }) + .mockResolvedValue({ items: [] }); + + await waitForExpect(() => { + expect(db.updateEntityCache).toBeCalledTimes(1); + }); + + expect(db.updateEntityCache).toHaveBeenCalledWith(expect.anything(), { + id: '', + state: { some: 'value', ttl: 0 }, + }); + + // Second run, the TTL should now reach 0 and the cache should be cleared + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [ + { + ...refreshState, + state: db.updateEntityCache.mock.calls[0][1].state, + }, + ], + }) + .mockResolvedValue({ items: [] }); + + db.updateEntityCache.mockReset(); + await waitForExpect(() => { + expect(db.updateEntityCache).toBeCalledTimes(1); + }); + + expect(db.updateEntityCache).toHaveBeenCalledWith(expect.anything(), { + id: '', + state: {}, }); await engine.stop(); diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts index 4d2d74f05b..346a912009 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts @@ -38,6 +38,8 @@ import { EntityProviderMutation, } from './types'; +const CACHE_TTL = 5; + class Connection implements EntityProviderConnection { readonly validateEntityEnvelope = entityEnvelopeSchemaValidator(); @@ -151,6 +153,29 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { track.markProcessorsCompleted(result); + if (result.ok) { + if (stableStringify(state) !== stableStringify(result.state)) { + await this.processingDatabase.transaction(async tx => { + await this.processingDatabase.updateEntityCache(tx, { + id, + state: { + ttl: CACHE_TTL, + ...result.state, + }, + }); + }); + } + } else { + const maybeTtl = state?.ttl; + const ttl = Number.isInteger(maybeTtl) ? (maybeTtl as number) : 0; + await this.processingDatabase.transaction(async tx => { + await this.processingDatabase.updateEntityCache(tx, { + id, + state: ttl > 0 ? { ...state, ttl: ttl - 1 } : {}, + }); + }); + } + for (const error of result.errors) { // TODO(freben): Try to extract the location out of the unprocessed // entity and add as meta to the log lines @@ -167,8 +192,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { hashBuilder = hashBuilder .update(stableStringify({ ...result.completedEntity })) .update(stableStringify([...result.deferredEntities])) - .update(stableStringify([...result.relations])) - .update(stableStringify(Object.fromEntries(result.state))); + .update(stableStringify([...result.relations])); } const resultHash = hashBuilder.digest('hex'); @@ -208,7 +232,6 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { id, processedEntity: result.completedEntity, resultHash, - state: result.state, errors: errorsString, relations: result.relations, deferredEntities: result.deferredEntities, diff --git a/plugins/catalog-backend/src/next/DefaultLocationService.test.ts b/plugins/catalog-backend/src/next/DefaultLocationService.test.ts index 939f54e793..78dd9c1009 100644 --- a/plugins/catalog-backend/src/next/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/next/DefaultLocationService.test.ts @@ -39,7 +39,7 @@ describe('DefaultLocationServiceTest', () => { store.listLocations.mockResolvedValueOnce([]); orchestrator.process.mockResolvedValueOnce({ ok: true, - state: new Map(), + state: {}, completedEntity: { apiVersion: 'backstage.io/v1alpha1', kind: 'Location', @@ -65,7 +65,7 @@ describe('DefaultLocationServiceTest', () => { orchestrator.process.mockResolvedValueOnce({ ok: true, - state: new Map(), + state: {}, completedEntity: { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -124,7 +124,7 @@ describe('DefaultLocationServiceTest', () => { }; orchestrator.process.mockResolvedValueOnce({ ok: true, - state: new Map(), + state: {}, completedEntity: { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -151,7 +151,7 @@ describe('DefaultLocationServiceTest', () => { it('should return exists false when the location does not exist beforehand', async () => { orchestrator.process.mockResolvedValueOnce({ ok: true, - state: new Map(), + state: {}, completedEntity: { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', diff --git a/plugins/catalog-backend/src/next/DefaultLocationService.ts b/plugins/catalog-backend/src/next/DefaultLocationService.ts index 7d485bea40..678ce9791c 100644 --- a/plugins/catalog-backend/src/next/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/next/DefaultLocationService.ts @@ -87,7 +87,6 @@ export class DefaultLocationService implements LocationService { { entity, locationKey: `${spec.type}:${spec.target}` }, ]; const entities: Entity[] = []; - const state = new Map(); // ignored while (unprocessedEntities.length) { const currentEntity = unprocessedEntities.pop(); if (!currentEntity) { @@ -95,7 +94,7 @@ export class DefaultLocationService implements LocationService { } const processed = await this.orchestrator.process({ entity: currentEntity.entity, - state, + state: {}, // we process without the existing cache }); if (processed.ok) { diff --git a/plugins/catalog-backend/src/next/DefaultRefreshService.test.ts b/plugins/catalog-backend/src/next/DefaultRefreshService.test.ts index 5df57c6897..79ced3ea43 100644 --- a/plugins/catalog-backend/src/next/DefaultRefreshService.test.ts +++ b/plugins/catalog-backend/src/next/DefaultRefreshService.test.ts @@ -138,7 +138,7 @@ describe('Refresh integration', () => { relations: [], errors: [], deferredEntities, - state: new Map(), + state: {}, }; }, }, diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index a34be2ca6a..6caab23545 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -78,6 +78,7 @@ import { import { CatalogEnvironment } from '../service/CatalogBuilder'; import { createNextRouter } from './NextRouter'; import { DefaultRefreshService } from './DefaultRefreshService'; +import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules'; /** * A builder that helps wire up all of the component parts of the catalog. @@ -304,9 +305,11 @@ export class NextCatalogBuilder { refreshInterval: this.refreshInterval, }); const integrations = ScmIntegrations.fromConfig(config); + const rulesEnforcer = DefaultCatalogRulesEnforcer.fromConfig(config); const orchestrator = new DefaultCatalogProcessingOrchestrator({ processors, integrations, + rulesEnforcer, logger, parser, policy, diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index afc3806d36..2a28951e6b 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -17,7 +17,6 @@ import { getVoidLogger } from '@backstage/backend-common'; import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/config'; import { Knex } from 'knex'; import * as uuid from 'uuid'; import { Logger } from 'winston'; @@ -67,123 +66,6 @@ describe('Default Processing Database', () => { await db('refresh_state').insert(ref); }; - describe('addUprocessedEntities', () => { - function mockEntity(name: string, type: string): Entity { - return { - apiVersion: '1', - kind: 'Component', - metadata: { - name, - }, - spec: { - type, - }, - }; - } - - it.each(databases.eachSupportedId())( - 'updates refresh state with varying location keys, %p', - async databaseId => { - const mockWarn = jest.fn(); - const { db } = await createDatabase(databaseId, { - debug: jest.fn(), - warn: mockWarn, - } as unknown as Logger); - await db.transaction(async tx => { - const knexTx = tx as Knex.Transaction; - - const steps = [ - { - locationKey: undefined, - expectedLocationKey: null, - type: 'a', - expectedType: 'a', - }, - { - locationKey: undefined, - expectedLocationKey: null, - type: 'b', - expectedType: 'b', - }, - { - locationKey: 'x', - expectedLocationKey: 'x', - type: 'c', - expectedType: 'c', - }, - { - locationKey: 'y', - expectedLocationKey: 'x', - type: 'd', - expectedType: 'c', - expectConflict: true, - }, - { - locationKey: undefined, - expectedLocationKey: 'x', - type: 'e', - expectedType: 'c', - expectConflict: true, - }, - { - locationKey: 'x', - expectedLocationKey: 'x', - type: 'f', - expectedType: 'f', - }, - ]; - for (const step of steps) { - mockWarn.mockClear(); - - await db.addUnprocessedEntities(tx, { - sourceKey: 'testing', - entities: [ - { - entity: mockEntity('1', step.type), - locationKey: step.locationKey, - }, - ], - }); - - if (step.expectConflict) { - // eslint-disable-next-line jest/no-conditional-expect - expect(mockWarn).toHaveBeenCalledWith( - expect.stringMatching(/^Detected conflicting entityRef/), - ); - } else { - // eslint-disable-next-line jest/no-conditional-expect - expect(mockWarn).not.toHaveBeenCalled(); - } - - const entities = await knexTx( - 'refresh_state', - ).select(); - expect(entities).toEqual([ - expect.objectContaining({ - entity_ref: 'component:default/1', - location_key: step.expectedLocationKey, - }), - ]); - const entity = JSON.parse(entities[0].unprocessed_entity) as Entity; - expect(entity.spec?.type).toEqual(step.expectedType); - - await expect( - knexTx( - 'refresh_state_references', - ).select(), - ).resolves.toEqual([ - expect.objectContaining({ - source_key: 'testing', - target_entity_ref: 'component:default/1', - }), - ]); - } - }); - }, - 60_000, - ); - }); - describe('updateProcessedEntity', () => { let id: string; let processedEntity: Entity; @@ -213,7 +95,6 @@ describe('Default Processing Database', () => { id, processedEntity, resultHash: '', - state: new Map(), relations: [], deferredEntities: [], }), @@ -232,7 +113,6 @@ describe('Default Processing Database', () => { id, processedEntity, resultHash: '', - state: new Map(), relations: [], deferredEntities: [], locationKey: 'key', @@ -285,15 +165,11 @@ describe('Default Processing Database', () => { last_discovery_at: '2021-04-01 13:37:00', }); - const state = new Map(); - state.set('hello', { t: 'something' }); - await db.transaction(tx => db.updateProcessedEntity(tx, { id, processedEntity, resultHash: '', - state, relations: [], deferredEntities: [], locationKey: 'key', @@ -308,9 +184,6 @@ describe('Default Processing Database', () => { expect(entities[0].processed_entity).toEqual( JSON.stringify(processedEntity), ); - expect(entities[0].cache).toEqual( - JSON.stringify(Object.fromEntries(state)), - ); expect(entities[0].errors).toEqual("['something broke']"); expect(entities[0].location_key).toEqual('key'); }, @@ -352,7 +225,6 @@ describe('Default Processing Database', () => { id, processedEntity, resultHash: '', - state: new Map(), relations: relations, deferredEntities: [], }), @@ -404,7 +276,6 @@ describe('Default Processing Database', () => { id, processedEntity, resultHash: '', - state: new Map(), relations: [], deferredEntities, }), @@ -420,6 +291,196 @@ describe('Default Processing Database', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'updates unprocessed entities with varying location keys, %p', + async databaseId => { + const mockLogger = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }; + const { knex, db } = await createDatabase( + databaseId, + mockLogger as unknown as Logger, + ); + + await insertRefreshStateRow(knex, { + entity_id: id, + entity_ref: 'location:default/fakelocation', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: '2021-04-01 13:37:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + + await db.transaction(async tx => { + const knexTx = tx as Knex.Transaction; + + const steps = [ + { + locationKey: undefined, + expectedLocationKey: null, + testKey: 'a', + expectedTestKey: 'a', + }, + { + locationKey: undefined, + expectedLocationKey: null, + testKey: 'b', + expectedTestKey: 'b', + }, + { + locationKey: 'x', + expectedLocationKey: 'x', + testKey: 'c', + expectedTestKey: 'c', + }, + { + locationKey: 'y', + expectedLocationKey: 'x', + testKey: 'd', + expectedTestKey: 'c', + expectConflict: true, + }, + { + locationKey: undefined, + expectedLocationKey: 'x', + testKey: 'e', + expectedTestKey: 'c', + expectConflict: true, + }, + { + locationKey: 'x', + expectedLocationKey: 'x', + testKey: 'f', + expectedTestKey: 'f', + }, + ]; + for (const step of steps) { + mockLogger.debug.mockClear(); + mockLogger.warn.mockClear(); + mockLogger.error.mockClear(); + + await db.updateProcessedEntity(tx, { + id, + processedEntity, + resultHash: '', + relations: [], + deferredEntities: [ + { + entity: { + apiVersion: '1', + kind: 'Component', + metadata: { + name: '1', + }, + spec: { + type: step.testKey, + }, + }, + locationKey: step.locationKey, + }, + ], + }); + + if (step.expectConflict) { + // eslint-disable-next-line jest/no-conditional-expect + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringMatching(/^Detected conflicting entityRef/), + ); + } else { + // eslint-disable-next-line jest/no-conditional-expect + expect(mockLogger.warn).not.toHaveBeenCalled(); + } + + const states = await knexTx( + 'refresh_state', + ).select(); + expect(states).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + entity_ref: 'component:default/1', + location_key: step.expectedLocationKey, + }), + expect.objectContaining({ + entity_ref: 'location:default/fakelocation', + location_key: null, + }), + ]), + ); + const unprocessed = states.find( + state => state.entity_ref === 'component:default/1', + )!; + const entity = JSON.parse(unprocessed.unprocessed_entity) as Entity; + expect(entity.spec?.type).toEqual(step.expectedTestKey); + + await expect( + knexTx( + 'refresh_state_references', + ).select(), + ).resolves.toEqual([ + expect.objectContaining({ + source_entity_ref: 'location:default/fakelocation', + target_entity_ref: 'component:default/1', + }), + ]); + + expect(mockLogger.error).not.toHaveBeenCalled(); + } + }); + }, + 60_000, + ); + }); + + describe('updateEntityCache', () => { + it.each(databases.eachSupportedId())( + 'updates the entityCache, %p', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + const id = '123'; + await insertRefreshStateRow(knex, { + entity_id: id, + entity_ref: 'location:default/fakelocation', + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: '2021-04-01 13:37:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + + const state = { hello: { t: 'something' } }; + + await db.transaction(tx => + db.updateEntityCache(tx, { + id, + state, + }), + ); + + const entities = await knex( + 'refresh_state', + ).select(); + expect(entities.length).toBe(1); + expect(entities[0].cache).toEqual(JSON.stringify(state)); + + await db.transaction(tx => + db.updateEntityCache(tx, { + id, + state: undefined, + }), + ); + + const entities2 = await knex( + 'refresh_state', + ).select(); + expect(entities2.length).toBe(1); + expect(entities2[0].cache).toEqual('{}'); + }, + 60_000, + ); }); describe('replaceUnprocessedEntities', () => { @@ -695,6 +756,14 @@ describe('Default Processing Database', () => { 'should add new locations using the delta options, %p', async databaseId => { const { knex, db } = await createDatabase(databaseId); + + // Existing state and references should stay + await createLocations(knex, ['location:default/existing']); + await insertRefRow(knex, { + source_key: 'lols', + target_entity_ref: 'location:default/existing', + }); + await db.transaction(async tx => { await db.replaceUnprocessedEntities(tx, { type: 'delta', @@ -736,6 +805,20 @@ describe('Default Processing Database', () => { t.target_entity_ref === 'location:default/new-root', ), ).toBeTruthy(); + + expect( + currentRefreshState.some( + t => t.entity_ref === 'location:default/existing', + ), + ).toBeTruthy(); + + expect( + currentRefRowState.some( + t => + t.source_key === 'lols' && + t.target_entity_ref === 'location:default/existing', + ), + ).toBeTruthy(); }, 60_000, ); diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index 8ba9d54b8f..3dfb4beb80 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -15,7 +15,6 @@ */ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/config'; import { ConflictError, NotFoundError } from '@backstage/errors'; import { Knex } from 'knex'; import lodash from 'lodash'; @@ -32,7 +31,6 @@ import { DbRelationsRow, } from './tables'; import { - AddUnprocessedEntitiesOptions, GetProcessableEntitiesResult, ProcessingDatabase, RefreshStateItem, @@ -41,6 +39,7 @@ import { UpdateProcessedEntityOptions, ListAncestorsOptions, ListAncestorsResult, + UpdateEntityCacheOptions, } from './types'; // The number of items that are sent per batch to the database layer, when @@ -70,7 +69,6 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { id, processedEntity, resultHash, - state, errors, relations, deferredEntities, @@ -80,7 +78,6 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { .update({ processed_entity: JSON.stringify(processedEntity), result_hash: resultHash, - cache: JSON.stringify(Object.fromEntries(state || [])), errors, location_key: locationKey, }) @@ -141,61 +138,16 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { .where('entity_id', id); } - private deduplicateRelations(rows: DbRelationsRow[]): DbRelationsRow[] { - return lodash.uniqBy( - rows, - r => `${r.source_entity_ref}:${r.target_entity_ref}:${r.type}`, - ); - } + async updateEntityCache( + txOpaque: Transaction, + options: UpdateEntityCacheOptions, + ): Promise { + const tx = txOpaque as Knex.Transaction; + const { id, state } = options; - private async createDelta( - tx: Knex.Transaction, - options: ReplaceUnprocessedEntitiesOptions, - ): Promise<{ toAdd: DeferredEntity[]; toRemove: string[] }> { - if (options.type === 'delta') { - return { - toAdd: options.added, - toRemove: options.removed.map(e => stringifyEntityRef(e.entity)), - }; - } - - // Grab all of the existing references from the same source, and their locationKeys as well - const oldRefs = await tx( - 'refresh_state_references', - ) - .where({ source_key: options.sourceKey }) - .leftJoin('refresh_state', { - target_entity_ref: 'entity_ref', - }) - .select(['target_entity_ref', 'location_key']); - - const items = options.items.map(deferred => ({ - deferred, - ref: stringifyEntityRef(deferred.entity), - })); - - const oldRefsSet = new Map( - oldRefs.map(r => [r.target_entity_ref, r.location_key]), - ); - const newRefsSet = new Set(items.map(item => item.ref)); - - const toAdd = new Array(); - const toRemove = oldRefs - .map(row => row.target_entity_ref) - .filter(ref => !newRefsSet.has(ref)); - - for (const item of items) { - if (!oldRefsSet.has(item.ref)) { - // Add any entity that does not exist in the database - toAdd.push(item.deferred); - } else if (oldRefsSet.get(item.ref) !== item.deferred.locationKey) { - // Remove and then re-add any entity that exists, but with a different location key - toRemove.push(item.ref); - toAdd.push(item.deferred); - } - } - - return { toAdd, toRemove }; + await tx('refresh_state') + .update({ cache: JSON.stringify(state ?? {}) }) + .where('entity_id', id); } async replaceUnprocessedEntities( @@ -322,143 +274,40 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { } if (toAdd.length) { - await this.addUnprocessedEntities(tx, { - sourceKey: options.sourceKey, - entities: toAdd, - }); - } - } + for (const { entity, locationKey } of toAdd) { + const entityRef = stringifyEntityRef(entity); - /** - * Add a set of deferred entities for processing. - * The entities will be added at the front of the processing queue. - */ - async addUnprocessedEntities( - txOpaque: Transaction, - options: AddUnprocessedEntitiesOptions, - ): Promise { - const tx = txOpaque as Knex.Transaction; - - // Keeps track of the entities that we end up inserting to update refresh_state_references afterwards - const stateReferences = new Array(); - const conflictingStateReferences = new Array(); - - // Upsert all of the unprocessed entities into the refresh_state table, by - // their entity ref. - for (const { entity, locationKey } of options.entities) { - const entityRef = stringifyEntityRef(entity); - const serializedEntity = JSON.stringify(entity); - - // We optimistically try to update any existing refresh state first, as this is by far - // the most common case. - const refreshResult = await tx('refresh_state') - .update({ - unprocessed_entity: serializedEntity, - location_key: locationKey, - last_discovery_at: tx.fn.now(), - // We only get to this point if a processed entity actually had any changes, or - // if an entity provider requested this mutation, meaning that we can safely - // bump the deferred entities to the front of the queue for immediate processing. - next_update_at: tx.fn.now(), - }) - .where('entity_ref', entityRef) - .andWhere(inner => { - if (!locationKey) { - return inner.whereNull('location_key'); - } - return inner - .where('location_key', locationKey) - .orWhereNull('location_key'); - }); - - if (refreshResult === 0) { - // In the event that we can't update an existing refresh state, we first try to insert a new row try { - let query = tx('refresh_state').insert({ - entity_id: uuid(), - entity_ref: entityRef, - unprocessed_entity: serializedEntity, - errors: '', - location_key: locationKey, - next_update_at: tx.fn.now(), - last_discovery_at: tx.fn.now(), - }); - - // TODO(Rugvip): only tested towards Postgres and SQLite - // We have to do this because the only way to detect if there was a conflict with - // SQLite is to catch the error, while Postgres needs to ignore the conflict to not - // break the ongoing transaction. - if (tx.client.config.client !== 'sqlite3') { - query = query.onConflict('entity_ref').ignore(); + let ok = await this.insertUnprocessedEntity(tx, entity, locationKey); + if (!ok) { + ok = await this.updateUnprocessedEntity(tx, entity, locationKey); } - const result: { /* postgres */ rowCount?: number } = await query; - if (result.rowCount === 0) { - throw new ConflictError( - 'Insert failed due to conflicting entity_ref', + if (ok) { + await tx( + 'refresh_state_references', + ).insert({ + source_key: options.sourceKey, + target_entity_ref: entityRef, + }); + } else { + const conflictingKey = await this.checkLocationKeyConflict( + tx, + entityRef, + locationKey, ); + if (conflictingKey) { + this.options.logger.warn( + `Source ${options.sourceKey} detected conflicting entityRef ${entityRef} already referenced by ${conflictingKey} and now also ${locationKey}`, + ); + } } } catch (error) { - if ( - !error.message.includes('UNIQUE constraint failed') && - error.name !== 'ConflictError' - ) { - throw error; - } - // If the row can't be inserted, we have a conflict, but it could be either - // because of a conflicting locationKey or a race with another instance, so check - // whether the conflicting entity has the same entityRef but a different locationKey - const [conflictingEntity] = await tx( - 'refresh_state', - ) - .where({ entity_ref: entityRef }) - .select(); - - // If the location key matches it means we just had a race trigger, which we can safely ignore - if ( - !conflictingEntity || - conflictingEntity.location_key !== locationKey - ) { - this.options.logger.warn( - `Detected conflicting entityRef ${entityRef} already referenced by ${conflictingEntity.location_key} and now also ${locationKey}`, - ); - conflictingStateReferences.push(entityRef); - continue; - } + this.options.logger.error( + `Failed to add '${entityRef}' from source '${options.sourceKey}', ${error}`, + ); } } - - // Skipped on locationKey conflict - stateReferences.push(entityRef); - } - - // Replace all references for the originating entity or source and then create new ones - if ('sourceKey' in options) { - await tx('refresh_state_references') - .whereNotIn('target_entity_ref', conflictingStateReferences) - .andWhere({ source_key: options.sourceKey }) - .delete(); - await tx.batchInsert( - 'refresh_state_references', - stateReferences.map(entityRef => ({ - source_key: options.sourceKey, - target_entity_ref: entityRef, - })), - BATCH_SIZE, - ); - } else { - await tx('refresh_state_references') - .whereNotIn('target_entity_ref', conflictingStateReferences) - .andWhere({ source_entity_ref: options.sourceEntityRef }) - .delete(); - await tx.batchInsert( - 'refresh_state_references', - stateReferences.map(entityRef => ({ - source_entity_ref: options.sourceEntityRef, - target_entity_ref: entityRef, - })), - BATCH_SIZE, - ); } } @@ -508,9 +357,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { resultHash: i.result_hash || '', nextUpdateAt: timestampToDateTime(i.next_update_at), lastDiscoveryAt: timestampToDateTime(i.last_discovery_at), - state: i.cache - ? JSON.parse(i.cache) - : new Map(), + state: i.cache ? JSON.parse(i.cache) : undefined, errors: i.errors, locationKey: i.location_key, } as RefreshStateItem), @@ -591,4 +438,243 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { throw rethrowError(e); } } + + /** + * Attempts to update an existing refresh state row, returning true if it was + * updated and false if there was no entity with a matching ref and location key. + * + * Updating the entity will also cause it to be scheduled for immediate processing. + */ + private async updateUnprocessedEntity( + tx: Knex.Transaction, + entity: Entity, + locationKey?: string, + ): Promise { + const entityRef = stringifyEntityRef(entity); + const serializedEntity = JSON.stringify(entity); + + const refreshResult = await tx('refresh_state') + .update({ + unprocessed_entity: serializedEntity, + location_key: locationKey, + last_discovery_at: tx.fn.now(), + // We only get to this point if a processed entity actually had any changes, or + // if an entity provider requested this mutation, meaning that we can safely + // bump the deferred entities to the front of the queue for immediate processing. + next_update_at: tx.fn.now(), + }) + .where('entity_ref', entityRef) + .andWhere(inner => { + if (!locationKey) { + return inner.whereNull('location_key'); + } + return inner + .where('location_key', locationKey) + .orWhereNull('location_key'); + }); + + return refreshResult === 1; + } + + /** + * Attempts to insert a new refresh state row for the given entity, returning + * true if successful and false if there was a conflict. + */ + private async insertUnprocessedEntity( + tx: Knex.Transaction, + entity: Entity, + locationKey?: string, + ): Promise { + const entityRef = stringifyEntityRef(entity); + const serializedEntity = JSON.stringify(entity); + + try { + let query = tx('refresh_state').insert({ + entity_id: uuid(), + entity_ref: entityRef, + unprocessed_entity: serializedEntity, + errors: '', + location_key: locationKey, + next_update_at: tx.fn.now(), + last_discovery_at: tx.fn.now(), + }); + + // TODO(Rugvip): only tested towards Postgres and SQLite + // We have to do this because the only way to detect if there was a conflict with + // SQLite is to catch the error, while Postgres needs to ignore the conflict to not + // break the ongoing transaction. + if (tx.client.config.client !== 'sqlite3') { + query = query.onConflict('entity_ref').ignore() as any; // type here does not match runtime + } + + // Postgres gives as an object with rowCount, SQLite gives us an array + const result: { rowCount?: number; length?: number } = await query; + return result.rowCount === 1 || result.length === 1; + } catch (error) { + // SQLite reached this rather than the rowCount check above + if (error.message.includes('UNIQUE constraint failed')) { + return false; + } + throw error; + } + } + + /** + * Checks whether a refresh state exists for the given entity that has a + * location key that does not match the provided location key. + * + * @returns The conflicting key if there is one. + */ + private async checkLocationKeyConflict( + tx: Knex.Transaction, + entityRef: string, + locationKey?: string, + ): Promise { + const row = await tx('refresh_state') + .select('location_key') + .where('entity_ref', entityRef) + .first(); + + const conflictingKey = row?.location_key; + + // If there's no existing key we can't have a conflict + if (!conflictingKey) { + return undefined; + } + + if (conflictingKey !== locationKey) { + return conflictingKey; + } + return undefined; + } + + private deduplicateRelations(rows: DbRelationsRow[]): DbRelationsRow[] { + return lodash.uniqBy( + rows, + r => `${r.source_entity_ref}:${r.target_entity_ref}:${r.type}`, + ); + } + + private async createDelta( + tx: Knex.Transaction, + options: ReplaceUnprocessedEntitiesOptions, + ): Promise<{ toAdd: DeferredEntity[]; toRemove: string[] }> { + if (options.type === 'delta') { + return { + toAdd: options.added, + toRemove: options.removed.map(e => stringifyEntityRef(e.entity)), + }; + } + + // Grab all of the existing references from the same source, and their locationKeys as well + const oldRefs = await tx( + 'refresh_state_references', + ) + .where({ source_key: options.sourceKey }) + .leftJoin('refresh_state', { + target_entity_ref: 'entity_ref', + }) + .select(['target_entity_ref', 'location_key']); + + const items = options.items.map(deferred => ({ + deferred, + ref: stringifyEntityRef(deferred.entity), + })); + + const oldRefsSet = new Map( + oldRefs.map(r => [r.target_entity_ref, r.location_key]), + ); + const newRefsSet = new Set(items.map(item => item.ref)); + + const toAdd = new Array(); + const toRemove = oldRefs + .map(row => row.target_entity_ref) + .filter(ref => !newRefsSet.has(ref)); + + for (const item of items) { + if (!oldRefsSet.has(item.ref)) { + // Add any entity that does not exist in the database + toAdd.push(item.deferred); + } else if (oldRefsSet.get(item.ref) !== item.deferred.locationKey) { + // Remove and then re-add any entity that exists, but with a different location key + toRemove.push(item.ref); + toAdd.push(item.deferred); + } + } + + return { toAdd, toRemove }; + } + + /** + * Add a set of deferred entities for processing. + * The entities will be added at the front of the processing queue. + */ + private async addUnprocessedEntities( + txOpaque: Transaction, + options: { + sourceEntityRef: string; + entities: DeferredEntity[]; + }, + ): Promise { + const tx = txOpaque as Knex.Transaction; + + // Keeps track of the entities that we end up inserting to update refresh_state_references afterwards + const stateReferences = new Array(); + const conflictingStateReferences = new Array(); + + // Upsert all of the unprocessed entities into the refresh_state table, by + // their entity ref. + for (const { entity, locationKey } of options.entities) { + const entityRef = stringifyEntityRef(entity); + + const updated = await this.updateUnprocessedEntity( + tx, + entity, + locationKey, + ); + if (updated) { + stateReferences.push(entityRef); + continue; + } + + const inserted = await this.insertUnprocessedEntity( + tx, + entity, + locationKey, + ); + if (inserted) { + stateReferences.push(entityRef); + continue; + } + + // If the row can't be inserted, we have a conflict, but it could be either + // because of a conflicting locationKey or a race with another instance, so check + // whether the conflicting entity has the same entityRef but a different locationKey + const conflictingKey = await this.checkLocationKeyConflict( + tx, + entityRef, + locationKey, + ); + if (conflictingKey) { + this.options.logger.warn( + `Detected conflicting entityRef ${entityRef} already referenced by ${conflictingKey} and now also ${locationKey}`, + ); + conflictingStateReferences.push(entityRef); + } + } + + // Replace all references for the originating entity or source and then create new ones + await tx('refresh_state_references') + .whereNotIn('target_entity_ref', conflictingStateReferences) + .andWhere({ source_entity_ref: options.sourceEntityRef }) + .delete(); + await tx.batchInsert( + 'refresh_state_references', + stateReferences.map(entityRef => ({ + source_entity_ref: options.sourceEntityRef, + target_entity_ref: entityRef, + })), + BATCH_SIZE, + ); + } } diff --git a/plugins/catalog-backend/src/next/database/types.ts b/plugins/catalog-backend/src/next/database/types.ts index 3dfe8e5592..3f1a4784fb 100644 --- a/plugins/catalog-backend/src/next/database/types.ts +++ b/plugins/catalog-backend/src/next/database/types.ts @@ -20,29 +20,23 @@ import { DateTime } from 'luxon'; import { Transaction } from '../../database/types'; import { DeferredEntity } from '../processing/types'; -export type AddUnprocessedEntitiesOptions = - | { - sourceEntityRef: string; - entities: DeferredEntity[]; - } - | { - sourceKey: string; - entities: DeferredEntity[]; - }; - export type AddUnprocessedEntitiesResult = {}; export type UpdateProcessedEntityOptions = { id: string; processedEntity: Entity; resultHash: string; - state?: Map; errors?: string; relations: EntityRelationSpec[]; deferredEntities: DeferredEntity[]; locationKey?: string; }; +export type UpdateEntityCacheOptions = { + id: string; + state?: JsonObject; +}; + export type UpdateProcessedEntityErrorsOptions = { id: string; errors?: string; @@ -57,7 +51,7 @@ export type RefreshStateItem = { resultHash: string; nextUpdateAt: DateTime; lastDiscoveryAt: DateTime; // remove? - state: Map; + state?: JsonObject; errors?: string; locationKey?: string; }; @@ -118,6 +112,14 @@ export interface ProcessingDatabase { options: UpdateProcessedEntityOptions, ): Promise; + /** + * Updates the cache associated with an entity. + */ + updateEntityCache( + txOpaque: Transaction, + options: UpdateEntityCacheOptions, + ): Promise; + /** * Updates only the errors of a processed entity */ diff --git a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts new file mode 100644 index 0000000000..098f3ac14d --- /dev/null +++ b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { + Entity, + EntityPolicies, + LocationEntity, + LocationSpec, + LOCATION_ANNOTATION, + ORIGIN_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; +import { ScmIntegrations } from '@backstage/integration'; +import { + CatalogProcessor, + CatalogProcessorCache, + CatalogProcessorEmit, + CatalogProcessorParser, + results, +} from '../../ingestion'; +import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules'; +import { DefaultCatalogProcessingOrchestrator } from './DefaultCatalogProcessingOrchestrator'; +import { defaultEntityDataParser } from '../../ingestion/processors/util/parse'; +import { ConfigReader } from '@backstage/config'; + +class FooBarProcessor implements CatalogProcessor { + getProcessorName = () => 'foo-bar'; + + async validateEntityKind(entity: Entity) { + return entity.kind.toLocaleLowerCase('en-US') === 'foobar'; + } + + async postProcessEntity( + entity: Entity, + _location: LocationSpec, + emit: CatalogProcessorEmit, + cache: CatalogProcessorCache, + ) { + if (await cache.get('emit')) { + emit( + results.entity( + { type: 'url', target: './new-place' }, + { + apiVersion: 'my-api/v1', + kind: 'FooBar', + metadata: { + name: 'my-new-foo-bar', + }, + }, + ), + ); + emit( + results.relation({ + type: 'my-type', + source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, + target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, + }), + ); + } + return entity; + } +} + +describe('DefaultCatalogProcessingOrchestrator', () => { + describe('basic processing', () => { + const entity = { + apiVersion: 'my-api/v1', + kind: 'FooBar', + metadata: { + name: 'my-foo-bar', + annotations: { + [LOCATION_ANNOTATION]: 'url:./here', + [ORIGIN_LOCATION_ANNOTATION]: 'url:./there', + }, + }, + }; + + const orchestrator = new DefaultCatalogProcessingOrchestrator({ + processors: [new FooBarProcessor()], + integrations: ScmIntegrations.fromConfig(new ConfigReader({})), + logger: getVoidLogger(), + parser: defaultEntityDataParser, + policy: EntityPolicies.allOf([]), + rulesEnforcer: { isAllowed: () => true }, + }); + + it('runs a minimal processing', async () => { + await expect(orchestrator.process({ entity })).resolves.toEqual({ + ok: true, + completedEntity: entity, + deferredEntities: [], + errors: [], + relations: [], + state: { + cache: {}, + }, + }); + }); + + it('emits some things', async () => { + await expect( + orchestrator.process({ + entity, + state: { cache: { 'foo-bar': { emit: true } } }, + }), + ).resolves.toEqual({ + ok: true, + completedEntity: entity, + deferredEntities: [ + { + locationKey: 'url:./new-place', + entity: { + apiVersion: 'my-api/v1', + kind: 'FooBar', + metadata: { + name: 'my-new-foo-bar', + annotations: { + [LOCATION_ANNOTATION]: 'url:./new-place', + [ORIGIN_LOCATION_ANNOTATION]: 'url:./there', + }, + }, + }, + }, + ], + errors: [], + relations: [ + { + type: 'my-type', + source: { kind: 'foobar', name: 'my-source', namespace: 'default' }, + target: { kind: 'foobar', name: 'my-target', namespace: 'default' }, + }, + ], + state: { + cache: { 'foo-bar': { emit: true } }, + }, + }); + }); + + it('accepts any state input', async () => { + await expect( + orchestrator.process({ entity, state: null as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: [] as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: Symbol() as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: undefined }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: 3 as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: '}{' as any }), + ).resolves.toMatchObject({ + ok: true, + }); + await expect( + orchestrator.process({ entity, state: { cache: null } }), + ).resolves.toMatchObject({ + ok: true, + }); + }); + }); + + describe('rules', () => { + it('enforces catalog rules', async () => { + const entity: LocationEntity = { + apiVersion: 'backstage.io/v1beta1', + kind: 'Location', + metadata: { + name: 'l', + annotations: { + [ORIGIN_LOCATION_ANNOTATION]: 'url:https://example.com/origin.yaml', + [LOCATION_ANNOTATION]: 'url:https://example.com/origin.yaml', + }, + }, + spec: { + type: 'url', + target: 'http://example.com/entity.yaml', + }, + }; + + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); + const processor: jest.Mocked = { + validateEntityKind: jest.fn(async () => true), + readLocation: jest.fn(async (_l, _o, emit) => { + emit(results.entity({ type: 't', target: 't' }, entity)); + return true; + }), + }; + const parser: CatalogProcessorParser = jest.fn(); + const rulesEnforcer: jest.Mocked = { + isAllowed: jest.fn(), + }; + + const orchestrator = new DefaultCatalogProcessingOrchestrator({ + processors: [processor], + integrations, + logger: getVoidLogger(), + parser, + policy: EntityPolicies.allOf([]), + rulesEnforcer, + }); + + rulesEnforcer.isAllowed.mockReturnValueOnce(true); + await expect( + orchestrator.process({ entity, state: {} }), + ).resolves.toEqual(expect.objectContaining({ ok: true })); + + rulesEnforcer.isAllowed.mockReturnValueOnce(false); + await expect( + orchestrator.process({ entity, state: {} }), + ).resolves.toEqual(expect.objectContaining({ ok: false })); + }); + }); +}); diff --git a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts index d7f51807a3..6261c0e2ff 100644 --- a/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/next/processing/DefaultCatalogProcessingOrchestrator.ts @@ -21,8 +21,10 @@ import { LocationSpec, parseLocationReference, stringifyEntityRef, + stringifyLocationReference, } from '@backstage/catalog-model'; -import { ConflictError, InputError } from '@backstage/errors'; +import { ConflictError, InputError, NotAllowedError } from '@backstage/errors'; +import { JsonValue } from '@backstage/config'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; import { Logger } from 'winston'; @@ -44,13 +46,17 @@ import { toAbsoluteUrl, validateEntity, validateEntityEnvelope, + isObject, } from './util'; +import { CatalogRulesEnforcer } from '../../ingestion/CatalogRules'; +import { ProcessorCacheManager } from './ProcessorCacheManager'; type Context = { entityRef: string; location: LocationSpec; originLocation: LocationSpec; collector: ProcessorOutputCollector; + cache: ProcessorCacheManager; }; export class DefaultCatalogProcessingOrchestrator @@ -63,23 +69,30 @@ export class DefaultCatalogProcessingOrchestrator logger: Logger; parser: CatalogProcessorParser; policy: EntityPolicy; + rulesEnforcer: CatalogRulesEnforcer; }, ) {} async process( request: EntityProcessingRequest, ): Promise { - return this.processSingleEntity(request.entity); + return this.processSingleEntity(request.entity, request.state); } private async processSingleEntity( unprocessedEntity: Entity, + state: JsonValue | undefined, ): Promise { const collector = new ProcessorOutputCollector( this.options.logger, unprocessedEntity, ); + // Cache that is scoped to the entity and processor + const cache = new ProcessorCacheManager( + isObject(state) && isObject(state.cache) ? state.cache : {}, + ); + try { // This will be checked and mutated step by step below let entity: Entity = unprocessedEntity; @@ -105,6 +118,7 @@ export class DefaultCatalogProcessingOrchestrator originLocation: parseLocationReference( getEntityOriginLocationRef(entity), ), + cache, collector, }; @@ -117,10 +131,32 @@ export class DefaultCatalogProcessingOrchestrator } entity = await this.runPostProcessStep(entity, context); + // Check that any emitted entities are permitted to originate from that + // particular location according to the catalog rules + const collectorResults = context.collector.results(); + for (const deferredEntity of collectorResults.deferredEntities) { + if ( + !this.options.rulesEnforcer.isAllowed( + deferredEntity.entity, + context.originLocation, + ) + ) { + throw new NotAllowedError( + `Entity ${stringifyEntityRef( + deferredEntity.entity, + )} at ${stringifyLocationReference( + context.location, + )}, originated at ${stringifyLocationReference( + context.originLocation, + )}, is not of an allowed kind for that location`, + ); + } + } + return { - ...context.collector.results(), + ...collectorResults, completedEntity: entity, - state: new Map(), + state: { cache: cache.collect() }, ok: true, }; } catch (error) { @@ -148,6 +184,7 @@ export class DefaultCatalogProcessingOrchestrator context.location, context.collector.onEmit, context.originLocation, + context.cache.forProcessor(processor), ); } catch (e) { throw new InputError( @@ -281,6 +318,7 @@ export class DefaultCatalogProcessingOrchestrator false, context.collector.onEmit, this.options.parser, + context.cache.forProcessor(processor, target), ); if (read) { didRead = true; @@ -318,6 +356,7 @@ export class DefaultCatalogProcessingOrchestrator result, context.location, context.collector.onEmit, + context.cache.forProcessor(processor), ); } catch (e) { throw new InputError( diff --git a/plugins/catalog-backend/src/next/processing/ProcessorCacheManager.test.ts b/plugins/catalog-backend/src/next/processing/ProcessorCacheManager.test.ts new file mode 100644 index 0000000000..44c96da1ca --- /dev/null +++ b/plugins/catalog-backend/src/next/processing/ProcessorCacheManager.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { CatalogProcessor } from '../../ingestion/processors'; +import { ProcessorCacheManager } from './ProcessorCacheManager'; + +class MyProcessor implements CatalogProcessor { + getProcessorName = () => 'my-processor'; +} + +class OtherProcessor implements CatalogProcessor {} + +describe('ProcessorCacheManager', () => { + const myProcessor = new MyProcessor(); + const otherProcessor = new OtherProcessor(); + + it('should forward existing state and collect new state', async () => { + const cache = new ProcessorCacheManager({ + 'my-processor': { 'my-key': 'my-value' }, + }); + + // Should be empty to begin with + expect(cache.collect()).toEqual({}); + + // instance should be cached + const processorCache = cache.forProcessor(myProcessor); + expect(processorCache).toBe(cache.forProcessor(myProcessor)); + + // Initial values should be visible, writes should not + await expect(processorCache.get('my-key')).resolves.toBe( + 'my-value', + ); + + // If set hasn't been called yet we should get the existing data + expect(cache.collect()).toEqual({ + 'my-processor': { 'my-key': 'my-value' }, + }); + + processorCache.set('my-new-key', 'my-new-value'); + // Once set has been called the old values should disappear + expect(cache.collect()).toEqual({ + 'my-processor': { 'my-new-key': 'my-new-value' }, + }); + + // Getting the cache should return the initial state value + await expect(processorCache.get('my-key')).resolves.toBe( + 'my-value', + ); + await expect( + processorCache.get('my-new-key'), + ).resolves.toBeUndefined(); + + // There should be isolation between processors + await expect( + cache.forProcessor(otherProcessor).get('my-key'), + ).resolves.toBeUndefined(); + + // Collecting the state and passing it to a new manager should make the new values visible + const newCache = new ProcessorCacheManager(cache.collect()); + await expect( + newCache.forProcessor(myProcessor).get('my-new-key'), + ).resolves.toBe('my-new-value'); + }); +}); + +describe('ScopedProcessorCache', () => { + const myProcessor = new MyProcessor(); + + it('should forward existing state and collect new state', async () => { + const cache = new ProcessorCacheManager({ + 'my-processor': { 'scope-1': { 'my-key': 'my-value' } }, + }); + + const scopedCache1 = cache.forProcessor(myProcessor, 'scope-1'); + const scopedCache2 = cache.forProcessor(myProcessor, 'scope-2'); + + // Should be empty to begin with + expect(cache.collect()).toEqual({ + 'my-processor': { 'scope-1': { 'my-key': 'my-value' } }, + }); + + await scopedCache2.set('my-new-key-2', 'my-new-value-2'); + + expect(cache.collect()).toEqual({ + 'my-processor': { + 'scope-1': { 'my-key': 'my-value' }, + 'scope-2': { 'my-new-key-2': 'my-new-value-2' }, + }, + }); + + await scopedCache1.set('my-new-key', 'my-new-value'); + + await expect(scopedCache1.get('my-key')).resolves.toBe('my-value'); + await expect( + scopedCache1.get('my-new-key'), + ).resolves.toBeUndefined(); + + expect(cache.collect()).toEqual({ + 'my-processor': { + 'scope-1': { 'my-new-key': 'my-new-value' }, + 'scope-2': { 'my-new-key-2': 'my-new-value-2' }, + }, + }); + }); +}); diff --git a/plugins/catalog-backend/src/next/processing/ProcessorCacheManager.ts b/plugins/catalog-backend/src/next/processing/ProcessorCacheManager.ts new file mode 100644 index 0000000000..16945b7cc9 --- /dev/null +++ b/plugins/catalog-backend/src/next/processing/ProcessorCacheManager.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { JsonObject, JsonValue } from '@backstage/config'; +import { CatalogProcessor } from '../../ingestion/processors'; +import { CatalogProcessorCache } from '../../ingestion/processors/types'; +import { isObject } from './util'; + +class SingleProcessorSubCache implements CatalogProcessorCache { + private newState?: JsonObject; + + constructor(private readonly existingState?: JsonObject) {} + + async get( + key: string, + ): Promise { + return this.existingState?.[key] as ItemType | undefined; + } + + async set( + key: string, + value: ItemType, + ): Promise { + if (!this.newState) { + this.newState = {}; + } + + this.newState[key] = value; + } + + collect(): JsonObject | undefined { + return this.newState ?? this.existingState; + } +} + +class SingleProcessorCache implements CatalogProcessorCache { + private newState?: JsonObject; + private subCaches: Map = new Map(); + + constructor(private readonly existingState?: JsonObject) {} + + async get( + key: string, + ): Promise { + return this.existingState?.[key] as ItemType | undefined; + } + + async set( + key: string, + value: ItemType, + ): Promise { + if (!this.newState) { + this.newState = {}; + } + + this.newState[key] = value; + } + + withKey(key: string) { + const existingSubCache = this.subCaches.get(key); + if (existingSubCache) { + return existingSubCache; + } + const existing = this.existingState?.[key]; + const subCache = new SingleProcessorSubCache( + isObject(existing) ? existing : undefined, + ); + this.subCaches.set(key, subCache); + return subCache; + } + + collect(): JsonObject | undefined { + let obj = this.newState ?? this.existingState; + for (const [key, subCache] of this.subCaches) { + const subCacheValue = subCache.collect(); + if (subCacheValue) { + obj = { ...obj, [key]: subCacheValue }; + } + } + return obj; + } +} + +export class ProcessorCacheManager { + private caches = new Map(); + + constructor(private readonly existingState: JsonObject) {} + + forProcessor( + processor: CatalogProcessor, + key?: string, + ): CatalogProcessorCache { + // constructor name will be deprecated in the future when we make `getProcessorName` required in the implementation + const name = processor.getProcessorName?.() ?? processor.constructor.name; + const cache = this.caches.get(name); + if (cache) { + return key ? cache.withKey(key) : cache; + } + + const existing = this.existingState[name]; + + const newCache = new SingleProcessorCache( + isObject(existing) ? existing : undefined, + ); + this.caches.set(name, newCache); + return key ? newCache.withKey(key) : newCache; + } + + collect(): JsonObject { + const result: JsonObject = {}; + for (const [key, value] of this.caches.entries()) { + result[key] = value.collect(); + } + + return result; + } +} diff --git a/plugins/catalog-backend/src/next/processing/types.ts b/plugins/catalog-backend/src/next/processing/types.ts index 9e30d07404..8b533714ce 100644 --- a/plugins/catalog-backend/src/next/processing/types.ts +++ b/plugins/catalog-backend/src/next/processing/types.ts @@ -19,13 +19,13 @@ import { JsonObject } from '@backstage/config'; export type EntityProcessingRequest = { entity: Entity; - state: Map; // Versions for multiple deployments etc + state?: JsonObject; // Versions for multiple deployments etc }; export type EntityProcessingResult = | { ok: true; - state: Map; + state: JsonObject; completedEntity: Entity; deferredEntities: DeferredEntity[]; relations: EntityRelationSpec[]; diff --git a/plugins/catalog-backend/src/next/processing/util.ts b/plugins/catalog-backend/src/next/processing/util.ts index 8b06a76d16..ad8fe8be60 100644 --- a/plugins/catalog-backend/src/next/processing/util.ts +++ b/plugins/catalog-backend/src/next/processing/util.ts @@ -24,6 +24,7 @@ import { ORIGIN_LOCATION_ANNOTATION, stringifyEntityRef, } from '@backstage/catalog-model'; +import { JsonObject, JsonValue } from '@backstage/config'; import { InputError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; import path from 'path'; @@ -76,6 +77,10 @@ export function toAbsoluteUrl( } } +export function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + export const validateEntity = entitySchemaValidator(); export const validateEntityEnvelope = entityEnvelopeSchemaValidator(); diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index e32e071ebc..40683bb4bf 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -56,7 +56,7 @@ import { StaticLocationProcessor, UrlReaderProcessor, } from '../ingestion'; -import { CatalogRulesEnforcer } from '../ingestion/CatalogRules'; +import { DefaultCatalogRulesEnforcer } from '../ingestion/CatalogRules'; import { RepoLocationAnalyzer } from '../ingestion/LocationAnalyzer'; import { jsonPlaceholderResolver, @@ -240,7 +240,7 @@ export class CatalogBuilder { const policy = this.buildEntityPolicy(); const processors = this.buildProcessors(); - const rulesEnforcer = CatalogRulesEnforcer.fromConfig(config); + const rulesEnforcer = DefaultCatalogRulesEnforcer.fromConfig(config); const parser = this.parser || defaultEntityDataParser; const locationReader = new LocationReaders({ diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md new file mode 100644 index 0000000000..3a646aba26 --- /dev/null +++ b/plugins/catalog-graph/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-catalog-graph + +## 0.1.1 + +### Patch Changes + +- c0eb1fb9df: Make zooming configurable for `` and disable it by default. + This resolves an issue that scrolling on the entity page is sometimes captured + by the graph making the page hard to use. +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/catalog-client@0.4.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index 0e06e15f4c..0034cf0c3b 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -9,11 +9,13 @@ The plugin comes with these features: A card that displays the directly related entities to the current entity. This card is for use on the entity page. The card can be customized, for example filtering for specific relations. +
diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 345c0595eb..ff2cf1cc4e 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-catalog-import +## 0.6.0 + +### Minor Changes + +- 690657799e: Add initial support for customizing the catalog import page. + + It is now possible to pass a custom layout to the import page, as it's already + supported by the search page. If no custom layout is passed, the default layout + is used. + + ```typescript + }> + +
+ + + + Start tracking your component in Backstage by adding it to the + software catalog. + + + + + + Hello World + + + + + + + + + + ``` + + Previously it was possible to disable and customize the automatic pull request + feature by passing options to `` (`pullRequest.disable` and + `pullRequest.preparePullRequest`). This functionality is moved to the + `CatalogImportApi` which now provides an optional `preparePullRequest()` + function. The function can either be overridden to generate a different content + for the pull request, or removed to disable this feature. + + The export of the long term deprecated legacy `` is removed, migrate to + `` instead. + +### Patch Changes + +- 9ef2987a83: The import form is now aware of locations that already exist. It lists them separately and shows a button for triggering a refresh. +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/integration-react@0.1.10 + ## 0.5.21 ### Patch Changes diff --git a/plugins/catalog-import/api-report.md b/plugins/catalog-import/api-report.md index 4deb66e01b..9874956b5f 100644 --- a/plugins/catalog-import/api-report.md +++ b/plugins/catalog-import/api-report.md @@ -16,9 +16,9 @@ import { EntityName } from '@backstage/catalog-model'; import { FieldErrors } from 'react-hook-form'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; -import { OAuthApi } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; +import { ScmAuthApi } from '@backstage/integration-react'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { SubmitHandler } from 'react-hook-form'; import { TextFieldProps } from '@material-ui/core/TextField/TextField'; @@ -96,7 +96,7 @@ export const catalogImportApiRef: ApiRef; export class CatalogImportClient implements CatalogImportApi { constructor(options: { discoveryApi: DiscoveryApi; - githubAuthApi: OAuthApi; + scmAuthApi: ScmAuthApi; identityApi: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; catalogApi: CatalogApi; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 8a152d9e50..832a4e0cdf 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.5.21", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.18", - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/integration": "^0.6.4", - "@backstage/integration-react": "^0.1.9", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/integration": "^0.6.5", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -56,9 +56,9 @@ "lodash": "^4.17.21" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index a87587c9c5..373acd4b5f 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -47,8 +47,8 @@ jest.doMock('@octokit/rest', () => { }); import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; -import { OAuthApi } from '@backstage/core-plugin-api'; import { ScmIntegrations } from '@backstage/integration'; +import { ScmAuthApi } from '@backstage/integration-react'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { msw } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; @@ -63,8 +63,8 @@ describe('CatalogImportClient', () => { const mockBaseUrl = 'http://backstage:9191/api/catalog'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - const githubAuthApi: jest.Mocked = { - getAccessToken: jest.fn(), + const scmAuthApi: jest.Mocked = { + getCredentials: jest.fn().mockResolvedValue({ token: 'token' }), }; const identityApi = { getUserId: () => { @@ -112,7 +112,7 @@ describe('CatalogImportClient', () => { beforeEach(() => { catalogImportClient = new CatalogImportClient({ discoveryApi, - githubAuthApi, + scmAuthApi, scmIntegrationsApi, identityApi, catalogApi, diff --git a/plugins/catalog-import/src/api/CatalogImportClient.ts b/plugins/catalog-import/src/api/CatalogImportClient.ts index c804d1193d..752899e6e8 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.ts @@ -20,12 +20,12 @@ import { ConfigApi, DiscoveryApi, IdentityApi, - OAuthApi, } from '@backstage/core-plugin-api'; import { GitHubIntegrationConfig, ScmIntegrationRegistry, } from '@backstage/integration'; +import { ScmAuthApi } from '@backstage/integration-react'; import { Octokit } from '@octokit/rest'; import { Base64 } from 'js-base64'; import { PartialEntity } from '../types'; @@ -36,21 +36,21 @@ import { trimEnd } from 'lodash'; export class CatalogImportClient implements CatalogImportApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; - private readonly githubAuthApi: OAuthApi; + private readonly scmAuthApi: ScmAuthApi; private readonly scmIntegrationsApi: ScmIntegrationRegistry; private readonly catalogApi: CatalogApi; private readonly configApi: ConfigApi; constructor(options: { discoveryApi: DiscoveryApi; - githubAuthApi: OAuthApi; + scmAuthApi: ScmAuthApi; identityApi: IdentityApi; scmIntegrationsApi: ScmIntegrationRegistry; catalogApi: CatalogApi; configApi: ConfigApi; }) { this.discoveryApi = options.discoveryApi; - this.githubAuthApi = options.githubAuthApi; + this.scmAuthApi = options.scmAuthApi; this.identityApi = options.identityApi; this.scmIntegrationsApi = options.scmIntegrationsApi; this.catalogApi = options.catalogApi; @@ -157,6 +157,7 @@ the component will become available.\n\nFor more information, read an \ if (ghConfig) { return await this.submitGitHubPrToRepo({ ...ghConfig, + repositoryUrl, fileContent, title, body, @@ -215,7 +216,7 @@ the component will become available.\n\nFor more information, read an \ entities: EntityName[]; }> > { - const token = await this.githubAuthApi.getAccessToken(['repo']); + const { token } = await this.scmAuthApi.getCredentials({ url }); const octo = new Octokit({ auth: token, baseUrl: githubIntegrationConfig.apiBaseUrl, @@ -270,6 +271,7 @@ the component will become available.\n\nFor more information, read an \ title, body, fileContent, + repositoryUrl, githubIntegrationConfig, }: { owner: string; @@ -277,9 +279,15 @@ the component will become available.\n\nFor more information, read an \ title: string; body: string; fileContent: string; + repositoryUrl: string; githubIntegrationConfig: GitHubIntegrationConfig; }): Promise<{ link: string; location: string }> { - const token = await this.githubAuthApi.getAccessToken(['repo']); + const { token } = await this.scmAuthApi.getCredentials({ + url: repositoryUrl, + additionalScope: { + repoWrite: true, + }, + }); const octo = new Octokit({ auth: token, diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index 57376c33f2..69ae38207d 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -55,8 +55,8 @@ describe('', () => { catalogImportApiRef, new CatalogImportClient({ discoveryApi: {} as any, - githubAuthApi: { - getAccessToken: async () => 'token', + scmAuthApi: { + getCredentials: async () => ({ token: 'token', headers: {} }), }, identityApi, scmIntegrationsApi: {} as any, diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index fe3c3673f8..92a6077792 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -61,10 +61,8 @@ describe('', () => { catalogImportApiRef, new CatalogImportClient({ discoveryApi: {} as any, - githubAuthApi: { - getAccessToken: async () => 'token', - }, identityApi, + scmAuthApi: {} as any, scmIntegrationsApi: {} as any, catalogApi: {} as any, configApi: new ConfigReader({}), diff --git a/plugins/catalog-import/src/plugin.ts b/plugins/catalog-import/src/plugin.ts index d7d6370de3..2dd90d36bc 100644 --- a/plugins/catalog-import/src/plugin.ts +++ b/plugins/catalog-import/src/plugin.ts @@ -21,10 +21,12 @@ import { createRoutableExtension, createRouteRef, discoveryApiRef, - githubAuthApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { scmIntegrationsApiRef } from '@backstage/integration-react'; +import { + scmAuthApiRef, + scmIntegrationsApiRef, +} from '@backstage/integration-react'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { catalogImportApiRef, CatalogImportClient } from './api'; @@ -40,7 +42,7 @@ export const catalogImportPlugin = createPlugin({ api: catalogImportApiRef, deps: { discoveryApi: discoveryApiRef, - githubAuthApi: githubAuthApiRef, + scmAuthApi: scmAuthApiRef, identityApi: identityApiRef, scmIntegrationsApi: scmIntegrationsApiRef, catalogApi: catalogApiRef, @@ -48,7 +50,7 @@ export const catalogImportPlugin = createPlugin({ }, factory: ({ discoveryApi, - githubAuthApi, + scmAuthApi, identityApi, scmIntegrationsApi, catalogApi, @@ -56,7 +58,7 @@ export const catalogImportPlugin = createPlugin({ }) => new CatalogImportClient({ discoveryApi, - githubAuthApi, + scmAuthApi, scmIntegrationsApi, identityApi, catalogApi, diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 24de326b44..615b3c53b8 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-react +## 0.5.0 + +### Minor Changes + +- dbcaa6387a: Extends the `CatalogClient` interface with a `refreshEntity` method. + +### Patch Changes + +- cc464a56b3: This makes Type and Lifecycle columns consistent for all table cases and adds a new line in Description column for better readability +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/core-app-api@0.1.14 + ## 0.4.6 ### Patch Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 54d8af11b6..0df00be9b3 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.4.6", + "version": "0.5.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.18", - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-app-api": "^0.1.13", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-app-api": "^0.1.14", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@backstage/version-bridge": "^0.1.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,8 +49,8 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/dev-utils": "^0.2.8", + "@backstage/cli": "^0.7.13", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index aa568740a2..2366374a64 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog +## 0.6.16 + +### Patch Changes + +- dbcaa6387a: Updates the `AboutCard` with a refresh button that allows the entity to be scheduled for refresh. +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/integration-react@0.1.10 + ## 0.6.15 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index a5b30acbcf..9d3bd10963 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "0.6.15", + "version": "0.6.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,14 +31,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.18", - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.4", - "@backstage/integration-react": "^0.1.9", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/integration": "^0.6.5", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -55,9 +55,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 1dbf849a2d..3b64ee6b73 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -220,6 +220,10 @@ describe('', () => { apiVersion: 'v1', kind: 'Component', metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://backstage.io/catalog-info.yaml', + }, name: 'software', }, spec: { @@ -252,6 +256,35 @@ describe('', () => { ); }); + it('should not render refresh button if the location is not an url', async () => { + const entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + const apis = ApiRegistry.with( + scmIntegrationsApiRef, + ScmIntegrationsApi.fromConfig(new ConfigReader({})), + ).with(catalogApiRef, catalogApi); + + const { queryByTitle } = await renderInTestApp( + + + + + , + ); + + expect(queryByTitle('Schedule entity refresh')).not.toBeInTheDocument(); + }); + it('renders techdocs link', async () => { const entity = { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index ec3d47a204..68777d4109 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -17,6 +17,7 @@ import { Entity, ENTITY_DEFAULT_NAMESPACE, + LOCATION_ANNOTATION, RELATION_CONSUMES_API, RELATION_PROVIDES_API, stringifyEntityRef, @@ -147,6 +148,8 @@ export function AboutCard({ variant }: AboutCardProps) { cardContentClass = classes.fullHeightCardContent; } + const isUrl = + entity.metadata.annotations?.[LOCATION_ANNOTATION]?.startsWith('url:'); const refreshEntity = useCallback(async () => { await catalogApi.refreshEntity(stringifyEntityRef(entity)); alertApi.post({ message: 'Refresh scheduled', severity: 'info' }); @@ -158,13 +161,15 @@ export function AboutCard({ variant }: AboutCardProps) { title="About" action={ <> - - - + {isUrl && ( + + + + )} ; +// Warning: (ae-forgotten-export) The symbol "DomainCardProps" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "DomainCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const DomainCard: ({ entity }: DomainCardProps) => JSX.Element; + // Warning: (ae-missing-release-tag) "DomainExplorerContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 3f4c72cef4..dfbf220a5c 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.3.16", + "version": "0.3.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/plugin-explore-react": "^0.0.6", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -49,9 +49,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/explore/src/components/index.ts b/plugins/explore/src/components/index.ts index fa98ec78da..cefc549684 100644 --- a/plugins/explore/src/components/index.ts +++ b/plugins/explore/src/components/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export { DomainCard } from './DomainCard'; export { ExploreLayout } from './ExploreLayout'; diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts index cc1b097bfe..04f0240968 100644 --- a/plugins/explore/src/index.ts +++ b/plugins/explore/src/index.ts @@ -20,7 +20,7 @@ * @packageDocumentation */ -export { ExploreLayout } from './components'; +export * from './components'; export * from './extensions'; export { explorePlugin, explorePlugin as plugin } from './plugin'; export * from './routes'; diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index 0a1cb0f2a0..3239b7beb7 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-firehydrant +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + ## 0.1.3 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 957a15ba60..ab1b1ed507 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,9 +22,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,9 +35,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index eb71860a90..c967dd6a35 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-fossa +## 0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.2.16 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index fc9101a32f..fb51e3f548 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.16", + "version": "0.2.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,9 +49,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index a4b215fe77..3837bb68a0 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-gcp-projects +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index ce84fdebd0..47d9ab5fc4 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -43,9 +43,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 75dcd64cbc..69ee6a876f 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-git-release-manager +## 0.2.7 + +### Patch Changes + +- 023350f910: Remove 'refresh' icon from success dialog's OK-CTA. User feedback deemed it confusing. +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + ## 0.2.6 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 53630b3537..e396ff3f5b 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,9 +39,9 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index e30934ce6a..fe2642d378 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-github-actions +## 0.4.19 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.4.18 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index c6460fb0ce..8a66e45eb1 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.4.18", + "version": "0.4.19", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/integration": "^0.6.4", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/integration": "^0.6.5", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,9 +52,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 090f262b3f..7681ffa394 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-deployments +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/integration-react@0.1.10 + ## 0.1.16 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index fb7efe3319..9f71b13bf9 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.16", + "version": "0.1.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,13 +21,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.4", - "@backstage/integration-react": "^0.1.9", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/integration": "^0.6.5", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -39,9 +39,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 8428707fe9..cc9c6eeb12 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-gitops-profiles +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 1ba8680599..11735a57a5 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -44,9 +44,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index cc42a0fda1..893508eba1 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-graphiql +## 0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.2.16 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index e13c7ca610..2814c3fe5a 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.16", + "version": "0.2.17", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -44,9 +44,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index 4e371bbac3..6d2f2b9523 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-home +## 0.4.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/home/package.json b/plugins/home/package.json index 0f35e334eb..30b1a2f25c 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.0", + "version": "0.4.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -34,9 +34,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index b8dd2d06fc..bbebac1a80 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-ilert +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.1.11 ### Patch Changes diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 0b26f53e54..9a32a9761c 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@date-io/luxon": "2.x", "@material-ui/core": "^4.12.2", @@ -39,9 +39,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 67021b0942..e729a3c2e8 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-jenkins-backend +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.1.4 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 3650f80423..c38ec102bd 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.4", + "version": "0.1.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,10 +22,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-client": "^0.3.17", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -35,7 +35,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.13", "@types/jenkins": "^0.23.1", "@types/supertest": "^2.0.8", "msw": "^0.29.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 79b46fc12f..cb5fd853eb 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-jenkins +## 0.5.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.5.6 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 8b33930fd8..1ef6ea682e 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.5.6", + "version": "0.5.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,9 +48,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index 0bbfb2a45a..88111cd1de 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kafka-backend +## 0.2.10 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.2.9 ### Patch Changes diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 0583bccf68..25b9e32679 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.9", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -44,7 +44,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.3", + "@backstage/cli": "^0.7.13", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 148391b69c..097333adfb 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-kafka +## 0.2.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.2.15 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 90e521940c..c5bbfebbaf 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.2.15", + "version": "0.2.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,9 +35,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 4d2158d6a1..004476d8c1 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes-backend +## 0.3.16 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- 7a0c334707: Provide access to the Kubernetes dashboard when viewing a specific resource +- Updated dependencies + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + - @backstage/plugin-kubernetes-common@0.1.4 + ## 0.3.15 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 3154cab61d..f588771294 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.3.15", + "version": "0.3.16", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.8", - "@backstage/plugin-kubernetes-common": "^0.1.3", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/plugin-kubernetes-common": "^0.1.4", "@google-cloud/container": "^2.2.0", "@kubernetes/client-node": "^0.15.0", "@types/express": "^4.17.6", @@ -54,7 +54,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.8", + "@backstage/cli": "^0.7.13", "@types/aws4": "^1.5.1", "supertest": "^6.1.3", "aws-sdk-mock": "^5.2.1", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 49a312fa46..98c5a6c7e1 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-kubernetes-common +## 0.1.4 + +### Patch Changes + +- 7a0c334707: Provide access to the Kubernetes dashboard when viewing a specific resource +- Updated dependencies + - @backstage/catalog-model@0.9.3 + ## 0.1.3 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 445ba3f7d9..462f8fc0f7 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.1.3", + "version": "0.1.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,11 +35,11 @@ "url": "https://github.com/backstage/backstage/issues" }, "dependencies": { - "@backstage/catalog-model": "^0.9.0", + "@backstage/catalog-model": "^0.9.3", "@kubernetes/client-node": "^0.15.0" }, "devDependencies": { - "@backstage/cli": "^0.7.6" + "@backstage/cli": "^0.7.13" }, "jest": { "roots": [ diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index acd836551d..665bd51f3e 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kubernetes +## 0.4.14 + +### Patch Changes + +- 7a0c334707: Provide access to the Kubernetes dashboard when viewing a specific resource +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + - @backstage/plugin-kubernetes-common@0.1.4 + ## 0.4.13 ### Patch Changes diff --git a/plugins/kubernetes/api-report.md b/plugins/kubernetes/api-report.md index fc1127595d..fcbf349d7e 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -33,6 +33,11 @@ export function formatClusterLink( options: FormatClusterLinkOptions, ): string | undefined; +// Warning: (ae-missing-release-tag) "isKubernetesAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isKubernetesAvailable: (entity: Entity) => boolean; + // Warning: (ae-forgotten-export) The symbol "KubernetesAuthProvidersApi" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "KubernetesAuthProviders" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index bcb5736304..b4d1953c7d 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.4.13", + "version": "0.4.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,12 +31,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", - "@backstage/plugin-kubernetes-common": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.5.0", + "@backstage/plugin-kubernetes-common": "^0.1.4", "@backstage/theme": "^0.2.10", "@kubernetes/client-node": "^0.15.0", "@material-ui/core": "^4.12.2", @@ -51,9 +51,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 11c9b488fb..f98d3ca45e 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -27,6 +27,12 @@ const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; const KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION = 'backstage.io/kubernetes-label-selector'; +export const isKubernetesAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[KUBERNETES_ANNOTATION]) || + Boolean( + entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION], + ); + type Props = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; diff --git a/plugins/kubernetes/src/index.ts b/plugins/kubernetes/src/index.ts index 63f68f0af1..08af757d47 100644 --- a/plugins/kubernetes/src/index.ts +++ b/plugins/kubernetes/src/index.ts @@ -25,6 +25,6 @@ export { kubernetesPlugin as plugin, EntityKubernetesContent, } from './plugin'; -export { Router } from './Router'; +export { Router, isKubernetesAvailable } from './Router'; export * from './kubernetes-auth-provider'; export * from './utils/clusterLinks'; diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 4dea3fa104..e8c8f483dc 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-lighthouse +## 0.2.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + ## 0.2.25 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 6508119b07..4599a4c572 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.2.25", + "version": "0.2.26", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,11 +32,11 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/config": "^0.1.6", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,9 +48,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index ff73f6ec25..700b3e792e 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-newrelic +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index b3406901f3..bbfa78d287 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -43,9 +43,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index f7574b484c..d0794b36b8 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-org +## 0.3.24 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.3.23 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index b40d625e50..0c49c8cdb7 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.3.23", + "version": "0.3.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.2", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -37,9 +37,9 @@ "qs": "^6.10.1" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 3095cbb03e..2fa52a29a3 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-pagerduty +## 0.3.14 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.3.13 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index ce4766abb8..b440f01e04 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.3.13", + "version": "0.3.14", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,9 +48,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 01d9ff10ba..d2bc07ab7e 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-rollbar-backend +## 0.1.15 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.1.14 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index e6f92d4065..7c872cc704 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.14", + "version": "0.1.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", "axios": "^0.21.1", "camelcase-keys": "^6.2.2", @@ -48,7 +48,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.4", + "@backstage/cli": "^0.7.13", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 5ec024c522..e25ffa74b7 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-rollbar +## 0.3.15 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.3.14 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 66037f7b8b..db70d54f34 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.3.14", + "version": "0.3.15", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,9 +49,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b798fb1154..39f129763a 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend +## 0.15.6 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.15.5 ### Patch Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 9b2863ecd5..30d8690b83 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.5", + "version": "0.15.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/catalog-client": "^0.3.19", - "@backstage/catalog-model": "^0.9.2", - "@backstage/config": "^0.1.9", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.2", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.2", "@gitbeaker/core": "^30.2.0", "@gitbeaker/node": "^30.2.0", @@ -68,7 +68,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.17", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 13b781a4fc..071d0939d2 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder +## 0.11.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + - @backstage/integration-react@0.1.10 + ## 0.11.4 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 74fa424eb7..a322da960e 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "0.11.4", + "version": "0.11.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,15 +31,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-client": "^0.3.19", - "@backstage/catalog-model": "^0.9.2", - "@backstage/config": "^0.1.9", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.2", - "@backstage/integration": "^0.6.4", - "@backstage/integration-react": "^0.1.9", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/integration": "^0.6.5", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -64,9 +64,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index ee9953942a..daba328467 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-search-backend-module-pg +## 0.2.1 + +### Patch Changes + +- febddedcb2: Bump `lodash` to remediate `SNYK-JS-LODASH-590103` security vulnerability +- Updated dependencies + - @backstage/backend-common@0.9.4 + ## 0.2.0 ### Minor Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 3dfb1fe307..a7196bc283 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.1", + "@backstage/backend-common": "^0.9.4", "@backstage/search-common": "^0.2.0", "@backstage/plugin-search-backend-node": "^0.4.2", "lodash": "^4.17.21", @@ -28,7 +28,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.6", - "@backstage/cli": "^0.7.10" + "@backstage/cli": "^0.7.13" }, "files": [ "dist", diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 51e901e72c..4e251afe70 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search +## 0.4.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + ## 0.4.11 ### Patch Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 7916b8068b..81a124e48a 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "0.4.11", + "version": "0.4.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,12 +30,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.2", - "@backstage/config": "^0.1.9", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.2", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/search-common": "^0.2.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -50,9 +50,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 79ed687484..dd2ee145db 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-sentry +## 0.3.22 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.3.21 ### Patch Changes diff --git a/plugins/sentry/README.md b/plugins/sentry/README.md index e0c56e842c..073bac1208 100644 --- a/plugins/sentry/README.md +++ b/plugins/sentry/README.md @@ -93,7 +93,7 @@ You can add it by overriding the `sentryApiRef`: ```ts // packages/app/src/apis.ts -import { createApiFactory } from '@backstage/core'; +import { createApiFactory } from '@backstage/core-plugin-api'; import { MockSentryApi, sentryApiRef } from '@backstage/plugin-sentry'; export const apis = [ diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 4d4c2ad283..88bae10ef6 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.3.21", + "version": "0.3.22", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -48,15 +48,15 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", - "@types/luxon": "^1.27.0", + "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "@types/react": "*", "cross-fetch": "^3.0.6", diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index 3c11025e30..6fbbabaa26 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-shortcuts +## 0.1.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.1.8 ### Patch Changes diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index a3da6bc4ee..73ebc77452 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -24,7 +24,11 @@ If you don't have a `plugins.ts` file see: [troubleshooting](#troubleshooting) ### Add the `` component within your ``: ```tsx -import { Sidebar, SidebarDivider, SidebarSpace } from '@backstage/core'; +import { + Sidebar, + SidebarDivider, + SidebarSpace, +} from '@backstage/core-components'; import { Shortcuts } from '@backstage/plugin-shortcuts'; export const SidebarComponent = () => ( diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index eb3a4ab883..1130284ea1 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.1.8", + "version": "0.1.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -37,9 +37,9 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index dfaa7bd9a9..7511708444 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-sonarqube +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.2.2 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 92e1b68643..39b789c29b 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.2.2", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,10 +33,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -49,9 +49,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index cfdd106a8a..8c5da07db3 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-splunk-on-call +## 0.3.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.3.10 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 47a84560e7..289b9f25dd 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.3.10", + "version": "0.3.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -47,15 +47,15 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", - "@types/luxon": "^1.25.0", + "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.29.0", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 7b44997aec..39513b49f2 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-tech-radar +## 0.4.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.4.7 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index c6ce45a07d..7629003a23 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.4.7", + "version": "0.4.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -45,9 +45,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index bdd2422f73..e0de2aa5aa 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-backend +## 0.10.3 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.10.2 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 542576b84c..35b80eab9c 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "0.10.2", + "version": "0.10.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,12 +31,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.3", - "@backstage/catalog-client": "^0.3.17", - "@backstage/catalog-model": "^0.9.1", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.4", + "@backstage/integration": "^0.6.5", "@backstage/search-common": "^0.2.0", "@backstage/techdocs-common": "^0.10.1", "@types/express": "^4.17.6", @@ -51,7 +51,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.7.12", + "@backstage/cli": "^0.7.13", "@backstage/test-utils": "^0.1.17", "@types/dockerode": "^3.2.1", "msw": "^0.29.0", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 6982721155..cfdf608721 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-techdocs +## 0.11.3 + +### Patch Changes + +- be13dfe61a: Make techdocs context search bar width adjust on smaller screens. +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/integration@0.6.5 + - @backstage/plugin-catalog@0.6.16 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + - @backstage/config@0.1.10 + - @backstage/integration-react@0.1.10 + - @backstage/plugin-search@0.4.12 + ## 0.11.2 ### Patch Changes diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 082dfc1db4..087cdf803a 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -167,6 +167,11 @@ export const EntityTechdocsContent: (_props: { entity?: Entity | undefined; }) => JSX.Element; +// Warning: (ae-missing-release-tag) "isTechDocsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isTechDocsAvailable: (entity: Entity) => boolean; + // Warning: (ae-missing-release-tag) "PanelType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index dda1702f67..f134a5d64b 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.11.2", + "version": "0.11.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,16 +32,16 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.1", - "@backstage/config": "^0.1.8", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.4", - "@backstage/integration-react": "^0.1.9", - "@backstage/plugin-catalog": "^0.6.15", - "@backstage/plugin-catalog-react": "^0.4.6", - "@backstage/plugin-search": "^0.4.11", + "@backstage/integration": "^0.6.5", + "@backstage/integration-react": "^0.1.10", + "@backstage/plugin-catalog": "^0.6.16", + "@backstage/plugin-catalog-react": "^0.5.0", + "@backstage/plugin-search": "^0.4.12", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -61,9 +61,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 45b0dc756c..102cda979a 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -25,6 +25,9 @@ import { MissingAnnotationEmptyState } from '@backstage/core-components'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; +export const isTechDocsAvailable = (entity: Entity) => + Boolean(entity?.metadata?.annotations?.[TECHDOCS_ANNOTATION]); + export const Router = () => { return ( diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index ba24f1c39d..50e25c5582 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -44,4 +44,4 @@ export { TechDocsReaderPage, } from './plugin'; export * from './reader'; -export { EmbeddedDocsRouter, Router } from './Router'; +export { EmbeddedDocsRouter, Router, isTechDocsAvailable } from './Router'; diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index f74b639fa8..74af68a523 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-todo-backend +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@0.6.5 + - @backstage/catalog-client@0.4.0 + - @backstage/catalog-model@0.9.3 + - @backstage/backend-common@0.9.4 + - @backstage/config@0.1.10 + ## 0.1.11 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index 23da38e948..b09562c894 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.11", + "version": "0.1.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -25,12 +25,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.0", - "@backstage/catalog-client": "^0.3.16", - "@backstage/catalog-model": "^0.9.0", - "@backstage/config": "^0.1.8", + "@backstage/backend-common": "^0.9.4", + "@backstage/catalog-client": "^0.4.0", + "@backstage/catalog-model": "^0.9.3", + "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", - "@backstage/integration": "^0.6.2", + "@backstage/integration": "^0.6.5", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", "express": "^4.17.1", @@ -40,7 +40,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.7.9", + "@backstage/cli": "^0.7.13", "@types/supertest": "^2.0.8", "msw": "^0.29.0", "supertest": "^6.1.3" diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index 1204835afe..f62716c1d9 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-todo +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + - @backstage/plugin-catalog-react@0.5.0 + - @backstage/catalog-model@0.9.3 + ## 0.1.10 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index e7db92f26e..2e0aa27d22 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.1.10", + "version": "0.1.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,11 +27,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.2", - "@backstage/core-components": "^0.4.2", + "@backstage/catalog-model": "^0.9.3", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.2", - "@backstage/plugin-catalog-react": "^0.4.6", + "@backstage/plugin-catalog-react": "^0.5.0", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 5bba04be17..8afdbb9475 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-user-settings +## 0.3.6 + +### Patch Changes + +- 038b9763d1: Add search to FeatureFlags +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.3.5 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 706a70e0a2..bf03f000fe 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -43,10 +43,10 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", "@backstage/core-plugin-api": "^0.1.8", - "@backstage/dev-utils": "^0.2.9", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx index 5a3f077920..a92ef43d32 100644 --- a/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx +++ b/plugins/user-settings/src/components/FeatureFlags/UserSettingsFeatureFlags.tsx @@ -15,7 +15,13 @@ */ import React, { useCallback, useState } from 'react'; -import { List } from '@material-ui/core'; +import { + List, + TextField, + IconButton, + Grid, + Typography, +} from '@material-ui/core'; import { EmptyFlags } from './EmptyFlags'; import { FlagItem } from './FeatureFlagsItem'; @@ -25,6 +31,7 @@ import { useApi, } from '@backstage/core-plugin-api'; import { InfoCard } from '@backstage/core-components'; +import ClearIcon from '@material-ui/icons/Clear'; export const UserSettingsFeatureFlags = () => { const featureFlagsApi = useApi(featureFlagsApiRef); @@ -35,6 +42,8 @@ export const UserSettingsFeatureFlags = () => { ); const [state, setState] = useState>(initialFlagState); + const [filterInput, setFilterInput] = useState(''); + const inputRef = React.useRef(); const toggleFlag = useCallback( (flagName: string) => { @@ -59,10 +68,60 @@ export const UserSettingsFeatureFlags = () => { return ; } + const clearFilterInput = () => { + setFilterInput(''); + inputRef?.current?.focus(); + }; + + let filteredFeatureFlags = Array.from(featureFlags); + + const filterInputParts = filterInput + .split(/\s/) + .map(part => part.trim().toLowerCase()); + + filterInputParts.forEach( + part => + (filteredFeatureFlags = filteredFeatureFlags.filter(featureFlag => + featureFlag.name.toLowerCase().includes(part), + )), + ); + + const Header = () => ( + + + Feature Flags + + {featureFlags.length >= 10 && ( + + ref && ref.focus()} + InputProps={{ + ...(filterInput.length && { + endAdornment: ( + + + + ), + }), + }} + onChange={e => setFilterInput(e.target.value)} + value={filterInput} + /> + + )} + + ); + return ( - + }> - {featureFlags.map(featureFlag => { + {filteredFeatureFlags.map(featureFlag => { const enabled = Boolean(state[featureFlag.name]); return ( diff --git a/plugins/welcome/CHANGELOG.md b/plugins/welcome/CHANGELOG.md index c19c69f877..75a5be76d9 100644 --- a/plugins/welcome/CHANGELOG.md +++ b/plugins/welcome/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-welcome +## 0.3.5 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.3.4 ### Patch Changes diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 325ca7bf07..09619bffb4 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-welcome", "description": "An old Backstage plugin that provides a welcome page", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -31,7 +31,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", @@ -43,9 +43,9 @@ "react-use": "^17.2.4" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index 4fc709be49..0ee8f4bcef 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-xcmetrics +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.5.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 0a58945c8b..d3df9b6718 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,7 +21,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.4.2", + "@backstage/core-components": "^0.5.0", "@backstage/core-plugin-api": "^0.1.8", "@backstage/errors": "^0.1.1", "@backstage/theme": "^0.2.10", @@ -35,15 +35,15 @@ "recharts": "^1.8.5" }, "devDependencies": { - "@backstage/cli": "^0.7.12", - "@backstage/core-app-api": "^0.1.13", - "@backstage/dev-utils": "^0.2.9", + "@backstage/cli": "^0.7.13", + "@backstage/core-app-api": "^0.1.14", + "@backstage/dev-utils": "^0.2.10", "@backstage/test-utils": "^0.1.17", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", - "@types/luxon": "^1.27.0", + "@types/luxon": "^2.0.4", "@types/node": "^14.14.32", "cross-fetch": "^3.0.6", "msw": "^0.29.0" diff --git a/plugins/xcmetrics/src/components/BuildList/BuildList.tsx b/plugins/xcmetrics/src/components/BuildList/BuildList.tsx index f9e8bfdd98..5a6e1bd322 100644 --- a/plugins/xcmetrics/src/components/BuildList/BuildList.tsx +++ b/plugins/xcmetrics/src/components/BuildList/BuildList.tsx @@ -39,7 +39,7 @@ export const BuildList = () => { const tableRef = useRef(); const initialFilters = { - from: DateTime.now().minus({ year: 1 }).toISODate(), + from: DateTime.now().minus({ years: 1 }).toISODate(), to: DateTime.now().toISODate(), }; diff --git a/yarn.lock b/yarn.lock index 67cf37ff51..29c234baed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2193,14 +2193,6 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs3@^7.9.6": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.4.tgz#403139af262b9a6e8f9ba04a6fdcebf8de692bf1" - integrity sha512-lWcAqKeB624/twtTc3w6w/2o9RqJPaNBhPGK6DKLSiwuVWC7WFkypWyNg+CpZoyJH0jVzv1uMtXZ/5/lQOLtCg== - dependencies: - core-js-pure "^3.16.0" - regenerator-runtime "^0.13.4" - "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.14.8" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" @@ -2302,6 +2294,16 @@ "@babel/helper-validator-identifier" "^7.14.9" to-fast-properties "^2.0.0" +"@backstage/catalog-client@^0.3.18": + version "0.3.19" + resolved "https://registry.npmjs.org/@backstage/catalog-client/-/catalog-client-0.3.19.tgz#109b0fde1cc2d9a306635f3775fc8f6174172b14" + integrity sha512-0ydcC/1ISNgR8SggOUnMNNHtsRwrKN5GX+AXgTVdZk4qpz1MqG1+fzrNld9V7KVvXdsbGDZAl/b5a7/FJEpCqg== + dependencies: + "@backstage/catalog-model" "^0.9.2" + "@backstage/config" "^0.1.9" + "@backstage/errors" "^0.1.2" + cross-fetch "^3.0.6" + "@backstage/core-api@^0.2.23": version "0.2.23" resolved "https://registry.npmjs.org/@backstage/core-api/-/core-api-0.2.23.tgz#c0ec13407ff7c78d376eb18e9d8e1490d54d995b" @@ -2365,6 +2367,51 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/core-components@^0.4.2": + version "0.4.2" + resolved "https://registry.npmjs.org/@backstage/core-components/-/core-components-0.4.2.tgz#a80dceabdfb43b794b43d6436919688ff4da15ca" + integrity sha512-WACyEaD+k2v2MEeJ52Kyj/Rw3a2agIqW9FXnYP+xCAWia8kw1cBX8eMXRblsEV8xwheTQzyd1lzeIpdkyGhhQg== + dependencies: + "@backstage/config" "^0.1.9" + "@backstage/core-plugin-api" "^0.1.8" + "@backstage/errors" "^0.1.2" + "@backstage/theme" "^0.2.10" + "@material-table/core" "^3.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + "@testing-library/react-hooks" "^3.4.2" + "@types/dagre" "^0.7.44" + "@types/prop-types" "^15.7.3" + "@types/react" "*" + "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" + classnames "^2.2.6" + clsx "^1.1.0" + d3-selection "^2.0.0" + d3-shape "^2.0.0" + d3-zoom "^2.0.0" + dagre "^0.8.5" + immer "^9.0.1" + lodash "^4.17.15" + pluralize "^8.0.0" + prop-types "^15.7.2" + qs "^6.9.4" + rc-progress "^3.0.0" + react "^16.12.0" + react-dom "^16.12.0" + react-helmet "6.1.0" + react-hook-form "^7.12.2" + react-markdown "^5.0.2" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-sparklines "^1.7.0" + react-syntax-highlighter "^15.4.3" + react-text-truncate "^0.16.0" + react-use "^17.2.4" + remark-gfm "^1.0.0" + zen-observable "^0.8.15" + "@backstage/core@*": version "0.7.14" resolved "https://registry.npmjs.org/@backstage/core/-/core-0.7.14.tgz#863844fe40bb6a29bcc2d297e42055633b0e886f" @@ -2411,6 +2458,30 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/plugin-catalog-react@^0.4.0": + version "0.4.6" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.4.6.tgz#f33f3cd734f110d3c547f7eae4e25d14806ec796" + integrity sha512-DQO9/oE4mWty6cyUCHiDbf9jFOEPtS0HB+DHiXHHzI7nCC/89p05RiKZpo3ctd5p5oENAYmWO4G7eVJkf5NjiA== + dependencies: + "@backstage/catalog-client" "^0.3.18" + "@backstage/catalog-model" "^0.9.1" + "@backstage/core-app-api" "^0.1.13" + "@backstage/core-components" "^0.4.2" + "@backstage/core-plugin-api" "^0.1.8" + "@backstage/integration" "^0.6.4" + "@backstage/version-bridge" "^0.1.0" + "@material-ui/core" "^4.12.2" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.57" + "@types/react" "*" + jwt-decode "^3.1.0" + lodash "^4.17.15" + qs "^6.9.4" + react "^16.13.1" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^17.2.4" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -5014,19 +5085,19 @@ resolved "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-2.0.0.tgz#1108b9ea661ca6c81e4a8bfa63a09eb27d5bc2db" integrity sha512-35cfQ4YWlnZnmZKmIxlGPUPLtbkF8lr/A/1Sk1eC0ddLMwQN06dOuLc+dI3YLQS+T+MoNt3DIQ0NynwgKPilig== -"@octokit/webhooks-types@4.4.0": - version "4.4.0" - resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-4.4.0.tgz#0985964b72d577221574cca614770e0743fe0830" - integrity sha512-gK9L2pWLZ1CWWlf9zwzFBQeA3dgyWdRe6gqAedR5T7adFZpoHg4De6GeXIFySV796ooQtKhW4eFfQLFQF5ydAA== +"@octokit/webhooks-types@4.7.0": + version "4.7.0" + resolved "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-4.7.0.tgz#0d86cb32a6502da41d2a33b243102f96012bddbb" + integrity sha512-LWTPedtJtcdA1+k+iERtCr7UnO/d+Cr00/vRPhjASTHz2G/ZtSAnDNjDv5c7X2R7OJsPE6eFUMT7frwjjuAbZA== "@octokit/webhooks@^9.14.1": - version "9.14.1" - resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.14.1.tgz#d1b8fcc4c1c18ee46ec30cef9e84043b57c8d2dd" - integrity sha512-INsqtHKQJys5NbuE71kq6uR8BMSSkZ2L9dLanAc1jylGroQ80SW1TYXLuJlcYNakNrHCCe4c5qvNKfR/KJXrJA== + version "9.15.0" + resolved "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-9.15.0.tgz#ed43660c1911755747b5c1b2847081eb96d8e0a0" + integrity sha512-IFHXmTT4rVz8TbH5oH8qmJkM9UJLo75mmaMydKdn0j/ffLuTQ/+j6+Jl5dyboUo6AtUwaklFacszViVCKHME6A== dependencies: "@octokit/request-error" "^2.0.2" "@octokit/webhooks-methods" "^2.0.0" - "@octokit/webhooks-types" "4.4.0" + "@octokit/webhooks-types" "4.7.0" aggregate-error "^3.1.0" "@open-draft/until@^1.0.3": @@ -6533,11 +6604,16 @@ resolved "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.4.tgz#fc42ebde7d9cfa7c5f2668f117449b02348e41fd" integrity sha512-Xwy8o12ak+iYgFr/KCVaVK5Sy+jFMiiPAID3+ObvMlBzy26XQJw5xu+a6rlHsrJENXj/AwJOGsJpVohUjAzSKQ== -"@types/cors@2.8.10", "@types/cors@^2.8.6": +"@types/cors@2.8.10": version "2.8.10" resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4" integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ== +"@types/cors@^2.8.6": + version "2.8.12" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080" + integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw== + "@types/cssnano@*": version "4.0.0" resolved "https://registry.npmjs.org/@types/cssnano/-/cssnano-4.0.0.tgz#f1bb29d6d0813861a3d87e02946b2988d0110d4e" @@ -7014,10 +7090,10 @@ resolved "https://registry.npmjs.org/@types/lunr/-/lunr-2.3.3.tgz#ec985618fd2712c010f8edab4f1ae7784ad7c583" integrity sha512-09sXZZVsB3Ib41U0fC+O1O+4UOZT1bl/e+/QubPxpqDWHNEchvx/DEb1KJMOwq6K3MTNzZFoNSzVdR++o1DVnw== -"@types/luxon@^1.25.0", "@types/luxon@^1.27.0": - version "1.27.0" - resolved "https://registry.npmjs.org/@types/luxon/-/luxon-1.27.0.tgz#1e3b5a7f8ca6944349c43498b4442b742c71ab0b" - integrity sha512-rr2lNXsErnA/ARtgFn46NtQjUa66cuwZYeo/2K7oqqxhJErhXgHBPyNKCo+pfOC3L7HFwtao8ebViiU9h4iAxA== +"@types/luxon@^2.0.4": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/luxon/-/luxon-2.0.4.tgz#f7b5a86ccd843c0ccaddfaedd9ee1081bc1cde3b" + integrity sha512-l3xuhmyF2kBldy15SeY6d6HbK2BacEcSK1qTF1ISPtPHr29JH0C1fndz9ExXLKpGl0J6pZi+dGp1i5xesMt60Q== "@types/markdown-to-jsx@^6.11.3": version "6.11.3" @@ -7075,9 +7151,9 @@ "@types/node" "*" "@types/morgan@^1.9.0": - version "1.9.2" - resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.2.tgz#450f958a4d3fb0694a3ba012b09c8106f9a2885e" - integrity sha512-edtGMEdit146JwwIeyQeHHg9yID4WSolQPxpEorHmN3KuytuCHyn2ELNr5Uxy8SerniFbbkmgKMrGM933am5BQ== + version "1.9.3" + resolved "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540" + integrity sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q== dependencies: "@types/node" "*" @@ -7327,9 +7403,9 @@ "@types/react" "*" "@types/react@*", "@types/react@^16.9": - version "16.14.8" - resolved "https://registry.npmjs.org/@types/react/-/react-16.14.8.tgz#4aee3ab004cb98451917c9b7ada3c7d7e52db3fe" - integrity sha512-QN0/Qhmx+l4moe7WJuTxNiTsjBwlBGHqKGvInSQCBdo7Qio0VtOqwsC0Wq7q3PbJlB0cR4Y4CVo1OOe6BOsOmA== + version "16.14.15" + resolved "https://registry.npmjs.org/@types/react/-/react-16.14.15.tgz#95d8fa3148050e94bcdc5751447921adbe19f9e6" + integrity sha512-jOxlBV9RGZhphdeqJTCv35VZOkjY+XIEY2owwSk84BNDdDv2xS6Csj6fhi+B/q30SR9Tz8lDNt/F2Z5RF3TrRg== dependencies: "@types/prop-types" "*" "@types/scheduler" "*" @@ -7545,12 +7621,11 @@ "@types/estree" "*" "@types/terser-webpack-plugin@^5.0.4": - version "5.0.4" - resolved "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-5.0.4.tgz#852949e1d124e6c895cfa8cb17eb6265e4fdfe59" - integrity sha512-1iyfZpMNNA/h/Q8uBpwuVhxKfKQHc98PD9NaCTrg22nj6d8aUvT79KBMtRLmR43v1PtCB0r1/gWGdNXrrMEK7A== + version "5.2.0" + resolved "https://registry.npmjs.org/@types/terser-webpack-plugin/-/terser-webpack-plugin-5.2.0.tgz#6aaec696593216917f9f03266bed222f8253483b" + integrity sha512-iHDR2pRfFjGyDqCALX2tgUgFtGoQf2AJhKpC2XD1IMBQVJF2bny6WChGRDKj9eaZJl4F2RmvBhxJNtVPj7aTRw== dependencies: - terser "^5.3.8" - webpack "^5.1.0" + terser-webpack-plugin "*" "@types/testing-library__jest-dom@^5.9.1": version "5.9.5" @@ -10206,13 +10281,15 @@ canvas@^2.6.1: simple-get "^3.0.3" canvg@^3.0.6: - version "3.0.7" - resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.7.tgz#e45b87a64116af906917f7cad57d370ea372d682" - integrity sha512-4sq6iL5Q4VOXS3PL1BapiXIZItpxYyANVzsAKpTPS5oq4u3SKbGfUcbZh2gdLCQ3jWpG/y5wRkMlBBAJhXeiZA== + version "3.0.8" + resolved "https://registry.npmjs.org/canvg/-/canvg-3.0.8.tgz#9125a4b7033d2f237402241b192587385f4589e6" + integrity sha512-9De5heHfVRgCkln3CGEeSJMviN5U2RyxL4uutYoe8HxI60BjH2XnT2ZUHIp+ZaAZNTUd5Asqfut8WEEdANqfAg== dependencies: - "@babel/runtime-corejs3" "^7.9.6" + "@babel/runtime" "^7.12.5" "@types/raf" "^3.4.0" + core-js "^3.8.3" raf "^3.4.1" + regenerator-runtime "^0.13.7" rgbcolor "^1.0.1" stackblur-canvas "^2.0.0" svg-pathdata "^5.0.5" @@ -10538,14 +10615,6 @@ cli-table@^0.3.1: dependencies: colors "1.0.3" -cli-truncate@2.1.0, cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - cli-truncate@^0.2.1: version "0.2.1" resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" @@ -10554,6 +10623,14 @@ cli-truncate@^0.2.1: slice-ansi "0.0.4" string-width "^1.0.1" +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cli-width@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" @@ -10790,6 +10867,11 @@ colorette@^1.2.1, colorette@^1.2.2: resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== +colorette@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + colors@1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" @@ -11258,10 +11340,10 @@ core-js@^3.0.4, core-js@^3.6.5, core-js@^3.8.2: resolved "https://registry.npmjs.org/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044" integrity sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw== -core-js@^3.6.0: - version "3.17.3" - resolved "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz#8e8bd20e91df9951e903cabe91f9af4a0895bc1e" - integrity sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw== +core-js@^3.6.0, core-js@^3.8.3: + version "3.18.1" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.18.1.tgz#289d4be2ce0085d40fc1244c0b1a54c00454622f" + integrity sha512-vJlUi/7YdlCZeL6fXvWNaLUPh/id12WXj3MbkMw5uOyF0PfWPBNOCNbs53YqgrvtujLNlt9JQpruyIKkUZ+PKA== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -12621,7 +12703,12 @@ domhandler@^4.0.0: dependencies: domelementtype "^2.1.0" -dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.9: +dompurify@^2.0.12: + version "2.3.3" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" + integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== + +dompurify@^2.1.1, dompurify@^2.2.9: version "2.3.1" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz#a47059ca21fd1212d3c8f71fdea6943b8bfbdf6a" integrity sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw== @@ -12922,7 +13009,7 @@ enhanced-resolve@^5.8.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.0, enquirer@^2.3.5: +enquirer@^2.3.0, enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -13508,7 +13595,7 @@ exec-sh@^0.3.2: resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== -execa@4.1.0, execa@^4.0.0, execa@^4.0.1: +execa@4.1.0, execa@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -13866,12 +13953,7 @@ fast-redact@^2.0.0: resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-2.1.0.tgz#dfe3c1ca69367fb226f110aa4ec10ec85462ffdf" integrity sha512-0LkHpTLyadJavq9sRzzyqIoMZemWli77K2/MGOkafrR64B9ItrvZ9aT+jluvNDsv0YEHjSNhlMBtbokuoqii4A== -fast-safe-stringify@^2.0.4, fast-safe-stringify@^2.0.7: - version "2.0.7" - resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" - integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== - -fast-safe-stringify@^2.0.6: +fast-safe-stringify@^2.0.6, fast-safe-stringify@^2.0.7: version "2.0.8" resolved "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.8.tgz#dc2af48c46cf712b683e849b2bbd446b32de936f" integrity sha512-lXatBjf3WPjmWD6DpIZxkeSsCOwqI0maYMpgDlx8g4U2qi4lbjA9oH/HD2a87G+KfsUmo5WbJFmqBZlPxtptag== @@ -13979,7 +14061,7 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" -figures@^3.0.0, figures@^3.2.0: +figures@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== @@ -17457,10 +17539,10 @@ jest-worker@^26.5.0, jest-worker@^26.6.2: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^27.0.2: - version "27.0.6" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed" - integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA== +jest-worker@^27.0.6: + version "27.2.2" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.2.tgz#636deeae8068abbf2b34b4eb9505f8d4e5bd625c" + integrity sha512-aG1xq9KgWB2CPC8YdMIlI8uZgga2LFNcGbHJxO8ctfXAydSaThR4EewKQGg3tBOC+kS3vhPGgymsBdi9VINjPw== dependencies: "@types/node" "*" merge-stream "^2.0.0" @@ -18360,22 +18442,21 @@ linkify-it@^3.0.1: dependencies: uc.micro "^1.0.1" -lint-staged@^10.1.0: - version "10.2.11" - resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-10.2.11.tgz#713c80877f2dc8b609b05bc59020234e766c9720" - integrity sha512-LRRrSogzbixYaZItE2APaS4l2eJMjjf5MbclRZpLJtcQJShcvUzKXsNeZgsLIZ0H0+fg2tL4B59fU9wHIHtFIA== +lint-staged@^11.1.2: + version "11.1.2" + resolved "https://registry.npmjs.org/lint-staged/-/lint-staged-11.1.2.tgz#4dd78782ae43ee6ebf2969cad9af67a46b33cd90" + integrity sha512-6lYpNoA9wGqkL6Hew/4n1H6lRqF3qCsujVT0Oq5Z4hiSAM7S6NksPJ3gnr7A7R52xCtiZMcEUNNQ6d6X5Bvh9w== dependencies: - chalk "^4.0.0" - cli-truncate "2.1.0" - commander "^5.1.0" - cosmiconfig "^6.0.0" - debug "^4.1.1" - dedent "^0.7.0" - enquirer "^2.3.5" - execa "^4.0.1" - listr2 "^2.1.0" - log-symbols "^4.0.0" - micromatch "^4.0.2" + chalk "^4.1.1" + cli-truncate "^2.1.0" + commander "^7.2.0" + cosmiconfig "^7.0.0" + debug "^4.3.1" + enquirer "^2.3.6" + execa "^5.0.0" + listr2 "^3.8.2" + log-symbols "^4.1.0" + micromatch "^4.0.4" normalize-path "^3.0.0" please-upgrade-node "^3.2.0" string-argv "0.3.1" @@ -18415,19 +18496,18 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr2@^2.1.0: - version "2.1.3" - resolved "https://registry.npmjs.org/listr2/-/listr2-2.1.3.tgz#f527e197de12ad8c488c566921fa2da34cbc67f6" - integrity sha512-6oy3QhrZAlJGrG8oPcRp1hix1zUpb5AvyvZ5je979HCyf48tIj3Hn1TG5+rfyhz30t7HfySH/OIaVbwrI2kruA== +listr2@^3.8.2: + version "3.12.1" + resolved "https://registry.npmjs.org/listr2/-/listr2-3.12.1.tgz#75e515b86c66b60baf253542cc0dced6b60fedaf" + integrity sha512-oB1DlXlCzGPbvWhqYBZUQEPJKqsmebQWofXG6Mpbe3uIvoNl8mctBEojyF13ZyqwQ91clCWXpwsWp+t98K4FOQ== dependencies: - chalk "^4.0.0" cli-truncate "^2.1.0" - figures "^3.2.0" - indent-string "^4.0.0" + colorette "^1.4.0" log-update "^4.0.0" p-map "^4.0.0" - rxjs "^6.5.5" + rxjs "^6.6.7" through "^2.3.8" + wrap-ansi "^7.0.0" listr@^0.14.3: version "0.14.3" @@ -18793,14 +18873,14 @@ log-update@^4.0.0: wrap-ansi "^6.2.0" logform@^2.1.1, logform@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz#40f036d19161fc76b68ab50fdc7fe495544492f2" - integrity sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg== + version "2.3.0" + resolved "https://registry.npmjs.org/logform/-/logform-2.3.0.tgz#a3997a05985de2ebd325ae0d166dffc9c6fe6b57" + integrity sha512-graeoWUH2knKbGthMtuG1EfaSPMZFZBIrhuJHhkS5ZseFBrc7DupCzihOQAzsK/qIKPQaPJ/lFQFctILUY5ARQ== dependencies: colors "^1.2.1" - fast-safe-stringify "^2.0.4" fecha "^4.2.0" ms "^2.1.1" + safe-stable-stringify "^1.1.0" triple-beam "^1.3.0" loglevel@^1.6.7: @@ -19497,13 +19577,13 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0, micromatch@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== dependencies: braces "^3.0.1" - picomatch "^2.0.5" + picomatch "^2.2.3" miller-rabin@^4.0.0: version "4.0.1" @@ -19839,12 +19919,12 @@ ms@2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -ms@2.1.2, ms@^2.0.0, ms@^2.1.1: +ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.3: +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -20734,9 +20814,9 @@ openapi-sampler@^1.0.0-beta.15: json-pointer "^0.6.0" openid-client@^4.1.1, openid-client@^4.2.1: - version "4.7.3" - resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.7.3.tgz#ea4f6f9ff6203dfbbe3d9bb4415d5dce751b0a70" - integrity sha512-YLwZQLSjo3gdSVxw/G25ddoRp9oCpXkREZXssmenlejZQPsnTq+yQtFUcBmC7u3VVkx+gwqXZF7X0CtAAJrRRg== + version "4.9.0" + resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.9.0.tgz#bdfc9194435316df419f759ce177635146b43074" + integrity sha512-ThBbvRUUZwxUKBVK2UpDNIZ3eJkvtqWI8s5Dm+naV+gJdL+yRhT+8ywqct1gy5uL+xVS5+A/nhFcpJIisH2x6Q== dependencies: aggregate-error "^3.1.0" got "^11.8.0" @@ -21620,11 +21700,16 @@ pgtools@^0.3.0: pg-connection-string "^0.1.3" yargs "^5.0.0" -picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2: version "2.2.2" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -22935,9 +23020,9 @@ react-hook-form@^6.15.4: integrity sha512-prq82ofMbnRyj5wqDe8hsTRcdR25jQ+B8KtCS7BLCzjFHAwNuCjRwzPuP4eYLsEBjEIeYd6try+pdLdw0kPkpg== react-hook-form@^7.12.2, react-hook-form@^7.13.0: - version "7.15.3" - resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.15.3.tgz#7d222dcfd43137be127dca2843149f09bd25d4b1" - integrity sha512-z30aZoEHkWE8oZvad4OcYSBI0kQua/T5sFGH9tB2HfeykFnP/pGXNap8lDio4/U1yPj2ffpbvRIvqKd/6jjBVA== + version "7.16.1" + resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.16.1.tgz#669046df378a71949e5cf8a2398cbe20d5cb27bc" + integrity sha512-kcLDmSmlyLUFx2UU5bG/o4+3NeK753fhKodJa8gkplXohGkpAq0/p+TR24OWjZmkEc3ES7ppC5v5d6KUk+fJTA== react-hot-loader@^4.12.21: version "4.13.0" @@ -24259,7 +24344,7 @@ run-script-webpack-plugin@^0.0.11: resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.0.11.tgz#04c510bed06b907fa2285e75feece71a25691171" integrity sha512-QmuBhiqBPmhQLpO5vMBHVTAGyoPBnrCM5gQ3IzgieiImBXiBbXcIv4kysCT1gilFNFxQk22oKQfiIhWbT/zXCw== -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.5, rxjs@^6.6.0: +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0: version "6.6.2" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== @@ -24273,7 +24358,7 @@ rxjs@^6.6.3: dependencies: tslib "^1.9.0" -rxjs@^6.6.6: +rxjs@^6.6.6, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -24326,6 +24411,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-stable-stringify@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a" + integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -24407,7 +24497,7 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" -schema-utils@^3.1.0: +schema-utils@^3.1.0, schema-utils@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== @@ -24984,6 +25074,14 @@ source-map-support@^0.5.16, source-map-support@^0.5.17, source-map-support@^0.5. buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@~0.5.20: + version "0.5.20" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" + integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" @@ -26119,6 +26217,18 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" +terser-webpack-plugin@*, terser-webpack-plugin@^5.1.3: + version "5.2.4" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.4.tgz#ad1be7639b1cbe3ea49fab995cbe7224b31747a1" + integrity sha512-E2CkNMN+1cho04YpdANyRrn8CyN4yMy+WdFKZIySFZrGXZxJwJP6PMNGGc/Mcr6qygQHUUqRxnAPmi0M9f00XA== + dependencies: + jest-worker "^27.0.6" + p-limit "^3.1.0" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + terser "^5.7.2" + terser-webpack-plugin@^1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c" @@ -26149,18 +26259,6 @@ terser-webpack-plugin@^4.2.3: terser "^5.3.4" webpack-sources "^1.4.3" -terser-webpack-plugin@^5.1.3: - version "5.1.4" - resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1" - integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA== - dependencies: - jest-worker "^27.0.2" - p-limit "^3.1.0" - schema-utils "^3.0.0" - serialize-javascript "^6.0.0" - source-map "^0.6.1" - terser "^5.7.0" - terser@^4.1.2, terser@^4.6.3: version "4.8.0" resolved "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" @@ -26170,7 +26268,7 @@ terser@^4.1.2, terser@^4.6.3: source-map "~0.6.1" source-map-support "~0.5.12" -terser@^5.3.4, terser@^5.3.8, terser@^5.7.0: +terser@^5.3.4: version "5.7.1" resolved "https://registry.npmjs.org/terser/-/terser-5.7.1.tgz#2dc7a61009b66bb638305cb2a824763b116bf784" integrity sha512-b3e+d5JbHAe/JSjwsC3Zn55wsBIM7AsHLjKxT31kGCldgbpFePaFo+PiddtO6uwRZWRw7sPXmAN8dTW61xmnSg== @@ -26179,6 +26277,15 @@ terser@^5.3.4, terser@^5.3.8, terser@^5.7.0: source-map "~0.7.2" source-map-support "~0.5.19" +terser@^5.7.2: + version "5.9.0" + resolved "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz#47d6e629a522963240f2b55fcaa3c99083d2c351" + integrity sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ== + dependencies: + commander "^2.20.0" + source-map "~0.7.2" + source-map-support "~0.5.20" + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -27796,36 +27903,6 @@ webpack@^5, webpack@^5.48.0: watchpack "^2.2.0" webpack-sources "^3.2.0" -webpack@^5.1.0: - version "5.49.0" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.49.0.tgz#e250362b781a9fb614ba0a97ed67c66b9c5310cd" - integrity sha512-XarsANVf28A7Q3KPxSnX80EkCcuOer5hTOEJWJNvbskOZ+EK3pobHarGHceyUZMxpsTHBHhlV7hiQyLZzGosYw== - dependencies: - "@types/eslint-scope" "^3.7.0" - "@types/estree" "^0.0.50" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.4.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.8.0" - es-module-lexer "^0.7.1" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.4" - json-parse-better-errors "^1.0.2" - loader-runner "^4.2.0" - mime-types "^2.1.27" - neo-async "^2.6.2" - schema-utils "^3.1.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.2.0" - webpack-sources "^3.2.0" - websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9"