diff --git a/.changeset/angry-dolphins-camp.md b/.changeset/angry-dolphins-camp.md new file mode 100644 index 0000000000..3c598f4a66 --- /dev/null +++ b/.changeset/angry-dolphins-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Add access restrictions to the JWKS external access method config schema diff --git a/.changeset/big-eagles-grab.md b/.changeset/big-eagles-grab.md new file mode 100644 index 0000000000..9ece8f64df --- /dev/null +++ b/.changeset/big-eagles-grab.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for github:environment:create action and improve its test cases diff --git a/.changeset/blue-pumas-cheer.md b/.changeset/blue-pumas-cheer.md new file mode 100644 index 0000000000..f6cbf180ff --- /dev/null +++ b/.changeset/blue-pumas-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Added the Kubernetes plugin to `create-app` diff --git a/.changeset/breezy-cats-kiss.md b/.changeset/breezy-cats-kiss.md new file mode 100644 index 0000000000..900016c1e8 --- /dev/null +++ b/.changeset/breezy-cats-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Fixing issue with extension blueprints `inputs` merging. diff --git a/.changeset/breezy-jeans-tie.md b/.changeset/breezy-jeans-tie.md new file mode 100644 index 0000000000..4601389a4c --- /dev/null +++ b/.changeset/breezy-jeans-tie.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch +'@backstage/plugin-auth-backend-module-atlassian-provider': patch +'@backstage/plugin-auth-backend-module-bitbucket-provider': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-onelogin-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch +'@backstage/plugin-auth-backend-module-github-provider': patch +'@backstage/plugin-auth-backend-module-gitlab-provider': patch +'@backstage/plugin-auth-backend-module-google-provider': patch +'@backstage/plugin-auth-backend-module-oauth2-provider': patch +'@backstage/plugin-auth-backend-module-oidc-provider': patch +'@backstage/plugin-auth-backend-module-okta-provider': patch +--- + +Add `signIn` to authentication provider configuration schema diff --git a/.changeset/breezy-rings-fly.md b/.changeset/breezy-rings-fly.md new file mode 100644 index 0000000000..c3cc1c143d --- /dev/null +++ b/.changeset/breezy-rings-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes diff --git a/.changeset/bright-donkeys-buy.md b/.changeset/bright-donkeys-buy.md new file mode 100644 index 0000000000..ded16ccc2c --- /dev/null +++ b/.changeset/bright-donkeys-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. diff --git a/.changeset/bright-trainers-brake.md b/.changeset/bright-trainers-brake.md new file mode 100644 index 0000000000..2299d83a32 --- /dev/null +++ b/.changeset/bright-trainers-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add frontend-dynamic-container role to eslint config factory diff --git a/.changeset/calm-crabs-drop.md b/.changeset/calm-crabs-drop.md new file mode 100644 index 0000000000..09bed7c091 --- /dev/null +++ b/.changeset/calm-crabs-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. diff --git a/.changeset/chilly-days-peel.md b/.changeset/chilly-days-peel.md new file mode 100644 index 0000000000..d82344d364 --- /dev/null +++ b/.changeset/chilly-days-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. diff --git a/.changeset/chilly-trains-sleep.md b/.changeset/chilly-trains-sleep.md new file mode 100644 index 0000000000..cde7600c8b --- /dev/null +++ b/.changeset/chilly-trains-sleep.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-events-backend-module-aws-sqs': patch +'@backstage/plugin-catalog-backend-module-aws': patch +'@backstage/backend-common': patch +--- + +Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. diff --git a/.changeset/clever-pans-brake.md b/.changeset/clever-pans-brake.md new file mode 100644 index 0000000000..1f49c7a842 --- /dev/null +++ b/.changeset/clever-pans-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Added new APIs for testing extensions diff --git a/.changeset/clever-waves-judge.md b/.changeset/clever-waves-judge.md deleted file mode 100644 index 585ad01bbe..0000000000 --- a/.changeset/clever-waves-judge.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-techdocs-react': patch -'@backstage/plugin-techdocs': patch ---- - -Fixed a bug with the TechDocsReaderPageProvider not re-rendering when setShadowDom is called, meaning that the useShadowDom hooks were inconsistent. This issue caused the TextSize addon changes not to reapply during navigation. diff --git a/.changeset/cool-insects-remember.md b/.changeset/cool-insects-remember.md new file mode 100644 index 0000000000..ab33015377 --- /dev/null +++ b/.changeset/cool-insects-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-notifications': patch +--- + +Add examples for notification:send scaffolder action & improve related tests diff --git a/.changeset/cool-schools-vanish.md b/.changeset/cool-schools-vanish.md new file mode 100644 index 0000000000..9b2ded5248 --- /dev/null +++ b/.changeset/cool-schools-vanish.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixing loading of additional config files with new `ConfigSources` diff --git a/.changeset/create-app-1722413762.md b/.changeset/create-app-1722413762.md new file mode 100644 index 0000000000..b50d431d4b --- /dev/null +++ b/.changeset/create-app-1722413762.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Bumped create-app version. diff --git a/.changeset/cuddly-zebras-crash.md b/.changeset/cuddly-zebras-crash.md new file mode 100644 index 0000000000..473ac4053f --- /dev/null +++ b/.changeset/cuddly-zebras-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Use ES2022 in CLI bundler diff --git a/.changeset/curvy-pillows-joke.md b/.changeset/curvy-pillows-joke.md new file mode 100644 index 0000000000..25966a51c0 --- /dev/null +++ b/.changeset/curvy-pillows-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. diff --git a/.changeset/cyan-shrimps-push.md b/.changeset/cyan-shrimps-push.md new file mode 100644 index 0000000000..cb326b4da3 --- /dev/null +++ b/.changeset/cyan-shrimps-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. diff --git a/.changeset/dry-squids-tap.md b/.changeset/dry-squids-tap.md new file mode 100644 index 0000000000..0a95332eed --- /dev/null +++ b/.changeset/dry-squids-tap.md @@ -0,0 +1,27 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Introduce a new way to encapsulate extension kinds that replaces the extension creator pattern with `createExtensionBlueprint` + +This allows the creation of extension instances with the following pattern: + +```tsx +// create the extension blueprint which is used to create instances +const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + factory(params: { text: string }) { + return [coreExtensionData.reactElement(

{params.text}

)]; + }, +}); + +// create an instance of the extension blueprint with params +const testExtension = EntityCardBlueprint.make({ + name: 'foo', + params: { + text: 'Hello World', + }, +}); +``` diff --git a/.changeset/dull-ghosts-double.md b/.changeset/dull-ghosts-double.md new file mode 100644 index 0000000000..0bc5a09791 --- /dev/null +++ b/.changeset/dull-ghosts-double.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-home': patch +--- + +Updated alpha definitions of extension data references. diff --git a/.changeset/early-trees-dance.md b/.changeset/early-trees-dance.md new file mode 100644 index 0000000000..55b2c08f89 --- /dev/null +++ b/.changeset/early-trees-dance.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +The `ExtensionBoundary` now by default infers whether it's routable from whether it outputs a route path. diff --git a/.changeset/eighty-emus-leave.md b/.changeset/eighty-emus-leave.md new file mode 100644 index 0000000000..8e02c7cf94 --- /dev/null +++ b/.changeset/eighty-emus-leave.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +'@backstage/plugin-techdocs': patch +--- + +Use annotation constants from new techdocs-common package. diff --git a/.changeset/eighty-jokes-deny.md b/.changeset/eighty-jokes-deny.md new file mode 100644 index 0000000000..58944252f4 --- /dev/null +++ b/.changeset/eighty-jokes-deny.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +Add support for status filtering in scaffolder tasks endpoint diff --git a/.changeset/eighty-mirrors-flow.md b/.changeset/eighty-mirrors-flow.md new file mode 100644 index 0000000000..57e978a78c --- /dev/null +++ b/.changeset/eighty-mirrors-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Move the `Link` component to the `RoutedTabs` instead of the `HeaderTabs` component diff --git a/.changeset/fair-hairs-mix.md b/.changeset/fair-hairs-mix.md new file mode 100644 index 0000000000..6c06c7bf72 --- /dev/null +++ b/.changeset/fair-hairs-mix.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-permission-common': patch +--- + +Add the MetadataResponse type from @backstage/plugin-permission-node, since this +type might be used in frontend code. diff --git a/.changeset/fair-pillows-know.md b/.changeset/fair-pillows-know.md deleted file mode 100644 index f0e5034393..0000000000 --- a/.changeset/fair-pillows-know.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch -'@backstage/plugin-scaffolder-backend-module-gitlab': patch -'@backstage/plugin-scaffolder-backend-module-rails': patch ---- - -Updated configuration instructions diff --git a/.changeset/fair-pumas-hang.md b/.changeset/fair-pumas-hang.md new file mode 100644 index 0000000000..156779b76a --- /dev/null +++ b/.changeset/fair-pumas-hang.md @@ -0,0 +1,43 @@ +--- +'@backstage/plugin-scaffolder-backend-module-confluence-to-markdown': patch +'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch +'@backstage/plugin-search-backend-module-stack-overflow-collator': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +'@backstage/plugin-catalog-backend-module-bitbucket-server': patch +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +'@backstage/plugin-auth-backend-module-aws-alb-provider': patch +'@backstage/plugin-scaffolder-backend-module-bitbucket': patch +'@backstage/plugin-scaffolder-backend-module-gerrit': patch +'@backstage/plugin-scaffolder-backend-module-sentry': patch +'@backstage/plugin-catalog-backend-module-puppetdb': patch +'@backstage/plugin-scaffolder-backend-module-gitea': patch +'@backstage/plugin-catalog-backend-module-msgraph': patch +'@backstage/plugin-search-backend-module-techdocs': patch +'@backstage/plugin-catalog-backend-module-gerrit': patch +'@backstage/plugin-catalog-backend-module-github': patch +'@backstage/plugin-catalog-backend-module-gitlab': patch +'@backstage/plugin-search-backend-module-explore': patch +'@backstage/plugin-catalog-backend-module-azure': patch +'@backstage/plugin-notifications-backend': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-notifications-node': patch +'@backstage/plugin-permission-backend': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +'@backstage/plugin-devtools-backend': patch +'@backstage/plugin-techdocs-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-kubernetes-node': patch +'@backstage/plugin-signals-backend': patch +'@backstage/config-loader': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-auth-backend': patch +'@backstage/create-app': patch +'@backstage/plugin-app-backend': patch +'@backstage/plugin-auth-node': patch +'@backstage/cli': patch +--- + +Make sure node-fetch is version 2.7.0 or greater diff --git a/.changeset/fast-bulldogs-relax.md b/.changeset/fast-bulldogs-relax.md new file mode 100644 index 0000000000..65895345e9 --- /dev/null +++ b/.changeset/fast-bulldogs-relax.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-cloud': patch +--- + +Added autocompletion support for resource `branches` diff --git a/.changeset/few-wasps-hug.md b/.changeset/few-wasps-hug.md new file mode 100644 index 0000000000..66cf34fc0b --- /dev/null +++ b/.changeset/few-wasps-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Correct `EntityDisplayName`'s icon alignment with the text. diff --git a/.changeset/flat-papayas-push.md b/.changeset/flat-papayas-push.md new file mode 100644 index 0000000000..f9e20613fd --- /dev/null +++ b/.changeset/flat-papayas-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-azure': patch +--- + +Added examples for publish:azure action and updated its test cases diff --git a/.changeset/flat-plums-grow.md b/.changeset/flat-plums-grow.md new file mode 100644 index 0000000000..7057216786 --- /dev/null +++ b/.changeset/flat-plums-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in `FormProps` diff --git a/.changeset/forty-ties-agree.md b/.changeset/forty-ties-agree.md new file mode 100644 index 0000000000..caa45841a2 --- /dev/null +++ b/.changeset/forty-ties-agree.md @@ -0,0 +1,18 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added a new `IconBundleBlueprint` that lets you create icon bundle extensions that can be installed in an App in order to override or add new app icons. + +```tsx +import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + +const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, +}); +``` diff --git a/.changeset/forty-ties-disagree.md b/.changeset/forty-ties-disagree.md new file mode 100644 index 0000000000..214097b35d --- /dev/null +++ b/.changeset/forty-ties-disagree.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Support icon overriding with the new `IconBundleBlueprint` API. diff --git a/.changeset/friendly-cherries-applaud.md b/.changeset/friendly-cherries-applaud.md new file mode 100644 index 0000000000..c0160858f2 --- /dev/null +++ b/.changeset/friendly-cherries-applaud.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +The MetadataResponse type has been moved to @backstage/plugin-permission-common +to match the recent move of MetadataResponseSerializedRule, and should be +imported from there going forward. To avoid an immediate breaking change, this +type is still re-exported from this package, but is marked as deprecated and +will be removed in a future release. diff --git a/.changeset/friendly-chicken-cry.md b/.changeset/friendly-chicken-cry.md new file mode 100644 index 0000000000..3eadca8777 --- /dev/null +++ b/.changeset/friendly-chicken-cry.md @@ -0,0 +1,8 @@ +--- +'@backstage/frontend-test-utils': patch +'@backstage/frontend-app-api': patch +'@backstage/core-compat-api': patch +'@backstage/plugin-app-visualizer': patch +--- + +Updated usage of `useRouteRef`, which can now always return `undefined`. diff --git a/.changeset/friendly-feet-refuse.md b/.changeset/friendly-feet-refuse.md new file mode 100644 index 0000000000..677d21d48a --- /dev/null +++ b/.changeset/friendly-feet-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-rails': patch +--- + +Add examples for fetch:rails scaffolder action & improve related tests diff --git a/.changeset/funny-bears-sort.md b/.changeset/funny-bears-sort.md new file mode 100644 index 0000000000..a3dad0f489 --- /dev/null +++ b/.changeset/funny-bears-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Switched the `process` polyfill to use `require.resolve` for greater compatability. diff --git a/.changeset/funny-wolves-learn.md b/.changeset/funny-wolves-learn.md new file mode 100644 index 0000000000..2010ccd68b --- /dev/null +++ b/.changeset/funny-wolves-learn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:issues:create examples diff --git a/.changeset/gentle-avocados-obey.md b/.changeset/gentle-avocados-obey.md deleted file mode 100644 index 80bf5f89c7..0000000000 --- a/.changeset/gentle-avocados-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Only bootstrap global-agent if it's actually being used diff --git a/.changeset/gentle-dryers-smile.md b/.changeset/gentle-dryers-smile.md new file mode 100644 index 0000000000..b061461da1 --- /dev/null +++ b/.changeset/gentle-dryers-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +The `createHealthRouter` utility that allows you to create a health check router is now exported via `@backstage/backend-defaults/rootHttpRouter`. diff --git a/.changeset/good-steaks-report.md b/.changeset/good-steaks-report.md new file mode 100644 index 0000000000..5f9ce25470 --- /dev/null +++ b/.changeset/good-steaks-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitea': patch +--- + +Added test cases for publish:gitea examples diff --git a/.changeset/green-planets-reflect.md b/.changeset/green-planets-reflect.md new file mode 100644 index 0000000000..513f0480a5 --- /dev/null +++ b/.changeset/green-planets-reflect.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-test-utils': patch +'@backstage/frontend-app-api': patch +--- + +Added support for v2 extensions, which declare their inputs and outputs without using a data map. diff --git a/.changeset/grumpy-owls-suffer.md b/.changeset/grumpy-owls-suffer.md new file mode 100644 index 0000000000..977055eb33 --- /dev/null +++ b/.changeset/grumpy-owls-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Allow the `createGitlabProjectVariableAction` to use oauth tokens diff --git a/.changeset/grumpy-wolves-hang.md b/.changeset/grumpy-wolves-hang.md deleted file mode 100644 index 8ab3740e74..0000000000 --- a/.changeset/grumpy-wolves-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fix issue with CLI that was preventing upgrading from 1.28 diff --git a/.changeset/healthy-fireants-jump.md b/.changeset/healthy-fireants-jump.md new file mode 100644 index 0000000000..7a11015b80 --- /dev/null +++ b/.changeset/healthy-fireants-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Allow for more granular control of TechDocsReaderPage styling. Theme overrides can now be provided to TechDocs without affecting the theme in other areas of Backstage. diff --git a/.changeset/healthy-timers-divide.md b/.changeset/healthy-timers-divide.md new file mode 100644 index 0000000000..0b9d700723 --- /dev/null +++ b/.changeset/healthy-timers-divide.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-search-backend': patch +'@backstage/plugin-search-common': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +Fix package metadata diff --git a/.changeset/heavy-numbers-love.md b/.changeset/heavy-numbers-love.md new file mode 100644 index 0000000000..c18d144636 --- /dev/null +++ b/.changeset/heavy-numbers-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Updated default backend plugin to use `RootConfigService` instead of `Config`. This also removes the dependency on `@backstage/config` as it's no longer used. diff --git a/.changeset/hip-fishes-guess.md b/.changeset/hip-fishes-guess.md new file mode 100644 index 0000000000..909448c673 --- /dev/null +++ b/.changeset/hip-fishes-guess.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-defaults': patch +--- + +The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. diff --git a/.changeset/hip-hairs-exist.md b/.changeset/hip-hairs-exist.md new file mode 100644 index 0000000000..d8cb939dfb --- /dev/null +++ b/.changeset/hip-hairs-exist.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-node': patch +--- + +Added setAllowedLocationTypes while introducing a new extension point called CatalogLocationsExtensionPoint diff --git a/.changeset/itchy-experts-tie.md b/.changeset/itchy-experts-tie.md new file mode 100644 index 0000000000..c5bf99ecaf --- /dev/null +++ b/.changeset/itchy-experts-tie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for github:repo:create action and improved test cases diff --git a/.changeset/khaki-lamps-peel.md b/.changeset/khaki-lamps-peel.md new file mode 100644 index 0000000000..82a2cbc9de --- /dev/null +++ b/.changeset/khaki-lamps-peel.md @@ -0,0 +1,26 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + +A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + +Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + +```ts +const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, +}); +``` diff --git a/.changeset/late-games-protect.md b/.changeset/late-games-protect.md new file mode 100644 index 0000000000..584780f836 --- /dev/null +++ b/.changeset/late-games-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +Add scaffolder option to display object items in separate rows on review page diff --git a/.changeset/light-pianos-exercise.md b/.changeset/light-pianos-exercise.md new file mode 100644 index 0000000000..4bf161eb06 --- /dev/null +++ b/.changeset/light-pianos-exercise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Memoize entity graph nodes when applying an `entityFilter` to prevent repeated redraws diff --git a/.changeset/little-bulldogs-guess.md b/.changeset/little-bulldogs-guess.md new file mode 100644 index 0000000000..3e646a033f --- /dev/null +++ b/.changeset/little-bulldogs-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The experimental module federation build now has the ability to force the use of development versions of `react` and `react-dom` by setting the `FORCE_REACT_DEVELOPMENT` flag. diff --git a/.changeset/little-suns-fly.md b/.changeset/little-suns-fly.md new file mode 100644 index 0000000000..887df4be6d --- /dev/null +++ b/.changeset/little-suns-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Added option to ingest groups based on their group membership in Azure Entra ID diff --git a/.changeset/mean-apricots-perform.md b/.changeset/mean-apricots-perform.md new file mode 100644 index 0000000000..c93514cb7f --- /dev/null +++ b/.changeset/mean-apricots-perform.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Adds support for Backstage's new frontend system, available via the `/alpha` sub-path export. diff --git a/.changeset/metal-planes-nail.md b/.changeset/metal-planes-nail.md new file mode 100644 index 0000000000..2cfa12a435 --- /dev/null +++ b/.changeset/metal-planes-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. diff --git a/.changeset/metal-rice-call.md b/.changeset/metal-rice-call.md new file mode 100644 index 0000000000..46e30dd26f --- /dev/null +++ b/.changeset/metal-rice-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Introduces the HasSubdomainsCard component that displays the subdomains of a given domain diff --git a/.changeset/mighty-apricots-taste.md b/.changeset/mighty-apricots-taste.md new file mode 100644 index 0000000000..80b70020cb --- /dev/null +++ b/.changeset/mighty-apricots-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. diff --git a/.changeset/mighty-dolls-retire.md b/.changeset/mighty-dolls-retire.md new file mode 100644 index 0000000000..b31c443a38 --- /dev/null +++ b/.changeset/mighty-dolls-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Internal refactor to remove unnecessary `routable` prop in the implementation of the `createEntityContentExtension` alpha export. diff --git a/.changeset/mighty-geckos-kiss.md b/.changeset/mighty-geckos-kiss.md new file mode 100644 index 0000000000..0761d8b257 --- /dev/null +++ b/.changeset/mighty-geckos-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Update `patchMkdocsYmlPrebuild` to modify `repo_url` and `edit_uri` independently. diff --git a/.changeset/modern-parrots-protect.md b/.changeset/modern-parrots-protect.md new file mode 100644 index 0000000000..bb4df2c248 --- /dev/null +++ b/.changeset/modern-parrots-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +support `ajv-errors` for scaffolder validation to allow for customizing the error messages diff --git a/.changeset/modern-poems-mate.md b/.changeset/modern-poems-mate.md new file mode 100644 index 0000000000..06926daac9 --- /dev/null +++ b/.changeset/modern-poems-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend': patch +--- + +Allow using notifications without users in the catalog diff --git a/.changeset/moody-llamas-breathe.md b/.changeset/moody-llamas-breathe.md deleted file mode 100644 index 418cf7480e..0000000000 --- a/.changeset/moody-llamas-breathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Add dependencies that are needed by cross-imports from backend-defaults diff --git a/.changeset/neat-bears-divide.md b/.changeset/neat-bears-divide.md new file mode 100644 index 0000000000..a94b17f74d --- /dev/null +++ b/.changeset/neat-bears-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +The `env` option of `ConfigSources.default` now correctly allows undefined members. diff --git a/.changeset/neat-gifts-join.md b/.changeset/neat-gifts-join.md new file mode 100644 index 0000000000..cd140c4692 --- /dev/null +++ b/.changeset/neat-gifts-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-devtools-backend': patch +--- + +Removed unused code for lockfile analysis. diff --git a/.changeset/neat-socks-cheer.md b/.changeset/neat-socks-cheer.md new file mode 100644 index 0000000000..ffc76bec3f --- /dev/null +++ b/.changeset/neat-socks-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-node': minor +--- + +**BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. diff --git a/.changeset/new-scissors-try.md b/.changeset/new-scissors-try.md new file mode 100644 index 0000000000..a9e9a45ce7 --- /dev/null +++ b/.changeset/new-scissors-try.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-microsoft-provider': patch +--- + +Updated the Microsoft authenticator to accurately define required scopes, but to also omit the required and additional scopes when requesting resource scopes. diff --git a/.changeset/nice-peas-retire.md b/.changeset/nice-peas-retire.md new file mode 100644 index 0000000000..7b7b13706d --- /dev/null +++ b/.changeset/nice-peas-retire.md @@ -0,0 +1,6 @@ +--- +'@backstage/integration': minor +'@backstage/backend-defaults': patch +--- + +Updated `GitlabUrlReader.readUrl` and `GitlabUrlReader.readTree` to accept a user-provided token, supporting both bearer and private tokens. diff --git a/.changeset/nine-cherries-decide.md b/.changeset/nine-cherries-decide.md new file mode 100644 index 0000000000..b20c24aa85 --- /dev/null +++ b/.changeset/nine-cherries-decide.md @@ -0,0 +1,14 @@ +--- +'@backstage/backend-common': patch +--- + +The remaining exports in the package have now been deprecated: + +- `cacheToPluginCacheManager` +- `createLegacyAuthAdapters` +- `LegacyCreateRouter` +- `legacyPlugin` +- `loggerToWinstonLogger` +- `makeLegacyPlugin` + +Users of these export should fully [migrate to the new backend system](https://backstage.io/docs/backend-system/building-backends/migrating). diff --git a/.changeset/nine-glasses-nail.md b/.changeset/nine-glasses-nail.md new file mode 100644 index 0000000000..138acce3dd --- /dev/null +++ b/.changeset/nine-glasses-nail.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-pg': patch +--- + +Removing `@backstage/backend-app-api` dependency diff --git a/.changeset/nine-seahorses-relate.md b/.changeset/nine-seahorses-relate.md new file mode 100644 index 0000000000..9a5bb43823 --- /dev/null +++ b/.changeset/nine-seahorses-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-common': patch +--- + +Add the MetadataResponseSerializedRule type from @backstage/plugin-permission-node, since this type might be used in frontend code. diff --git a/.changeset/ninety-icons-smile.md b/.changeset/ninety-icons-smile.md new file mode 100644 index 0000000000..f137879706 --- /dev/null +++ b/.changeset/ninety-icons-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Remove usage of deprecated functionality from @backstage/config-loader diff --git a/.changeset/odd-books-share.md b/.changeset/odd-books-share.md new file mode 100644 index 0000000000..858a2564ad --- /dev/null +++ b/.changeset/odd-books-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Preserve default `allowedLocationTypes` when `setAllowedLocationTypes()` of `CatalogLocationsExtensionPoint` is not called. diff --git a/.changeset/old-tools-smell.md b/.changeset/old-tools-smell.md new file mode 100644 index 0000000000..4dbd65e779 --- /dev/null +++ b/.changeset/old-tools-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Add support for accessing extensions definitions provided by a plugin via `plugin.getExtension(...)`. For this to work the extensions must be defined using the v2 format, typically using an extension blueprint. diff --git a/.changeset/olive-books-sort.md b/.changeset/olive-books-sort.md new file mode 100644 index 0000000000..d7bfd925ce --- /dev/null +++ b/.changeset/olive-books-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +The MetadataResponseSerializedRule type has been moved to @backstage/plugin-permission-common, and should be imported from there going forward. To avoid an immediate breaking change, this type is still re-exported from this package, but is marked as deprecated and will be removed in a future release. diff --git a/.changeset/orange-gifts-protect.md b/.changeset/orange-gifts-protect.md new file mode 100644 index 0000000000..1de9e2a6ba --- /dev/null +++ b/.changeset/orange-gifts-protect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-bitbucket-server': patch +--- + +Added examples for publish:bitbucketServer action and improve its test cases diff --git a/.changeset/pink-gorillas-brake.md b/.changeset/pink-gorillas-brake.md new file mode 100644 index 0000000000..188884f13c --- /dev/null +++ b/.changeset/pink-gorillas-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fix extra divider displayed on user list picker component diff --git a/.changeset/plenty-tools-exist.md b/.changeset/plenty-tools-exist.md new file mode 100644 index 0000000000..0ce22a74f2 --- /dev/null +++ b/.changeset/plenty-tools-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:issue:edit examples diff --git a/.changeset/popular-panthers-hear.md b/.changeset/popular-panthers-hear.md new file mode 100644 index 0000000000..c31586ea82 --- /dev/null +++ b/.changeset/popular-panthers-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs redirect feature now includes a notification to the user before they are redirected. diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..4699170212 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,312 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.99", + "@backstage/app-defaults": "1.5.8", + "example-app-next": "0.0.13", + "app-next-example-plugin": "0.0.13", + "example-backend": "0.0.28", + "@backstage/backend-app-api": "0.8.0", + "@backstage/backend-common": "0.23.3", + "@backstage/backend-defaults": "0.4.0", + "@backstage/backend-dev-utils": "0.1.4", + "@backstage/backend-dynamic-feature-service": "0.2.15", + "example-backend-legacy": "0.2.100", + "@backstage/backend-openapi-utils": "0.1.15", + "@backstage/backend-plugin-api": "0.7.0", + "@backstage/backend-tasks": "0.5.27", + "@backstage/backend-test-utils": "0.4.4", + "@backstage/catalog-client": "1.6.5", + "@backstage/catalog-model": "1.5.0", + "@backstage/cli": "0.26.11", + "@backstage/cli-common": "0.1.14", + "@backstage/cli-node": "0.2.7", + "@backstage/codemods": "0.1.49", + "@backstage/config": "1.2.0", + "@backstage/config-loader": "1.8.1", + "@backstage/core-app-api": "1.14.0", + "@backstage/core-compat-api": "0.2.7", + "@backstage/core-components": "0.14.9", + "@backstage/core-plugin-api": "1.9.3", + "@backstage/create-app": "0.5.17", + "@backstage/dev-utils": "1.0.35", + "e2e-test": "0.2.18", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.4", + "@backstage/eslint-plugin": "0.1.8", + "@backstage/frontend-app-api": "0.7.3", + "@backstage/frontend-plugin-api": "0.6.7", + "@backstage/frontend-test-utils": "0.1.10", + "@backstage/integration": "1.13.0", + "@backstage/integration-aws-node": "0.1.12", + "@backstage/integration-react": "1.1.29", + "@backstage/release-manifests": "0.0.11", + "@backstage/repo-tools": "0.9.4", + "@techdocs/cli": "1.8.16", + "techdocs-cli-embedded-app": "0.2.98", + "@backstage/test-utils": "1.5.8", + "@backstage/theme": "0.5.6", + "@backstage/types": "1.1.1", + "@backstage/version-bridge": "1.0.8", + "yarn-plugin-backstage": "0.0.1", + "@backstage/plugin-api-docs": "0.11.7", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.7", + "@backstage/plugin-app-backend": "0.3.71", + "@backstage/plugin-app-node": "0.1.22", + "@backstage/plugin-app-visualizer": "0.1.8", + "@backstage/plugin-auth-backend": "0.22.9", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.2.3", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.1.14", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.1.5", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.2.17", + "@backstage/plugin-auth-backend-module-github-provider": "0.1.19", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.1.19", + "@backstage/plugin-auth-backend-module-google-provider": "0.1.19", + "@backstage/plugin-auth-backend-module-guest-provider": "0.1.8", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.1.17", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.2.3", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.1.15", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.2.3", + "@backstage/plugin-auth-backend-module-okta-provider": "0.0.15", + "@backstage/plugin-auth-backend-module-onelogin-provider": "0.1.3", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.1.16", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.2.3", + "@backstage/plugin-auth-node": "0.4.17", + "@backstage/plugin-auth-react": "0.1.4", + "@backstage/plugin-bitbucket-cloud-common": "0.2.21", + "@backstage/plugin-catalog": "1.21.1", + "@backstage/plugin-catalog-backend": "1.24.0", + "@backstage/plugin-catalog-backend-module-aws": "0.3.17", + "@backstage/plugin-catalog-backend-module-azure": "0.1.42", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.2.5", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.2.9", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.1.36", + "@backstage/plugin-catalog-backend-module-gcp": "0.1.23", + "@backstage/plugin-catalog-backend-module-gerrit": "0.1.39", + "@backstage/plugin-catalog-backend-module-github": "0.6.5", + "@backstage/plugin-catalog-backend-module-github-org": "0.1.17", + "@backstage/plugin-catalog-backend-module-gitlab": "0.3.21", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.0.5", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.4.27", + "@backstage/plugin-catalog-backend-module-ldap": "0.7.0", + "@backstage/plugin-catalog-backend-module-logs": "0.0.1", + "@backstage/plugin-catalog-backend-module-msgraph": "0.5.30", + "@backstage/plugin-catalog-backend-module-openapi": "0.1.40", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.1.28", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.1.20", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.4.9", + "@backstage/plugin-catalog-common": "1.0.25", + "@backstage/plugin-catalog-graph": "0.4.7", + "@backstage/plugin-catalog-import": "0.12.1", + "@backstage/plugin-catalog-node": "1.12.4", + "@backstage/plugin-catalog-react": "1.12.2", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.6", + "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.3", + "@backstage/plugin-config-schema": "0.1.57", + "@backstage/plugin-devtools": "0.1.16", + "@backstage/plugin-devtools-backend": "0.3.8", + "@backstage/plugin-devtools-common": "0.1.11", + "@backstage/plugin-events-backend": "0.3.9", + "@backstage/plugin-events-backend-module-aws-sqs": "0.3.8", + "@backstage/plugin-events-backend-module-azure": "0.2.8", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.8", + "@backstage/plugin-events-backend-module-gerrit": "0.2.8", + "@backstage/plugin-events-backend-module-github": "0.2.8", + "@backstage/plugin-events-backend-module-gitlab": "0.2.8", + "@backstage/plugin-events-backend-test-utils": "0.1.32", + "@backstage/plugin-events-node": "0.3.8", + "@internal/plugin-todo-list": "1.0.29", + "@internal/plugin-todo-list-backend": "1.0.29", + "@internal/plugin-todo-list-common": "1.0.20", + "@backstage/plugin-home": "0.7.7", + "@backstage/plugin-home-react": "0.1.15", + "@backstage/plugin-kubernetes": "0.11.12", + "@backstage/plugin-kubernetes-backend": "0.18.3", + "@backstage/plugin-kubernetes-cluster": "0.0.13", + "@backstage/plugin-kubernetes-common": "0.8.1", + "@backstage/plugin-kubernetes-node": "0.1.16", + "@backstage/plugin-kubernetes-react": "0.4.1", + "@backstage/plugin-notifications": "0.2.3", + "@backstage/plugin-notifications-backend": "0.3.3", + "@backstage/plugin-notifications-backend-module-email": "0.1.3", + "@backstage/plugin-notifications-common": "0.0.5", + "@backstage/plugin-notifications-node": "0.2.3", + "@backstage/plugin-org": "0.6.27", + "@backstage/plugin-org-react": "0.1.26", + "@backstage/plugin-permission-backend": "0.5.46", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.1.19", + "@backstage/plugin-permission-common": "0.8.0", + "@backstage/plugin-permission-node": "0.8.0", + "@backstage/plugin-permission-react": "0.4.24", + "@backstage/plugin-proxy-backend": "0.5.3", + "@backstage/plugin-scaffolder": "1.23.0", + "@backstage/plugin-scaffolder-backend": "1.23.0", + "@backstage/plugin-scaffolder-backend-module-azure": "0.1.14", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.2.12", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.1.12", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.1.12", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.2.23", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.2.46", + "@backstage/plugin-scaffolder-backend-module-gcp": "0.1.0", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.1.14", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.1.12", + "@backstage/plugin-scaffolder-backend-module-github": "0.4.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.4.4", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.0.5", + "@backstage/plugin-scaffolder-backend-module-rails": "0.4.39", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.1.30", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.3.6", + "@backstage/plugin-scaffolder-common": "1.5.4", + "@backstage/plugin-scaffolder-node": "0.4.8", + "@backstage/plugin-scaffolder-node-test-utils": "0.1.9", + "@backstage/plugin-scaffolder-react": "1.10.0", + "@backstage/plugin-search": "1.4.14", + "@backstage/plugin-search-backend": "1.5.14", + "@backstage/plugin-search-backend-module-catalog": "0.1.28", + "@backstage/plugin-search-backend-module-elasticsearch": "1.5.3", + "@backstage/plugin-search-backend-module-explore": "0.1.28", + "@backstage/plugin-search-backend-module-pg": "0.5.32", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.1.15", + "@backstage/plugin-search-backend-module-techdocs": "0.1.27", + "@backstage/plugin-search-backend-node": "1.2.27", + "@backstage/plugin-search-common": "1.2.13", + "@backstage/plugin-search-react": "1.7.13", + "@backstage/plugin-signals": "0.0.8", + "@backstage/plugin-signals-backend": "0.1.8", + "@backstage/plugin-signals-node": "0.1.8", + "@backstage/plugin-signals-react": "0.0.4", + "@backstage/plugin-techdocs": "1.10.7", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.35", + "@backstage/plugin-techdocs-backend": "1.10.9", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.12", + "@backstage/plugin-techdocs-node": "1.12.8", + "@backstage/plugin-techdocs-react": "1.2.6", + "@backstage/plugin-user-settings": "0.8.9", + "@backstage/plugin-user-settings-backend": "0.2.21", + "@backstage/plugin-user-settings-common": "0.0.1", + "@backstage/plugin-techdocs-common": "0.0.0" + }, + "changesets": [ + "angry-dolphins-camp", + "big-eagles-grab", + "blue-pumas-cheer", + "breezy-jeans-tie", + "breezy-rings-fly", + "bright-donkeys-buy", + "bright-trainers-brake", + "calm-crabs-drop", + "chilly-days-peel", + "chilly-trains-sleep", + "clever-pans-brake", + "cool-insects-remember", + "cool-schools-vanish", + "create-app-1722413762", + "cuddly-zebras-crash", + "curvy-pillows-joke", + "cyan-shrimps-push", + "dry-squids-tap", + "dull-ghosts-double", + "early-trees-dance", + "eighty-emus-leave", + "eighty-jokes-deny", + "eighty-mirrors-flow", + "fair-hairs-mix", + "fair-pumas-hang", + "fast-bulldogs-relax", + "few-wasps-hug", + "flat-plums-grow", + "forty-ties-agree", + "forty-ties-disagree", + "friendly-cherries-applaud", + "friendly-chicken-cry", + "friendly-feet-refuse", + "gentle-dryers-smile", + "good-steaks-report", + "green-planets-reflect", + "grumpy-owls-suffer", + "healthy-timers-divide", + "heavy-numbers-love", + "hip-fishes-guess", + "hip-hairs-exist", + "khaki-lamps-peel", + "late-games-protect", + "light-pianos-exercise", + "little-bulldogs-guess", + "little-suns-fly", + "mean-apricots-perform", + "metal-planes-nail", + "metal-rice-call", + "mighty-apricots-taste", + "mighty-dolls-retire", + "mighty-geckos-kiss", + "modern-parrots-protect", + "neat-bears-divide", + "neat-gifts-join", + "neat-socks-cheer", + "new-scissors-try", + "nice-peas-retire", + "nine-cherries-decide", + "nine-glasses-nail", + "nine-seahorses-relate", + "ninety-icons-smile", + "odd-books-share", + "olive-books-sort", + "orange-gifts-protect", + "pink-gorillas-brake", + "plenty-tools-exist", + "purple-carrots-crash", + "quick-roses-juggle", + "rare-foxes-compete", + "red-radios-promise", + "renovate-147ac48", + "renovate-f04beb1", + "rich-mugs-dress", + "selfish-bees-think", + "seven-eggs-admire", + "shaggy-dodos-applaud", + "shy-games-poke", + "shy-waves-share", + "silly-candles-sin", + "silly-cycles-tan", + "six-rats-kick", + "slow-ducks-rush", + "slow-ligers-drum", + "slow-toes-jog", + "small-bottles-cough", + "small-ears-poke", + "small-spoons-shout", + "smooth-countries-relate", + "soft-gorillas-refuse", + "spicy-lies-listen", + "spicy-planets-provide", + "spotty-planets-accept", + "strange-papayas-beg", + "strong-otters-compete", + "stupid-dots-relate", + "sweet-oranges-buy", + "swift-kings-sparkle", + "tall-snakes-fix", + "tasty-ads-rescue", + "thick-hotels-know", + "thirty-adults-grab", + "thirty-paws-hope", + "tiny-oranges-pretend", + "tough-goats-hang", + "tough-lies-repair", + "tricky-ducks-juggle", + "two-emus-work", + "violet-jokes-wave", + "warm-monkeys-marry", + "wicked-bobcats-teach", + "wild-eggs-exist", + "wise-spiders-walk", + "witty-bears-behave", + "witty-geese-battle", + "witty-timers-marry", + "young-games-visit", + "young-peaches-shake" + ] +} diff --git a/.changeset/purple-carrots-crash.md b/.changeset/purple-carrots-crash.md new file mode 100644 index 0000000000..62f90677b8 --- /dev/null +++ b/.changeset/purple-carrots-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. diff --git a/.changeset/quick-roses-juggle.md b/.changeset/quick-roses-juggle.md new file mode 100644 index 0000000000..981bd8591a --- /dev/null +++ b/.changeset/quick-roses-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-common': minor +--- + +Initial release of the techdocs-common package. diff --git a/.changeset/rare-foxes-compete.md b/.changeset/rare-foxes-compete.md new file mode 100644 index 0000000000..64dd9b557e --- /dev/null +++ b/.changeset/rare-foxes-compete.md @@ -0,0 +1,57 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Extensions have been changed to be declared with an array of inputs and outputs, rather than a map of named data refs. This change was made to reduce confusion around the role of the input and output names, as well as enable more powerful APIs for overriding extensions. + +An extension that was previously declared like this: + +```tsx +const exampleExtension = createExtension({ + name: 'example', + inputs: { + items: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( +
+ Example + {inputs.items.map(item => { + return
{item.output.element}
; + })} +
+ ), + }; + }, +}); +``` + +Should be migrated to the following: + +```tsx +const exampleExtension = createExtension({ + name: 'example', + inputs: { + items: createExtensionInput([coreExtensionData.reactElement]), + }, + output: [coreExtensionData.reactElement], + factory({ inputs }) { + return [ + coreExtensionData.reactElement( +
+ Example + {inputs.items.map(item => { + return
{item.get(coreExtensionData.reactElement)}
; + })} +
, + ), + ]; + }, +}); +``` diff --git a/.changeset/rare-peas-dream.md b/.changeset/rare-peas-dream.md deleted file mode 100644 index 443f8c566d..0000000000 --- a/.changeset/rare-peas-dream.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-defaults': patch ---- - -Repack the package to fix issues with typescript with named exports diff --git a/.changeset/real-lizards-sit.md b/.changeset/real-lizards-sit.md new file mode 100644 index 0000000000..747f041508 --- /dev/null +++ b/.changeset/real-lizards-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fix undefined in the title of Scaffolder Runs on the page load diff --git a/.changeset/red-radios-promise.md b/.changeset/red-radios-promise.md new file mode 100644 index 0000000000..ea876c3480 --- /dev/null +++ b/.changeset/red-radios-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +Adds new optional `excludeRepos` configuration option to the Gitlab catalog provider. diff --git a/.changeset/renovate-147ac48.md b/.changeset/renovate-147ac48.md new file mode 100644 index 0000000000..257255dff1 --- /dev/null +++ b/.changeset/renovate-147ac48.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated dependency `@graphiql/react` to `^0.23.0`. diff --git a/.changeset/renovate-f04beb1.md b/.changeset/renovate-f04beb1.md new file mode 100644 index 0000000000..12d24ccc53 --- /dev/null +++ b/.changeset/renovate-f04beb1.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-explore': patch +--- + +Updated dependency `@backstage-community/plugin-explore-common` to `^0.0.4`. diff --git a/.changeset/rich-bears-march.md b/.changeset/rich-bears-march.md deleted file mode 100644 index ec4a4a9404..0000000000 --- a/.changeset/rich-bears-march.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@techdocs/cli': patch ---- - -Import discovery from backend-defaults instead of backend-common diff --git a/.changeset/rich-mugs-dress.md b/.changeset/rich-mugs-dress.md new file mode 100644 index 0000000000..5008e33a70 --- /dev/null +++ b/.changeset/rich-mugs-dress.md @@ -0,0 +1,7 @@ +--- +'@backstage/config-loader': patch +--- + +Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and +`ConfigSources.defaultForTargets`, which results in omission of a ConfigSource +for the default app-config.yaml configuration file if it's not present. diff --git a/.changeset/selfish-bees-think.md b/.changeset/selfish-bees-think.md new file mode 100644 index 0000000000..15598ea551 --- /dev/null +++ b/.changeset/selfish-bees-think.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Fixed the routing of the new health check service, the health endpoints should now properly be available at `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness`. diff --git a/.changeset/seven-days-film.md b/.changeset/seven-days-film.md new file mode 100644 index 0000000000..9d757b0611 --- /dev/null +++ b/.changeset/seven-days-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Fixed a type issue where plugin and modules depending on multiton services would not receive the correct type. diff --git a/.changeset/seven-eggs-admire.md b/.changeset/seven-eggs-admire.md new file mode 100644 index 0000000000..d68b38e2d7 --- /dev/null +++ b/.changeset/seven-eggs-admire.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated dockerfile and `app-config.production.yaml` to make it easier to get started with example data diff --git a/.changeset/shaggy-dodos-applaud.md b/.changeset/shaggy-dodos-applaud.md new file mode 100644 index 0000000000..b7f968aa39 --- /dev/null +++ b/.changeset/shaggy-dodos-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Switched the target from `'ES2022'` to `'es2022'` for better compatibility with older versions of `swc`. diff --git a/.changeset/shaggy-mugs-return.md b/.changeset/shaggy-mugs-return.md new file mode 100644 index 0000000000..6af5d1d18f --- /dev/null +++ b/.changeset/shaggy-mugs-return.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Update configuration schema to match actual behavior diff --git a/.changeset/shy-games-poke.md b/.changeset/shy-games-poke.md new file mode 100644 index 0000000000..1d2abff39c --- /dev/null +++ b/.changeset/shy-games-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Add support for stream transport for debugging purposes diff --git a/.changeset/shy-waves-share.md b/.changeset/shy-waves-share.md new file mode 100644 index 0000000000..861590fd01 --- /dev/null +++ b/.changeset/shy-waves-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. diff --git a/.changeset/silent-experts-move.md b/.changeset/silent-experts-move.md deleted file mode 100644 index 06441a62c6..0000000000 --- a/.changeset/silent-experts-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Subpath export `package.json` should be of a unique name to avoid typescript resolution issues diff --git a/.changeset/silly-candles-sin.md b/.changeset/silly-candles-sin.md new file mode 100644 index 0000000000..b1d55113fd --- /dev/null +++ b/.changeset/silly-candles-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. diff --git a/.changeset/silly-cycles-tan.md b/.changeset/silly-cycles-tan.md new file mode 100644 index 0000000000..517ce526ea --- /dev/null +++ b/.changeset/silly-cycles-tan.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. +This was already documented and supported by the plugin. diff --git a/.changeset/silly-scissors-turn.md b/.changeset/silly-scissors-turn.md new file mode 100644 index 0000000000..985a6fb4fd --- /dev/null +++ b/.changeset/silly-scissors-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Included permission config and enabled it out of the box diff --git a/.changeset/silver-pillows-begin.md b/.changeset/silver-pillows-begin.md new file mode 100644 index 0000000000..8fe1ccc230 --- /dev/null +++ b/.changeset/silver-pillows-begin.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Internal type refactor. diff --git a/.changeset/six-mails-smell.md b/.changeset/six-mails-smell.md new file mode 100644 index 0000000000..3845c72626 --- /dev/null +++ b/.changeset/six-mails-smell.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Support merging of `inputs` in extension blueprints, but stop merging `output`. In addition, the original factory in extension blueprints now returns a data container that both provides access to the returned data, but can also be forwarded as output. diff --git a/.changeset/six-rats-kick.md b/.changeset/six-rats-kick.md new file mode 100644 index 0000000000..23958a2502 --- /dev/null +++ b/.changeset/six-rats-kick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:projectAccessToken:create example diff --git a/.changeset/slow-ducks-rush.md b/.changeset/slow-ducks-rush.md new file mode 100644 index 0000000000..c09f883e9c --- /dev/null +++ b/.changeset/slow-ducks-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. diff --git a/.changeset/slow-ligers-drum.md b/.changeset/slow-ligers-drum.md new file mode 100644 index 0000000000..867cc07cee --- /dev/null +++ b/.changeset/slow-ligers-drum.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Fix null check in `isJsonObject` utility function for scaffolder review state component diff --git a/.changeset/slow-toes-jog.md b/.changeset/slow-toes-jog.md new file mode 100644 index 0000000000..245feab544 --- /dev/null +++ b/.changeset/slow-toes-jog.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Fixed a bug on the WelcomeTitle component where the welcome message wasn't correct when the language was set to Spanish diff --git a/.changeset/small-bottles-cough.md b/.changeset/small-bottles-cough.md new file mode 100644 index 0000000000..40098a84c7 --- /dev/null +++ b/.changeset/small-bottles-cough.md @@ -0,0 +1,58 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + +We're looking for ways to make it possible to augment services without the need to replace the entire service. + +Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + +```diff +// @backstage/backend-defaults + ++ export const urlReaderFactoriesServiceRef = createServiceRef({ ++ id: 'core.urlReader.factories', ++ scope: 'plugin', ++ multiton: true, ++ }); + +... + +export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, ++ factories: urlReaderFactoriesServiceRef, + }, +- async factory({ config, logger }) { ++ async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, ++ factories, + }); + }, +}); +``` + +With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + +```ts +// packages/backend/index.ts +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; +... + +backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, +})); + +... + +``` diff --git a/.changeset/small-ears-poke.md b/.changeset/small-ears-poke.md new file mode 100644 index 0000000000..de906a8643 --- /dev/null +++ b/.changeset/small-ears-poke.md @@ -0,0 +1,7 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +**BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + +This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. diff --git a/.changeset/small-spoons-shout.md b/.changeset/small-spoons-shout.md new file mode 100644 index 0000000000..76c209edc6 --- /dev/null +++ b/.changeset/small-spoons-shout.md @@ -0,0 +1,13 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: The lockfile (`yarn.lock`) dependency analysis and mutations have been removed from several commands. + +The `versions:bump` command will no longer attempt to bump and deduplicate dependencies by modifying the lockfile, it will only update `package.json` files. + +The `versions:check` command has been removed, since its only purpose was verification and mutation of the lockfile. We recommend using the `yarn dedupe` command instead, or the `yarn-deduplicate` package if you're using Yarn classic. + +The check that was built into the `package start` command has been removed, it will no longer warn about lockfile mismatches. + +The packages in the Backstage ecosystem handle package duplications much better now than when these CLI features were first introduced, so the need for these features has diminished. By removing them, we drastically reduce the integration between the Backstage CLI and Yarn, making it much easier to add support for other package managers in the future. diff --git a/.changeset/smooth-countries-relate.md b/.changeset/smooth-countries-relate.md new file mode 100644 index 0000000000..5e86ee8ab5 --- /dev/null +++ b/.changeset/smooth-countries-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Use `entityPresentationApi` for the node title and the icon. diff --git a/.changeset/soft-clocks-bake.md b/.changeset/soft-clocks-bake.md deleted file mode 100644 index 29d7658869..0000000000 --- a/.changeset/soft-clocks-bake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Added small notes to AboutCard to discourage customizability PRs diff --git a/.changeset/soft-gorillas-refuse.md b/.changeset/soft-gorillas-refuse.md new file mode 100644 index 0000000000..323c4d20d2 --- /dev/null +++ b/.changeset/soft-gorillas-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The default services for `startTestBackend` and `ServiceFactoryTester` now includes the Root Health Service. diff --git a/.changeset/spicy-lies-listen.md b/.changeset/spicy-lies-listen.md new file mode 100644 index 0000000000..6bc5e95846 --- /dev/null +++ b/.changeset/spicy-lies-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fix helper text margin for scaffolder EntityNamePicker and EntityTagsPicker when using outlined text field diff --git a/.changeset/spicy-planets-provide.md b/.changeset/spicy-planets-provide.md new file mode 100644 index 0000000000..f2929df850 --- /dev/null +++ b/.changeset/spicy-planets-provide.md @@ -0,0 +1,46 @@ +--- +'@backstage/plugin-notifications-backend-module-email': minor +--- + +**BREAKING** Following `NotificationTemplateRenderer` methods now return a Promise and **must** be awaited: `getSubject`, `getText` and `getHtml`. + +Required changes and example usage: + +```diff +import { notificationsEmailTemplateExtensionPoint } from '@backstage/plugin-notifications-backend-module-email'; +import { Notification } from '@backstage/plugin-notifications-common'; ++import { getNotificationSubject, getNotificationTextContent, getNotificationHtmlContent } from 'my-notification-processing-library` +export const notificationsModuleEmailDecorator = createBackendModule({ + pluginId: 'notifications', + moduleId: 'email.templates', + register(reg) { + reg.registerInit({ + deps: { + emailTemplates: notificationsEmailTemplateExtensionPoint, + }, + async init({ emailTemplates }) { + emailTemplates.setTemplateRenderer({ +- getSubject(notification) { ++ async getSubject(notification) { +- return `New notification from ${notification.source}`; ++ const subject = await getNotificationSubject(notification); ++ return `New notification from ${subject}`; + }, +- getText(notification) { ++ async getText(notification) { +- return notification.content; ++ const text = await getNotificationTextContent(notification); ++ return text; + }, +- getHtml(notification) { ++ async getHtml(notification) { +- return `

${notification.content}

`; ++ const html = await getNotificationHtmlContent(notification); ++ return html; + }, + }); + }, + }); + }, +}); +``` diff --git a/.changeset/spotty-planets-accept.md b/.changeset/spotty-planets-accept.md new file mode 100644 index 0000000000..7342f89bd1 --- /dev/null +++ b/.changeset/spotty-planets-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Cleaned up codebase of RepoUrlPicker diff --git a/.changeset/strange-papayas-beg.md b/.changeset/strange-papayas-beg.md new file mode 100644 index 0000000000..6ce33fb029 --- /dev/null +++ b/.changeset/strange-papayas-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Added test cases for gitlab:pipeline:trigger examples diff --git a/.changeset/strong-otters-compete.md b/.changeset/strong-otters-compete.md new file mode 100644 index 0000000000..fdcb982b88 --- /dev/null +++ b/.changeset/strong-otters-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Add support for dynamic plugins via the EXPERIMENTAL_MODULE_FEDERATION environment variable when running `yarn start`. diff --git a/.changeset/stupid-dots-relate.md b/.changeset/stupid-dots-relate.md new file mode 100644 index 0000000000..a83777bff9 --- /dev/null +++ b/.changeset/stupid-dots-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +--- + +Add examples for `fetch:cookiecutter` scaffolder action & improve related tests diff --git a/.changeset/sweet-oranges-buy.md b/.changeset/sweet-oranges-buy.md new file mode 100644 index 0000000000..d9f3343e3d --- /dev/null +++ b/.changeset/sweet-oranges-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added support to be able to define `zod` config schema in Blueprints, with built in schema merging from the Blueprint and the extension instances. diff --git a/.changeset/swift-kings-sparkle.md b/.changeset/swift-kings-sparkle.md new file mode 100644 index 0000000000..84ceae962b --- /dev/null +++ b/.changeset/swift-kings-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +`AwsOrganizationCloudAccountProcessor` configuration field `roleArn` is deprecated in favor of new field `accountId` diff --git a/.changeset/tall-snakes-fix.md b/.changeset/tall-snakes-fix.md new file mode 100644 index 0000000000..351c781375 --- /dev/null +++ b/.changeset/tall-snakes-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +fix typo in `getPluginRequestToken` comments diff --git a/.changeset/tasty-ads-rescue.md b/.changeset/tasty-ads-rescue.md new file mode 100644 index 0000000000..d522856bdd --- /dev/null +++ b/.changeset/tasty-ads-rescue.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Avoid excessive numbers of error listeners on cache clients diff --git a/.changeset/ten-penguins-roll.md b/.changeset/ten-penguins-roll.md new file mode 100644 index 0000000000..152bb1524b --- /dev/null +++ b/.changeset/ten-penguins-roll.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': minor +--- + +By default, set notification as read when opening snackbar or web notification link diff --git a/.changeset/thick-hotels-know.md b/.changeset/thick-hotels-know.md new file mode 100644 index 0000000000..f0c7129256 --- /dev/null +++ b/.changeset/thick-hotels-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix scaffolder action `catalog:write` to write to directories that don't already exist diff --git a/.changeset/thick-lizards-divide.md b/.changeset/thick-lizards-divide.md deleted file mode 100644 index 84c1ab311a..0000000000 --- a/.changeset/thick-lizards-divide.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/backend-app-api': patch -'@backstage/backend-common': patch ---- - -Fixing exporting of classes properly from new packages diff --git a/.changeset/thick-squids-drive.md b/.changeset/thick-squids-drive.md new file mode 100644 index 0000000000..0e798063c0 --- /dev/null +++ b/.changeset/thick-squids-drive.md @@ -0,0 +1,34 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +--- + +Added support for being able to override extension definitions. + +```tsx +const TestCard = EntityCardBlueprint.make({ + ... +}); + +TestCard.override({ + // override attachment points + attachTo: { id: 'something-else', input: 'overridden' }, + // extend the config schema + config: { + schema: { + newConfig: z => z.string().optional(), + } + }, + // override factory + *factory(originalFactory, { inputs, config }){ + const originalOutput = originalFactory(); + + yield coreExentsionData.reactElement( + + {originalOutput.get(coreExentsionData.reactElement)} + + ); + } +}); + +``` diff --git a/.changeset/thin-carrots-eat.md b/.changeset/thin-carrots-eat.md new file mode 100644 index 0000000000..570f21f83d --- /dev/null +++ b/.changeset/thin-carrots-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-github': patch +--- + +Added examples for action github:pages and improved its test cases diff --git a/.changeset/thirty-adults-grab.md b/.changeset/thirty-adults-grab.md new file mode 100644 index 0000000000..ed986a8289 --- /dev/null +++ b/.changeset/thirty-adults-grab.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-node': patch +'@backstage/plugin-catalog-node': patch +--- + +Explicit declare if the service ref accepts `single` or `multiple` implementations. diff --git a/.changeset/thirty-paws-hope.md b/.changeset/thirty-paws-hope.md new file mode 100644 index 0000000000..0de607f331 --- /dev/null +++ b/.changeset/thirty-paws-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +The request to delete the session cookie when running the app in protected mode is now done with a plain `fetch` rather than `FetchApi`. This fixes a bug where the app would immediately try to sign-in again when removing the cookie during logout. diff --git a/.changeset/tiny-dodos-prove-2.md b/.changeset/tiny-dodos-prove-2.md new file mode 100644 index 0000000000..91ac021c67 --- /dev/null +++ b/.changeset/tiny-dodos-prove-2.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Deprecate existing `ExtensionCreators` in favour of their new Blueprint counterparts. diff --git a/.changeset/tiny-dodos-prove.md b/.changeset/tiny-dodos-prove.md new file mode 100644 index 0000000000..6392e5050e --- /dev/null +++ b/.changeset/tiny-dodos-prove.md @@ -0,0 +1,9 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Refactor `.make` method on Blueprints into two different methods, `.make` and `.makeWithOverrides`. + +When using `createExtensionBlueprint` you can define parameters for the factory function, if you wish to take advantage of these parameters you should use `.make` when creating an extension instance of a Blueprint. If you wish to override more things other than the standard `attachTo`, `name`, `namespace` then you should use `.makeWithOverrides` instead. + +`.make` is reserved for simple creation of extension instances from Blueprints using higher level parameters, whereas `.makeWithOverrides` is lower level and you have more control over the final extension. diff --git a/.changeset/tiny-oranges-pretend.md b/.changeset/tiny-oranges-pretend.md new file mode 100644 index 0000000000..87227fb962 --- /dev/null +++ b/.changeset/tiny-oranges-pretend.md @@ -0,0 +1,19 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Extension data references can now be defined in a way that encapsulates the ID string in the type, in addition to the data type itself. The old way of creating extension data references is deprecated and will be removed in a future release. + +For example, the following code: + +```ts +export const myExtension = createExtensionDataRef('my-plugin.my-data'); +``` + +Should be updated to the following: + +```ts +export const myExtension = createExtensionDataRef().with({ + id: 'my-plugin.my-data', +}); +``` diff --git a/.changeset/tough-goats-hang.md b/.changeset/tough-goats-hang.md new file mode 100644 index 0000000000..2608c03b2e --- /dev/null +++ b/.changeset/tough-goats-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket diff --git a/.changeset/tough-lies-repair.md b/.changeset/tough-lies-repair.md new file mode 100644 index 0000000000..b2f848e292 --- /dev/null +++ b/.changeset/tough-lies-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Fix issue with `RepoUrlPicker` not refreshing the credentials for a different host diff --git a/.changeset/tricky-ducks-juggle.md b/.changeset/tricky-ducks-juggle.md new file mode 100644 index 0000000000..48c0d215bd --- /dev/null +++ b/.changeset/tricky-ducks-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Added configuration for the `packages` options to config schema diff --git a/.changeset/tricky-rules-destroy.md b/.changeset/tricky-rules-destroy.md new file mode 100644 index 0000000000..196702f8c4 --- /dev/null +++ b/.changeset/tricky-rules-destroy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-node': patch +--- + +Fix TechDocs Edit URL for nested docs diff --git a/.changeset/two-emus-work.md b/.changeset/two-emus-work.md new file mode 100644 index 0000000000..3c60086043 --- /dev/null +++ b/.changeset/two-emus-work.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +New command now supports setting package license diff --git a/.changeset/violet-jokes-wave.md b/.changeset/violet-jokes-wave.md new file mode 100644 index 0000000000..623cff2583 --- /dev/null +++ b/.changeset/violet-jokes-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix for `repo build --all` not properly detecting the experimental public entry point. diff --git a/.changeset/warm-monkeys-marry.md b/.changeset/warm-monkeys-marry.md new file mode 100644 index 0000000000..5ea8deae01 --- /dev/null +++ b/.changeset/warm-monkeys-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-sentry': patch +--- + +Added test cases for sentry:project:create examples diff --git a/.changeset/wicked-bobcats-teach.md b/.changeset/wicked-bobcats-teach.md new file mode 100644 index 0000000000..041e336f7d --- /dev/null +++ b/.changeset/wicked-bobcats-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-react': patch +--- + +feat: Hide visibility of CookieAuthRedirect diff --git a/.changeset/wild-eggs-exist.md b/.changeset/wild-eggs-exist.md new file mode 100644 index 0000000000..68c49e343b --- /dev/null +++ b/.changeset/wild-eggs-exist.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': patch +--- + +Add custom action for merge request: **auto** + +The **Auto** action selects the committed action between _create_ and _update_. + +The **Auto** action fetches files using the **/projects/repository/tree endpoint**. +After fetching, it checks if the file exists locally and in the repository. If it does, it chooses **update**; otherwise, it chooses **create**. diff --git a/.changeset/wise-spiders-walk.md b/.changeset/wise-spiders-walk.md new file mode 100644 index 0000000000..65793041c5 --- /dev/null +++ b/.changeset/wise-spiders-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. diff --git a/.changeset/witty-bears-behave.md b/.changeset/witty-bears-behave.md new file mode 100644 index 0000000000..174467b9eb --- /dev/null +++ b/.changeset/witty-bears-behave.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Updated functions for `getHarnessEditContentsUrl`, `getHarnessFileContentsUrl`, `getHarnessArchiveUrl`, `getHarnessLatestCommitUrl` and `parseHarnessUrl` to handle account and org level urls diff --git a/.changeset/witty-geese-battle.md b/.changeset/witty-geese-battle.md new file mode 100644 index 0000000000..082784cc8b --- /dev/null +++ b/.changeset/witty-geese-battle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. diff --git a/.changeset/witty-queens-run.md b/.changeset/witty-queens-run.md new file mode 100644 index 0000000000..2d869caa91 --- /dev/null +++ b/.changeset/witty-queens-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications-backend-module-email': patch +--- + +Notification email processor supports allowing or denying specific email addresses from receiving notifications diff --git a/.changeset/witty-timers-marry.md b/.changeset/witty-timers-marry.md new file mode 100644 index 0000000000..4193acb6fc --- /dev/null +++ b/.changeset/witty-timers-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-bitbucket-cloud-common': patch +--- + +Added method `listBranchesByRepository` to `BitbucketCloudClient` diff --git a/.changeset/young-birds-push.md b/.changeset/young-birds-push.md new file mode 100644 index 0000000000..54afa48aaf --- /dev/null +++ b/.changeset/young-birds-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Introduce an optional spec.type attribute on the Domain and System entity kinds diff --git a/.changeset/young-games-visit.md b/.changeset/young-games-visit.md new file mode 100644 index 0000000000..467abcec86 --- /dev/null +++ b/.changeset/young-games-visit.md @@ -0,0 +1,14 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +'@backstage/frontend-app-api': patch +'@backstage/core-compat-api': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-user-settings': patch +'@backstage/plugin-search-react': patch +'@backstage/plugin-techdocs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-search': patch +--- + +Added config input type to the extensions diff --git a/.changeset/young-houses-unite.md b/.changeset/young-houses-unite.md deleted file mode 100644 index 47ffe43c21..0000000000 --- a/.changeset/young-houses-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-app-api': patch ---- - -Fixing issue with `MiddlewareFactory` deprecation wrapping diff --git a/.changeset/young-peaches-shake.md b/.changeset/young-peaches-shake.md new file mode 100644 index 0000000000..75c93f8f3b --- /dev/null +++ b/.changeset/young-peaches-shake.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': minor +--- + +Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9cd3bc0690..a0c83a42bd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,10 +9,10 @@ yarn.lock @backstage/maintainers @backst */yarn.lock @backstage/maintainers @backstage-service /.changeset/*.md /beps/0001-notifications-system @backstage/maintainers @backstage/notifications-maintainers -/docs/assets/search @backstage/discoverability-maintainers -/docs/features/search @backstage/discoverability-maintainers +/docs/assets/search @backstage/search-maintainers +/docs/features/search @backstage/search-maintainers /docs/features/techdocs @backstage/techdocs-maintainers -/docs/plugins/integrating-search-into-plugins.md @backstage/discoverability-maintainers +/docs/plugins/integrating-search-into-plugins.md @backstage/search-maintainers /packages/cli/src/commands/onboard @backstage/sharks /packages/backend-openapi-utils @backstage/maintainers @backstage/reviewers @backstage/openapi-tooling-maintainers /packages/techdocs-cli @backstage/techdocs-maintainers @@ -50,8 +50,8 @@ yarn.lock @backstage/maintainers @backst /plugins/explore-react @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers /plugins/fossa @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers /plugins/git-release-manager @backstage/maintainers @backstage/reviewers @erikengervall -/plugins/home @backstage/discoverability-maintainers -/plugins/home-* @backstage/discoverability-maintainers +/plugins/home @backstage/home-maintainers +/plugins/home-* @backstage/home-maintainers /plugins/kafka @backstage/maintainers @backstage/reviewers @andrewthauer /plugins/kafka-backend @backstage/maintainers @backstage/reviewers @andrewthauer /plugins/kubernetes @backstage/kubernetes-maintainers @@ -70,13 +70,11 @@ yarn.lock @backstage/maintainers @backst /plugins/rollbar-backend @backstage/maintainers @backstage/reviewers @andrewthauer /plugins/scaffolder @backstage/maintainers @backstage/reviewers @backstage/scaffolder-maintainers /plugins/scaffolder-* @backstage/maintainers @backstage/reviewers @backstage/scaffolder-maintainers -/plugins/search @backstage/discoverability-maintainers -/plugins/search-* @backstage/discoverability-maintainers +/plugins/search @backstage/search-maintainers +/plugins/search-* @backstage/search-maintainers /plugins/signals @backstage/maintainers @backstage/notifications-maintainers /plugins/signals-* @backstage/maintainers @backstage/notifications-maintainers /plugins/sonarqube @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers -/plugins/stack-overflow @backstage/discoverability-maintainers -/plugins/stack-overflow-backend @backstage/discoverability-maintainers /plugins/techdocs @backstage/techdocs-maintainers /plugins/techdocs-* @backstage/techdocs-maintainers /plugins/user-settings-backend @backstage/maintainers @backstage/reviewers @backstage/sda-se-reviewers diff --git a/.github/issue-labeler.yml b/.github/issue-labeler.yml index 6d74ce8902..c6c6ef115f 100644 --- a/.github/issue-labeler.yml +++ b/.github/issue-labeler.yml @@ -1,6 +1,6 @@ area:techdocs: - '/(techdocs|tech-docs|tech docs)/i' -area:discoverability: +area:search: - '/search/i' area:events: - '/events/i' diff --git a/.github/labeler.yml b/.github/labeler.yml index a58da1d2f7..e6568091b8 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -4,7 +4,11 @@ area:catalog: - plugins/catalog/**/* - plugins/catalog-*/**/* - packages/catalog-*/**/* -area:discoverability: # search + home +area:home: + - changed-files: + - any-glob-to-any-file: + - plugins/home/**/* +area:search: - changed-files: - any-glob-to-any-file: - plugins/search/**/* diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index 6ff0ef56da..bad61de7ff 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -7,6 +7,7 @@ ADRs airbrake Airbrake Airbrakes +ajv Alaria Alef allowlisted @@ -84,7 +85,9 @@ debounce Debounce debuggability declaratively +deduplicate deduplicated +deduplication deliverables denormalized dependabot @@ -242,6 +245,7 @@ Monorepo monorepos msgraph msw +multiton mutex mutexes mysql diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index e334a29b82..c39fdf7f7c 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -27,7 +27,7 @@ jobs: run: git fetch --depth 1 origin ${{ github.base_ref }} - name: setup-node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x registry-url: https://registry.npmjs.org/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 135076ba40..168d6205e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -71,7 +71,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -216,7 +216,7 @@ jobs: run: git fetch origin ${{ github.event.pull_request.base.ref }} - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 923293667a..bdbe22aae8 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -7,6 +7,7 @@ on: jobs: cron: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - name: Harden Runner uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index 8b4709337a..9f305f9a20 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -31,7 +31,7 @@ jobs: ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }} - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index a0cdae5774..eafa4a8fcd 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js 18.x - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x registry-url: https://registry.npmjs.org/ # Needed for auth @@ -66,7 +66,7 @@ jobs: run: ls microsite/build && ls microsite/build/storybook - name: Deploy both microsite and storybook to gh-pages - uses: JamesIves/github-pages-deploy-action@5c6e9e9f3672ce8fd37b9856193d2a537941e66c # v4.6.1 + uses: JamesIves/github-pages-deploy-action@94f3c658273cf92fb48ef99e5fbc02bd2dc642b2 # v4.6.3 with: branch: gh-pages folder: microsite/build diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index db9489dbb8..5488b9a189 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js 18.x - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 81e95fab97..7ae5637986 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -161,7 +161,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/pr-review-comment-trigger.yaml b/.github/workflows/pr-review-comment-trigger.yaml index ccf02d5aee..d6f36da686 100644 --- a/.github/workflows/pr-review-comment-trigger.yaml +++ b/.github/workflows/pr-review-comment-trigger.yaml @@ -30,7 +30,7 @@ jobs: run: | mkdir -p ./pr echo $PR_NUMBER > ./pr/pr_number - - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + - uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: pr_number-${{ github.event.pull_request.number }} path: pr/ diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ecf18d71ad..4ef8cc7181 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -58,7 +58,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: 'Upload artifact' - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: SARIF file path: results.sarif @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/upload-sarif@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 with: sarif_file: results.sarif diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 9886c94830..da6bd2592d 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index db35945550..62dd7e0c13 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -12,27 +12,33 @@ jobs: with: egress-policy: audit - # Setup node & install deps before checkout, keeping install quick - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 - with: - node-version: 18.x - - name: Install dependencies - # Just the deps for the assemble manifest script - run: npm install semver@7.3.5 fs-extra@10.0.0 @manypkg/get-packages@1.1.1 - - name: Checkout uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: - path: backstage # 'v' prefix is added here for the tag, we keep it out of the manifest logic ref: v${{ github.event.client_payload.version }} + - name: use node.js 18.x + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version: 18.x + registry-url: https://registry.npmjs.org/ # Needed for auth + + - name: yarn install + uses: backstage/actions/yarn-install@a674369920067381b450d398b27df7039b7ef635 # v0.6.5 + with: + cache-prefix: ${{ runner.os }}-v18.x + + - name: Build yarn plugin + working-directory: packages/yarn-plugin + run: yarn build + # Checkout backstage/versions into /backstage/versions, which is where store the output - name: Checkout versions uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: repository: backstage/versions - path: backstage/versions + path: versions token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} - name: Configure Git @@ -43,11 +49,23 @@ jobs: - name: Create release # This grabs the scripts from master in order to support backfills run: | - cd backstage mkdir -p scripts wget -O scripts/assemble-manifest.js https://raw.githubusercontent.com/backstage/backstage/master/scripts/assemble-manifest.js node scripts/assemble-manifest.js ${{ github.event.client_payload.version }} - cd versions + + # Copies the build output of the yarn-plugin package to the appropriate + # directory, allowing the plugin to be installed with a command like + # `yarn plugin import https://versions.backstage.io/v1/tags/main/yarn-plugin` + - name: Add yarn plugin to release + working-directory: packages/yarn-plugin + run: >- + cp + bundles/@yarnpkg/plugin-backstage.js + ../../versions/v1/releases/${{ github.event.client_payload.version }}/yarn-plugin + + - name: Commit to versions repo + working-directory: versions + run: | git add . git commit -am "${{ github.event.client_payload.version }}" git push @@ -65,7 +83,7 @@ jobs: workflow_id: 'release.yml', ref: 'master', inputs: { - version: require('./backstage/packages/create-app/package.json').version, - releaseVersion: require('./backstage/package.json').version + version: require('./packages/create-app/package.json').version, + releaseVersion: require('./package.json').version }, }); diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index fb3456bfb6..af2e4da251 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js 18.x - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x registry-url: https://registry.npmjs.org/ # Needed for auth @@ -29,7 +29,7 @@ jobs: cache-prefix: ${{ runner.os }}-v18.x - name: Create Snyk report - uses: snyk/actions/node@d406fd286b663eb8c6f8adcced4f7bcd199c0a3f # master + uses: snyk/actions/node@6312a53377a551c0258438bf25fb8f378afbc977 # master continue-on-error: true # Snyk CLI exits with error when vulnerabilities are found with: args: > diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index a3c72c1d7b..1bdf0d30cf 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Monitor and Synchronize Snyk Policies - uses: snyk/actions/node@d406fd286b663eb8c6f8adcced4f7bcd199c0a3f # master + uses: snyk/actions/node@6312a53377a551c0258438bf25fb8f378afbc977 # master with: command: monitor args: > @@ -46,7 +46,7 @@ jobs: # Above we run the `monitor` command, this runs the `test` command which is # the one that generates the SARIF report that we can upload to GitHub. - name: Create Snyk report - uses: snyk/actions/node@d406fd286b663eb8c6f8adcced4f7bcd199c0a3f # master + uses: snyk/actions/node@6312a53377a551c0258438bf25fb8f378afbc977 # master continue-on-error: true # To make sure that SARIF upload gets called with: args: > @@ -58,6 +58,6 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} NODE_OPTIONS: --max-old-space-size=7168 - name: Upload Snyk report - uses: github/codeql-action/upload-sarif@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/upload-sarif@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 with: sarif_file: snyk.sarif diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index bb46bbd4c1..386306dcb1 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Use Node.js 18.x - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x - name: yarn install diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index e19187b6ce..b2fb2dc6ec 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -55,7 +55,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/init@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -66,7 +66,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/autobuild@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -80,4 +80,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@23acc5c183826b7a8a97bce3cecc52db901f8251 # v3.25.10 + uses: github/codeql-action/analyze@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 diff --git a/.github/workflows/verify_e2e-kubernetes.yml b/.github/workflows/verify_e2e-kubernetes.yml index b7465945ea..4647505cbf 100644 --- a/.github/workflows/verify_e2e-kubernetes.yml +++ b/.github/workflows/verify_e2e-kubernetes.yml @@ -29,7 +29,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index d6491f1e3c..fa303ce1c6 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -53,7 +53,7 @@ jobs: git config --global user.name 'GitHub e2e user' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index d7575cadf7..bc782c4bd3 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -50,7 +50,7 @@ jobs: git config --global user.name 'GitHub e2e user' - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 1a5bc8075b..08dcddde8e 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -31,7 +31,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js 18.x - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5 diff --git a/.github/workflows/verify_microsite_accessibility.yml b/.github/workflows/verify_microsite_accessibility.yml index c9ed1696cd..220e2633c3 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Use Node.js 18.x - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: 18.x diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 83ddac8429..e9da9f0398 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -37,7 +37,7 @@ jobs: fetch-depth: 0 # Required to retrieve git history - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth @@ -51,7 +51,7 @@ jobs: - run: yarn build-storybook - - uses: chromaui/action@5991575112b472a4fa85429f29cc17e2fb83a2a2 # v11 + - uses: chromaui/action@fdbe7756d4dbf493e2fbb822df73be7accd07e1c # v11 with: token: ${{ secrets.GITHUB_TOKEN }} # projectToken intentionally shared to allow collaborators to run Chromatic on forks diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 736193cad6..109e2ee96e 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: use node.js ${{ matrix.node-version }} - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth diff --git a/.gitignore b/.gitignore index 5ba3da3148..6db4bf549c 100644 --- a/.gitignore +++ b/.gitignore @@ -94,6 +94,7 @@ typings/ # dotenv environment variables file .env .env.test +.envrc # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/ADOPTERS.md b/ADOPTERS.md index 5fe359bf1e..3b534052f1 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -271,3 +271,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Scania](https://www.scania.com) | [Santosh Rangavajjula](https://linkedin.com/in/rsbth) | We are implementing backstage at Scania to consolidate operational information from Gitlab, Jira, Confluence, Artifactory, Servicenow and other tools into one place. | | [Senora.dev](https://senora.dev) | [Shaked Braimok Yosef](https://github.com/ShakedBraimok) | We are using Backstage as a service catalog for our costumers. | | [Covestro](https://www.covestro.com) | [Julien Gedeon](https://github.com/cvvcv1) | We use Backstage as our Internal developer portal for the Digital R&D department. | +| [Grupo OLX](https://www.olx.com.br/) | [@marciorgb](https://github.com/marciorgb), [@jeffbraga](https://github.com/jeffbraga), [@rogerfernandes](https://github.com/rogerfernandes), [@stockrt](https://github.com/stockrt) | At Grupo OLX, we are using Backstage to reduce friction and enhance developer autonomy by creating Golden Paths, which automate previously manual processes. Additionally, Backstage's catalog integrates various views of our organizational structure and workflows, offering a comprehensive overview of how different components interconnect. | +| [DPG Media](https://www.dpgmediagroup.com) | [@benjidotsh](https://github.com/benjidotsh), [@lvstb](https://github.com/lvstb), [@ssliman](https://github.com/ssliman), [@edewel-beep](https://github.com/edewel-beep) | Access to infrastructure, documentation and internal tooling | +| [Baloise](https://www.baloise.ch) | [@dweber019](https://github.com/dweber019), [@mikevader](https://github.com/mikevader) | We are using Backstage as internal developer protal for use cases like software catalog, documentation, infrastructure. | +| [Cigna](https://www.cigna.com/) / [Evernorth](https://www.evernorth.com/) | [@raphtalia](https://github.com/raphtalia), [Devon Paluso](mailto:Devon.Paluso@evernorth.com), [Bryan Rego](mailto:Bryan.Rego@evernorth.com), [Ian Skillings](mailto:Ian.Skillings@evernorth.com) | Internal developer portal providing TechDocs, software templates, centralized secret management, with information consolidation from ServiceNow, Okta, GitHub, GitLab, and WebEx. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce0e97c781..f10116844b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ You'll see how you get both logs for the frontend `webpack-dev-server` which ser Visit and you should see the bleeding edge of Backstage ready for contributions! -If you want to get a better understanding of the layout of the repo not that you have a local copy running feel free to review the [Backstage Project Structure](https://backstage.io/docs/getting-started/project-structure) documentation. +If you want to get a better understanding of the layout of the repo now that you have a local copy running feel free to review the [Backstage Project Structure](https://backstage.io/docs/getting-started/#general-folder-structure) documentation. ## Coding Guidelines diff --git a/OWNERS.md b/OWNERS.md index 1437629881..b145205539 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -32,11 +32,11 @@ Scope: The catalog plugin and catalog model | Patrik Oldsberg | Spotify | Cubic Belugas | [Rugvip](https://github.com/Rugvip) | `Rugvip#0019` | | Vincenzo Scamporlino | Spotify | Cubic Belugas | [vinzscam](http://github.com/vinzscam) | `vinzscam#6944` | -### Discoverability +### Home -Team: @backstage/discoverability-maintainers +Team: @backstage/home-maintainers -Scope: Discoverability within Backstage, including the home page, information architecture, and search +Scope: The Backstage home page and information architecture | Name | Organization | Team | GitHub | Discord | | ------------------------ | ------------ | -------------- | ---------------------------------------- | ----------------- | @@ -46,6 +46,18 @@ Scope: Discoverability within Backstage, including the home page, information ar | Raghunandan Balachandran | Spotify | Infinite Buck$ | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | | Bailey Brooks | Spotify | Infinite Buck$ | [bailey](http://github.com/bailey) | \_bailey4 | +### Search + +Team: @backstage/search-maintainers + +Scope: The Backstage Search plugin + +| Name | Organization | Team | GitHub | Discord | +| ------------------------ | ------------ | -------------- | --------------------------------------------- | ----------------- | +| Emma Indal | Spotify | Infinite Buck$ | [emmaindal](http://github.com/emmaindal) | emmaindal#7503 | +| Raghunandan Balachandran | Spotify | Infinite Buck$ | [soapraj](http://github.com/soapraj) | raghunandanb#1114 | +| Alex Lorenzi | Spotify | ProTean | [AlexLorenzi](https://github.com/AlexLorenzi) | alexlorenzi | + ### Helm Charts Team: @backstage/helm-chart-maintainers @@ -159,6 +171,18 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | ------------------- | ------------ | ------------------------------------- | ---------------- | | Bogdan Nechyporenko | Bol.com | [acierto](https://github.com/acierto) | `bogdan_haarlem` | +### Documentation + +Team: @backstage/documentation-maintainers + +Scope: The Backstage Documentation + +| Name | Organization | GitHub | Discord | +| --------------- | ------------- | ----------------------------------------------------- | ------------- | +| Andre Wanlin | Spotify | [awanlin](https://github.com/awanlin) | `ahhhndre` | +| Aramis Sennyey | | [aramissennyeydd](https://github.com/aramissennyeydd) | `Aramis#7984` | +| Peter Macdonald | VodafoneZiggo | [Parsifal-M](https://github.com/Parsifal-M) | `parsifal` | + ## Sponsors | Name | Organization | GitHub | Email | @@ -187,6 +211,7 @@ Scope: The Scaffolder frontend and backend plugins, and related tooling. | Mark Avery | Cvent | [webark](https://github.com/webark) | `webark#8471` | | Miklós Kiss | Roadie.io | [kissmikijr](https://github.com/kissmikijr) | `Miklos#7416` | | Patrick Jungermann | Bonial International GmbH | [pjungermann](https://github.com/pjungermann) | `pjungermann#6933` | +| Peter Macdonald | VodafoneZiggo | [Parsifal-M](https://github.com/Parsifal-M) | `parsifal` | | Phil Kuang | FactSet Research Systems | [kuangp](https://github.com/kuangp) | `pkuang#3202` | | Sebastian Poxhofer | N26 | [secustor](https://github.com/secustor) | `secustor` | | Taras Mankovski | Frontside | [taras](https://github.com/taras) | `tarasm#1256` | diff --git a/README-fr_FR.md b/README-fr_FR.md new file mode 100644 index 0000000000..e0cffaa5c5 --- /dev/null +++ b/README-fr_FR.md @@ -0,0 +1,77 @@ +[![headline](docs/assets/headline.png)](https://backstage.io/) + +# [Backstage](https://backstage.io) + +Français \| [English](README.md) \| [한국어](README-ko_kr.md) \| [中文版](README-zh_Hans.md) + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![CNCF Status](https://img.shields.io/badge/cncf%20status-incubation-blue.svg)](https://www.cncf.io/projects) +[![Discord](https://img.shields.io/discord/687207715902193673?logo=discord&label=Discord&color=5865F2&logoColor=white)](https://discord.gg/backstage-687207715902193673) +![Code style](https://img.shields.io/badge/code_style-prettier-ff69b4.svg) +[![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage) +[![](https://img.shields.io/github/v/release/backstage/backstage)](https://github.com/backstage/backstage/releases) +[![Uffizzi](https://img.shields.io/endpoint?url=https%3A%2F%2Fapp.uffizzi.com%2Fapi%2Fv1%2Fpublic%2Fshields%2Fgithub.com%2Fbackstage%2Fbackstage)](https://app.uffizzi.com/ephemeral-environments/backstage/backstage) +[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/7678/badge)](https://bestpractices.coreinfrastructure.org/projects/7678) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/backstage/backstage/badge)](https://securityscorecards.dev/viewer/?uri=github.com/backstage/backstage) + +## Qu'est-ce que Backstage? + +[Backstage](https://backstage.io/) est un framework open source conçu pour créer des portails à destination des développeurs. Avec l'idée de fournir un catalogue d'applications centralisé, Backstage remet de l'ordre dans vos microservices et votre infrastructure, ce qui permet à vos équipes produit de coder rapidement et de manière efficace sans perdre en autonomie. + +Backstage rassemble tous vos outils d'infrastructure, services et documentations pour créer un environnement de développement fluide de bout en bout. + +![software-catalog](docs/assets/header.png) + +Out-of-the-box, Backstage comprend : + +- [Backstage Software Catalog](https://backstage.io/docs/features/software-catalog/) pour gérer tous vos logiciels tels que les microservices, les bibliothèques, les pipelines de données, les sites web et les modèles ML. +- [Backstage Software Templates](https://backstage.io/docs/features/software-templates/) pour créer rapidement de nouveaux projets et uniformiser vos outils selon les meilleures pratiques de votre organisation. +- [Backstage TechDocs](https://backstage.io/docs/features/techdocs/) pour faciliter la création, la maintenance, la recherche et l'utilisation de la documentation technique, en adoptant une approche "docs like code". +- De plus, un écosystème croissant de [plugins open source](https://github.com/backstage/backstage/tree/master/plugins) qui étendent davantage la personnalisation et les fonctionnalités de Backstage. + +Backstage a été initialement développé par Spotify, mais il est désormais hébergé par la [Cloud Native Computing Foundation (CNCF)](https://www.cncf.io) en tant que projet en stade d'incubation. Pour en savoir plus, consultez l'[annonce](https://backstage.io/blog/2022/03/16/backstage-turns-two#out-of-the-sandbox-and-into-incubation). + +## Roadmap du projet + +Pour des informations sur la roadmap détaillée du projet, y compris les jalons atteints, consultez [la Roadmap](https://backstage.io/docs/overview/roadmap). + +## Démarrage + +Pour commencer à utiliser Backstage, consultez [Getting Started documentation](https://backstage.io/docs/getting-started). + +## Documentation + +La documentation de Backstage inclus: + +- [Main documentation](https://backstage.io/docs) +- [Software Catalog](https://backstage.io/docs/features/software-catalog/) +- [Architecture](https://backstage.io/docs/overview/architecture-overview) ([Decisions](https://backstage.io/docs/architecture-decisions/)) +- [Designing for Backstage](https://backstage.io/docs/dls/design) +- [Storybook - UI components](https://backstage.io/storybook) + +## Communauté + +Si vous voulez contribuer et vous impliquer dans notre communauté, voici les ressources à votre disposition : + +- [Discord chatroom](https://discord.gg/backstage-687207715902193673) - Pour obtenir de l'aide ou discuter du projet +- [Contributing to Backstage](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) - Rendez-vous ici si vous souhaitez contribuer +- [RFCs](https://github.com/backstage/backstage/labels/rfc) - Contribuez à définir la direction technique +- [FAQ](https://backstage.io/docs/FAQ) +- [Code of Conduct](CODE_OF_CONDUCT.md) - C'est comme ça qu'on fonctionne +- [Adopters](ADOPTERS.md) - Les entreprises utilisant déjà Backstage +- [Blog](https://backstage.io/blog/) - Annonces et mises à jour +- [Newsletter](https://spoti.fi/backstagenewsletter) - Abonnez-vous à notre newsletter par mail +- [Backstage Community Sessions](https://github.com/backstage/community) - Participez aux rencontres mensuelles et découvrez la communauté Backstage +- Donnez-nous une étoile ⭐️ - Si vous utilisez Backstage ou si vous trouvez que c'est un projet intéressant, nous apprécierions beaucoup votre soutien ❤️ + +## Licence + +Copyright 2020-2024 © Les auteurs de Backstage. Tous droits réservés. La Linux Foundation détient des marques déposées et utilise des marques commerciales. Pour une liste des marques de commerce de la Linux Foundation, veuillez consulter notre page d'utilisation des marques: https://www.linuxfoundation.org/trademark-usage + +Sous licence Apache, version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +## Sécurité + +Veuillez signaler les problèmes de sécurité sensibles en utilisant le [programme de bug-bounty](https://hackerone.com/spotify) de Spotify plutôt que GitHub. + +Pour plus de détails, consultez notre processus complet de [publication de sécurité](SECURITY.md). diff --git a/README.md b/README.md index 47ae539b6b..6c27d01db2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ [![headline](docs/assets/headline.png)](https://backstage.io/) +> [!NOTE] +> 🏖 From July 1st through 16th, due to maintainers being on summer vacations, expect the project to move a little slower than normal, and support to be limited. Normal service will resume after that! 🏝 + # [Backstage](https://backstage.io) -English \| [한국어](README-ko_kr.md) \| [中文版](README-zh_Hans.md) +English \| [한국어](README-ko_kr.md) \| [中文版](README-zh_Hans.md) \| [Français](README-fr_FR.md) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-incubation-blue.svg)](https://www.cncf.io/projects) @@ -55,7 +58,7 @@ To engage with our community, you can use the following resources: - [Discord chatroom](https://discord.gg/backstage-687207715902193673) - Get support or discuss the project - [Contributing to Backstage](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) - Start here if you want to contribute - [RFCs](https://github.com/backstage/backstage/labels/rfc) - Help shape the technical direction -- [FAQ](https://backstage.io/docs/FAQ) - Frequently Asked Questions +- [FAQ](https://backstage.io/docs/faq) - Frequently Asked Questions - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates diff --git a/beps/0001-notifications-system/README.md b/beps/0001-notifications-system/README.md index 80751179dd..541e75e555 100644 --- a/beps/0001-notifications-system/README.md +++ b/beps/0001-notifications-system/README.md @@ -1,6 +1,6 @@ --- title: Backstage Notifications System -status: provisional +status: implementable authors: - '@Rugvip' - '@drodil' diff --git a/beps/0002-dynamic-frontend-plugins/README.md b/beps/0002-dynamic-frontend-plugins/README.md index f26a3b488d..34c6e2d435 100644 --- a/beps/0002-dynamic-frontend-plugins/README.md +++ b/beps/0002-dynamic-frontend-plugins/README.md @@ -156,12 +156,247 @@ Each plugin is required to provide manifest file (metadata) in predefined format Plugin discovery is a pre-requisite for Plugin registry. This should be responsible for scanning for available plugins and generating/modifying the plugin registry to always keep it up to date. +### CLI + +The following sections contains proposal of new CLI scripts to support the dynamic frontend plugins. + +#### Static and Dynamic Plugin Build Differences + +The major difference between static and dynamic plugins lies in their dependencies and how they are "installed" into a Backstage instance and whether they are bundled into final JS output during the build. + +While static plugins can define their dependencies using the dependencies field inside `package.json`, dynamic (module federation-based) plugins can't. + +A dynamic plugin requires all dependencies to be present in the build output. Module sharing is then used to optimize the "output size" at runtime. If a shared package is not found in the browser scope, or if the version range does not match the requirement, it falls back and loads additional JS assets from the "private" build and adds them to the scope. + +Webpack is also not the best tool for building traditional NPM packages. Dependencies can be externalized, but Webpack brings additional code into the bundles. Using Webpack external dependencies also conflicts with module federation shared modules. Externals will not be included even as a shared module fallback. As a result, such shared module can't be used because the dependency will not be defined in the init container. + +If the vision is to still support "static" plugins installed via NPM, it makes sense to keep the static and dynamic plugin builds separate and build two different bundles. Both can still be published as a single NPM package (not recommended), but to achieve optimal performance for both, there cannot be a shared output. + +#### Dynamic export CLI + +```sh +backstage-cli plugin export-dynamic +``` + +Similar to the current build command, instead of producing static plugin output, it builds a dynamic plugin using Webpack and module federation plugins. + +**Internal arguments** + +Some configuration is required, but they can be internal using the script `cwd`. + +- _path_ + - path to the plugin `package.json` +- _export-name_ + - Export name from where the plugin root is accessible + - defaults to `pluginEntry` + +**Shared modules** + +> NOTE: Details of module sharing are described in the [Module sharing](#module-sharing) section. + +Short summary of dependency sharing: + +- Predefined set of singleton packages +- Generated granular config of modules for MUI dependencies +- Share all other dependencies listed in package.json + +To allow additional customization of module sharing, extra configuration will be required. The extra module haring configuration can be embedded inside `package.json`. + +- [Schema proposal draft](./dependency-sharing-schema.json) +- [Config draft](./share-config-sample.json) + +#### Generate module sharing map + +> NOTE: RedHat has similar internal generators that are used in combination with Scalprum. These can be built upon, moved to Scalprum, and exposed. If there is interest, the Backstage maintainers do not have to be responsible for maintaining the source code. + +A simple program to traverse the AST and find exports from the `main` or `module` script of a dependency. Any exposed modules from the dependency entry will be traced to their source files, and a module map can be generated. + +> NOTE: It is important that import paths in source code are also directly targeting the "traced" modules or are transformed at build time. + +**The script should be triggered by the plugin build cli** + +Generates a module map (exported module source file location) for module sharing. + +**Arguments** + +- _path_ + - path to the module root (directory with package.json) + +[module map generator using TS example](#simplistic-minimal-example-of-module-map-generator) + +### Dynamic assets server + +A simple static asset server is sufficient to serve the dynamic assets. The asset server can collect the necessary metadata at boot or runtime. The asset server can be added to the `app-backend` + +It's important to consider old deployments as part of this storage, to make sure that dynamic chunks are still available to users who have not yet refreshed the page to load the latest version of the app. + +#### Plugin separation + +- Each plugin should be separated into a directory. +- Each plugin can be viewed as a `private` workspace package in a npm workspaces environment. + +```sh +path/to/assets/plugin/dynamic-plugins/ +└── dynamic-backstage.plugin-user-settings + ├── dist + ├── package.json + └── plugin-manifest.json +``` + +#### Boot time initialization + +**Dynamically build plugins** + +Only dynamic plugins can be hosted in the backend. The plugins has to provide the [manifest](#plugin-manifest). It the manifest is not found, plugin will not be considered dynamic and will be enabled for loading. + +**File system assets management** + +Once plugins are installed and fully dynamic, the asset server has to "discover" what plugins are loadable. + +This can be done by scanning the "assets" (dynamic-plugins in the dir example) for individual plugin manifests. Simple `glob` lookup for `path/dynamic-plugins/**/plugins-manifest.json` pattern should give required results. + +With this data the assets server should be able to generate a "plugin registry" with necessary metadata for the frontend. See the [Plugin Registry](#plugin-registry-1) for format details. + +The during the collection, the program can also add the `assetHost` prefix to the manifest location attribute described in the [webpack configuration](#sample-webpack-configuration) section. + +**Alternative means of assets management** + +To manage the assets by other means than just using filesystem, the file system should be treated only as an entry point for the plugins. Leveraging other means such as DB/cache/object storage should be also an option. The storage method should be mandated as long as a plugin registry and assets are available. + +**Singleton dependencies verification (optional)** + +With [module sharing](#module-sharing) and its singleton option, it is critical that the singleton dependencies versions are matching. + +The singleton configuration from the shell application can be extracted as well as from all dynamic plugins (included in the generated [manifest](#plugin-manifest)). A semver check can be used to prevent a plugin from initializing if a version mismatch is found. A version check also has to be made between plugins. New plugin singleton dependencies must match both the shell and all previously loaded plugins. + +#### Runtime updates + +It is fully expected that plugins can be added or removed at the asset server runtime. The same tasks can be performed as during boot time initialization. It is critical that the plugin registry is not cached and updated immediately if assets change. + +The server **should not remove older JS/CSS assets** immediately after updates. It is highly likely user can have older version still loaded in their browsers. A retention period is required for the assets. Using proper content hashes for builds will help with keep the duplicate assets to a minimum. + +#### Asset server API + +> NOTE Routes names are purely arbitrary at this point. + +The API can be very simplistic + +_GET /dynamic-plugins/plugin-registry_ + +Returns generated [Plugin Registry](#plugin-registry-1). + +\*GET /dynamic-plugins/assets/\*\* + +Static assets endpoint. Can be handled via the `express.static` middleware. + +### Plugin loading + +> NOTE: Only viable for the new frontend plugin system. + +The feature loader is a perfect place to initialize a new dynamic plugin as it already accepts async loaders. + +An example of [integration with scalprum](https://github.com/backstage/backstage/commit/23085aa8dfbc73d4648c100cf06b6b67e8e31764). + +#### Integration to core + +**Dynamic Feature configuration** + +The dynamic remote loading can be added directly into the [`createApp`](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/wiring/createApp.tsx#L234) function. + +The current `feature` type can be expanded with a `DynamicFrontendFeature` type: + +```ts +// current +type Features: (FrontendFeature | CreateAppFeatureLoader)[]; + +// with dynamic plugins +type DynamicFrontendFeature = { + name: string; + manifestLocation: string // URI with host enhanced with the "DEFAULT_API_PATH" or "assetHost" prefix +} + +type Features: (FrontendFeature | CreateAppFeatureLoader | DynamicFrontendFeature)[]; +``` + +**Dynamic Feature boot loading** + +Scalprum can be initialized with the metadata originating from the dynamic backend plugin. In order to properly load a plugin with `publicPath` set to `auto`, a small transformation of the manifest has to happen before it is loaded. A sample Scalprum config with such transformation: + +```TS +import { initialize } from '@scalprum/core'; +import { ScalprumProviderProps } from '@scalprum/react-core'; + +// get the pluginRegistry from the backend plugin +const pluginRegistry = await loadRemoteConfig() + +const pluginSDKOptions: ScalprumProviderProps['pluginSDKOptions'] = { + pluginLoaderOptions: { + transformPluginManifest: manifest => { + return { + ...manifest, + loadScripts: manifest.loadScripts.map(script => { + // Any additional transformations can happen here in case the manifest format needs to be adjusted further. + // the init scripts URL is enhanced with the asset location to add the host: + return `${manifest.customProperties.assetHost ?? DEFAULT_API_PATH}${script}`; + }), + }; + }, + }, +}; + +// Initialize Scalprum instance +const scalprum = initialize({ + appsConfig: pluginRegistry, + pluginLoaderOptions: pluginSDKOptions.pluginLoaderOptions, +}); +``` + +Because the [`appLoader`](https://github.com/backstage/backstage/blob/master/packages/frontend-app-api/src/wiring/createApp.tsx#L193) is already async, it is a perfect place to load the plugin registry and init the dynamic plugins. + +Initializing the dynamic feature is just a case of mapping the `DynamicFrontendFeature` to `FrontendFeature` via Scalprum: + +```ts +import { processManifest, getModule } from '@scalprum/core'; +// a ID of the module withing module federation container, can be customized, depends on the build +const DEFAULT_MODULE_NAME = 'pluginEntry'; + +async function loadScalprumFeature({ manifestLocation, name }) { + // initialize dynamic remote within scalprum cache + await processManifest(manifestLocation, name, DEFAULT_MODULE_NAME); + // gets the actual JS module from scalprum + const feature = await getModule(name, DEFAULT_MODULE_NAME); + + return feature; +} +const providedFeatures: FrontendFeature[] = []; +for (const entry of options?.features ?? []) { + if ('manifestLocation' in entry && 'name' in entry) { + // some error handling required + const dynamicFeature = loadScalprumFeature(entry); + providedFeatures.push(dynamicFeature); + } else if ('load' in entry) { + // plugin with load function + } else { + // #static plugin + } +} + +const app = createSpecializedApp({ + icons: options?.icons, + config, + features: [...discoveredFeatures, ...providedFeatures], + bindRoutes: options?.bindRoutes, +}).createRoot(); +``` + +The federated modules are now available as if they were static plugins. + ## Design Details -> NOTE The details are based on the Janus implementation of dynamic frontend plugins. The implementation leverages [Scalprum](https://github.com/scalprum/scaffolding) which is a Webpack based dynamic plugin manager for React applications. - ### Module federation implementation experiments +> NOTE Experiments were tested and documented. Decision was mated to leverage [Scalprum](https://github.com/scalprum/scaffolding) and [dynamic-plugins-sdk](https://github.com/openshift/dynamic-plugin-sdk). Instead of native webpack module federation the [@module-federation/enhanced](https://www.npmjs.com/package/@module-federation/enhanced) plugin will be used to ensure future compatibility with other bundlers. + Test should consist of trying to run permutations of webpack/Rspack/vite based shell apps/plugins and discover if we can freely choose any tool, or if we should restrict the tooling to just a subset of the available options. The outcome of initial testing is positive and it is possible to mix and match different build tools and consume different remote modules in a single shell application. @@ -217,70 +452,7 @@ This config ensures that only those modules (from @mui/material) that are used i This can be checked by debugging network traffic and shared scopes: -![notifications system architecture diagram](./scope-sharing.png) - -#### Limitations - -A limitation of module sharing is that each module name can only appear once in each context. This means that if you're trying to build a bundle of shared modules with a deeper dependency tree, you may end up needing to resolve conflicts by renaming some modules that appear multiple times with different versions. This will in turn limit sharing of those modules with other bundles. - -There is also a conflict with the chunk splitting currently used in the Backstage CLI that means that some singleton modules need to be configured to be eagerly loaded. More broadly we may also want to re-evaluate the chunk splitting strategy in the Backstage CLI when adding support for module federation. - -### Plugin manifest - -Each plugin should have a manifest file with important metadata. This metadata is used to load the remote assets to browser. The plugin manifest should be part of a build output. - -A manifest should have: - -- name of plugin -- how can be the init container accessed -- what is the base URL (assets pathname) -- name of the entry script(s) - -#### Scalprum compatible manifest - -```TS -type PluginManifest = { - name: string; - version: string; - dependencies?: Record; - customProperties?: AnyObject; - baseURL: string; - extensions: Extension[]; - loadScripts: string[]; - registrationMethod: 'callback' | 'custom'; - buildHash?: string; -} -``` - -```js -{ - "name": "backstage.plugin-github-actions", - "version": "0.6.6", - "extensions": [], - "registrationMethod": "callback", // where container init is available in browser - "baseURL": "auto", - "loadScripts": [ - "backstage.plugin-github-actions.804b91040fcbca6585ce.js" - ], - "buildHash": "804b91040fcbca6585ce1bcd4b1f8aa2" -} -``` - -##### `registrationMethod` - -Refers to [webpack output.libraryTarget](https://webpack.js.org/configuration/output/#outputlibrarytarget). - -Callback refers to `jsonp` and a the `custom` is used if other available target configuration has been picked. - -It is recommended to use either `global` or `jsonp` as these are environment agnostic (browser VS node). `jsonp` requires additional configurations. The `global` is preferable due to its simplicity. - -##### `baseURL` - -The `baseURL` is derived from [webpack public path](https://webpack.js.org/configuration/output/#outputpublicpath) - -The public path can also be set to `auto` to remove the need to specify origin or pathname and resolve the pathname at runtime. - -In Scalprum, some manifest [post processing](https://github.com/janus-idp/backstage-showcase/blob/main/packages/app/src/components/DynamicRoot/DynamicRoot.tsx#L323) is required to load the initial scripts if the `auto` baseURL is chosen. +![scope sharing sample](./scope-sharing.png) ### Plugin registry @@ -290,6 +462,7 @@ Plugin registry can be fairly simplistic. It can be as simple as JSON file conta type RegistryEntry = { name: string // plugin name manifestLocation: string // path to the manifest resource + assetHost: string // host part of URL to deal with the `auto` publicPath } // object for easy access @@ -308,7 +481,8 @@ Example of such registry { "backstage.plugin-github-actions": { "name": "backstage.plugin-github-actions", - "manifestLocation": "https://foo-bar.com/api/plugin-storage/plugin-manifest.json" + "manifestLocation": "/api/plugin-storage/plugin-manifest.json", + "assetHost": "https://foo-bar.com" }, // ..rest of plugins } @@ -318,6 +492,7 @@ Example of such registry { "name": "backstage.plugin-github-actions", "manifestLocation": "https://foo-bar.com/api/plugin-storage/plugin-manifest.json" + "assetHost": "https://foo-bar.com" }, // ...rest of plugins ] @@ -353,7 +528,6 @@ Example of registry with embedded manifests "pluginManifest": { "name": "backstage.plugin-github-actions", "version": "0.6.6", - "extensions": [], "registrationMethod": "callback", "baseURL": "auto", "loadScripts": [ @@ -372,7 +546,6 @@ Example of registry with embedded manifests "pluginManifest": { "name": "backstage.plugin-github-actions", "version": "0.6.6", - "extensions": [], "registrationMethod": "callback", "baseURL": "auto", "loadScripts": [ @@ -386,155 +559,374 @@ Example of registry with embedded manifests ``` -### Webpack build configuration +### Frontend plugin build configuration -> NOTE This is a sample current configuration in the Janus project. It uses Scalprum webpack based build plugin to generate the output. It does not take the [all options](#Module-federation-implementation-experiments) in consideration. This section will likely change considerably. +#### Build tooling -Part of a Scalprum tooling is also [dynamic-plugins-sdk](https://github.com/openshift/dynamic-plugin-sdk). Right now the package is a part of a different project, but that is about to change. More details in the [Scalprum roadmap](https://github.com/scalprum/scaffolding/blob/main/ROADMAP.md). +- webpack module bundler as a base +- use `@openshift/dynamic-plugin-sdk-webpack` to build dynamic frontend output + - with `@module-federation/enhanced` plugins instead of native webpack -#### Sample plugin configuration using Scalprum SDK +#### Module sharing + +Webpack allows for dependency/module sharing across different remote modules. For performance's sake, all dependencies should be shared. The sharing is version-restricted, and there can be multiple versions of the same package at the same time. This ensures that remote modules will have the necessary dependency version available, either from the shared scope or as a fallback from the respective build. + +**Singleton sharing** + +Singleton sharing is necessary for context sharing between remote modules and between remote modules and shell application. + +Only a minimal set of dependencies should be shared as singletons as it gives access to all packages to the parent context. Also **version checking is disabled for singleton packages!** + +Singleton "must have" list: + +- @scalprum/core +- @scalprum/react-core +- @openshift/dynamic-plugin-sdk +- react +- react-dom +- react-router-dom + +Singleton "might have to" list: + +- @material-ui/core/styles +- @material-ui/styles + +**@mui/_ and @material-ui/_ like packages** + +> NOTE: Can leverage [Generate module sharing map](#generate-module-sharing-map). + +Component libraries are usually large (thousands of svg icons in @mui/icons) and it is inefficient to share them as a whole. **Tree shaking is disabled for shared modules**. That means sharing large packages in multiple versions will result in a bloated JS in browsers. + +Sharing components like these can be done on module level. Instead of sharing the entire package, share its individual components: + +```js +const shared = { + // will result in bloated bundles and degraded performance + '@mui/material': { + requiredVersion: 'x.x.x', // semver + }, + // sharing per module level, only button module is included in the output + '@mui/material/Button': { + version: 'x.x.x', // might have to specifically set the version + requiredVersion: 'x.x.x', // semver + }, +}; +``` + +Sharing dependencies on module level leads to optimal bundle size. Even if all modules from a dependency are listed, Webpack will only bundle modules that are discovered in the source files. That means the list can be generated at build time with no risk of bloating the output with unused modules. + +The module sharing "matching" is based on import paths. If relative import paths are used `import {foo} from 'bar'` it will not be matched to the absolute import path of a shared module `import foo from 'bar/foo'`. Therefore absolute import paths have to enforced or path transformation has to happen at build time. For JS builds, babel can be used, for ts builds a `ts-patch` utility with custom plugins is an option. + +#### Limitations + +A limitation of module sharing is that each module name can only appear once in each context. This means that if you're trying to build a bundle of shared modules with a deeper dependency tree, you may end up needing to resolve conflicts by renaming some modules that appear multiple times with different versions. This will in turn limit sharing of those modules with other bundles. + +There is also a conflict with the chunk splitting currently used in the Backstage CLI that means that some singleton modules need to be configured to be eagerly loaded. More broadly we may also want to re-evaluate the chunk splitting strategy in the Backstage CLI when adding support for module federation. + +#### Webpack chunk optimization + +Custom webpack chunk splitting configuration can be problematic, especially when modifying runtime and vendor chunks. Module federation creates its own chunks. Shared modules that are not set up to be eagerly loaded (using the `eager` configuration) **cannot be included inside the entry script**. With a custom chunk splitting setup, they can potentially be forced into the entry script, causing runtime errors. On the other hand, some critical runtime code that has to be in the entry script, cna be forced out of it. This is particularly problematic for the "shell" application. + +Chunk optimization should be disabled for the initial implementation. + +#### Webpack Public path option + +The `publicPath` output config in webpack is a mandatory attribute for federated modules. However, at build time, it is impossible to guess where the assets are served from. From origin to the pathname, this is specific to each installation. + +We can leverage the [auto](https://webpack.js.org/guides/public-path/#automatic-publicpath) option. However this means that some manifest transformation has to happen at runtime when entry scripts are loaded into the browser. More on that in the [Plugin manifest](#plugin-manifest), [CDN Plugin](#dynamic-assets-server-plugin), and [Plugin loading](#plugin-loading) sections. + +#### Sample webpack configuration ```TS -import { DynamicRemotePlugin } from '@openshift/dynamic-plugin-sdk-webpack'; -const sharedModules = { - /** - * Mandatory singleton packages for sharing - */ - react: { - singleton: true, - requiredVersion: '*', - }, - 'react-dom': { - singleton: true, - requiredVersion: '*', - }, - 'react-router-dom': { - singleton: true, - requiredVersion: '*', - }, - 'react-router': { - singleton: true, - requiredVersion: '*', - }, - ... - /** - * Full list of shared modules in Janus - * https://github.com/janus-idp/backstage-plugins/blob/87a6b045c7b0f301ebed8b8f99dc1741fa2b044b/packages/cli/src/lib/bundler/scalprumConfig.ts#L16 - */ +type DynamicPluginConfig = { + name: string; + version?: string; +}; +// { name: "@backstage/plugin-api-docs", version: "1.0.0" } +// generates "dynamic-backstage.plugin-api-docs" +const getName = (plugin: DynamicPluginConfig) => { + let pluginName: string; + if (plugin.name.includes('/')) { + const fragments = plugin.name.split('/'); + pluginName = `${fragments[0].replace('@', '')}.${fragments[1]}`; + } else { + pluginName = plugin.name; + } + return `dynamic-${pluginName}`; } + +// plugin = { name: "@backstage/plugin-api-docs", version: "1.0.0" } based on package.json +const pluginName = getName(plugin); + +const { ModuleFederationPlugin, ContainerPlugin } = require('@module-federation/enhanced'); + const dynamicPluginPlugin = new DynamicRemotePlugin({ extensions: [], - sharedModules, - entryScriptFilename: `${options.pluginMetadata.name}.[contenthash].js`, - pluginMetadata: { - // version cna be used from the package.json version field - version: '1.0.0', - /** - * Name can be easily derived from the plugin name - * https://github.com/janus-idp/backstage-plugins/blob/87a6b045c7b0f301ebed8b8f99dc1741fa2b044b/packages/cli/src/commands/export-dynamic-plugin/frontend.ts#L40 - */ - name: 'backstage.plugin-github-actions', - /** - * Path to the plugin entry point. - * It can default to the same entry point as in regular build. - * Plugins can expose multiple modules. We have found that one is sufficient from backstage plugins. - */ - exposedModules: { - PluginRoot: './src/index.ts' + sharedModules, // from shared config and generated from `package.json` dependencies + entryScriptFilename: `[name].[contenthash].js`, // using contenthash for cashing purposes + moduleFederationSettings: { + libraryType: 'global', // use the globalThis object rather than jsonp + // instruct the sdk plugin to use @module-federation/enhanced tooling + pluginOverride: { + ModuleFederationPlugin, + ContainerPlugin, } }, -}); -``` - -The `DynamicRemotePlugin` webpack plugin takes care of the rest, including the manifest generation. - -#### Sample plugin raw webpack configuration - -The Scalprum config translates to a following base webpack plugin configuration: - -```JS -import { container } from 'webpack'; - -const dynamicPlugin = new container.ModuleFederationPlugin({ - name: 'backstage.plugin-github-actions', - library: { - type: 'global', - /** - * Some library.type has name limitation - * for example, if "type": "var" is used, the library.name can contain the "-" character - */ - name: 'backstage.plugin-github-actions' + pluginMetadata: { + name: pluginName, + version: plugin.version || '0.0.0', + exposedModules: { + // path to the default export of the frontend plugin entry point + // the path should be sourced from the "main" attribute withing package.json + pluginEntry: './src/index.ts', + }, }, - filename: 'backstage.plugin-github-actions.[contenthash].js', - exposes: { - PluginRoot: './src/index.ts' - }, - // list of shared modules like "react", "react-dom", etc - shared: sharedModules, }); + + +const config: Configuration = { + context: pluginRoot, + output: { + chunkFilename: `${pluginName}.[contenthash].js`, + path: path.resolve(pluginRoot, 'dist'), + publicPath: 'auto', + }, + entry: {}, + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + }, + // extend existing webpack plugins that are relevant for dynamic plugins + plugins: [dynamicPluginPlugin, ...], + module: { + rules: [ + { + // needs TS config as well + test: /\.js$/, + exclude: /(node_modules)/, + use: { + loader: 'swc-loader', + }, + }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset/resource', + }, + // additional loaders for CSS if necessary, can use the same as the current backstage shell app + ], + }, + }; + return config; ``` -Additional plugin would have to be written from scratch to generate the plugin manifest file. +**Webpack plugins** -### Sample shell application webpack configuration +Existing webpack plugins can be used. Subset of shared plugins should be identified from current configurations and shared between the dynamic plugins and the shell application. -Shell application (backstage) has very simple module federation configuration. It does not require anything special to be able to inject plugins. Main requirement is to provide core shared packages like react and react-dom. +### Plugin manifest -```JS -import { container } from 'webpack'; +Plugin manifest is a simple JSON file containing critical metadata to initialize a remote plugin. -const scalprumPlugin = new container.ModuleFederationPlugin({ - name: 'backstageHost', - filename: 'backstageHost.[contenthash].js', - // same share modules list as with plugin config - shared: [sharedModules], -}); +The `@openshift/dynamic-plugin-sdk-webpack` plugin will output manifest file in the following format: + +```ts +type PluginManifest = { + name: string; + version: string; + dependencies?: Record; + customProperties?: AnyObject; + baseURL: string; + loadScripts: string[]; + registrationMethod: 'callback' | 'custom'; + buildHash?: string; +}; ``` -Nothing else is required. +This manifest is directly usable in Scalprum at runtime. -### Plugin storage +**baseURL** -Where will the dynamic plugin assets be hosted? Module federation does not strictly require the remote assets to be all hosted on the same origin. +This attribute is populated by the `output.publicPath` configuration. In this case, it will be a value of `auto`. That is not usable by the Scalprum loader. It has to be a URL. -Theoretically plugins can be hosted on some "public CDN" which is detached from individual backstage instances. +Because the origin can't be reliably "guessed" at build time, the value has to be filled by the CDN server. The attribute can be either overridden or, preferably, added to the `customProperties` attribute. -Assets can be also be hosted in the same way as they have always been. +A default`DEFAULT_API_PATH` should be used. Covering majority of the use cases. -It's important to consider old deployments as part of this storage, to make sure that dynamic chunks are still available to users who have not yet refreshed the page to load the latest version of the app. +```ts +type CustomProperties = { + assetHost?: string; // for local development should be http://localhost:PORT + // for deployment, should match the api host https://backstage.company.org +}; +``` -### Plugin discovery +Any additional metadata about plugins, can be also stored to the `customProperties`. Either at build time or runtime. That depends on the use case. The values can be read at runtime during plugin initialization. -How to notify/send data to browser +```ts +type CustomProperties = { + backstage: Record; +}; +``` -### Plugin initialization +**Exposing singleton packages config** -Currently all plugins have to be initialized at UI bootstrap (page refresh). The new UI async API can be used to initialize the remote assets. +Singleton shared packages do not follow the version matching strategy. It is a weak point in the dependency sharing strategy. Exposing the data in the manifest files provides the option to check the singleton configuration at runtime and prevent runtime crashes of plugins. -The `CreateAppFeatureLoader` can be leveraged to initialize the remote container. +```ts +type SingletonShareConfig = { + singleton: true; + version: string; + packageName: string; +}; -```TS -const allPluginManifests = {...} // get this from the config +type SingletonShareObject = { + [packageName: string]: SingletonShareConfig; +}; +``` -const asyncFeatureLoaders: CreateAppFeatureLoader[] = Object.values(allPluginManifests).map(({ manifest }) => { - return { - getLoaderName: () => manifest.name, - load: (options) => { - // initialize the remote container, depends on tooling - const plugin = initDynamicPlugin(manifest) - } +**Backstage plugin manifest** + +The plugin manifest for backstage with the additional attributes: + +```ts +type SingletonShareConfig = { + singleton: true; + version: string; + packageName: string; +}; + +type SingletonShareObject = { + [packageName: string]: SingletonShareConfig; +}; + +type CustomProperties = { + assetHost?: string; + backstage: Record; + singletonPackages: SingletonShareObject; +}; + +type PluginManifest = { + name: string; + version: string; + dependencies?: Record; + customProperties: CustomProperties; + baseURL: 'auto'; + loadScripts: string[]; + registrationMethod: 'custom'; + buildHash?: string; +}; +``` + +### Simplistic minimal example of module map generator + +```js +const path = require('path'); +const typescript = require('typescript'); +const fs = require('fs'); + +/** @type {typescript.CompilerOptions} */ +const defaultCompilerOptions = { + targe: 'es2015', + module: 'es2015', + esModuleInterop: true, + allowJs: true, + strict: false, + skipLibCheck: true, + noEmit: true, + // needs to be configured to the root directory dependency + rootDir: path.resolve(__dirname, 'dist'), + baseUrl: path.resolve(__dirname, 'dist'), +}; + +// these need to be configured, usually getting metadata from the lib package.json +const rootDit = path.resolve(__dirname, 'dist'); +const base = path.resolve(__dirname, 'dist', 'index.js'); + +function getDynamicModuleMap(libName) { + const compiler = typescript.createCompilerHost(defaultCompilerOptions, [ + rootDit, + ]); + const program = typescript.createProgram( + [base], + defaultCompilerOptions, + compiler, + ); + + const moduleResolutionCache = typescript.createModuleResolutionCache( + rootDit, + x => x, + defaultCompilerOptions, + ); + const errorDiagnostics = typescript + .getPreEmitDiagnostics(program) + .filter(d => d.category === typescript.DiagnosticCategory.Error); + if (errorDiagnostics.length > 0) { + const { getCanonicalFileName, getCurrentDirectory, getNewLine } = compiler; + + console.error( + typescript.formatDiagnostics(errorDiagnostics, { + getCanonicalFileName, + getCurrentDirectory, + getNewLine, + }), + ); + + throw new Error(`Detected TypeScript errors while parsing modules`); } -}) + const typeChecker = program.getTypeChecker(); -const app = createApp({ - features: [ - ...asyncFeatureLoaders, - // rest of classic features - ] -}) + /** @param {typescript.SourceFile} sourceFile */ + const getExportNames = sourceFile => + typeChecker + .getExportsOfModule(typeChecker.getSymbolAtLocation(sourceFile)) + .map(symbol => symbol.getName()); + + const baseExports = getExportNames(program.getSourceFile(base)); + const compilerDir = compiler.getCurrentDirectory(); + const resolvedModules = baseExports + .map(name => { + const res = typescript.resolveModuleName( + name, + base, + defaultCompilerOptions, + compiler, + moduleResolutionCache, + ); + if (res.resolvedModule?.resolvedFileName) { + return { + name, + file: + libName + + res.resolvedModule.resolvedFileName + .replace(compilerDir, '') + .replace(/\.js$/, ''), + }; + } + return undefined; + }) + .filter(x => x !== undefined); + + const moduleMapFileName = path.resolve(__dirname, 'module-map.json'); + fs.writeFileSync(moduleMapFileName, JSON.stringify(resolvedModules, null, 2)); +} + +const libName = '@mui/foo'; + +getDynamicModuleMap(libName); + +/** + * [ + * { + * "name": "foo", + * "file": "@mui/foo/dist/foo" + * }, + * { + * "name": "baz", + * "file": "@mui/foo/dist/baz" + * } + * ] + */ ``` ### Plugin declarative configuration diff --git a/beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json b/beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json new file mode 100644 index 0000000000..4777f81f78 --- /dev/null +++ b/beps/0002-dynamic-frontend-plugins/dependency-sharing-schema.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "additionalProperties": false, + "properties": { + "shareModules": { + "type": "object", + "patternProperties": { + "^(@[^/]+/)?[^/]+$": { + "$ref": "#/definitions/sharedModule" + } + }, + "additionalProperties": false + }, + "excludeModules": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "definitions": { + "sharedModule": { + "type": "object", + "required": ["requiredVersion"], + "properties": { + "requiredVersion": { + "$ref": "#/definitions/semverString" + }, + "version": { + "$ref": "#/definitions/semverString" + }, + "singleton": { + "type": "boolean" + } + } + }, + "semverString": { + "type": "string", + "pattern": "^(\\^|\\~)?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } +} diff --git a/beps/0002-dynamic-frontend-plugins/share-config-sample.json b/beps/0002-dynamic-frontend-plugins/share-config-sample.json new file mode 100644 index 0000000000..fe5c9943ad --- /dev/null +++ b/beps/0002-dynamic-frontend-plugins/share-config-sample.json @@ -0,0 +1,17 @@ +{ + "$schema": "./dependency-sharing-schema.json", + "shareModules": { + "foo-bar": { + "requiredVersion": "1.0.0" + }, + "@bax/foo-bar": { + "requiredVersion": "1.0.0", + "singleton": true + }, + "@bax-baz/foo-bar2": { + "requiredVersion": "^1.2.0", + "version": "1.4.0" + } + }, + "excludeModules": ["foo-bar-baz"] +} diff --git a/beps/0009-plugin-metadata/README.md b/beps/0009-plugin-metadata/README.md index c0ae487daa..59101f121c 100644 --- a/beps/0009-plugin-metadata/README.md +++ b/beps/0009-plugin-metadata/README.md @@ -1,6 +1,6 @@ --- title: Plugin Metadata -status: implementable +status: implemented authors: - '@Rugvip' owners: @@ -10,16 +10,8 @@ project-areas: creation-date: yyyy-mm-dd --- - - # BEP: Plugin Metadata - - [**Discussion Issue**](https://github.com/backstage/backstage/issues/25076) - [Summary](#summary) diff --git a/docs/assets/auth/github-provider-not-configured-to-support-sign-in.png b/docs/assets/auth/github-provider-not-configured-to-support-sign-in.png new file mode 100644 index 0000000000..1bbd94ecc3 Binary files /dev/null and b/docs/assets/auth/github-provider-not-configured-to-support-sign-in.png differ diff --git a/docs/assets/auth/github-unable-to-reolve-identity.png b/docs/assets/auth/github-unable-to-reolve-identity.png new file mode 100644 index 0000000000..622fd75a4a Binary files /dev/null and b/docs/assets/auth/github-unable-to-reolve-identity.png differ diff --git a/docs/assets/getting-started/login-screen.png b/docs/assets/getting-started/login-screen.png new file mode 100755 index 0000000000..1af52cb2c9 Binary files /dev/null and b/docs/assets/getting-started/login-screen.png differ diff --git a/docs/auth/atlassian/provider.md b/docs/auth/atlassian/provider.md index c8db009805..96271f04a0 100644 --- a/docs/auth/atlassian/provider.md +++ b/docs/auth/atlassian/provider.md @@ -79,8 +79,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-atlassian-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-atlassian-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `atlassianAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/auth0/provider.md b/docs/auth/auth0/provider.md index dcd02d226f..ae44114906 100644 --- a/docs/auth/auth0/provider.md +++ b/docs/auth/auth0/provider.md @@ -64,4 +64,4 @@ Auth0 requires a session, so you need to give the session a secret key. To add the provider to the frontend, add the `auth0AuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/aws-alb/provider.md b/docs/auth/aws-alb/provider.md index dc878e8971..8f21b7dbf0 100644 --- a/docs/auth/aws-alb/provider.md +++ b/docs/auth/aws-alb/provider.md @@ -8,37 +8,6 @@ description: Adding AWS ALB as an authentication provider in Backstage Backstage can de deployed behind [AWS Application Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html) and get the user seamlessly authenticated. -## Installation - -### Backend - -:::note -These instructions are written for the [new backend system](../../backend-system/index.md). -::: - -Add the `@backstage/plugin-auth-backend-module-aws-alb-provider` to your backend installation. - -```sh -# From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-aws-alb-provider -``` - -Then, add it to your backend's source, - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); - -backend.add(import('@backstage/plugin-auth-backend')); -// highlight-add-next-line -backend.add(import('@backstage/plugin-auth-backend-module-aws-alb-provider')); - -await backend.start(); -``` - -### Frontend - -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. - ## Configuration The provider configuration can be added to your `app-config.yaml` under the root @@ -57,4 +26,40 @@ auth: - resolver: emailLocalPartMatchingUserEntityName ``` +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-aws-alb-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-aws-alb-provider')); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `awsalb` as the provider name. + If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index b03201893c..1ca35437ba 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -37,6 +37,13 @@ auth: development: clientId: ${AUTH_BITBUCKET_CLIENT_ID} clientSecret: ${AUTH_BITBUCKET_CLIENT_SECRET} + signIn: + resolvers: + # typically you would pick one of these + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: userIdMatchingUserEntityAnnotation + - resolver: usernameMatchingUserEntityAnnotation ``` The Bitbucket provider is a structure with two configuration keys: @@ -45,44 +52,42 @@ The Bitbucket provider is a structure with two configuration keys: `b59241722e3c3b4816e2` - `clientSecret`: The Secret tied to the generated Key. +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. +- `userIdMatchingUserEntityAnnotation`: Matches the `userId` from the auth provider with the User entity that has a matching `bitbucket.org/user-id` annotation. If no match is found it will throw a `NotFoundError`. +- `usernameMatchingUserEntityAnnotation`: Matches the `username` from the auth provider with the User entity that has a matching `bitbucket.org/username` annotation. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-bitbucket-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-bitbucket-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `bitbucketAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). - -## Using Bitbucket for sign-in - -In order to use the Bitbucket provider for sign-in, you must configure it with a -`signIn.resolver`. See the -[Sign-In Resolver documentation](../identity-resolver.md) for more details on -how this is done. Note that for the Bitbucket provider, you'll want to use -`bitbucket` as the provider ID, and `providers.bitbucket.create` for the provider -factory. - -The `@backstage/plugin-auth-backend` plugin also comes with two built-in -resolvers that can be used if desired. The first one is the -`bitbucketUsernameSignInResolver`, which identifies users by matching their -Bitbucket username to `bitbucket.org/username` annotations of `User` entities in -the catalog. Note that you must populate your catalog with matching entities or -users will not be able to sign in. - -The second resolver is the `bitbucketUserIdSignInResolver`, which works the -same way, but uses the Bitbucket user ID instead, and matches on the -`bitbucket.org/user-id` annotation. - -The following is an example of how to use one of the built-in resolvers: - -```ts -import { providers } from '@backstage/plugin-auth-backend'; - -// ... - providerFactories: { - bitbucket: providers.bitbucket.create({ - signIn: { - resolver: - providers.bitbucket.resolvers.usernameMatchingUserEntityAnnotation(), - }, - }), - }, -``` +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md index 96c7db263e..82aebc20b2 100644 --- a/docs/auth/bitbucketServer/provider.md +++ b/docs/auth/bitbucketServer/provider.md @@ -10,8 +10,8 @@ users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. ## Create an Application Link in Bitbucket Server -To add Bitbucket Server authentication, you must create an outgoing application link. Follow the steps described in -the [Bitbucket Server documentation](https://confluence.atlassian.com/bitbucketserver/configure-an-outgoing-link-1108483656.html) +To add Bitbucket Server authentication, you must create an incoming application link. Follow the steps described in +the [Bitbucket Server documentation](https://confluence.atlassian.com/bitbucketserver/configure-an-incoming-link-1108483657.html) to create one. ## Configuration @@ -37,7 +37,7 @@ The Bitbucket Server provider is a structure with two configuration keys: ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `bitbucketServerAuthApi` reference and `SignInPage` component as shown -in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +in [Adding the provider to the sign-in page](../index.md#sign-in-configuration). ## Using Bitbucket Server for sign-in diff --git a/docs/auth/cloudflare/access.md b/docs/auth/cloudflare/access.md deleted file mode 100644 index 52d9fdfc21..0000000000 --- a/docs/auth/cloudflare/access.md +++ /dev/null @@ -1,174 +0,0 @@ ---- -id: provider -title: Cloudflare Access Provider -sidebar_label: Cloudflare Access -description: Adding Cloudflare Access as an authentication provider in Backstage ---- - -Similar to GCP IAP Proxy Provider or AWS ALB provider, developers can offload authentication -support to Cloudflare Access. - -This tutorial shows how to use authentication on Cloudflare Access sitting in -front of Backstage. - -It is assumed a Cloudflare tunnel is already serving traffic in front of a -Backstage instance configured to serve the frontend app from the backend and is -already gated using Cloudflare Access. - -## Configuration - -Let's start by adding the following `auth` configuration in your -`app-config.yaml` or `app-config.production.yaml` or similar: - -```yaml -auth: - providers: - cfaccess: - # You can find the team name in the Cloudflare Zero Trust dashboard. - teamName: - # This service tokens section is optional -- you only need it if you have - # some Cloudflare Service Tokens that you want to be able to log in to your - # Backstage instance. - serviceTokens: - - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' - subject: 'bot-user@your-company.com' - # This picks what sign in resolver(s) you want to use. - signIn: - resolvers: - - resolver: emailMatchingUserEntityProfileEmail -``` - -This config section must be in place for the provider to load at all. - -The `signIn` section picks what sign-in resolver(s) to use for sign-in attempts. -It is responsible for matching the upstream provider's sign-in result to a -corresponding Backstage identity, or to throw an error if the attempt should be -rejected for any reason. The `emailMatchingUserEntityProfileEmail` is a common -choice: it tries to match the email of the signed-in user to a `User` kind -entity in the catalog whose profile email matches that. - -If the builtin sign in resolvers do not match your needs, you can skip the -`signIn` section and instead [provide a custom resolver](#advanced-custom-sign-in-resolver). - -## Backend Changes - -We need to add the provider package as a dependency to our backend: - -```bash title="from your Backstage root directory" -yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-cloudflare-access-provider -``` - -And to tell the backend to load it: - -```ts title="in packages/backend/src/index.ts" -backend.add(import('@backstage/plugin-auth-backend')); -/* highlight-add-start */ -backend.add( - import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), -); -/* highlight-add-end */ -``` - -Now the backend is ready to serve auth requests on the -`/api/auth/cfaccess/refresh` endpoint. All that's left is to update the frontend -sign-in mechanism to poll that endpoint through Cloudflare Access, on the user's -behalf. - -## Frontend Changes - -It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in your app like this: - -```tsx title="in packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => , - }, - /* highlight-add-end */ - // ... -}); -``` - -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for -pointers on how to set up the sign-in page to also work smoothly for local -development. - -## Advanced: Custom Sign-in Resolver - -If none of the built-in sign in resolvers fit your needs, you need to provide a -customized version of the module. Now you should _not_ -`backend.add(import(...))`, instead you will do the following. - -```ts title="in packages/backend/plugin/auth.ts" -/* highlight-add-start */ -import { createCloudflareAccessAuthenticator } from '@backstage/plugin-auth-backend-module-cloudflare-access-provider'; -import { - coreServices, - createBackendModule, -} from '@backstage/backend-plugin-api'; -import { - authProvidersExtensionPoint, - createProxyAuthProviderFactory, -} from '@backstage/plugin-auth-node'; - -const customAuth = createBackendModule({ - // This ID must be exactly "auth" because that's the plugin it targets - pluginId: 'auth', - // This ID must be unique, but can be anything - moduleId: 'custom-auth-provider', - register(reg) { - reg.registerInit({ - deps: { - providers: authProvidersExtensionPoint, - cache: coreServices.cache, - }, - async init({ providers, cache }) { - providers.registerProvider({ - // This ID must match the actual provider config, e.g. addressing - // auth.providers.github means that this must be "github". - providerId: 'cfaccess', - // Use createProxyAuthProviderFactory instead if it's one of the proxy - // based providers rather than an OAuth based one - factory: createProxyAuthProviderFactory({ - authenticator: createCloudflareAccessAuthenticator({ cache }), - async signInResolver(info, ctx) { - // This is where the body of the sign-in resolver goes! - const { profile } = info; - if (!profile.email) { - throw new Error( - 'Login failed, user profile does not contain an email', - ); - } - return ctx.signInWithCatalogUser({ - filter: { - 'spec.profile.email': profile.email, - }, - }); - }, - }), - }); - }, - }); - }, -}); -/* highlight-add-end */ - -backend.add(import('@backstage/plugin-auth-backend')); -/* highlight-remove-start */ -backend.add( - import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), -); -/* highlight-remove-end */ -/* highlight-add-next-line */ -backend.add(customAuth); -``` - -The body of the sign-in resolver is up to you to write! The example code above -is just a copy of what `emailMatchingUserEntityProfileEmail` does. The `info` -parameter contains all of the results of the sign-in attempt so far. The `ctx` -context [has several useful functions](https://backstage.io/docs/reference/plugin-auth-node.authresolvercontext/) -for issuing tokens in various ways. diff --git a/docs/auth/cloudflare/provider.md b/docs/auth/cloudflare/provider.md new file mode 100644 index 0000000000..516ea9d647 --- /dev/null +++ b/docs/auth/cloudflare/provider.md @@ -0,0 +1,82 @@ +--- +id: provider +title: Cloudflare Access Provider +sidebar_label: Cloudflare Access +description: Adding Cloudflare Access as an authentication provider in Backstage +--- + +Similar to GCP IAP Proxy Provider or AWS ALB provider, developers can offload authentication +support to Cloudflare Access. + +This tutorial shows how to use authentication on Cloudflare Access sitting in +front of Backstage. + +It is assumed a Cloudflare tunnel is already serving traffic in front of a +Backstage instance configured to serve the frontend app from the backend and is +already gated using Cloudflare Access. + +## Configuration + +Let's start by adding the following `auth` configuration in your +`app-config.yaml` or `app-config.production.yaml` or similar: + +```yaml +auth: + providers: + cfaccess: + # You can find the team name in the Cloudflare Zero Trust dashboard. + teamName: + # This service tokens section is optional -- you only need it if you have + # some Cloudflare Service Tokens that you want to be able to log in to your + # Backstage instance. + serviceTokens: + - token: '1uh2fh19efvfh129f1f919u21f2f19jf2.access' + subject: 'bot-user@your-company.com' + # This picks what sign in resolver(s) you want to use. + signIn: + resolvers: + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName +``` + +This config section must be in place for the provider to load at all. + +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-cloudflare-access-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-cloudflare-access-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `cfaccess` as the provider name. + +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/github/provider.md b/docs/auth/github/provider.md index 1873730bd6..76b63c6617 100644 --- a/docs/auth/github/provider.md +++ b/docs/auth/github/provider.md @@ -26,6 +26,14 @@ Settings for local development: - Homepage URL: `http://localhost:3000` - Authorization callback URL: `http://localhost:7007/api/auth/github/handler/frame` +### Difference between GitHub Apps and GitHub OAuth Apps + +GitHub Apps handle OAuth scope at the app installation level, meaning that the +`scope` parameter for the call to `getAccessToken` in the frontend has no +effect. When calling `getAccessToken` in open source plugins, one should still +include the appropriate scope, but also document in the plugin README what +scopes are required for GitHub Apps. + ## Configuration The provider configuration can then be added to your `app-config.yaml` under the @@ -79,16 +87,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-github-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `githubAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). - -## Difference between GitHub Apps and GitHub OAuth Apps - -GitHub Apps handle OAuth scope at the app installation level, meaning that the -`scope` parameter for the call to `getAccessToken` in the frontend has no -effect. When calling `getAccessToken` in open source plugins, one should still -include the appropriate scope, but also document in the plugin README what -scopes are required for GitHub Apps. +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/gitlab/provider.md b/docs/auth/gitlab/provider.md index f5613582fd..73546411bf 100644 --- a/docs/auth/gitlab/provider.md +++ b/docs/auth/gitlab/provider.md @@ -78,8 +78,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gitlab-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-gitlab-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `gitlabAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index 7e6178154c..6479ac7268 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -58,102 +58,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. -## Backend Changes +## Backend Installation -There is a module for this provider that you will need to add to your backend. +To add the provider to the backend we will first need to install the package by running this command: -First you'll want to run this command to add the module: - -```sh - yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gcp-iap-provider +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-gcp-iap-provider ``` -Then you will need to add this to your backend: - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); +Then we will need to add this line: +```ts title="in packages/backend/src/index.ts" backend.add(import('@backstage/plugin-auth-backend')); /* highlight-add-start */ backend.add(import('@backstage/plugin-auth-backend-module-gcp-iap-provider')); /* highlight-add-end */ ``` -### Legacy Backend Changes +## Adding the provider to the Backstage frontend -If you are still using the legacy backend you will need to make the changes outlined here. +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `gcp-iap` as the provider name. -This provider is not enabled by default in the auth backend code, because besides the config section above, it also needs to be given one or more callbacks in actual code as well as described below. - -Add a `providerFactories` entry to the router in -`packages/backend/src/plugins/auth.ts`. - -```ts title="packages/backend/src/plugins/auth.ts" -import { providers } from '@backstage/plugin-auth-backend'; -import { stringifyEntityRef } from '@backstage/catalog-model'; - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - providerFactories: { - 'gcp-iap': providers.gcpIap.create({ - // Replace the auth handler if you want to customize the returned user - // profile info (can be left out; the default implementation is shown - // below which only returns the email). You may want to amend this code - // with something that loads additional user profile data out of e.g. - // GSuite or LDAP or similar. - async authHandler({ iapToken }) { - return { profile: { email: iapToken.email } }; - }, - signIn: { - // You need to supply an identity resolver, that takes the profile - // and the IAP token and produces the Backstage token with the - // relevant user info. - async resolver({ profile, result: { iapToken } }, ctx) { - // Somehow compute the Backstage token claims. Just some sample code - // shown here, but you may want to query your LDAP server, or - // GSuite or similar, based on the IAP token sub/email claims - const id = iapToken.email.split('@')[0]; - const sub = stringifyEntityRef({ kind: 'User', name: id }); - const ent = [ - sub, - stringifyEntityRef({ kind: 'Group', name: 'team-name' }), - ]; - return ctx.issueToken({ claims: { sub, ent } }); - }, - }, - }), - }, - }); -} -``` - -Now the backend is ready to serve auth requests on the -`/api/auth/gcp-iap/refresh` endpoint. All that's left is to update the frontend -sign-in mechanism to poll that endpoint through the IAP, on the user's behalf. - -## Frontend Changes - -It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in `packages/app/src/App.tsx` like this: - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => , - }, - /* highlight-add-end */ - // .. -}); -``` - -See the [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) section for more information. +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/google/provider.md b/docs/auth/google/provider.md index f7802d43f7..b19d21d63f 100644 --- a/docs/auth/google/provider.md +++ b/docs/auth/google/provider.md @@ -24,9 +24,9 @@ To support Google authentication, you must create OAuth credentials: - Add yourself as a test user, if using External user type 6. Set **Application Type** to `Web Application` with these settings: - `Name`: Backstage (or your custom app name) - - `Authorized JavaScript origins`: http://localhost:3000 + - `Authorized JavaScript origins`: - `Authorized Redirect URIs`: - http://localhost:7007/api/auth/google/handler/frame + 7. Click Create ## Configuration @@ -72,8 +72,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-google-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-google-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `googleAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/guest/provider.md b/docs/auth/guest/provider.md index b28274d2ba..b541178571 100644 --- a/docs/auth/guest/provider.md +++ b/docs/auth/guest/provider.md @@ -25,8 +25,7 @@ This will only work with the new backend system. There is no support for this in Add the `@backstage/plugin-auth-backend-module-guest-provider` to your backend installation. -```sh -# From your Backstage root directory +```sh title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-guest-provider ``` diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 4140276df8..dbebf3056c 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -124,6 +124,8 @@ The list of available resolvers is different for each provider, since they often depend on the information model returned from the upstream provider service. Consult the documentation of the respective provider to find the list. +In the example above `emailMatchingUserEntityProfileEmail` and `emailLocalPartMatchingUserEntityName` are common to all auth providers and `usernameMatchingUserEntityName` is specific to GitHub. + ### Building Custom Resolvers If the builtins don't work for you, you can also provide a completely custom @@ -191,7 +193,7 @@ backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); backend.add(customAuth); ``` -Check out [the naming patterns article](../backend-system/architecture/07-naming-patterns.md) for what rules +Check out [the naming patterns article](../backend-system/architecture/08-naming-patterns.md) for what rules apply regarding how to form valid IDs. In this example we also put the module declaration directly in `packages/backend/src/index.ts` but that's just for simplicity. You can place it anywhere you like, including in other packages, and @@ -401,3 +403,22 @@ const customAuth = createBackendModule({ ``` Remember to `backend.add` the created module just like above. + +## Common Sign-In Resolver Errors + +There are two common Sign-In Resolver errors you might run into. + +First is: "The 'Auth Provider Name' provider is not configured to support sign-in". Here is what this looks like for the GitHub Auth provider: + +![The GitHub provider is not configured to support sign-in](../assets/auth/github-provider-not-configured-to-support-sign-in.png) + +This error can be caused by the following: + +- The `signIn.resolvers` have not be added to your Auth Provider configuration. Adding this will resolve the error. +- There is a syntax error in your Auth Provider configuration. Running `yarn backstage-cli config:check --strict` will help identify the syntax error. + +The second common error is: "Failed to sign-in, unable to resolve user identity". Here is what this looks like for the GitHub Auth provider: + +![Failed to sign-in, unable to resolve user identity](../assets/auth/github-unable-to-reolve-identity.png) + +This error is caused by the Sign-In Resolver you configured being unable to find a matching User in the Catalog. To fix this you need to import User, and Group, data from some source of truth for this data at your Organization. To do this you can use one of the existing Org Data providers like the ones for [Entra ID (Azure AD/MS Graph)](../integrations/azure/org.md), [GitHub](../integrations/github/org.md), [GitLab](../integrations/gitlab/org.md), etc. or if none of those fit your needs you can create a [Custom Entity Provider](../features/software-catalog/external-integrations.md#custom-entity-providers). diff --git a/docs/auth/index.md b/docs/auth/index.md index f925e52917..5188d7e2a4 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -12,12 +12,7 @@ access to external resources. :::note Note -Identity management and the Sign-In page in Backstage is NOT a method for blocking -access for unauthorized users. The identity system only serves to provide a personalized -experience and access to a Backstage Identity Token, which can be passed to backend plugins. -This also means that your Backstage backend APIs are by default unauthenticated. -Thus, if your Backstage instance is exposed to the Internet, anyone can access -information in the Backstage. You can learn more [here](../overview/threat-model.md#integrator-responsibilities). +Identity management and the Sign-In page in Backstage will only block external access when using the new backend system, without setting `backend.auth.dangerouslyDisableDefaultAuthPolicy` in configuration. Even so, the frontend bundle is not protected from external access, protecting it requires the use of the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/). You can learn more about this in the [Threat Model](../overview/threat-model.md#operator-responsibilities). ::: @@ -31,7 +26,7 @@ Backstage comes with many common authentication providers in the core library: - [Azure Easy Auth](microsoft/azure-easyauth.md) - [Bitbucket](bitbucket/provider.md) - [Bitbucket Server](bitbucketServer/provider.md) -- [Cloudflare Access](cloudflare/access.md) +- [Cloudflare Access](cloudflare/provider.md) - [GitHub](github/provider.md) - [GitLab](gitlab/provider.md) - [Google](google/provider.md) @@ -158,7 +153,7 @@ Some auth providers are so-called "proxy" providers, meaning they're meant to be behind an authentication proxy. Examples of these are [Amazon Application Load Balancer](https://github.com/backstage/backstage/blob/master/contrib/docs/tutorials/aws-alb-aad-oidc-auth.md), [Azure EasyAuth](./microsoft/azure-easyauth.md), -[Cloudflare Access](./cloudflare/access.md), +[Cloudflare Access](./cloudflare/provider.md), [Google Identity-Aware Proxy](./google/gcp-iap-auth.md) and [OAuth2 Proxy](./oauth2-proxy/provider.md). diff --git a/docs/auth/microsoft/azure-easyauth.md b/docs/auth/microsoft/azure-easyauth.md index 5fec3354d7..c2a6eab218 100644 --- a/docs/auth/microsoft/azure-easyauth.md +++ b/docs/auth/microsoft/azure-easyauth.md @@ -7,87 +7,6 @@ description: Adding Azure's EasyAuth Proxy as an authentication provider in Back The Backstage `core-plugin-api` package comes with a Microsoft authentication provider that can authenticate users using Microsoft Entra ID (formerly Azure Active Directory) for PaaS service hosted in Azure that support Easy Auth, such as Azure App Services. -## Backend Changes - -Add the following into your `app-config.yaml` under the root `auth` configuration: - -```yaml title="app-config.yaml" -auth: - providers: - azureEasyAuth: - signIn: - resolvers: - - resolver: idMatchingUserEntityAnnotation - - resolver: emailMatchingUserEntityProfileEmail - - resolver: emailLocalPartMatchingUserEntityName -``` - -The `idMatchingUserEntityAnnotation` is -[a builtin sign-in resolver](../identity-resolver.md#using-builtin-resolvers) from `azureEasyAuth` provider. -It tries to find a user entity with [a `graph.microsoft.com/user-id` annotation](../../features/software-catalog/well-known-annotations.md#graphmicrosoftcomtenant-id-graphmicrosoftcomgroup-id-graphmicrosoftcomuser-id) -which matches the object ID of the user attempting to sign in. -If you want to provide your own sign-in resolver, -see [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers). - -Add the `@backstage/plugin-auth-backend-module-azure-easyauth-provider` to your backend installation. - -```sh -# From your Backstage root directory -yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-azure-easyauth-provider -``` - -Then, add it to your backend's source, - -```ts title="packages/backend/src/index.ts" -const backend = createBackend(); - -backend.add(import('@backstage/plugin-auth-backend')); -// highlight-add-next-line -backend.add( - import('@backstage/plugin-auth-backend-module-azure-easyauth-provider'), -); - -await backend.start(); -``` - -Now the backend is ready to serve auth requests on the -`/api/auth/azureEasyAuth/refresh` endpoint. All that's left is to update the frontend -sign-in mechanism to poll that endpoint through the Easy Auth proxy, on the user's behalf. - -## Frontend Changes - -To use this component, you'll need to configure the app's `SignInPage`. -It is recommended to use the `ProxiedSignInPage` for this provider when running in Azure, However for local development (or any other scenario running outside of Azure), you'll want to set up something different. -For the closest experience to Easy Auth, you could set up the `microsoft` provider locally, but that will requires setting up App Registrations & secrets which may be locked down by your organisation, in which case it may be easier to use guest login locally. -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for more details. - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => { - const configApi = useApi(configApiRef); - if (configApi.getString('auth.environment') !== 'development') { - return ; - } - return ( - - ); - }, - }, - /* highlight-add-end */ - // .. -}); -``` - ## Azure Configuration How to configure azure depends on the Azure service you're using to host Backstage. @@ -136,3 +55,60 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' existing = { } } ``` + +## Configuration + +Add the following into your `app-config.yaml` under the root `auth` configuration: + +```yaml title="app-config.yaml" +auth: + providers: + azureEasyAuth: + signIn: + resolvers: + # typically you would pick one of these + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: idMatchingUserEntityAnnotation +``` + +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. +- `idMatchingUserEntityAnnotation`: Matches the user Id from the auth provider with the User entity that has a matching `graph.microsoft.com/user-id` annotation. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-azure-easyauth-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-azure-easyauth-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `azureEasyAuth` as the provider name. + +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index b96f175183..60104b99b7 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -40,6 +40,18 @@ If you're using an existing app registration, and backstage already has a client If not, go to the **Certificates & Secrets** page, then the **Client secrets** tab and create a new client secret. Make a note of this value as you'll need it in the next section. +## Outbound Network Access + +If your environment has restrictions on outgoing access (e.g. through +firewall rules), make sure your Backstage backend has access to the following +hosts: + +- `login.microsoftonline.com`, to get and exchange authorization codes and access + tokens +- `graph.microsoft.com`, to fetch user profile information (as seen + in [this source code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)). + If this host is unreachable, users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in. + ## Configuration The provider configuration can then be added to your `app-config.yaml` under the @@ -55,8 +67,6 @@ auth: clientSecret: ${AZURE_CLIENT_SECRET} tenantId: ${AZURE_TENANT_ID} domainHint: ${AZURE_TENANT_ID} - additionalScopes: - - Mail.Send signIn: resolvers: # typically you would pick one of these @@ -74,7 +84,7 @@ The Microsoft provider is a structure with three mandatory configuration keys: Leave blank if your app registration is multi tenant. When specified, this reduces login friction for users with accounts in multiple tenants by automatically filtering away accounts from other tenants. For more details, see [Home Realm Discovery](https://learn.microsoft.com/en-us/azure/active-directory/manage-apps/home-realm-discovery-policy) -- `additionalScopes` (optional): List of scopes for the App Registration. The default and mandatory value is ['user.read']. +- `additionalScopes` (optional): List of scopes for the App Registration, to be requested in addition to the required ones. ### Resolvers @@ -92,20 +102,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-microsoft-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-microsoft-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `microsoftAuthApiRef` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). - -## Outbound Network Access - -If your environment has restrictions on outgoing access (e.g. through -firewall rules), make sure your Backstage backend has access to the following -hosts: - -- `login.microsoftonline.com`, to get and exchange authorization codes and access - tokens -- `graph.microsoft.com`, to fetch user profile information (as seen - in [this source code](https://github.com/seanfisher/passport-microsoft/blob/0456aa9bce05579c18e77f51330176eb26373658/lib/strategy.js#L93-L95)). - If this host is unreachable, users may see an `Authentication failed, failed to fetch user profile` error when they attempt to log in. +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/oauth2-proxy/provider.md b/docs/auth/oauth2-proxy/provider.md index dca074cc7d..0901685b2f 100644 --- a/docs/auth/oauth2-proxy/provider.md +++ b/docs/auth/oauth2-proxy/provider.md @@ -47,25 +47,27 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. -## Adding the provider to the Backstage frontend +## Backend Installation -It is recommended to use the `ProxiedSignInPage` for this provider, which is -installed in `packages/app/src/App.tsx` like this: +To add the provider to the backend we will first need to install the package by running this command: -```tsx title="packages/app/src/App.tsx" -/* highlight-add-next-line */ -import { ProxiedSignInPage } from '@backstage/core-components'; - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), - }, - /* highlight-add-end */ - // .. -}); +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-oauth2-proxy-provider ``` -See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page to also work smoothly for local development. +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-oauth2-proxy-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +See [Sign-In with Proxy Providers](../index.md#sign-in-with-proxy-providers) for pointers on how to set up the sign-in page, and to also make it work smoothly for local development. You'll use `oauth2Proxy` as the provider name. + +If you [provide a custom sign in resolver](https://backstage.io/docs/auth/identity-resolver#building-custom-resolvers), you can skip the `signIn` block entirely. diff --git a/docs/auth/okta/provider.md b/docs/auth/okta/provider.md index 6fe7a8b992..83352aaa0d 100644 --- a/docs/auth/okta/provider.md +++ b/docs/auth/okta/provider.md @@ -83,8 +83,25 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-okta-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-okta-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `oktaAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/onelogin/provider.md b/docs/auth/onelogin/provider.md index c3572367d2..002445a9d1 100644 --- a/docs/auth/onelogin/provider.md +++ b/docs/auth/onelogin/provider.md @@ -38,6 +38,12 @@ auth: clientId: ${AUTH_ONELOGIN_CLIENT_ID} clientSecret: ${AUTH_ONELOGIN_CLIENT_SECRET} issuer: https://.onelogin.com/oidc/2 + signIn: + resolvers: + # typically you would pick one of these + - resolver: emailMatchingUserEntityProfileEmail + - resolver: emailLocalPartMatchingUserEntityName + - resolver: usernameMatchingUserEntityName ``` The OneLogin provider is a structure with three configuration keys; **these are @@ -47,8 +53,41 @@ found on the SSO tab** for the OneLogin Application: - `clientSecret`: The client secret - `issuer`: The issuer URL +### Resolvers + +This provider includes several resolvers out of the box that you can use: + +- `emailMatchingUserEntityProfileEmail`: Matches the email address from the auth provider with the User entity that has a matching `spec.profile.email`. If no match is found it will throw a `NotFoundError`. +- `emailLocalPartMatchingUserEntityName`: Matches the [local part](https://en.wikipedia.org/wiki/Email_address#Local-part) of the email address from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. +- `usernameMatchingUserEntityName`: Matches the username from the auth provider with the User entity that has a matching `name`. If no match is found it will throw a `NotFoundError`. + +:::note Note + +The resolvers will be tried in order, but will only be skipped if they throw a `NotFoundError`. + +::: + +If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-onelogin-provider +``` + +Then we will need to add this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-auth-backend-module-onelogin-provider')); +/* highlight-add-end */ +``` + ## Adding the provider to the Backstage frontend To add the provider to the frontend, add the `oneloginAuthApi` reference and `SignInPage` component as shown in -[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/auth/vmware-cloud/provider.md b/docs/auth/vmware-cloud/provider.md index dd7c32f700..ac2cf3ee92 100644 --- a/docs/auth/vmware-cloud/provider.md +++ b/docs/auth/vmware-cloud/provider.md @@ -33,108 +33,6 @@ Cloud Console and within a Backstage app required to enable this capability. 1. Take note of the `App ID` in the resulting modal; this is the client ID to be used by Backstage. -## Install the provider in the backend - -### New backend system - -Apps using the [new backend system](../../backend-system/index.md), -can enable the VMware Cloud provider with a small modification like: - -```ts title="packages/backend/src/index.ts" -import { createBackend } from '@backstage/backend-defaults'; - -const backend = createBackend(); -backend.add(import('@backstage/plugin-auth-backend')); -/* highlight-add-start */ -backend.add( - import('@backstage/plugin-auth-backend-module-vmware-cloud-provider'), -); -/* highlight-add-end */ -backend.start(); -``` - -### Old backend system - -This provider was added after the migration of the auth-backend plugin to the -new backend system, so no default provider factory was added. Because of this, -the installation procedure for old-style backends is slightly more involved: - -```ts title="packages/backend/src/plugins/auth.ts" -import { - DEFAULT_NAMESPACE, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { - createRouter, - providers, - defaultAuthProviderFactories, -} from '@backstage/plugin-auth-backend'; -import { Router } from 'express'; -import { PluginEnvironment } from '../types'; -/* highlight-add-start */ -import { - commonSignInResolvers, - createOAuthProviderFactory, -} from '@backstage/plugin-auth-node'; -import { - vmwareCloudAuthenticator, -} from '@backstage/plugin-auth-backend-module-vmware-cloud-provider'; -/* highlight-add-end */ - -export default async function createPlugin( - env: PluginEnvironment, -): Promise { - return await createRouter({ - logger: env.logger, - config: env.config, - database: env.database, - discovery: env.discovery, - tokenManager: env.tokenManager, - providerFactories: { - ...defaultAuthProviderFactories, - /* highlight-add-start */ - vmwareCloudServices: createOAuthProviderFactory({ - authenticator: vmwareCloudAuthenticator, - signInResolver: - commonSignInResolvers.emailLocalPartMatchingUserEntityName(), - }), - /* highlight-add-end */ -``` - -In the above, `commonSignInResolvers.emailLocalPartMatchingUserEntityName()` -can be replaced with a more suitable resolver for the app in question. - -## Add to Sign-in Page - -See the [Sign-In Configuration](../index.md#sign-in-configuration) docs for -general guidance, but as an example: - -```tsx title="packages/app/src/App.tsx" -/* highlight-add-start */ -import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api'; -import { SignInPage } from '@backstage/core-components'; -/* highlight-add-end */ - -const app = createApp({ - /* highlight-add-start */ - components: { - SignInPage: props => ( - - ), - }, - /* highlight-add-end */ - // .. -}); -``` - ## Configuration Add the following to your `app-config.yaml` under the root `auth` configuration: @@ -161,13 +59,17 @@ Where `APP_ID` refers to the ID retrieved when creating the OAuth App, and `ORG_ID` is the [long ID of the Organization](https://docs.vmware.com/en/VMware-Cloud-services/services/Using-VMware-Cloud-Services/GUID-CF9E9318-B811-48CF-8499-9419997DC1F8.html#view-the-organization-id-1) in VMware Cloud for which you wish to enable sign-in. -Note that VMware Cloud requires OAuth Apps to use +:::note Note + +VMware Cloud requires OAuth Apps to use [PKCE](https://oauth.net/2/pkce/) when performing authorization code flows; the library used by this provider requires the use of Express session middleware to do this. Therefore the value `your session secret` under `auth.session.secret` should be replaced with a long, complex and unique string which will act as a key for signing session cookies set by Backstage. +::: + ### Resolvers This provider includes several resolvers out of the box that you can use: @@ -183,3 +85,28 @@ The resolvers will be tried in order, but will only be skipped if they throw a ` ::: If these resolvers do not fit your needs you can build a custom resolver, this is covered in the [Building Custom Resolvers](../identity-resolver.md#building-custom-resolvers) section of the Sign-in Identities and Resolvers documentation. + +## Backend Installation + +To add the provider to the backend we will first need to install the package by running this command: + +```bash title="from your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-auth-backend-module-vmware-cloud-provider +``` + +Then we will need to this line: + +```ts title="in packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-auth-backend')); +/* highlight-add-start */ +backend.add( + import('@backstage/plugin-auth-backend-module-vmware-cloud-provider'), +); +/* highlight-add-end */ +``` + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `vmwareCloudAuthApiRef` reference and +`SignInPage` component as shown in +[Adding the provider to the sign-in page](../index.md#sign-in-configuration). diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 777eddd3c4..57f6b20b73 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -38,7 +38,7 @@ export const fooServiceRef = createServiceRef({ The `fooServiceRef` that we create above should be exported, and can then be used to declare a dependency on the `FooService` interface and receive an implementation of it at runtime. -When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'.'`. For more naming patterns surrounding services, see the [naming patterns](./07-naming-patterns.md#services) page. +When creating a service reference you need to give it an ID. This ID needs to be globally unique, and should generally be of the format `'.'`. For more naming patterns surrounding services, see the [naming patterns](./08-naming-patterns.md#services) page. A note on naming: the frontend and backend systems intentionally use the separate names "APIs" and "Services" for concepts that are quite similar. This is to avoid confusion between the two, both in documentation and discussion, but also in code. While the two systems are quite similar, they are not identical, and they can't be used interchangeably. @@ -190,43 +190,76 @@ If a service defines a default factory, that factory will be used if there is no When defining a default factory for a service, it is possible for it to end up with duplicate implementations at runtime. This applies both to any shared root context in your factory, as well as plugin specific instances of your service. This is because package dependency version ranges may not line up perfectly, causing duplicate installations of the same package. This can happen both for two different plugins using the same service, but also across a plugin and its modules. If your service would break in this scenario, you should not define a default factory for it, but instead require that users of your service explicitly install a factory in their backend instance. -## Service Factory Options +## Service Factory Customization + +When declaring a service factory you may also want to make the export the building blocks of the implementation itself. This is to allow for further customization of the service implementation through code, beyond what is possible with static configuration, without the need to re-implement the entire service from scratch. For example, we might export our example `DefaultFooService` class, while moving construction to a static `create` factory method to make it easier to evolve: + +```ts +export class DefaultFooService { + static create(options: { transform: (foo: string) => string }) { + return new DefaultFooService(options.transform ?? (foo) => foo); + } + + private constructor(private readonly transform: (foo: string) => string) {} + + foo(foo: string): string { + return this.transform(foo); + } +} +``` + +By exporting `DefaultFooService` we now make it relatively simple for advanced users of our service to customize the implementation. To do so, they can define their own service factory that uses our provided implementation: + +```ts +export const customFooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: {}, + factory() { + return DefaultFooService.create({ + transform: foo => foo.toUpperCase(), + }); + }, +}); +``` + +This allows you to provide more advanced options for the service implementation that couldn't be expressed through static configuration. It also gives users of the service implementation access to other services through dependency injection, which can be useful for their customizations. + +## Service Factory Options Pattern :::note Note -This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration instead. +This pattern is discouraged, only use it when necessary. If possible you should prefer to make services configurable via static configuration or re-implementation instead. ::: -When declaring a service factory it's possible to include an options callback. This allows you to customize the factory through code when installing it in the backend. For example, this is how you install an explicit factory instance in the backend without any options: +In some cases it might be beneficial to allow users of your service factory to pass options to the factory itself, rather than to the service implementation. This can be enabled by also defining the service factory as a function that returns a reconfigured factory. For example: ```ts -const backend = createBackend(); - -backend.add(fooServiceFactory()); -``` - -Note that we call `fooServiceFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. Note that the options must always be optional. For example: - -```ts -export interface FooFactoryOptions { +const fooServiceFactoryWithOptions = (options?: { transform: (foo: string) => string; -} - -export const fooServiceFactory = createServiceFactory( - (options?: FooFactoryOptions) => ({ +}) => + createServiceFactory({ service: fooServiceRef, + deps: {}, factory() { - return new DefaultFooService(options?.transform); + return DefaultFooService.create({ + transform: options?.transform, + }); }, - }), + }); + +export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), ); ``` -This lets us use the options to customize the factory implementation in any way we want. From the outside the service factory looks just like before, except that we're now also able to pass options when installing the factory: +This makes it possible to use the `fooServiceFactory` directly, as well passing additional options to create a customized factory: ```ts -const backend = createBackend(); - -backend.add(fooServiceFactory({ transform: foo => foo.toUpperCase() })); +backend.add(fooServiceFactory); +// OR +backend.add(fooServiceFactory({ transform: foo => foo.toLowerCase() })); ``` + +This pattern is discouraged due to the inability to access other services through dependency injection. It is however used in a few places in the Backstage framework where the ability to directly pass options without re-implementing the service is very convenient, such as the `mockServices` from `@backstage/backend-test-utils`. diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index af59c2a48a..5aef71e24f 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -10,7 +10,7 @@ Plugins provide the actual base features of a Backstage backend. Each plugin ope ## Defining a Plugin -Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./07-naming-patterns.md) about proper naming patterns. +Plugins are created using the `createBackendPlugin` function, and should typically be exported from a plugin package. All plugins must have an ID and a `register` method, where the ID matches the plugin ID in the package name, without the `-backend` suffix. See also the [dedicated section](./08-naming-patterns.md) about proper naming patterns. ```ts // plugins/example-backend/src/plugin.ts diff --git a/docs/backend-system/architecture/06-modules.md b/docs/backend-system/architecture/06-modules.md index eb00e737e3..010f36109c 100644 --- a/docs/backend-system/architecture/06-modules.md +++ b/docs/backend-system/architecture/06-modules.md @@ -52,7 +52,7 @@ This allows you to install the module in your backend instance by just referenci ```ts backend.add( - import('backstage-plugin-catalog-backend-module-example-processor'), + import('@internal/backstage-plugin-catalog-backend-module-example-processor'), ); ``` diff --git a/docs/backend-system/architecture/07-feature-loaders.md b/docs/backend-system/architecture/07-feature-loaders.md new file mode 100644 index 0000000000..cf52a1feb3 --- /dev/null +++ b/docs/backend-system/architecture/07-feature-loaders.md @@ -0,0 +1,101 @@ +--- +id: feature-loaders +title: Backend Feature Loaders +sidebar_label: Feature Loaders +# prettier-ignore +description: Backend feature loaders +--- + +Backend feature loaders are used to programmatically select and install features in a Backstage backend. They can service a wide range of use cases, such as enabling or disabling features based on static configuration, dynamically load features at runtime, or conditionally load features based on the state of a system. + +Feature loaders are defined using the `createBackendFeatureLoader` function, exported by `@backstage/backend-plugin-api`. It accepts a `loader` function, as well as an optional `deps` object for declaring service dependencies. Unlike plugins and modules, feature loaders are limited to only depending on root-scoped services, but that still allows access to for example the [root config](../core-services/root-config.md) and [root logger](../core-services/root-logger.md) services. + +The `loader` function can be defined in many different ways, with the main requirement being that it returns a list of `BackendFeature`s in some form. A backend feature is the kind of object that you can pass to `backend.add(...)`, for example services factories, plugins, modules, or even other feature loaders. The `loader` function can be synchronous or asynchronous, and can be defined as a generator function to allow for more complex logic. + +## Examples + +The following are a few example of how feature loaders can be used: + +### Simple list of features + +A feature loader can simply return a list of features to be installed: + +```ts +export default createBackendFeatureLoader({ + loader() { + return [ + import('@backstage/plugin-search-backend/alpha'), + import('@backstage/plugin-search-backend-module-catalog/alpha'), + import('@backstage/plugin-search-backend-module-explore/alpha'), + import('@backstage/plugin-search-backend-module-techdocs/alpha'), + ]; + }, +}); +``` + +It can also encapsulate a collection of custom features: + +```ts +export default createBackendFeatureLoader({ + // Async loader is fine too + async loader() { + return [ + createBackendPlugin({ + ... + }), + createBackendModule({ + ... + }), + ] + }, +}); +``` + +### Conditional loading + +A feature loader can access root-scoped services, such as the config service. This allows for conditional loading of features based on configuration. It is often convenient to use a generator function for this purpose: + +```ts +export default createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + // The `*` in front of the function name makes it a generator function + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, +}); +``` + +### Dynamic logic + +A feature loader can also be asynchronous, and for example fetch data from an external source to determine which features to load: + +```ts +export default createBackendFeatureLoader({ + // The `async *` in front of the function name makes it an async generator function. + async *loader() { + const localMetadata = await readMetadataFromDisk(); + + if (localMetadata.enableSearch) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + + const remoteMetadata = await fetchMetadata(); + + if (remoteMetadata.enableExplore) { + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + } + if (remoteMetadata.enableTechDocs) { + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + } + }, +}); +``` diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/08-naming-patterns.md similarity index 100% rename from docs/backend-system/architecture/07-naming-patterns.md rename to docs/backend-system/architecture/08-naming-patterns.md diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index d38acb695e..67104c7ba9 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -497,7 +497,7 @@ catalog: providers: githubOrg: - id: production - githubUrl: 'https://github.com', + githubUrl: 'https://github.com' orgs: ['backstage'] schedule: frequency: PT30M @@ -536,8 +536,8 @@ catalog: providers: githubOrg: - id: production - githubUrl: 'https://github.com', - orgs: ['org-a', 'org-b'], + githubUrl: 'https://github.com' + orgs: ['org-a', 'org-b'] schedule: frequency: PT30M timeout: PT15M @@ -1254,7 +1254,6 @@ In order to add your own permission policy you'll need to do the following: ```ts import { createBackendModule } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { PolicyDecision, AuthorizeResult, @@ -1262,13 +1261,14 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class CustomPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { // TODO: Add code here that inspects the incoming request and user, and returns AuthorizeResult.ALLOW, AuthorizeResult.DENY, or AuthorizeResult.CONDITIONAL as needed. See the docs at https://backstage.io/docs/permissions/writing-a-policy for more information diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 379ce19587..4dfca00d4e 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -68,7 +68,7 @@ that's specific to your plugin. In the example above, the logger might tag messages with your plugin ID, and the HTTP router might prefix API routes with your plugin ID, depending on the implementation used. -See [the article on naming patterns](../architecture/07-naming-patterns.md) for +See [the article on naming patterns](../architecture/08-naming-patterns.md) for details on how to best choose names/IDs for plugins and related backend system items. @@ -124,7 +124,7 @@ export const catalogModuleExampleCustomProcessor = createBackendModule({ export { catalogModuleExampleCustomProcessor as default } from './module'; ``` -See [the article on naming patterns](../architecture/07-naming-patterns.md) for +See [the article on naming patterns](../architecture/08-naming-patterns.md) for details on how to best choose names/IDs for modules and related backend system items. diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 8a2dd0414d..addcc72c84 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -61,7 +61,7 @@ requests and return mock responses. This lets you stub out remote services rather than the local clients, leading to more thorough and robust tests. You can read more about how it works [in their documentation](https://mswjs.io/). -The `@backstage/backend-test-utils` package exports a `setupRequestMockHandlers` +The `@backstage/backend-test-utils` package exports a `registerMswTestHooks` function which ensures that the correct `jest` lifecycle hooks are invoked to set up and tear down your `msw` instance, and enables the option that completely rejects requests that don't match one of your mock rules. This ensures that your @@ -70,13 +70,13 @@ tests cannot accidentally leak traffic into production from tests. Example: ```ts -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; describe('read from remote', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('should auth and read successfully', async () => { expect.assertions(1); @@ -138,8 +138,10 @@ describe('MyDatabaseClass', () => { await knex('foo').insert({ value: 2 }); // drive your system under test as usual await expect(subject.foos()).resolves.toEqual([{ value: 2 }]); - }); + }, + ); }); +}); ``` If you want to pass the test database instance into backend plugins or services, diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 04d2e57ae8..3be12b2e26 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -28,6 +28,7 @@ import { coreServices } from '@backstage/backend-plugin-api'; - [Permissions Service](./permissions.md) - Permission system integration for authorization of user actions. - [Plugin Metadata Service](./plugin-metadata.md) - Built-in service for accessing metadata about the current plugin. - [Root Config Service](./root-config.md) - Access to static configuration. +- [Root Health Service](./root-health.md) - Health check endpoints for the backend. - [Root Http Router Service](./root-http-router.md) - HTTP route registration for root services. - [Root Lifecycle Service](./root-lifecycle.md) - Registration of backend startup and shutdown lifecycle hooks. - [Root Logger Service](./root-logger.md) - Root-level logging. diff --git a/docs/backend-system/core-services/logger.md b/docs/backend-system/core-services/logger.md index 015d2055d5..e18bd6a4c0 100644 --- a/docs/backend-system/core-services/logger.md +++ b/docs/backend-system/core-services/logger.md @@ -31,47 +31,3 @@ createBackendPlugin({ }, }); ``` - -## Root Logger - -The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. - -If you want to override the implementation for logging across all of the backend, this is the service that you should override. - -## Configuring the service - -The following example is how you can override the root logger service to add additional metadata to all log lines. - -```ts -import { coreServices } from '@backstage/backend-plugin-api'; -import { WinstonLogger } from '@backstage/backend-app-api'; - -const backend = createBackend(); - -backend.add( - createServiceFactory({ - service: coreServices.rootLogger, - deps: { - config: coreServices.rootConfig, - }, - async factory({ config }) { - const logger = WinstonLogger.create({ - meta: { - service: 'backstage', - // here's some additional information that is not part of the - // original implementation - podName: 'myk8spod', - }, - level: process.env.LOG_LEVEL || 'info', - format: - process.env.NODE_ENV === 'production' - ? format.json() - : WinstonLogger.colorFormat(), - transports: [new transports.Console()], - }); - - return logger; - }, - }), -); -``` diff --git a/docs/backend-system/core-services/root-config.md b/docs/backend-system/core-services/root-config.md index 7cdc10f38c..8505b9e7ec 100644 --- a/docs/backend-system/core-services/root-config.md +++ b/docs/backend-system/core-services/root-config.md @@ -60,3 +60,34 @@ backend.add( }), ); ``` + +For more advanced customization, there are several APIs from the `@backstage/config-loader` package that allow you to customize the implementation of the config service. The default implementation uses the `ConfigSources.default` method, which has the same options as the `rootConfigServiceFactory` function. You can use these to create your own config service implementation: + +```ts +import { ConfigSources } from '@backstage/config-loader'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootConfig, + deps: {}, + async factory() { + const source = ConfigSources.default({ + argv: [ + '--config', + '/backstage/app-config.development.yaml', + '--config', + '/backstage/app-config.yaml', + ], + remote: { reloadIntervalSeconds: 60 }, + }); + console.log(`Loading config from ${source}`); + return await ConfigSources.toConfig(source); + }, + }), +); +``` + +You can also use other config source such as `StaticConfigSource` and combine them with other sources using `ConfigSources.merge(...)`. You can also create your own config source by implementing the `ConfigSource` interface. diff --git a/docs/backend-system/core-services/root-health.md b/docs/backend-system/core-services/root-health.md new file mode 100644 index 0000000000..663dd699ae --- /dev/null +++ b/docs/backend-system/core-services/root-health.md @@ -0,0 +1,40 @@ +--- +id: root-health +title: Root Health Service +sidebar_label: Root Health +description: Documentation for the Health service +--- + +The Root Health service provides some health check endpoints for the backend. By default, the `rootHttpRouter` exposes a `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints, which return a JSON object with the status of the backend services according the implementation of the Root Health Service. + +## Configuring the service + +The following example shows how you can override the root health service implementation. + +```ts +import { RootHealthService, coreServices } from '@backstage/backend-plugin-api'; + +const backend = createBackend(); + +class MyRootHealthService implements RootHealthService { + async getLiveness() { + // provide your own implementation + return { status: 200, payload: { status: 'ok' } }; + } + + async getReadiness() { + // provide your own implementation + return { status: 200, payload: { status: 'ok' } }; + } +} + +backend.add( + createServiceFactory({ + service: coreServices.rootHealth, + deps: {}, + async factory({}) { + return new MyRootHealthService(); + }, + }), +); +``` diff --git a/docs/backend-system/core-services/root-logger.md b/docs/backend-system/core-services/root-logger.md index 09b7929efe..36bb276920 100644 --- a/docs/backend-system/core-services/root-logger.md +++ b/docs/backend-system/core-services/root-logger.md @@ -5,4 +5,53 @@ sidebar_label: Root Logger description: Documentation for the Root Logger service --- -TODO +## Root Logger + +The root logger is the logger that is used by other root services. It's where the implementation lies for creating child loggers around the backstage ecosystem including child loggers for plugins with the correct metadata and annotations. + +If you want to override the implementation for logging across all of the backend, this is the service that you should override. + +## Configuring the service + +The following example is how you can override the root logger service to add additional metadata to all log lines. + +```ts +import { coreServices } from '@backstage/backend-plugin-api'; +import { WinstonLogger } from '@backstage/backend-defaults/rootLogger'; +import { createConfigSecretEnumerator } from '@backstage/backend-defaults/rootConfig'; + +const backend = createBackend(); + +backend.add( + createServiceFactory({ + service: coreServices.rootLogger, + deps: { + config: coreServices.rootConfig, + }, + async factory({ config }) { + const logger = WinstonLogger.create({ + meta: { + service: 'backstage', + // here's some additional information that is not part of the + // original implementation + podName: 'myk8spod', + }, + level: process.env.LOG_LEVEL || 'info', + format: + process.env.NODE_ENV === 'production' + ? format.json() + : WinstonLogger.colorFormat(), + transports: [new transports.Console()], + }); + + const secretEnumerator = await createConfigSecretEnumerator({ + logger, + }); + logger.addRedactions(secretEnumerator(config)); + config.subscribe?.(() => logger.addRedactions(secretEnumerator(config))); + + return logger; + }, + }), +); +``` diff --git a/docs/deployment/docker.md b/docs/deployment/docker.md index 154f68c7d1..d47cb248f8 100644 --- a/docs/deployment/docker.md +++ b/docs/deployment/docker.md @@ -92,11 +92,14 @@ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ yarn install --frozen-lockfile --production --network-timeout 300000 +# This will include the examples, if you don't need these simply remove this line +COPY --chown=node:node examples ./examples + # Then copy the rest of the backend bundle, along with any other files we might want. COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ RUN tar xzf bundle.tar.gz && rm bundle.tar.gz -CMD ["node", "packages/backend", "--config", "app-config.yaml"] +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] ``` For more details on how the `backend:bundle` command and the `skeleton.tar.gz` @@ -251,12 +254,15 @@ RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./ # Copy any other files that we need at runtime -COPY --chown=node:node app-config.yaml ./ +COPY --chown=node:node app-config*.yaml ./ + +# This will include the examples, if you don't need these simply remove this line +COPY --chown=node:node examples ./examples # This switches many Node.js dependencies to production mode. ENV NODE_ENV production -CMD ["node", "packages/backend", "--config", "app-config.yaml"] +CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] ``` Note that a newly created Backstage app will typically not have a `plugins/` @@ -277,6 +283,7 @@ packages/*/dist packages/*/node_modules plugins/*/dist plugins/*/node_modules +*.local.yaml ``` Once you have added both the `Dockerfile` and `.dockerignore` to the root of @@ -312,14 +319,12 @@ first step in doing so is to remove the `app-backend` plugin from the backend package, which is done as follows: 1. Delete `packages/backend/src/plugins/app.ts` -2. Remove the following lines from `packages/backend/src/index.ts`: - ```tsx - import app from './plugins/app'; - // ... - const appEnv = useHotMemoize(module, () => createEnv('app')); - // ... - .addRouter('', await app(appEnv)); +2. Remove the following line from `packages/backend/src/index.ts`: + + ```ts + backend.add(import('@backstage/plugin-app-backend/alpha')); ``` + 3. Remove the `@backstage/plugin-app-backend` and the app package dependency (e.g. `app`) from `packages/backend/package.json`. If you don't remove the app package dependency the app will still be built and bundled with the diff --git a/docs/deployment/heroku.md b/docs/deployment/heroku.md index 1545c34733..51d0b4b439 100644 --- a/docs/deployment/heroku.md +++ b/docs/deployment/heroku.md @@ -5,40 +5,37 @@ sidebar_label: Heroku description: How to deploy Backstage to Heroku --- -Heroku is a Platform as a Service (PaaS) designed to handle application -deployment in a hands-off way. Heroku supports container deployment of Docker -images, a natural fit for Backstage. +Heroku is a Platform as a Service (PaaS) designed to simplify application deployment. -## Configuring the CLI +## Create App -First, install the -[heroku-cli](https://devcenter.heroku.com/articles/heroku-cli) and login: +Starting with an existing Backstage app or follow the [getting started guide](https://backstage.io/docs/getting-started/) to create a new one. + +Install the +[Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) and create a new Heroku app: ```shell -$ heroku login +cd your-app/ +heroku apps:create ``` -If you have not yet created a project through the Heroku interface, you can create it through the CLI. +## Domain + +Get Heroku app URL: ```shell -$ heroku create +heroku domains -a +.herokuapp.com ``` -You _might_ also need to set your Heroku app's stack to `container`: - -```bash -$ heroku stack:set container -a -``` - -Configuring your `app-config.yaml`: +The core [app-backend plugin](https://www.npmjs.com/package/@backstage/plugin-app-backend) allows a single Heroku app to serve the frontend and backend. To make this work you need to update the `baseUrl` and `port` in `app-config.production.yaml`: ```yaml app: - # Should be the same as backend.baseUrl when using the `app-backend` plugin - baseUrl: https://.herokuapp.com + baseUrl: https://.herokuapp.com backend: - baseUrl: https://.herokuapp.com + baseUrl: https://.herokuapp.com listen: port: $env: PORT @@ -46,28 +43,98 @@ backend: # https://devcenter.heroku.com/articles/dynos#web-dynos ``` -> Make sure your file is being copied into your container in the `Dockerfile`. +## Build Script -Before building the Docker image, run the [backstage host build commands](https://backstage.io/docs/deployment/docker#host-build). They must be run whenever you are going to publish a new image. +Add a build script in `package.json` to compile frontend during deployment: -Heroku runs a container registry on `registry.heroku.com`. To push Backstage -Docker images, log in to the container registry also: +```json +"scripts": { + "build": "yarn build:backend --config ../../app-config.yaml --config ../../app-config.production.yaml" +``` + +## Start Command + +Create a [Procfile](https://devcenter.heroku.com/articles/procfile) in the app's root: ```shell -$ heroku container:login +echo "web: yarn workspace backend start --config ../../app-config.yaml --config ../../app-config.production.yaml" > Procfile ``` -## Push and deploy a Docker image +## Database -Now we can push a Backstage [Docker image](docker.md) to Heroku's container -registry and release it to the `web` worker: +Provision a [Heroku Postgres](https://elements.heroku.com/addons/heroku-postgresql) database: -```bash -$ docker image build . -f packages/backend/Dockerfile --tag registry.heroku.com//web - -$ docker push registry.heroku.com//web - -$ heroku container:release web -a +```shell +heroku addons:create heroku-postgresql -a ``` -Now you should have Backstage up and running! 🎉 +Update `database` in `app-config.production.yaml`: + +```yaml +backend: + database: + client: pg + pluginDivisionMode: schema + ensureExists: false + ensureSchemaExists: true + connection: ${DATABASE_URL} +``` + +Allow postgres self-signed certificates: + +```shell +heroku config:set PGSSLMODE=no-verify -a +``` + +## Deployment + +Commit changes and push to Heroku to build and deploy: + +```shell +git add Procfile && git commit -am "configure heroku" +git push heroku main +``` + +View the app in the browser: + +```shell +heroku open -a +``` + +View logs: + +```shell +heroku logs -a +``` + +## Docker + +As an alternative to git deploys, Heroku also [supports container images](https://devcenter.heroku.com/articles/container-registry-and-runtime). + +Login to Heroku's container registry: + +```shell +heroku container:login +``` + +Configure the Heroku app to run a container image: + +```shell +heroku stack:set container -a +``` + +Locally run the [host build commands](https://backstage.io/docs/deployment/docker/#host-build), they must be run whenever you are going to publish a new image: + +```shell +yarn install --frozen-lockfile +yarn tsc +yarn build:backend --config ../../app-config.yaml --config ../../app-config.production.yaml +``` + +Build, push, and release the container image to the `web` dyno: + +```shell +docker image build . -f packages/backend/Dockerfile --tag registry.heroku.com//web +docker push registry.heroku.com//web +heroku container:release web -a +``` diff --git a/docs/faq/technical.md b/docs/faq/technical.md index d64d81d499..32511351f0 100644 --- a/docs/faq/technical.md +++ b/docs/faq/technical.md @@ -116,7 +116,7 @@ This makes it easier to create, find, and update documentation. [TechDocs is now open source.](https://backstage.io/docs/features/techdocs/) (See also: "[Will Spotify's internal plugins be open sourced, too?](https://backstage.io/docs/faq/product#will-spotifys-internal-plugins-be-open-sourced-too)" -above) +above). ### Are you planning to have plugins baked into the repo? Or should they be developed in separate repos? diff --git a/docs/features/kubernetes/authenticationstrategy.md b/docs/features/kubernetes/authenticationstrategy.md index a7d093248a..ffea69d610 100644 --- a/docs/features/kubernetes/authenticationstrategy.md +++ b/docs/features/kubernetes/authenticationstrategy.md @@ -173,8 +173,7 @@ To create the Backend module, run `yarn new`, select `backend-module`. Then fill This will create a new package at `plugins/kubernetes-backend-module-pinniped`. We are going to need also the `@backstage/plugin-kubernetes-node` and `@backstage/plugin-kubernetes-common` dependencies, the `@backstage/plugin-kubernetes-node` houses the [kubernetesAuthStrategyExtensionPoint](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-node/src/extensions.ts#L77) and a [Pinniped Helper](https://github.com/backstage/backstage/blob/ebe7afad9d19f279469168ca0d4feceb92c1ad36/plugins/kubernetes-node/src/auth/PinnipedHelper.ts#L53) class. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd plugins/kubernetes-backend-module-pinniped add @backstage/plugin-kubernetes-node yarn --cwd plugins/kubernetes-backend-module-pinniped add @backstage/plugin-kubernetes-common ``` diff --git a/docs/features/kubernetes/installation.md b/docs/features/kubernetes/installation.md index b3d03cc0b4..279c9cbf3f 100644 --- a/docs/features/kubernetes/installation.md +++ b/docs/features/kubernetes/installation.md @@ -15,8 +15,7 @@ If you haven't setup Backstage already, read the The first step is to add the Kubernetes frontend plugin to your Backstage application. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-kubernetes ``` @@ -53,8 +52,7 @@ work. Navigate to `packages/backend` of your Backstage app, and install the `@backstage/plugin-kubernetes-backend` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend ``` @@ -109,8 +107,7 @@ To get the Kubernetes plugin install using the New Backend System you will need Run this command to add the package: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-kubernetes-backend ``` diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md index 2da797d8ef..85ad86f83b 100644 --- a/docs/features/kubernetes/proxy.md +++ b/docs/features/kubernetes/proxy.md @@ -60,7 +60,6 @@ This feature assumes your backstage instance has enabled the [permissions framew A sample policy like: ```typescript -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, @@ -68,12 +67,13 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; class KubernetesDenyAllProxyEndpointPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (request.permission.name === 'kubernetes.proxy') { return { diff --git a/docs/features/search/getting-started.md b/docs/features/search/getting-started.md index 7989b60ae2..b2a074974f 100644 --- a/docs/features/search/getting-started.md +++ b/docs/features/search/getting-started.md @@ -16,8 +16,7 @@ If you haven't setup Backstage already, start ## Adding Search to the Frontend -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-search @backstage/plugin-search-react ``` @@ -133,8 +132,7 @@ For more information about using `Root.tsx`, please see Add the following plugins into your backend app: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-search-backend @backstage/plugin-search-backend-node ``` diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 6564fd0a44..9ce30da556 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -396,21 +396,17 @@ Recently, the Backstage maintainers [announced the new Backend System](https://b In "packages/backend/index.ts", install the search plugin [1], the search engine [2], and the search collators/decorators modules [3]: ```ts -import { searchPlugin } from '@backstage/plugin-search-backend/alpha'; -import { searchModuleElasticsearchEngine } from '@backstage/plugin-search-backend-module-elasticsearch/alpha'; -import { searchModuleCatalogCollator } from '@backstage/plugin-search-backend-module-catalog/alpha'; -import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha'; -import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha'; - const backend = createBackend(); // [1] adding the search plugin to the backend -backend.add(searchPlugin()); +backend.add(import('@backstage/plugin-search-backend/alpha')); // [2] (optional) the default search engine is Lunr, if you want to extend the search backend with another search engine. -backend.add(searchModuleElasticsearchEngine()); +backend.add( + import('@backstage/plugin-search-backend-module-elasticsearch/alpha'), +); // [3] extending search with collator modules to start index documents, take in optional schedule parameters. -backend.add(searchModuleCatalogCollator()); -backend.add(searchModuleTechDocsCollator()); -backend.add(searchModuleExploreCollator()); +backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); +backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); +backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); backend.start(); ``` diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index 61366a91f7..67382aa6ce 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -47,7 +47,7 @@ action is logged for further investigation. ### Local File (`type: file`) Configurations In addition to url locations, you can use the `file` location type to bring in content from the local file system. You should only use this for local development, test setups, and example data, not for production data. -You are also not able to use placeholders in them like `$text`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/tree/master/packages/catalog-model/examples) for an extensive example. +You are also not able to use placeholders in them like `$text`, `$json` or `$yaml`. You can however reference other files relative to the current file. See the full [catalog example data set here](https://github.com/backstage/backstage/tree/master/packages/catalog-model/examples) for an extensive example. Here is an example pulling in the `all.yaml` file from the examples folder. Note the use of `../../` to go up two levels from the current execution path of the backend. This is typically `packages/backend/`. @@ -58,6 +58,20 @@ catalog: target: ../../examples/all.yaml ``` +:::note +There might be cases where you need to test some `file` configurations in a Docker container. In a case like this, as the backend is serving the frontend in a default setup, the path would be from the root. Also, you need to **make sure to copy your files into your container**. + +Using the example above that would look like this: + +```yaml +catalog: + locations: + - type: file + target: ./examples/all.yaml +``` + +::: + ### Integration Processors Integrations may simply provide a mechanism to handle `url` location type for an @@ -177,3 +191,104 @@ here. Setting this value too low risks exhausting rate limits on external systems that are queried by processors, such as version control systems housing catalog-info files. + +## Subscribing to Catalog Errors + +Catalog errors are published to the [events plugin](https://github.com/backstage/backstage/tree/master/plugins/events-node): `@backstage/plugin-events-node`. You can subscribe to events and respond to errors, for example you may wish to log them. + +The first step is to add the events backend plugin to your Backstage application. Navigate to your Backstage application directory and add the plugin package. + +```ts title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-events-backend +``` + +Now you can install the events backend plugin in your backend. + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-events-backend/alpha')); +``` + +### Logging Errors + +If you want to log catalog errors you can install the `@backstage/plugin-catalog-backend-module-logs` module. + +Install the catalog logs module. + +```ts title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-logs +``` + +Add the module to your backend. + +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-catalog-backend-module-logs')); +``` + +This will log errors with a level of `warn`. + +You should now see logs as the catalog emits events. Example: + +``` +[1] 2024-06-07T00:00:28.787Z events warn Policy check failed for user:default/guest; caused by Error: Malformed envelope, /metadata/tags must be array entity=user:default/guest location=file:/Users/foobar/code/backstage-demo-instance/examples/org.yaml +``` + +### Custom Error Handling + +If you wish to handle catalog errors with specific logic different from logging the errors the following should help you get started. For example, you may wish to send a notification or create a ticket for someone to investigate. + +Create a backend module that subscribes to the catalog error events. The topic is `experimental.catalog.errors`. + +```ts title="packages/backend/src/index.ts" +import { CATALOG_ERRORS_TOPIC } from '@backstage/plugin-catalog-backend'; +import { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { eventsServiceRef, EventParams } from '@backstage/plugin-events-node'; + +interface EventsPayload { + entity: string; + location?: string; + errors: Error[]; +} + +interface EventsParamsWithPayload extends EventParams { + eventPayload: EventsPayload; +} + +const eventsModuleCatalogErrors = createBackendModule({ + pluginId: 'events', + moduleId: 'catalog-errors', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + logger: coreServices.logger, + }, + async init({ events, logger }) { + events.subscribe({ + id: 'catalog', + topics: [CATALOG_ERRORS_TOPIC], + async onEvent(params: EventParams): Promise { + const event = params as EventsParamsWithPayload; + const { entity, location, errors } = event.eventPayload; + // Add custom logic here for responding to errors + for (const error of errors) { + logger.warn(error.message, { + entity, + location, + }); + } + }, + }); + }, + }); + }, +}); +``` + +Now install your module. + +```ts title="packages/backend/src/index.ts" +backend.add(eventsModuleCatalogErrors); +``` diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 416eb27fbd..57c2283586 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -1243,6 +1243,18 @@ system belongs to, e.g. `artists`. This field is optional. | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | | [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | +### `spec.type` [optional] + +The type of system. There is currently no enforced set of values for this field, +so it is left up to the adopting organization to choose a nomenclature that +matches their catalog hierarchy. This field is optional. + +Some common values for this field could be: + +- `product` +- `service` +- `feature-set` + ## Kind: Domain Describes the following entity kind: @@ -1303,6 +1315,18 @@ which the domain is a part, e.g. `audio`. This field is optional. | --------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------- | | [`Domain`](#kind-domain) (default) | Same as this entity, typically `default` | [`partOf`, and reverse `hasPart`](well-known-relations.md#partof-and-haspart) | +### `spec.type` [optional] + +The type of domain. There is currently no enforced set of values for this field, +so it is left up to the adopting organization to choose a nomenclature that +matches their catalog hierarchy. This field is optional. + +Some common values for this field could be: + +- `product-area` +- `product-group` +- `bundle` + ## Kind: Location Describes the following entity kind: diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index 6eaee2f80d..98fae5a95e 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -84,6 +84,7 @@ import { EntityProvider, EntityProviderConnection, } from '@backstage/plugin-catalog-node'; +import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; /** * Provides entities from fictional frobs service. @@ -92,11 +93,17 @@ export class FrobsProvider implements EntityProvider { private readonly env: string; private readonly reader: UrlReader; private connection?: EntityProviderConnection; + private taskRunner: SchedulerServiceTaskRunner; /** [1] */ - constructor(env: string, reader: UrlReader) { + constructor( + env: string, + reader: UrlReader, + taskRunner: SchedulerServiceTaskRunner, + ) { this.env = env; this.reader = reader; + this.taskRunner = taskRunner; } /** [2] */ @@ -107,6 +114,12 @@ export class FrobsProvider implements EntityProvider { /** [3] */ async connect(connection: EntityProviderConnection): Promise { this.connection = connection; + this.taskRunner.run({ + id: this.getProviderName(), + fn: async () => { + await this.run(); + }, + }); } /** [4] */ @@ -248,24 +261,17 @@ export default async function createPlugin( ): Promise { const builder = CatalogBuilder.create(env); /* highlight-add-start */ - const frobs = new FrobsProvider('production', env.reader); + const taskRunner = env.scheduler.createScheduledTaskRunner({ + frequency: { minutes: 30 }, + timeout: { minutes: 10 }, + }); + const frobs = new FrobsProvider('production', env.reader, taskRunner); builder.addEntityProvider(frobs); /* highlight-add-end */ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - /* highlight-add-start */ - await env.scheduler.scheduleTask({ - id: 'run_frobs_refresh', - fn: async () => { - await frobs.run(); - }, - frequency: { minutes: 30 }, - timeout: { minutes: 10 }, - }); - /* highlight-add-end */ - // .. } ``` @@ -300,9 +306,17 @@ export const catalogModuleFrobsProvider = createBackendModule({ deps: { catalog: catalogProcessingExtensionPoint, reader: coreServices.urlReader, + /* highlight-add-start */ + scheduler: coreServices.scheduler, + /* highlight-add-end */ }, - async init({ catalog, reader }) { - catalog.addEntityProvider(new FrobsProvider('dev', reader)); + async init({ catalog, reader, scheduler }) { + const taskRunner = scheduler.createScheduledTaskRunner({ + frequency: { minutes: 30 }, + timeout: { minutes: 10 }, + }); + const frobs = new FrobsProvider('dev', reader, taskRunner); + catalog.addEntityProvider(frobs); }, }); }, @@ -318,6 +332,98 @@ backend.add(catalogModuleFrobsProvider); backend.start(); ``` +#### Follow-up: Config Defined Schedule + +If you want to go a step further and increase the configurability of your new `FrobsProvider`, you can define the schedule that the task runs at in `app-config.yaml` instead of requiring code changes to adjust. + +```yaml title="app-config.yaml" +catalog: + providers: + frobs-provider: + schedule: + initialDelay: { seconds: 30 } + frequency: { hours: 1 } + timeout: { minutes: 50 } +``` + +This approach will also allow you to customize the schedule per environment. You can also [add a schema to your config](../../conf/defining.md). + +#### New Backend + +```ts title="packages/backend/src/index.ts" +import { + SchedulerServiceTaskScheduleDefinition, + /* highlight-add-start */ + readSchedulerServiceTaskScheduleDefinitionFromConfig, + /* highlight-add-end */ +} from '@backstage/backend-plugin-api'; + +export const catalogModuleFrobsProvider = createBackendModule({ + pluginId: 'catalog', + moduleId: 'frobs-provider', + register(env) { + env.registerInit({ + deps: { + // ... other deps + /* highlight-add-start */ + rootConfig: coreServices.rootConfig, + /* highlight-add-end */ + }, + async init({ catalog, reader, scheduler, rootConfig }) { + /* highlight-add-start */ + const config = rootConfig.getConfig('catalog.providers.frobs-provider'); // Generally, catalog config goes under catalog.providers.pluginId + // Add a default schedule if you don't define one in config. + const schedule = config.has('schedule') + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + config.getConfig('schedule'), + ) + : { + frequency: { minutes: 30 }, + timeout: { minutes: 10 }, + }; + const taskRunner: SchedulerServiceTaskRunner = + scheduler.createScheduledTaskRunner(schedule); + /* highlight-add-end */ + + // rest of your code + }, + }); + }, +}); +``` + +#### Old Backend + +```ts title="packages/backend/src/plugins/catalog.ts" +/* highlight-add-next-line */ +import { FrobsProvider } from '../path/to/class'; +import { + /* highlight-add-start */ + readSchedulerServiceTaskScheduleDefinitionFromConfig, + /* highlight-add-end */ +} from '@backstage/backend-plugin-api'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + /* highlight-add-start */ + const config = env.config.getConfig('catalog.providers.frobs-provider'); // Generally, catalog config goes under catalog.providers.pluginId + // Add a default schedule if you don't define one in config. + const schedule = config.has('schedule') + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + config.getConfig('schedule'), + ) + : { + frequency: { minutes: 30 }, + timeout: { minutes: 10 }, + }; + const taskRunner = env.scheduler.createScheduledTaskRunner(schedule); + /* highlight-add-end */ + + // .. +} +``` + ### Example User Entity Provider If you have a 3rd party entity provider such as an internal HR system that you wish to use you are not limited to using our entity providers, (or simply wish to add to existing entity providers with your own data). diff --git a/docs/features/software-catalog/life-of-an-entity.md b/docs/features/software-catalog/life-of-an-entity.md index 13fe496fc1..1489688720 100644 --- a/docs/features/software-catalog/life-of-an-entity.md +++ b/docs/features/software-catalog/life-of-an-entity.md @@ -197,13 +197,15 @@ cannot be parsed successfully, etc. There are two main ways that these errors are surfaced. -First, the catalog backend will produce detailed logs that should contain -sufficient information for a reader to find the causes for errors. Since these -logs are typically not easily found by end users, this can mainly be a useful +First, the catalog backend will emit events using the [events backend plugin](https://github.com/backstage/backstage/tree/master/plugins/events-node). You can subscribe to the events. The events should contain +sufficient information for a reader to find the causes for errors. See the [configuration documentation](./configuration.md#subscribing-to-catalog-errors) for how to subscribe and log these error events. +Since these events are typically not easily found by end users, this can mainly be a useful tool for Backstage operators who want to debug problems either with statically registered entities that are under their control, or to help end users find problems. +> Prior to Backstage version v1.26.0 and `@backstage/plugin-catalog-backend` v1.21.9 catalog errors were logged by default. + Second, for most classes of errors, the entity itself will contain a status field that describes the problem. The contents of this field is shown at the top of your entity page in Backstage, if you have placed the corresponding error diff --git a/docs/features/software-templates/authorizing-scaffolder-template-details.md b/docs/features/software-templates/authorizing-scaffolder-template-details.md index 127677757c..942431c993 100644 --- a/docs/features/software-templates/authorizing-scaffolder-template-details.md +++ b/docs/features/software-templates/authorizing-scaffolder-template-details.md @@ -66,14 +66,14 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if ( isPermission(request.permission, templateParameterReadPermission) || isPermission(request.permission, templateStepReadPermission) ) { - if (user?.identity.userEntityRef === 'user:default/spiderman') + if (user?.info.userEntityRef === 'user:default/spiderman') return createScaffolderTemplateConditionalDecision(request.permission, { not: scaffolderTemplateConditions.hasTag({ tag: 'secret' }), }); @@ -109,11 +109,11 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, actionExecutePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return createScaffolderActionConditionalDecision(request.permission, { not: scaffolderActionConditions.hasActionId({ actionId: 'debug:log', @@ -147,11 +147,11 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, actionExecutePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return createScaffolderActionConditionalDecision(request.permission, { not: { allOf: [ @@ -190,25 +190,25 @@ import { class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-start */ if (isPermission(request.permission, taskCreatePermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; } } if (isPermission(request.permission, taskCancelPermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; } } if (isPermission(request.permission, taskReadPermission)) { - if (user?.identity.userEntityRef === 'user:default/spiderman') { + if (user?.info.userEntityRef === 'user:default/spiderman') { return { result: AuthorizeResult.ALLOW, }; @@ -239,7 +239,6 @@ Instead of the changes in `permission.ts` noted in the above example you will ma ```ts title="packages/backend/src/index.ts" import { createBackendModule } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { PolicyDecision, AuthorizeResult, @@ -247,13 +246,14 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha'; class ExamplePermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { // Various scaffolder permission checks ... diff --git a/docs/features/software-templates/builtin-actions.md b/docs/features/software-templates/builtin-actions.md index 5b1e4347da..301fb4c457 100644 --- a/docs/features/software-templates/builtin-actions.md +++ b/docs/features/software-templates/builtin-actions.md @@ -24,8 +24,7 @@ There are also several modules available for various SCM tools: Here's how to add an action module, first you need to run this command: -```sh -# From your Backstage root directory +```sh title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-scaffolder-backend-module-github ``` diff --git a/docs/features/software-templates/dry-run-testing.md b/docs/features/software-templates/dry-run-testing.md new file mode 100644 index 0000000000..dfabad265e --- /dev/null +++ b/docs/features/software-templates/dry-run-testing.md @@ -0,0 +1,79 @@ +--- +id: dry-run-testing +title: Dry Run Testing +description: How to enable and implement dry run testing in actions +--- + +Scaffolder templates can be tested using the dry run feature of scaffolder actions. This allows you to simulate the effects of running a scaffolder action without making any actual changes to your environment, for example creating a webhook in Github. Once dry run is enabled in the scaffolder action, you can add handling to actions you use in your scaffolder templates to define how an action should operate in a dry run scenario. + +## Enabling dry run testing + +To enable dry run for your scaffolder action you need to add `supportsDryRun: true` to the configuration object of `createTemplateAction` in the function where the behavior of your action is defined: + +```typescript +export function exampleAction() { + return createTemplateAction<{ + example: string; + }>({ + id: 'action:example', + description: 'Example action', + schema: { + input: { + type: 'object', + properties: { + example: { + title: 'example', + type: 'string', + }, + }, + }, + }, + supportsDryRun: true, + async handler(ctx) { + ... + }, + }); +} +``` + +## Adding handling for dry run + +To add handling for dry run functionality you need to add a check for `ctx.isDryRun` inside the handler of the configuration object which is being passed into `createTemplateAction` in the function where the behavior of your action is defined. Once the check is successful, you can perform the desired actions expected in a dry run, e.g. outputting non-sensitive inputs. + +```typescript +async handler(ctx) { + ... + + // If this is a dry run, log and return + if (ctx.isDryRun) { + ctx.logger.info(`Dry run complete`); + return; + } + + ... + }, +``` + +## Testing dry run handling + +You will also need to add tests for the dry run handling, for example: + +```typescript + it('should not perform action during dry run', async () => { + ... + + // Create the context object with the necessary properties for a dry run + const ctx = { + ...mockContext, + isDryRun: true, + input: { + ... + }, + }; + + // Call the handler with the context + await action.handler(ctx); + + expect(...); + }); +``` diff --git a/docs/features/software-templates/input-examples.md b/docs/features/software-templates/input-examples.md index 50495b1784..dd1228290f 100644 --- a/docs/features/software-templates/input-examples.md +++ b/docs/features/software-templates/input-examples.md @@ -26,6 +26,25 @@ parameters: ui:help: 'Hint: additional description...' ``` +#### Custom validation error message + +```yaml +parameters: + - title: Fill in some steps + properties: + name: + title: Simple text input + type: string + description: Description about input + maxLength: 8 + pattern: '^([a-zA-Z][a-zA-Z0-9]*)(-[a-zA-Z0-9]+)*$' + ui:autofocus: true + ui:help: 'Hint: additional description...' + errorMessage: + properties: + name: '1-8 alphanumeric tokens (first starts with letter) delimited by -' +``` + ### Multi line text input ```yaml @@ -256,3 +275,55 @@ spec: input: url: ${{ '/root' if parameters.path !== true else parameters.path }} ``` + +## Use placeholders to reference remote files + +:::note + +Testing of this functionality is not yet supported using _create/edit_. In addition, this functionality only works for remote files and not local files. You also cannot nest files. + +::: + +### template.yaml + +```yaml +spec: + parameters: + - $yaml: https://github.com/example/path/to/example.yaml + - title: Fill in some steps + properties: + path: + title: path + type: string + + steps: + - $yaml: https://github.com//example/path/to/action.yaml + + - id: fetch + name: Fetch template + action: fetch:template + input: + url: ${{ parameters.path if parameters.path else '/root' }} +``` + +### example.yaml + +```yaml +title: Provide simple information +required: + - url +properties: + url: + title: url + type: string +``` + +### action.yaml + +```yaml +id: publish +name: Publish files +action: publish:github +input: + repoUrl: ${{ parameters.url }} +``` diff --git a/docs/features/software-templates/ui-options-examples.md b/docs/features/software-templates/ui-options-examples.md index 7f0e5b225f..1a024569a6 100644 --- a/docs/features/software-templates/ui-options-examples.md +++ b/docs/features/software-templates/ui-options-examples.md @@ -441,3 +441,30 @@ repoUrl: `secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +## RepoBranchPicker + +The input props that can be specified under `ui:options` for the `RepoBranchPicker` field extension. + +### `requestUserCredentials` + +If defined will request user credentials to auth against the given SCM platform. + +```yaml +repoUrl: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN + additionalScopes: + github: + - workflow:write +``` + +`secretsKey` is the key used within the template secrets context to store the credential and `additionalScopes` is any additional permission scopes to request. + +The supported `additionalScopes` values are `gerrit`, `github`, `gitlab`, `bitbucket`, and `azure`. + +If you're also using the `RepoUrlPicker` field extension, you should simply duplicate this part from there. diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index e8b354bd45..98457d2e8d 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -145,6 +145,10 @@ When the action `handler` is called, we provide you a `context` as the only argument. It looks like the following: - `ctx.baseUrl` - a string where the template is located +- `ctx.checkpoint` - _Experimental_ allows to + implement [idempotency of the actions](https://github.com/backstage/backstage/tree/master/beps/0004-scaffolder-task-idempotency) + by not re-running the same function again if it was + executed successfully on the previous run. - `ctx.logger` - a Winston logger for additional logging inside your action - `ctx.logStream` - a stream version of the logger if needed - `ctx.workspacePath` - a string of the working directory of the template run @@ -153,7 +157,7 @@ argument. It looks like the following: - `ctx.output` - a function which you can call to set outputs that match the JSON schema or `zod` in `schema.output` for ex. `ctx.output('downloadUrl', myDownloadUrl)` - `createTemporaryDirectory` a function to call to give you a temporary - directory somewhere on the runner so you can store some files there rather + directory somewhere on the runner, so you can store some files there rather than polluting the `workspacePath` - `ctx.metadata` - an object containing a `name` field, indicating the template name. More metadata fields may be added later. @@ -191,7 +195,7 @@ const scaffolderModuleCustomExtensions = createBackendModule({ const backend = createBackend(); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); /* highlight-add-next-line */ -backend.add(scaffolderModuleCustomExtensions()); +backend.add(scaffolderModuleCustomExtensions); ``` If your custom action requires core services such as `config` or `cache` they can be imported in the dependencies and passed to the custom action function. @@ -217,6 +221,28 @@ import { }) ``` +### Using Checkpoints in Custom Actions (Experimental) + +Idempotent action could be achieved via the usage of checkpoints. + +Example: + +```ts title="plugins/my-company-scaffolder-actions-plugin/src/vendor/my-custom-action.ts" +const res = await ctx.checkpoint?.('create.projects', async () => { + const projectStgId = createStagingProjectId(); + const projectProId = createProductionProjectId(); + + return { + projectStgId, + projectProId, + }; +}); +``` + +You have to define the unique key in scope of the scaffolder task for your checkpoint. During the execution task engine +will check if the checkpoint with such key was already executed or not, if yes, and the run was successful, the callback +will be skipped and instead the stored value will be returned. + ### Register Custom Actions with the Legacy Backend System Once you have your Custom Action ready for usage with the scaffolder, you'll diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 15591eeb67..13dbe6da4b 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -477,6 +477,34 @@ template can be published to multiple providers. Note, that you will need to configure an [authentication provider](../../auth/index.md#configuring-authentication-providers), alongside the [`ScmAuthApi`](../../auth/index.md#scaffolder-configuration-software-templates) for your source code management (SCM) service to make this feature work. +### The Repository Branch Picker + +Similar to the repository picker, there is a picker for branches to support autocompletion. A full example could look like this: + +```yaml +- title: Choose a branch + required: + - repoBranch + properties: + repoBranch: + title: Repository Branch + type: string + ui:field: RepoBranchPicker + ui:options: + requestUserCredentials: + secretsKey: USER_OAUTH_TOKEN +``` + +Passing the `requestUserCredentials` object is required for autocompletion to work. +If you're also using the repository picker, you should simply duplicate this part from there. +For more information regarding the `requestUserCredentials` object, please refer to the [Using the Users `oauth` token](#using-the-users-oauth-token) section under [The Repository Picker](#the-repository-picker). + +For a list of all possible `ui:options` input props for `RepoBranchPicker`, please visit [here](./ui-options-examples.md#repobranchpicker). + +The `RepoBranchPicker` is a custom field that we provide part of the +`plugin-scaffolder`. You can provide your own custom fields by +[writing your own Custom Field Extensions](./writing-custom-field-extensions.md) + ### Accessing the signed-in users details Sometimes when authoring templates, you'll want to access the user that is running the template, and get details from the profile or the users `Entity` in the Catalog. @@ -533,6 +561,11 @@ catalogFilter: metadata.annotations.github.com/team-slug: { exists: true } ``` +#### Custom validation messages + +You may specify custom JSON Schema validation messages as supported by the +[ajv-errors](https://github.com/ajv-validator/ajv-errors) plugin library to [ajv](https://github.com/ajv-validator/ajv). + ## `spec.steps` - `Action[]` The `steps` is an array of the things that you want to happen part of this diff --git a/docs/features/techdocs/README.md b/docs/features/techdocs/README.md index a696b850a8..beec6d1fad 100644 --- a/docs/features/techdocs/README.md +++ b/docs/features/techdocs/README.md @@ -102,7 +102,7 @@ See [TechDocs Architecture](architecture.md) to get an overview of where the bel ## Get involved -Reach out to us in the **#docs-like-code** channel of our +Reach out to us in the **#techdocs** channel of our [Discord chatroom](https://github.com/backstage/backstage#community). ## Done diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index a861a50c87..8c2348972c 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -20,7 +20,7 @@ techdocs: # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running # into Docker in Docker situation. Read more here - # https://backstage.io/docs/features/techdocs/getting-started#disable-docker-in-docker-situation-optional + # https://backstage.io/docs/features/techdocs/getting-started/#disabling-docker-in-docker-situation-optional runIn: 'docker' diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 7909f8166f..a157d29c0d 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -21,8 +21,7 @@ The first step is to add the TechDocs plugin to your Backstage application. Navigate to your new Backstage application directory. And then to your `packages/app` directory, and install the `@backstage/plugin-techdocs` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-techdocs ``` @@ -106,8 +105,7 @@ That's it! Now, we need the TechDocs Backend plugin for the frontend to work. Navigate to `packages/backend` of your Backstage app, and install the `@backstage/plugin-techdocs-backend` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-techdocs-backend ``` @@ -199,8 +197,7 @@ To install TechDocs when using the New Backend system you will need to do the fo Navigate to `packages/backend` of your Backstage app, and install the `@backstage/plugin-techdocs-backend` package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-techdocs-backend ``` diff --git a/docs/frontend-system/architecture/03-extensions.md b/docs/frontend-system/architecture/03-extensions.md index 19345e0b26..2f3daba6f8 100644 --- a/docs/frontend-system/architecture/03-extensions.md +++ b/docs/frontend-system/architecture/03-extensions.md @@ -91,7 +91,9 @@ To create a new extension data reference to represent a type of shared extension ```ts export const reactElementExtensionDataRef = - createExtensionDataRef('my-plugin.reactElement'); + createExtensionDataRef().with({ + id: 'my-plugin.reactElement', + }); ``` The `ExtensionDataRef` can then be used to describe an output property of the extension. This will enforce typing on the return value of the extension factory: @@ -337,7 +339,7 @@ Similar to plugins the `ErrorBoundary` for extension allows to pass in a fallbac ### Analytics -Analytics information are provided through the `AnalyticsContext`, which will give `extensionId` & `pluginId` as context to analytics event fired inside of the extension. Additionally `RouteTracker` will capture an analytics event for routable extension to inform which extension metadata gets associated with a navigation event when the route navigated to is a gathered `mountPoint`. +Analytics information are provided through the `AnalyticsContext`, which will give `extensionId` & `pluginId` as context to analytics event fired inside of the extension. Additionally `RouteTracker` will capture an analytics event for routable extension to inform which extension metadata gets associated with a navigation event when the route navigated to is a gathered `mountPoint`. Whether an extension is routable is inferred from its outputs, but you can also explicitly control this behavior by passing the `routable` prop to `ExtensionBoundary`. The `ExtensionBoundary` can be used like the following in an extension creator: @@ -359,7 +361,7 @@ export function createSomeExtension< path: config.path, routeRef: options.routeRef, element: ( - + ), diff --git a/docs/frontend-system/architecture/07-routes.md b/docs/frontend-system/architecture/07-routes.md index 99cfbae937..9db4d007b6 100644 --- a/docs/frontend-system/architecture/07-routes.md +++ b/docs/frontend-system/architecture/07-routes.md @@ -101,23 +101,26 @@ export const IndexPage = () => { return (

Index Page

- - See "Foo" details - + {/* highlight-next-line */} + {getDetailsPath && ( + + See "Foo" details + + )}
); }; ``` -We use the `useRouteRef` hook to create a link generator function that returns the details page path. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page. +We use the `useRouteRef` hook to create a link generator function that returns the details page path. First we need to check whether the route is available, the link generator function will be `undefined` if it isn't. We then call the link generator, passing it an object with the kind, namespace, and name. These parameters are used to construct a concrete path to the "Foo" details page. Let's see how the details page can get the parameters from the URL: @@ -176,8 +179,11 @@ export const IndexPage = () => { return (

Index Page

- {/* highlight-next-line */} - Create Component + {/* highlight-start */} + {getCreateComponentPath && ( + Create Component + )} + {/* highlight-end */}
); }; @@ -289,42 +295,6 @@ export const createComponentExternalRouteRef = createExternalRouteRef({ }); ``` -### Optional External Route References - -It is possible to define an `ExternalRouteRef` as optional, so it is not required to bind it in the app. - -```tsx title="plugins/catalog/src/routes.ts" -import { createExternalRouteRef } from '@backstage/frontend-plugin-api'; - -export const createComponentExternalRouteRef = createExternalRouteRef({ - // highlight-next-line - optional: true, -}); -``` - -When calling `useRouteRef` with an optional external route, its return signature is changed to `RouteFunc | undefined`, and the returned value can be used to decide whether a certain link should be displayed or if an action should be taken: - -```tsx title="plugins/catalog/src/components/IndexPage.tsx" -import React from 'react'; -import { useRouteRef } from '@backstage/frontend-plugin-api'; -import { createComponentExternalRouteRef } from '../routes'; - -export const IndexPage = () => { - const getCreateComponentPath = useRouteRef(createComponentExternalRouteRef); - return ( -
-

Index Page

- {/* Rendering the link only if the getCreateComponentPath is defined */} - {/* highlight-start */} - {getCreateComponentPath && ( - Create Component - )} - {/* highlight-end */} -
- ); -}; -``` - ## Sub Route References The last kind of route ref that can be created is a `SubRouteRef`, which can be used to create a route ref with a fixed path relative to an absolute `RouteRef`. They are useful if you have a page that internally is mounted at a sub route of a page extension component, and you want other plugins to be able to route to that page. And they can be a useful utility to handle routing within a plugin itself as well. @@ -359,7 +329,7 @@ export const detailsSubRouteRef = createSubRouteRef({ Using subroutes in a page extension is as simple as this: -```tsx title="plugins/catalog/src/components/IndexPage.ts" +```tsx title="plugins/catalog/src/components/IndexPage.tsx" import React from 'react'; import { Routes, Route, useLocation } from 'react-router-dom'; import { useRouteRef } from '@backstage/frontend-plugin-api'; @@ -368,17 +338,21 @@ import { DetailsPage } from './DetailsPage'; export const IndexPage = () => { const { pathname } = useLocation(); + + // highlight-start const getIndexPath = useRouteRef(indexRouteRef); const getDetailsPath = useRouteRef(detailsSubRouteRef); + // highlight-end + return (

Index Page

{/* Linking to the details sub route */} - {pathname === getIndexPath() ? ( - // highlight-start + {/* highlight-start */} + {pathname === getIndexPath?.() ? ( { > Show details - // highlight-end + {/* highlight-end */} ) : ( - // highlight-next-line - Hide details + {/* highlight-next-line */} + Hide details )} {/* Registering the details sub route */} + {/* highlight-next-line */} } />
@@ -402,7 +377,7 @@ export const IndexPage = () => { This is how you can get the parameters of a sub route URL: -```tsx title="plugins/catalog/src/components/DetailsPage.ts" +```tsx title="plugins/catalog/src/components/DetailsPage.tsx" import React from 'react'; import { useParams } from 'react-router-dom'; @@ -426,7 +401,7 @@ export const DetailsPage = () => { Finally, see how a plugin can provide subroutes: -```tsx title="plugins/catalog/src/plugin.ts" +```tsx title="plugins/catalog/src/plugin.tsx" import React from 'react'; import { createPlugin, diff --git a/docs/frontend-system/architecture/08-naming-patterns.md b/docs/frontend-system/architecture/08-naming-patterns.md index 0916d9fd51..f031b80d84 100644 --- a/docs/frontend-system/architecture/08-naming-patterns.md +++ b/docs/frontend-system/architecture/08-naming-patterns.md @@ -38,33 +38,31 @@ Note that while we use this naming pattern for the plugin instance this is only | Description | Pattern | Examples | | ----------- | ------------------------------- | ------------------------------------------------------------------- | -| Creator | `createExtension` | `createPageExtension`, `createEntityCardExtension` | +| Blueprint | `Blueprint` | `PageBlueprint`, `EntityCardBlueprint` | | ID | `[:][/]` | `'core.nav'`, `'page:user-settings'`, `'entity-card:catalog/about'` | | Symbol | `[][]` | `coreNav`, `userSettingsPage`, `catalogAboutEntityCard` | -When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the extension creator function used to create the extension, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. +When you create a new extension you never provide the ID directly. Instead, you indirectly or directly provide the kind, namespace, and name parts that make up the ID. The kind is always provided by the blueprint creator, the only exception is if you use `createExtension` directly. Any extension that is provided by a plugin will by default have its namespace set to the plugin ID, so you generally only need to provide an explicit namespace if you want to override an existing extension. The name is also optional, and primarily used to distinguish between multiple extensions of the same kind and namespace. If a plugin doesn't need to distinguish between different extensions of the same kind, the name can be omitted. Example: ```ts -// This is an extension creator that is used to create an extension of the 'page' kind. -export function createPageExtension(options) { - return createExtension({ - kind: 'page', // Kinds are kebab-case - // ...options - }); -} +// This is an extension blueprint that is used to create an extension of the 'page' kind. +export const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + // ... +}); // The namespace is inferred from the plugin ID, in this case 'catalog' // The final ID for this extension will be 'page:catalog/entity' -const catalogEntityPage = createPageExtension({ +const catalogEntityPage = PageBlueprint.make({ name: 'entity', // ... }); // The name is omitted, because the catalog plugin only provides a single extension of this kind // The final ID for this extension will be 'search-result-list-item:catalog' -const catalogSearchResultListItem = createSearchResultListItemExtension({ +const catalogSearchResultListItem = SearchResultListItemBlueprint.make({ // ... }); @@ -100,9 +98,9 @@ export interface SearchResultItemExtensionData { } export const searchResultItemExtensionDataRef = - createExtensionDataRef( - 'search.search-result-item', - ); + createExtensionDataRef().with({ + id: 'search.search-result-item', + }); ``` #### Grouped Extension Data @@ -111,8 +109,12 @@ This way of defining extension data is similar to the standalone way, but it use ```ts export const coreExtensionData = { - reactElement: createExtensionDataRef('core.react-element'), - routePath: createExtensionDataRef('core.route-path'), + reactElement: createExtensionDataRef().with({ + id: 'core.react-element', + }), + routePath: createExtensionDataRef().with({ + id: 'core.route-path', + }), }; ``` @@ -127,9 +129,9 @@ export function createGraphiQLEndpointExtension(options) { // Use a TypeScript namespace to merge the extension data references with the extension creator export namespace createGraphiQLEndpointExtension { - export const endpointDataRef = createExtensionDataRef( - 'graphiql.graphiql-endpoint.endpoint', - ); + export const endpointDataRef = createExtensionDataRef().with({ + id: 'graphiql.graphiql-endpoint.endpoint', + }); } ``` diff --git a/docs/frontend-system/building-apps/03-built-in-extensions.md b/docs/frontend-system/building-apps/03-built-in-extensions.md index 4f8417f74f..a5e6476eb0 100644 --- a/docs/frontend-system/building-apps/03-built-in-extensions.md +++ b/docs/frontend-system/building-apps/03-built-in-extensions.md @@ -45,6 +45,7 @@ This extension is the first extension attached to the extension tree. It is resp | themes | The app themes list. | [createThemeExtension.themeDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension.themedataref) | false | See [default themes](#default-theme-extensions). | [createThemeExtension](https://backstage.io/docs/reference/frontend-plugin-api.createthemeextension) | | components | The app components list. | [createComponentExtension.componentDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createcomponentextension.componentdataref) | false | See [default components](#default-components-extensions). | [createComponentExtension](https://backstage.io/docs/reference/frontend-plugin-api.createcomponentextension) | | translations | The app translations list. | [createTranslationExtension.translationDataRef](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension.translationdataref) | false | - | [createTranslationExtension](https://backstage.io/docs/reference/frontend-plugin-api.createtranslationextension) | +| icons | The app icons list. | [IconBundleBlueprint.dataRefs.icons](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint.dataRefs.icons) | true | - | [IconBundleBlueprint](https://backstage.io/docs/reference/frontend-plugin-api.iconbundleblueprint) | #### Default theme extensions diff --git a/docs/frontend-system/building-apps/08-migrating.md b/docs/frontend-system/building-apps/08-migrating.md index 0abdd2aac3..716e0da772 100644 --- a/docs/frontend-system/building-apps/08-migrating.md +++ b/docs/frontend-system/building-apps/08-migrating.md @@ -314,6 +314,31 @@ const app = createApp({ }); ``` +### `icons` + +Icons are now installed as extensions, using the `IconBundleBlueprint` to make new instances which can be added to the app. + +```ts +import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + +const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, +}); + +const app = createApp({ + features: [ + createExtensionOverrides({ + extensions: [exampleIconBundle], + }), + ], +}); +``` + ### `bindRoutes` Route bindings can still be done using this option, but you now also have the ability to bind routes using static configuration instead. See the section on [binding routes](../architecture/07-routes.md#binding-external-route-references) for more information. diff --git a/docs/frontend-system/building-plugins/03-extension-types.md b/docs/frontend-system/building-plugins/03-extension-types.md index 2bea073ed6..1f3baaa710 100644 --- a/docs/frontend-system/building-plugins/03-extension-types.md +++ b/docs/frontend-system/building-plugins/03-extension-types.md @@ -38,6 +38,10 @@ Sign-in page extension have a single purpose - to implement a custom sign-in pag Theme extensions provide custom themes for the app. They are always attached to the app extension and you can have any number of themes extensions installed in an app at once, letting the user choose which theme to use. +### Icons - [Reference](../../reference/frontend-plugin-api.iconbundleblueprint.md) + +Icon bundle extensions provide the ability to replace or provide new icons to the app. You can use the above blueprint to make new extension instances which can be installed into the app. + ### Translation - [Reference](../../reference/frontend-plugin-api.createtranslationextension.md) Translation extension provide custom translation messages for the app. They can be used both to override the default english messages to custom ones, as well as provide translations for additional languages. diff --git a/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 3fb0365ad0..4c013440da 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -64,7 +64,7 @@ const app = createApp({ }) ``` -Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.light` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). +Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `themes.light` and `themes.dark` from [`@backstage/theme`](https://www.npmjs.com/package/@backstage/theme). ## Example of a custom theme diff --git a/docs/getting-started/config/authentication.md b/docs/getting-started/config/authentication.md index 874964696a..e97ed3d89c 100644 --- a/docs/getting-started/config/authentication.md +++ b/docs/getting-started/config/authentication.md @@ -50,7 +50,6 @@ Open `packages/app/src/App.tsx` and below the last `import` line, add: ```typescript title="packages/app/src/App.tsx" import { githubAuthApiRef } from '@backstage/core-plugin-api'; -import { SignInPage } from '@backstage/core-components'; ``` Search for `const app = createApp({` in this file, and below `apis,` add: diff --git a/docs/getting-started/config/database.md b/docs/getting-started/config/database.md index a560fe507b..0fbd66d5a3 100644 --- a/docs/getting-started/config/database.md +++ b/docs/getting-started/config/database.md @@ -73,8 +73,7 @@ to install and configure the client. Go to the root directory of your freshly installed Backstage App. Run the following to install the PostgreSQL client into your backend: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add pg ``` diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index 2fdb016e7b..95fd456dc0 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -21,8 +21,7 @@ to an entity in the software catalog. 1. Add the plugin's npm package to the repo: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn --cwd packages/app add @circleci/backstage-plugin ``` diff --git a/docs/getting-started/homepage.md b/docs/getting-started/homepage.md index 7e1a2ed909..d7c9b46faa 100644 --- a/docs/getting-started/homepage.md +++ b/docs/getting-started/homepage.md @@ -28,8 +28,7 @@ Now, let's get started by installing the home plugin and creating a simple homep #### 1. Install the plugin -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/app add @backstage/plugin-home ``` diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 99fdb3d8ed..8bb30d1db8 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -1,6 +1,6 @@ --- id: index -title: Installing a standalone server +title: Creating your Backstage App sidebar_label: Introduction description: How to install Backstage for your own use. --- diff --git a/docs/getting-started/keeping-backstage-updated.md b/docs/getting-started/keeping-backstage-updated.md index 1de8874d56..88e4a02ef2 100644 --- a/docs/getting-started/keeping-backstage-updated.md +++ b/docs/getting-started/keeping-backstage-updated.md @@ -63,18 +63,16 @@ When a given dependency version is the _same_ between different packages, the dependency is hoisted to the main `node_modules` folder in the monorepo root to be shared between packages. When _different_ versions of the same dependency are encountered, Yarn creates a `node_modules` folder within a particular package. +This can lead to multiple versions of the same package being installed and used +in the same app. -This can lead to confusing situations with type definitions, or anything with -global state. React [Context](https://reactjs.org/docs/context.html), for -example, depends on global referential equality. This can cause problems in -Backstage with API lookup, or config loading. +All Backstage core packages are implemented in such as way that package +duplication is **not** a problem. For example, duplicate installations of +packages like `@backstage/core-plugin-api`, `@backstage/core-components`, +`@backstage/plugin-catalog-react`, and `@backstage/backend-plugin-api` are all +acceptable. -To help resolve these situations, the Backstage CLI has -[versions:check](https://backstage.io/docs/tooling/cli/03-commands#versionscheck). This -will validate versions of `@backstage` packages in your app to check for -duplicate definitions: - -```bash -# Add --fix to attempt automatic resolution in yarn.lock -yarn backstage-cli versions:check -``` +While package duplication might be acceptable in many cases, you might want to +deduplicate packages for the purpose of optimizing bundle size and installation +speed. We recommend using deduplication utilities such as `yarn dedupe` to trim +down the number of duplicate packages. diff --git a/docs/getting-started/logging-in.md b/docs/getting-started/logging-in.md index fef8907e07..7d6fb5d55b 100644 --- a/docs/getting-started/logging-in.md +++ b/docs/getting-started/logging-in.md @@ -20,6 +20,8 @@ Run your Backstage app with `yarn dev`. Navigate to `http://localhost:3000`. If you're not already logged in, you should see a login screen like this, +![Screenshot of the login screen](../assets/getting-started/login-screen.png) + To login, you should choose the "Github" provider and click the "Sign in" button. This will redirect you to a Github OAuth page. Verify that the scopes mentioned on that page match the setup you did in [the authentication tutorial](./config/authentication.md). Once you click "Confirm", you will be brought back to the Backstage interface and signed in! If you are already logged in, you will be automatically brought to your Backstage instance. diff --git a/docs/integrations/aws-s3/discovery--old.md b/docs/integrations/aws-s3/discovery--old.md new file mode 100644 index 0000000000..09b6eb6cc0 --- /dev/null +++ b/docs/integrations/aws-s3/discovery--old.md @@ -0,0 +1,88 @@ +--- +id: discovery--old +title: AWS S3 Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from an AWS S3 Bucket +--- + +:::info +This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! +::: + +The AWS S3 integration has a special entity provider for discovering catalog +entities located in an S3 Bucket. If you have a bucket that contains multiple +catalog files, and you want to automatically discover them, you can use this +provider. The provider will crawl your S3 bucket and register entities +matching the configured path. This can be useful as an alternative to static +locations or manually adding things to the catalog. + +To use the entity provider, you'll need an AWS S3 integration +[set up](locations.md) with `accessKeyId` and `secretAccessKey`, and/or +a `roleArn` or none of these (e.g., profile- or instance-based credentials). + +At production deployments, you likely manage these with the permissions attached +to your instance. + +In your configuration, you add a provider config per bucket: + +```yaml +# app-config.yaml + +catalog: + providers: + awsS3: + yourProviderId: # identifies your dataset / provider independent of config changes + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + schedule: # same options as in TaskScheduleDefinition + # supports cron, ISO duration, "human duration" as used in code + frequency: { minutes: 30 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } +``` + +For simple setups, you can omit the provider ID at the config +which has the same effect as using `default` for it. + +```yaml +# app-config.yaml + +catalog: + providers: + awsS3: + # uses "default" as provider ID + bucketName: sample-bucket + prefix: prefix/ # optional + region: us-east-2 # optional, uses the default region otherwise + schedule: # same options as in TaskScheduleDefinition + # supports cron, ISO duration, "human duration" as used in code + frequency: { minutes: 30 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } +``` + +As this provider is not one of the default providers, you will first need to install +the AWS catalog plugin: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws +``` + +Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: + +```ts +/* packages/backend/src/plugins/catalog.ts */ + +import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; + +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + AwsS3EntityProvider.fromConfig(env.config, { + logger: env.logger, + scheduler: env.scheduler, + }), +); +``` diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index d664466a16..db9119680e 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -6,6 +6,10 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from an AWS S3 Bucket --- +:::info +This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +::: + The AWS S3 integration has a special entity provider for discovering catalog entities located in an S3 Bucket. If you have a bucket that contains multiple catalog files, and you want to automatically discover them, you can use this @@ -20,7 +24,7 @@ a `roleArn` or none of these (e.g., profile- or instance-based credentials). At production deployments, you likely manage these with the permissions attached to your instance. -At your configuration, you add a provider config per bucket: +In your configuration, you add a provider config per bucket: ```yaml # app-config.yaml @@ -62,24 +66,15 @@ catalog: As this provider is not one of the default providers, you will first need to install the AWS catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-aws ``` -Once you've done that, you'll also need to add the segment below to `packages/backend/src/plugins/catalog.ts`: +Then update your backend by adding the following line: -```ts -/* packages/backend/src/plugins/catalog.ts */ - -import { AwsS3EntityProvider } from '@backstage/plugin-catalog-backend-module-aws'; - -const builder = await CatalogBuilder.create(env); -/** ... other processors and/or providers ... */ -builder.addEntityProvider( - AwsS3EntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), -); +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-catalog-backend-module-aws/alpha')); +/* highlight-add-end */ ``` diff --git a/docs/integrations/azure/discovery--old.md b/docs/integrations/azure/discovery--old.md index be490d70fe..56a2fe559f 100644 --- a/docs/integrations/azure/discovery--old.md +++ b/docs/integrations/azure/discovery--old.md @@ -102,8 +102,7 @@ It may take some time before the branch is indexed and searchable. As this provider is not one of the default providers, you will first need to install the Azure catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md index 7e7b465118..c8a8a3af7c 100644 --- a/docs/integrations/azure/discovery.md +++ b/docs/integrations/azure/discovery.md @@ -102,8 +102,7 @@ It may take some time before the branch is indexed and searchable. As this provider is not one of the default providers, you will first need to install the Azure catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-azure ``` diff --git a/docs/integrations/azure/org--old.md b/docs/integrations/azure/org--old.md index 13d53b24b3..899120e88a 100644 --- a/docs/integrations/azure/org--old.md +++ b/docs/integrations/azure/org--old.md @@ -18,8 +18,7 @@ Microsoft Graph API. The package is not installed by default, therefore you have to add `@backstage/plugin-catalog-backend-module-msgraph` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph ``` diff --git a/docs/integrations/azure/org.md b/docs/integrations/azure/org.md index 89abd4813b..1868188cdf 100644 --- a/docs/integrations/azure/org.md +++ b/docs/integrations/azure/org.md @@ -18,8 +18,7 @@ Microsoft Graph API. The package is not installed by default, therefore you have to add `@backstage/plugin-catalog-backend-module-msgraph` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-msgraph ``` @@ -112,6 +111,17 @@ microsoftGraphOrg: search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' ``` +If you don't want to only ingest groups matching the `search` and/or `filter` query, but also the groups which are members of the matched groups, you can use the `includeSubGroups` configuration: + +```yaml +microsoftGraphOrg: + providerId: + group: + filter: securityEnabled eq false and mailEnabled eq true and groupTypes/any(c:c+eq+'Unified') + search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' + includeSubGroups: true +``` + In addition to these groups, one additional group will be created for your organization. All imported groups will be a child of this group. @@ -150,6 +160,18 @@ microsoftGraphOrg: loadPhotos: false ``` +If you are using `userGroupMember`, the configuration for `loadPhotos` should still be managed under `users:` while omitting `search` and `filters`. + +```yaml +microsoftGraphOrg: + providerId: + user: + loadPhotos: false + userGroupMember: + filter: "displayName eq 'Backstage Users'" + search: '"description:One" AND ("displayName:Video" OR "displayName:Drive")' +``` + ## Customizing Transformation Ingested entities can be customized by providing custom transformers. @@ -169,6 +191,23 @@ microsoftGraphOrg: select: ['id', 'displayName', 'description'] ``` +### Using Provider Config Transformer + +Dynamic configuration scaling allows the `msgraph` catalog plugin to adjust its settings at runtime without requiring a redeploy. This feature is useful for scenarios where configuration needs to be updated based on real-time events or changing conditions. For example, you can dynamically adjust synchronization schedules, filters, and search parameters to optimize performance and responsiveness. + +:::note +Adjusting fields that are not used on each scheduled ingestion (e.g., `id`, `schedule`) will have no effect. +::: + +:::warning +Dynamically changing configuration on the fly can introduce unintended consequences, such as system instability and configuration errors. Please review your transformer carefully to ensure that it is working as anticipated! +::: + +#### Example Use Cases: + +- **Filter Scaling**: Adjust filters like `userGroupMember` and `groupFilter` dynamically. +- **Search Parameter Adjustment**: Change search parameters such as `groupSearch` and `userSelect` on-the-fly. + ### Using Custom Transformers Transformers can be configured by extending `microsoftGraphOrgEntityProviderTransformExtensionPoint`. Here is an example: @@ -180,6 +219,7 @@ import { myUserTransformer, myGroupTransformer, myOrganizationTransformer, + myProviderConfigTransformer, } from './transformers'; backend.add( @@ -201,6 +241,9 @@ backend.add( microsoftGraphTransformers.setOrganizationTransformer( myOrganizationTransformer, ); + microsoftGraphTransformers.setProviderConfigTransformer( + myProviderConfigTransformer, + ); /* highlight-add-end */ }, }); @@ -209,7 +252,7 @@ backend.add( ); ``` -The `myUserTransformer`, `myGroupTransformer`, and `myOrganizationTransformer` transformer functions are from the examples in the section below. +The `myUserTransformer`, `myGroupTransformer`, `myOrganizationTransformer`, and `myProviderConfigTransformer` transformer functions are from the examples in the section below. ### Transformer Examples @@ -221,6 +264,7 @@ import { defaultGroupTransformer, defaultUserTransformer, defaultOrganizationTransformer, + MicrosoftGraphProviderConfig, } from '@backstage/plugin-catalog-backend-module-msgraph'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; @@ -263,6 +307,16 @@ export async function myOrganizationTransformer( ): Promise { return undefined; } + +// Example config transformer that expands the group filter to also include 'azure-group-a' +export async function myProviderConfigTransformer( + provider: MicrosoftGraphProviderConfig, +): Promise { + if (!provider.groupFilter?.includes('azure-group-a')) { + provider.groupFilter = `${provider.groupFilter} or displayName eq 'azure-group-a'`; + } + return provider; +} ``` ## Troubleshooting diff --git a/docs/integrations/bitbucketCloud/discovery.md b/docs/integrations/bitbucketCloud/discovery.md index 160827cb0a..04dc111d30 100644 --- a/docs/integrations/bitbucketCloud/discovery.md +++ b/docs/integrations/bitbucketCloud/discovery.md @@ -19,8 +19,7 @@ backend. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-bitbucket-cloud` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-cloud ``` diff --git a/docs/integrations/bitbucketServer/discovery.md b/docs/integrations/bitbucketServer/discovery.md index d9fc946460..99e04affa8 100644 --- a/docs/integrations/bitbucketServer/discovery.md +++ b/docs/integrations/bitbucketServer/discovery.md @@ -19,8 +19,7 @@ backend. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-bitbucket-server` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-bitbucket-server ``` diff --git a/docs/integrations/datadog-rum/installation.md b/docs/integrations/datadog-rum/installation.md index 4dc6c24134..6659bec182 100644 --- a/docs/integrations/datadog-rum/installation.md +++ b/docs/integrations/datadog-rum/installation.md @@ -28,6 +28,51 @@ app: If your [`app-config.yaml`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/app-config.yaml#L5) file does not have this configuration, you may have to adjust your [`packages/app/public/index.html`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/packages/app/public/index.html#L69) to include the Datadog RUM `init()` section manually. +Please note that the env value MUST be specified at build time + +:::note +In case after a proper configuration, the events still are not being captured: Copy and paste this section in to your `packages/app/public/index.html` under the `` tag. + +```html +<% if (config.has('app.datadogRum')) { %> + +<% } %> +``` + The `clientToken` and `applicationId` are generated from the Datadog RUM page following [these instructions](https://docs.datadoghq.com/real_user_monitoring/browser/). diff --git a/docs/integrations/gerrit/discovery--old.md b/docs/integrations/gerrit/discovery--old.md new file mode 100644 index 0000000000..250de80bdf --- /dev/null +++ b/docs/integrations/gerrit/discovery--old.md @@ -0,0 +1,73 @@ +--- +id: discovery--old +title: Gerrit Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from Gerrit repositories +--- + +:::info +This documentation is written for the old backend which has been replaced by [the new backend system](../../backend-system/index.md), being the default since Backstage [version 1.24](../../releases/v1.24.0.md). If have migrated to the new backend system, you may want to read [its own article](./discovery.md) instead. Otherwise, [consider migrating](../../backend-system/building-backends/08-migrating.md)! +::: + +The Gerrit integration has a special entity provider for discovering catalog entities +from Gerrit repositories. The provider uses the "List Projects" API in Gerrit to get +a list of repositories and will automatically ingest all `catalog-info.yaml` files +stored in the root of the matching projects. + +## Installation + +As this provider is not one of the default providers, you will first need to install +the Gerrit provider plugin: + +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gerrit +``` + +Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: + +```ts +/* packages/backend/src/plugins/catalog.ts */ +import { GerritEntityProvider } from '@backstage/plugin-catalog-backend-module-gerrit'; +const builder = await CatalogBuilder.create(env); +/** ... other processors and/or providers ... */ +builder.addEntityProvider( + GerritEntityProvider.fromConfig(env.config, { + logger: env.logger, + scheduler: env.scheduler, + }), +); +``` + +## Configuration + +To use the discovery processor, you'll need a Gerrit integration +[set up](locations.md). Then you can add any number of providers. + +```yaml +# app-config.yaml +catalog: + providers: + gerrit: + yourProviderId: # identifies your dataset / provider independent of config changes + host: gerrit-your-company.com + branch: master # Optional + query: 'state=ACTIVE&prefix=webapps' + schedule: + # supports cron, ISO duration, "human duration" as used in code + frequency: { minutes: 30 } + # supports ISO duration, "human duration" as used in code + timeout: { minutes: 3 } + backend: + host: gerrit-your-company.com + branch: master # Optional + query: 'state=ACTIVE&prefix=backend' +``` + +The provider configuration is composed of three parts: + +- **`host`**: the host of the Gerrit integration to use. +- **`branch`** _(optional)_: the branch where we will look for catalog entities (defaults to "master"). +- **`query`**: this string is directly used as the argument to the "List Project" API. + Typically, you will want to have some filter here to exclude projects that will + never contain any catalog files. diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index e2922c1f76..eca0417871 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -6,6 +6,10 @@ sidebar_label: Discovery description: Automatically discovering catalog entities from Gerrit repositories --- +:::info +This documentation is written for [the new backend system](../../backend-system/index.md) which is the default since Backstage [version 1.24](../../releases/v1.24.0.md). If you are still on the old backend system, you may want to read [its own article](./discovery--old.md) instead, and [consider migrating](../../backend-system/building-backends/08-migrating.md)! +::: + The Gerrit integration has a special entity provider for discovering catalog entities from Gerrit repositories. The provider uses the "List Projects" API in Gerrit to get a list of repositories and will automatically ingest all `catalog-info.yaml` files @@ -16,24 +20,17 @@ stored in the root of the matching projects. As this provider is not one of the default providers, you will first need to install the Gerrit provider plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gerrit ``` -Then add the plugin to the plugin catalog `packages/backend/src/plugins/catalog.ts`: +Then update your backend by adding the following line: -```ts -/* packages/backend/src/plugins/catalog.ts */ -import { GerritEntityProvider } from '@backstage/plugin-catalog-backend-module-gerrit'; -const builder = await CatalogBuilder.create(env); -/** ... other processors and/or providers ... */ -builder.addEntityProvider( - GerritEntityProvider.fromConfig(env.config, { - logger: env.logger, - scheduler: env.scheduler, - }), -); +```ts title="packages/backend/src/index.ts" +backend.add(import('@backstage/plugin-catalog-backend/alpha')); +/* highlight-add-start */ +backend.add(import('@backstage/plugin-catalog-backend-module-gerrit/alpha')); +/* highlight-add-end */ ``` ## Configuration diff --git a/docs/integrations/github/discovery--old.md b/docs/integrations/github/discovery--old.md index 138e54a538..752aa51bde 100644 --- a/docs/integrations/github/discovery--old.md +++ b/docs/integrations/github/discovery--old.md @@ -25,8 +25,7 @@ backend. They are not installed by default, therefore you have to add a dependency on `@backstage/plugin-catalog-backend-module-github` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` @@ -273,8 +272,7 @@ backend. They are not installed by default, therefore you have to add a dependency on `@backstage/plugin-catalog-backend-module-github` to your backend package, plus `@backstage/integration` for the basic credentials management: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/integration @backstage/plugin-catalog-backend-module-github ``` diff --git a/docs/integrations/github/discovery.md b/docs/integrations/github/discovery.md index 5c3a33416b..43e63a618c 100644 --- a/docs/integrations/github/discovery.md +++ b/docs/integrations/github/discovery.md @@ -24,8 +24,7 @@ You will have to add the GitHub Entity provider to your backend as it is not ins dependency on `@backstage/plugin-catalog-backend-module-github` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` @@ -40,8 +39,9 @@ backend.add(import('@backstage/plugin-catalog-backend-module-github/alpha')); ## Events Support The catalog module for GitHub comes with events support enabled. -This will make it subscribe to its relevant topics (`github.push`) -and expects these events to be published via the `EventsService`. +This will make it subscribe to its relevant topics (`github.push`, +`github.repository`) and expects these events to be published +via the `EventsService`. Additionally, you should install the [event router by `events-backend-module-github`](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-github/README.md) @@ -55,7 +55,15 @@ You can decide between the following options (extensible): - [via HTTP endpoint](https://github.com/backstage/backstage/tree/master/plugins/events-backend/README.md) - [via an AWS SQS queue](https://github.com/backstage/backstage/tree/master/plugins/events-backend-module-aws-sqs/README.md) -You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). The webhook will need to be configured to forward `push` events. +You can check the official docs to [configure your webhook](https://docs.github.com/en/developers/webhooks-and-events/webhooks/creating-webhooks) and to [secure your request](https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks). + +The webhook(s) will need to be configured to react to `push` and +`repository` events. + +Certain actions like `transferred` by the `repository` event type +will not be supported when you use repository webhooks. +Please check the GitHubs documentation for these event types and +its actions. ## Configuration diff --git a/docs/integrations/github/github-apps.md b/docs/integrations/github/github-apps.md index b5e252be74..298eba26e3 100644 --- a/docs/integrations/github/github-apps.md +++ b/docs/integrations/github/github-apps.md @@ -109,8 +109,8 @@ integrations: webhookSecret: ${AUTH_ORG_WEBHOOK_SECRET} ``` -:::Note -Note that in both examples above `apps` is an array which means you can add multiple GitHub Apps using `$include` or environment variables as long as they are each for a different GitHub Org as mentioned under the [Caveats](#caveats) section +:::note +Note that in both examples above `apps` is an array which means you can add multiple GitHub Apps using `$include` or environment variables as long as they are each for a different GitHub Org as mentioned under the [Caveats](#caveats) section. ::: ## Limiting the GitHub App installations diff --git a/docs/integrations/github/org--old.md b/docs/integrations/github/org--old.md index 3d1639a924..bf12cad036 100644 --- a/docs/integrations/github/org--old.md +++ b/docs/integrations/github/org--old.md @@ -29,8 +29,7 @@ the Processor method (not recommended), it is described separately below. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-github` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` @@ -334,8 +333,7 @@ frequency with which they are refreshed, separately from other processors. The `GithubOrgReaderProcessor` is not registered by default, so you have to install and register it in the catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github ``` diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md index f94ffe6c7a..18865cd630 100644 --- a/docs/integrations/github/org.md +++ b/docs/integrations/github/org.md @@ -38,8 +38,7 @@ You will have to add the GitHub Org provider to your backend as it is not instal dependency on `@backstage/plugin-catalog-backend-module-github-org` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-github-org ``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index 98fbb0fbce..70f12f181e 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -20,8 +20,7 @@ This provider can also be configured to ingest GitLab data based on [GitLab Webh As this provider is not one of the default providers, you will first need to install the gitlab catalog plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab ``` @@ -154,6 +153,7 @@ catalog: group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything + excludeRepos: [] # Optional. A list of project paths that should be excluded from discovery, e.g. group/subgroup/repo. Should not start or end with a slash. schedule: # Same options as in TaskScheduleDefinition. Optional for the Legacy Backend System # supports cron, ISO duration, "human duration" as used in code frequency: { minutes: 30 } diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index da20af9d4a..303a4db752 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -25,8 +25,7 @@ This provider can also be configured to ingest GitLab data based on [GitLab Syst As this provider is not one of the default providers, you will first need to install the Gitlab provider plugin: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-gitlab @backstage/plugin-catalog-backend-module-gitlab-org ``` @@ -225,11 +224,24 @@ Refer to the [GitLab Group Member Relation](https://docs.gitlab.com/ee/api/graph ### Users -For self hosted, all `User` entities are ingested from the entire instance. +For self hosted, all `User` entities are ingested from the entire instance by default. For gitlab.com `User` entities for users who have [direct or inherited membership](https://docs.gitlab.com/ee/user/project/members/index.html#membership-types) of the top-level group for the configured group path will be ingested. +In both cases (SaaS & self hosted), you can limit the ingested users to users directly assigned to the group defined in your `app-config.yaml` by setting the configuration key `restrictUsersToGroup: true`. This is especially useful when you have a large user base that you don't want to import by default. + +```yaml +catalog: + providers: + gitlab: + yourProviderId: + host: gitlab.com ## Could also be self hosted. + orgEnabled: true + group: org/teams # Required for gitlab.com when `orgEnabled: true`. Optional for self managed. Must not end with slash. Accepts only groups under the provided path (which will be stripped) + restrictUsersToGroup: true # Backstage will ingest only users directly assigned to org/teams. +``` + ### Limiting `User` and `Group` entity ingestion in the provider Optionally, you can limit the entity types ingested by the provider when using diff --git a/docs/integrations/ldap/org--old.md b/docs/integrations/ldap/org--old.md index 63f052371f..448e9bbed1 100644 --- a/docs/integrations/ldap/org--old.md +++ b/docs/integrations/ldap/org--old.md @@ -24,8 +24,7 @@ the Processor method (not recommended), it is described separately below. The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap ``` diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md index 38f19af257..b1aa460bd6 100644 --- a/docs/integrations/ldap/org.md +++ b/docs/integrations/ldap/org.md @@ -21,8 +21,7 @@ Backstage in general supports OpenLDAP compatible vendors, as well as Active Dir The provider is not installed by default, therefore you have to add a dependency to `@backstage/plugin-catalog-backend-module-ldap` to your backend package. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-ldap ``` @@ -86,6 +85,14 @@ catalog: These config blocks have a lot of options in them, so we will describe each "root" key within the block separately. +> NOTE: +> +> If you want to import users and groups from different LDAP servers, you can define multiple providers with different names. +> If they should come from the same server, you can define multiple users and groups blocks within the same provider using an array of users / groups. +> Entries coming from the same block will be able to detect group memberships based on the `memberOf` attribute. +> +> If you want only to import users or groups, you can omit the groups or users block. + ### target This is the URL of the targeted server, typically on the form diff --git a/docs/notifications/index.md b/docs/notifications/index.md new file mode 100644 index 0000000000..95d77a9b7f --- /dev/null +++ b/docs/notifications/index.md @@ -0,0 +1,320 @@ +--- +id: index +title: Getting Started +description: How to get started with the notifications and signals +--- + +The Backstage Notifications System provides a way for plugins and external services to send notifications to Backstage users. +These notifications are displayed in the dedicated page of the Backstage frontend UI or by frontend plugins per specific scenarios. +Additionally, notifications can be sent to external channels (like email) via "processors" implemented within plugins. + +Notifications can be optionally integrated with the signals (a push mechanism) to ensure users receive them immediately. + +### Upgrade to the latest version of Backstage + +To ensure your version of Backstage has all the latest notifications and signals related functionality, it’s important to upgrade to the latest version. The [Backstage upgrade helper](https://backstage.github.io/upgrade-helper/) is a great tool to help ensure that you’ve made all the necessary changes during the upgrade! + +## About notifications + +Notifications are messages sent to either individual users or groups. +They are not intended for inter-process communication of any kind. + +There are two basic types of notifications: + +- **Broadcast**: Messages sent to all users of Backstage. +- **Entity**: Messages delivered to specific listed entities, such as Users or Groups. + +Example of use-cases: + +- System-wide announcements or alerts +- Notifications for component owners: e.g., build failures, successful deployments, new vulnerabilities +- Notifications for individuals: e.g., updates you have subscribed to, new required training courses +- Notifications pertaining to a particular entity in the catalog: A notification might apply to an entity and the owning team. + +## Installation in Older Environments + +Newer versions of instances created by the create-app have both the notifications and signals plugins included by default, this section can be skipped right to the Configuration. + +Following installation instructions are valid for enabling the plugins in older environments. + +### Add Notifications Backend + +```bash +yarn workspace backend add @backstage/plugin-notifications-backend +``` + +Add the notifications to your `backend/src/index.ts`: + +```ts +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-notifications-backend')); +``` + +### Add Notifications Frontend + +```bash +yarn workspace app add @backstage/notifications +``` + +To add the notifications main menu, add following to your `packages/app/src/components/Root/Root.tsx`: + +```tsx +import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; + + + + + // ... + + + +; +``` + +Also add the route to notifications to `packages/app/src/App.tsx`: + +```tsx +import { NotificationsPage } from '@backstage/plugin-notifications'; + + + // ... + } /> +; +``` + +### Optional: Add Signals Backend + +Optionally add Signals to your backend by + +```bash +yarn workspace backend add @backstage/plugin-signals-backend +``` + +Add the signals to your `backend/src/index.ts`: + +```ts +const backend = createBackend(); +// ... +backend.add(import('@backstage/plugin-signals-backend')); +``` + +### Optional: Signals Frontend + +The use of signals is optional but improves user experience. + +Start with: + +```bash +yarn workspace app add @backstage/plugin-signals +``` + +To install the plugin, you have to add the following to your `packages/app/src/plugins.ts`: + +```ts +export { signalsPlugin } from '@backstage/plugin-signals'; +``` + +And make sure that your `packages/app/src/App.tsx` contains: + +```ts +import * as plugins from './plugins'; + +const app = createApp({ + // ... + plugins: Object.values(plugins), + // ... +}); +``` + +If the signals plugin is properly configured, it will be automatically discovered by the notifications plugin and used. + +## Configuration + +### Notifications Backend + +The Notifications backend plugin provides an API to create notifications, list notifications per logged-in user, and search based on parameters. + +The plugin uses a relational [database](https://backstage.io/docs/getting-started/config/database) for persistence, no specifics are introduced in this context. + +No additional configuration in the app-config is needed, except for optional additional modules for `processors`. + +### Notifications Frontend + +The recipients of notifications have to be entities in the catalog, e.g. of the User or Group kind. + +Otherwise no specific configuration is needed for the front-end notifications plugin. + +All parametrization is done through component properties, such as the `NotificationsSidebarItem`, which can be used as an active left-side menu item in the front-end. + +![Notifications Page](notificationsPage.png) + +In the `packages/app/src/components/Root/Root.tsx`, tweak the [properties](https://backstage.io/docs/reference/plugin-notifications.notificationssidebaritem) of the `` per specific needs. + +## Use + +New notifications can be sent either by a backend plugin or an external service through the REST API. + +### Backend + +Regardless of technical feasibility, a backend plugin should avoid directly accessing the notifications REST API. +Instead, it should integrate with the `@backstage/plugin-notifications-node` to `send` (create) a new notification. + +The reasons for this approach include the propagation of authorization in the API request and improved maintenance and backward compatibility in the future. + +```ts +import { notificationService } from '@backstage/plugin-notifications-node'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'myPlugin', + register(env) { + env.registerInit({ + deps: { + // ... + notificationService: notificationService, + }, + async init({ config, logger, httpRouter, notificationService }) { + httpRouter.use( + await createRouter({ + // ... + notificationService, + }), + ); + }, + }); + }, +}); +``` + +To emit a new notification: + +```ts +notificationService.send({ + recipients /* of the broadcast or entity type */, + payload /* actual message */, +}); +``` + +Refer the [API documentation](https://github.com/backstage/backstage/blob/master/plugins/notifications-node/api-report.md) for further details. + +### Signals + +The use of signals with notifications is optional but generally enhances user experience and performance. + +When a notification is created, a new signal is emitted to a general-purpose message bus to announce it to subscribed listeners. + +The frontend maintains a persistent connection (WebSocket) to receive these announcements from the notifications channel. +The specific details of the updated or created notification should be retrieved via a request to the notifications API, except for new notifications, where the payload is included in the signal for performance reasons. + +In a frontend plugin, to subscribe for notifications' signals: + +```ts +import { useSignal } from '@backstage/plugin-signals-react'; + +const { lastSignal } = useSignal('notifications'); + +React.useEffect(() => { + /* ... */ +}, [lastSignal, notificationsApi]); +``` + +### Consuming Notifications + +In a front-end plugin, the simplest way to query a notification is by its ID: + +```ts +import { useApi } from '@backstage/core-plugin-api'; +import { notificationsApiRef } from '@backstage/plugin-notifications'; + +const notificationsApi = useApi(notificationsApiRef); + +notificationsApi.getNotification(yourId); + +// or with connection to signals: +notificationsApi.getNotification(lastSignal.notification_id); +``` + +### Extending Notifications via Processors + +The notifications can be extended with `NotificationProcessor`. These processors allow to decorate notifications before they are sent or/and send the notifications to external services. + +Depending on the needs, a processor can modify the content of a notification or route it to different systems like email, Slack, or other services. + +A good example of how to write a processor is the [Email Processor](https://github.com/backstage/backstage/tree/master/plugins/notifications-backend-module-email). + +Start off by creating a notification processor: + +```ts +import { Notification } from '@backstage/plugin-notifications-common'; +import { NotificationProcessor } from '@backstage/plugin-notifications-node'; + +class MyNotificationProcessor implements NotificationProcessor { + async decorate(notification: Notification): Promise { + if (notification.origin === 'plugin-my-plugin') { + notification.payload.icon = 'my-icon'; + } + return notification; + } + + async send(notification: Notification): Promise { + nodemailer.sendEmail({ + from: 'backstage', + to: 'user', + subject: notification.payload.title, + text: notification.payload.description, + }); + } +} +``` + +Both of the processing functions are optional, and you can implement only one of them. + +Add the notification processor to the notification system by: + +```ts +import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; +import { Notification } from '@backstage/plugin-notifications-common'; + +export const myPlugin = createBackendPlugin({ + pluginId: 'myPlugin', + register(env) { + env.registerInit({ + deps: { + notifications: notificationsProcessingExtensionPoint, + // ... + }, + async init({ notifications }) { + // ... + notifications.addProcessor(new MyNotificationProcessor()); + }, + }); + }, +}); +``` + +### External Services + +When the emitter of a notification is a Backstage backend plugin, it is mandatory to use the integration via `@backstage/plugin-notifications-node` as described above. + +If the emitter is a service external to Backstage, an HTTP POST request can be issued directly to the API, assuming that authentication is properly configured. +Refer to the [service-to-service auth documentation](https://backstage.io/docs/auth/service-to-service-auth) for more details, focusing on the Static Tokens section for the simplest setup option. + +An example request for creating a broadcast notification might look like: + +```bash +curl -X POST https://[BACKSTAGE_BACKEND]/api/notifications -H "Content-Type: application/json" -H "Authorization: Bearer YOUR_BASE64_SHARED_KEY_TOKEN" -d '{"recipients":{"type":"broadcast"},"payload": {"title": "Title of broadcast message","link": "http://foo.com/bar","severity": "high","topic": "The topic"}}' +``` + +## Additional info + +An example of a backend plugin sending notifications can be found in https://github.com/backstage/backstage/tree/master/plugins/scaffolder-backend-module-notifications. + +Sources of the notifications and signal plugins: + +- https://github.com/backstage/backstage/blob/master/plugins/notifications + +- https://github.com/backstage/backstage/blob/master/plugins/notifications-backend + +- https://github.com/backstage/backstage/blob/master/plugins/notifications-node + +- https://github.com/backstage/backstage/blob/master/plugins/signals-react diff --git a/docs/notifications/notificationsPage.png b/docs/notifications/notificationsPage.png new file mode 100644 index 0000000000..7c96ab77af Binary files /dev/null and b/docs/notifications/notificationsPage.png differ diff --git a/docs/overview/threat-model.md b/docs/overview/threat-model.md index da97f3fc83..c0e2d61e25 100644 --- a/docs/overview/threat-model.md +++ b/docs/overview/threat-model.md @@ -37,6 +37,8 @@ The operator is ultimately responsible for auditing usage of internal and extern The operator is also responsible for maintaining the resolved NPM dependencies of their Backstage project. This involves ensuring that `yarn.lock` receives updated versions of packages that have vulnerabilities, when those fixed versions are in range of what the Backstage packages request in their respective `package.json` files. This is commonly done by employing automated tooling such as [Dependabot](https://dependabot.com/), [Snyk](https://snyk.io/), and/or [Renovate](https://docs.renovatebot.com/) on your own repository. When fixed versions exist that are _not_ in range of what Backstage packages request, or when larger operations such as switching out an entire dependency for another one is required, maintainers collaborate with contributors to try to address those dependency declarations in the main project as soon as possible. +The built-in protection against unauthorized access does not by default include protection of the frontend bundle. The frontend bundle includes all the code of your frontend plugins and code in minified form, as well as any other frontend resources like images, fonts, etc. If this is a concern, you can use the [experimental public entry point](https://backstage.io/docs/tutorials/enable-public-entry/) to create two separate frontend builds, where authenticated users only have access to the full one. + ## Common Backend Configuration There are many common facilities that are configured centrally and available to all Backstage backend plugins. For example there is a `DatabaseManager` that provides access to a SQL database, `TaskScheduler` for scheduling long-running tasks, `Logger` as a general logging facility, and `UrlReader` for reading content from external sources. These are all configured either directly in code, or within the `backend` block of the static configuration. The appropriate care needs to be taken to ensure that any secrets remain confidential and no malicious configuration is injected. diff --git a/docs/permissions/custom-rules.md b/docs/permissions/custom-rules.md index 9063ed7257..57565755fb 100644 --- a/docs/permissions/custom-rules.md +++ b/docs/permissions/custom-rules.md @@ -66,9 +66,8 @@ import { catalogConditions, createCatalogConditionalDecision, createCatalogPermi /* highlight-remove-next-line */ import { createConditionFactory } from '@backstage/plugin-permission-node'; /* highlight-add-next-line */ -import { PermissionPolicy, PolicyQuery, createConditionFactory } from '@backstage/plugin-permission-node'; +import { PermissionPolicy, PolicyQuery, PolicyQueryUser, createConditionFactory } from '@backstage/plugin-permission-node'; /* highlight-add-start */ -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, isResourcePermission } from '@backstage/plugin-permission-common'; /* highlight-add-end */ ... @@ -102,21 +101,21 @@ const isInSystem = createConditionFactory(isInSystemRule); class TestPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (isResourcePermission(request.permission, 'catalog-entity')) { return createCatalogConditionalDecision( request.permission, /* highlight-remove-start */ catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), /* highlight-remove-end */ /* highlight-add-start */ { anyOf: [ catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), isInSystem({ systemRef: 'interviewing' }), ], diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index 4dabf3fcbd..e8132e571b 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -50,8 +50,7 @@ The permissions framework uses a new `permission-backend` plugin to accept autho 1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn --cwd packages/backend add @backstage/plugin-permission-backend ``` diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index a674d82e25..f11bdff930 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -39,8 +39,7 @@ The source code is available here: 2. Add these packages as dependencies for your Backstage app: - ```sh - # From your Backstage root directory + ```sh title="From your Backstage root directory" yarn --cwd packages/backend add @internal/plugin-todo-list-backend @internal/plugin-todo-list-common yarn --cwd packages/app add @internal/plugin-todo-list ``` diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index aacd60af8c..724edb7ccb 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -169,15 +169,12 @@ Before running this step, please make sure you followed the steps described in [ In order to test the logic above, the integrators of your backstage instance need to change their permission policy to return `DENY` for our newly-created permission: ```ts title="packages/backend/src/plugins/permission.ts" -/* highlight-add-start */ -import { - BackstageIdentityResponse, -} from '@backstage/plugin-auth-node'; -/* highlight-add-end */ import { PermissionPolicy, - /* highlight-add-next-line */ + /* highlight-add-start */ PolicyQuery, + PolicyQueryUser, + /* highlight-add-end */ } from '@backstage/plugin-permission-node'; /* highlight-add-start */ import { isPermission } from '@backstage/plugin-permission-common'; @@ -190,7 +187,7 @@ class TestPermissionPolicy implements PermissionPolicy { /* highlight-add-start */ async handle( request: PolicyQuery, - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index b3aeb13a68..7ad551e926 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -237,12 +237,12 @@ Let's go back to the permission policy's handle function and try to authorize ou ```ts title="packages/backend/src/plugins/permission.ts" import { - BackstageIdentityResponse, IdentityClient } from '@backstage/plugin-auth-node'; import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { isPermission } from '@backstage/plugin-permission-common'; /* highlight-remove-next-line */ @@ -262,9 +262,9 @@ import { async handle( request: PolicyQuery, /* highlight-remove-next-line */ - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, /* highlight-add-next-line */ - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { @@ -276,7 +276,7 @@ async handle( return createTodoListConditionalDecision( request.permission, todoListConditions.isOwner({ - userId: user?.identity.userEntityRef ?? '', + userId: user?.info.userEntityRef ?? '', }), ); } diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index 60458aaf4d..4d673e3b4f 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -199,7 +199,8 @@ const routes = ( - {/* highlight-add-end */}} + {/* highlight-add-end */} + }> {/* ... */} diff --git a/docs/permissions/writing-a-policy.md b/docs/permissions/writing-a-policy.md index 3d4371f038..0f2d4fe091 100644 --- a/docs/permissions/writing-a-policy.md +++ b/docs/permissions/writing-a-policy.md @@ -10,7 +10,10 @@ That policy looked like this: ```typescript title="packages/backend/src/plugins/permission.ts" class TestPermissionPolicy implements PermissionPolicy { - async handle(request: PolicyQuery): Promise { + async handle( + request: PolicyQuery, + _user?: PolicyQueryUser, + ): Promise { if (request.permission.name === 'catalog.entity.delete') { return { result: AuthorizeResult.DENY, @@ -35,14 +38,6 @@ As we confirmed in the previous section, we know that this now prevents us from Let's change the policy to the following: ```ts -/* highlight-remove-next-line */ -import { IdentityClient } from '@backstage/plugin-auth-node'; -/* highlight-add-start */ -import { - BackstageIdentityResponse, - IdentityClient -} from '@backstage/plugin-auth-node'; - /* highlight-add-end */ import { AuthorizeResult, PolicyDecision, @@ -65,7 +60,7 @@ class TestPermissionPolicy implements PermissionPolicy { /* highlight-add-start */ async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-add-end */ /* highlight-remove-next-line */ @@ -81,7 +76,7 @@ class TestPermissionPolicy implements PermissionPolicy { return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), ); /* highlight-add-end */ @@ -95,7 +90,7 @@ Let's walk through the new code that we just added. Instead of returning an Definitive Policy Decision, we use factory methods to construct a [Conditional Policy Decision](https://backstage.io/docs/reference/plugin-permission-common.conditionalpolicydecision) (See the [Concepts page](./concepts.md) for more details). Since the policy doesn't have enough information to determine if `user` is the entity owner, this criteria is encapsulated within the conditional decision. However, `createCatalogConditionalDecision` will not compile unless `request.permission` is a catalog entity [`ResourcePermission`](https://backstage.io/docs/reference/plugin-permission-common.resourcepermission). This type constraint ensures that policies return conditional decisions that are compatible with the requested permission. To address this, we use [`isPermission`](https://backstage.io/docs/reference/plugin-permission-common.ispermission) to ["narrow"](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) the type of `request.permission` to `ResourcePermission<'catalog-entity'>`. This matches the runtime behavior that was in place before, but you'll notice that the type of `request.permission` has changed within the scope of that `if` statement. -The `catalogConditions` object contains all of the rules defined by the catalog plugin. These rules can be combined to form a [`PermissionCriteria`](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) object, but for this case we only need to use the `isEntityOwner` rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to `PermissionPolicy#handle` provides us with a `BackstageIdentityResponse` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous. +The `catalogConditions` object contains all of the rules defined by the catalog plugin. These rules can be combined to form a [`PermissionCriteria`](https://backstage.io/docs/reference/plugin-permission-common.permissioncriteria) object, but for this case we only need to use the `isEntityOwner` rule. This rule accepts a list of entity refs that represent User identity and Group membership used to determine ownership. The second argument to `PermissionPolicy#handle` provides us with a `PolicyQueryUser` object, from which we can grab the user's `ownershipEntityRefs`. We provide an empty array as a fallback since the user may be anonymous. You should now be able to see in your Backstage app that the unregister entity button is enabled for entities that you own, but disabled for all other entities! @@ -125,7 +120,7 @@ import { class TestPermissionPolicy implements PermissionPolicy { async handle( request: PolicyQuery, - user?: BackstageIdentityResponse, + user?: PolicyQueryUser, ): Promise { /* highlight-remove-next-line */ if (isPermission(request.permission, catalogEntityDeletePermission)) { @@ -134,7 +129,7 @@ class TestPermissionPolicy implements PermissionPolicy { return createCatalogConditionalDecision( request.permission, catalogConditions.isEntityOwner({ - claims: user?.identity.ownershipEntityRefs ?? [], + claims: user?.info.ownershipEntityRefs ?? [], }), ); } diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 7f155f3b05..1366a59099 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -71,8 +71,7 @@ Backstage application / backend exposes it. To actually attach and run the plugin router, you will make some modifications to your backend. -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add @internal/plugin-carmen-backend@^0.1.0 # Change this to match the plugin's package.json ``` diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index e13653706f..324e6751d4 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -17,7 +17,12 @@ See how to install Datadog Events in your app ### New Backend -The backend supplies a central logging service, [`rootLogger`](../backend-system/core-services/root-logger.md), as well as a plugin based logger, [`logger`](../backend-system/core-services/logger.md) from `coreServices`. To add additional granularity to your logs, you can create children from the plugin based logger, using the `.child()` method and provide is with JSON data. For example, if you wanted to log items for a specific span in your plugin, you could do +The backend supplies a central logging service, +[`rootLogger`](../backend-system/core-services/root-logger.md), as well as a plugin +based logger, [`logger`](../backend-system/core-services/logger.md) from `coreServices`. +To add additional granularity to your logs, you can create children from the plugin +based logger, using the `.child()` method and provide is with JSON data. For example, +if you wanted to log items for a specific span in your plugin, you could do ```ts export function createRouter({ logger }) { @@ -37,7 +42,9 @@ export function createRouter({ logger }) { } ``` -You can also add additional metadata to all logs for your Backstage instance by overriding the `rootLogger` implementation, you can see an example in [the `logger` docs](../backend-system/core-services/logger.md#configuring-the-service). +You can also add additional metadata to all logs for your Backstage instance by +overriding the `rootLogger` implementation, you can see an example in +[the `rootLogger` docs](../backend-system/core-services/root-logger.md#configuring-the-service). ### Old Backend @@ -63,9 +70,19 @@ An example log line could look as follows: ## Health Checks -### New Backend +### New Backend (post 1.29.0) -The new backend is moving towards health checks being plugin-based, as such there is no current plugin for providing a health check route. You can add this yourself easily though, +The new backend provides a `RootHealthService` which implements +`/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints +to provide health checks for the entire backend instance. + +You can read more about this new service and how to customize it in the +[Root Health Service documentation](../backend-system/core-services/root-health.md). + +### New Backend (pre 1.29.0) + +The new backend is moving towards health checks being plugin-based, as such there is no +current plugin for providing a health check route. You can add this yourself easily though, ```ts import { diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index a97b3d8166..7292ac9754 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -24,7 +24,7 @@ Running an individual test (e.g. `MyComponent.test.tsx`): To run both `MyComponent.test.tsx` and `MyControl.test.tsx` suite of tests: - yarn test MyCo + yarn test MyComponent MyControl :::note Note @@ -52,10 +52,6 @@ We use the light-weight [react-testing-library](https://github.com/kentcdodds/react-testing-library) to render React components. -## Testing Utilities - -TODO. - ## Writing Unit Tests The following principles are good guides for determining if you are writing high diff --git a/docs/publishing.md b/docs/publishing.md index d7a4f65008..50d78ebf09 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -75,7 +75,7 @@ Given one or more PRs towards master that we want to create a patch release for, ./scripts/patch-release-for-pr.js ... ``` -Wait until the script has finished executing, at the end of the output you will find a link of the format `https://github.com/backstage/backstage/compare/patch/...`. Open this link in your browser to create a PR for the patch release. Finish the sentence "This release fixes an issue where..." and create the PR. +Wait until the script has finished executing, at the end of the output you will find a link of the format `https://github.com/backstage/backstage/pull/new/patch-release-pr-...`. Open this link in your browser to create a PR for the patch release. Finish the sentence "This release fixes an issue where..." and create the PR. Once the PR has been approved and merged, the patch release will be automatically created. The patch release is complete when a notification has been posted to Discord in the `#announcements` channel. Keep an eye on "Deploy Packages" workflow and re-trigger if it fails. It is safe to re-trigger any part of this workflow, including the release step. diff --git a/docs/releases/v1.29.0-changelog.md b/docs/releases/v1.29.0-changelog.md new file mode 100644 index 0000000000..86cb84d1f1 --- /dev/null +++ b/docs/releases/v1.29.0-changelog.md @@ -0,0 +1,2495 @@ +# Release v1.29.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.29.0](https://backstage.github.io/upgrade-helper/?to=1.29.0) + +## @backstage/backend-app-api@0.8.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. +- f691c9b: **BREAKING**: Removed the ability to pass callback-form service factories through the `defaultServiceFactories` option of `createSpecializedBackend`. This is an immediate breaking change as usage of this function is expected to be very rare. + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 617a7d2: Internal refactor that avoids the use of service factory options. +- b60db08: Fixing exporting of classes properly from new packages +- 18b96b1: The ability to install backend features in callback form (`() => BackendFeature`) has been deprecated. This typically means that you need to update the installed features to use the latest version of `@backstage/backend-plugin-api`. If the feature is from a third-party package, please reach out to the package maintainer to update it. +- a63c4b6: Fixing issue with `MiddlewareFactory` deprecation wrapping +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.4.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 419f387: Refactor of `rootHttpRouterServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- cb14a05: Repack the package to fix issues with typescript with named exports +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- e28af58: Refactor of `rootConfigServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/backend-plugin-api@0.7.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: The `PermissionsService` no longer supports passing the deprecated `token` option, and the request options are now required. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules + +- 062c01c: Deprecated the ability to define options for service factories through `createServiceFactory`. In the future all service factories will return a plain `ServiceFactory` object, rather than allowing users to pass options to the factory. To allow for customization of a service implementation one can instead export one or a few building blocks that allows for simple re-implementation of the service instead. + + For example, instead of: + + ```ts + export const fooServiceFactory = createServiceFactory( + (options?: { bar: string }) => ({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }), + ); + ``` + + We instead encourage service implementations to provide an easy to use API for re-implementing the service for advanced use-cases: + + ```ts + /** @public */ + export class DefaultFooService implements FooService { + static create(options: { bar: string; logger: LoggerService }) { + return new DefaultFooService(options.logger, options.bar ?? 'default'); + } + + private constructor( + private readonly logger: string, + private readonly bar: string, + ) {} + + // The rest of the implementation + } + ``` + + A user that wishes to customize the service can then easily do so by defining their own factory: + + ```ts + export const customFooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return DefaultFooService.create({ logger, bar: 'baz' }); + }, + }); + ``` + + This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. + + In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: + + ```ts + const fooServiceFactoryWithOptions = (options?: { bar: string }) => + createServiceFactory({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }); + + export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), + ); + ``` + + This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. + + A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. + +- fe47a3e: All service config types were renamed to option types in order to standardize frontend and backend `create*` function signatures: + + - The `ServiceRefConfig` type was renamed to`ServiceRefOptions`; + - The `RootServiceFactoryConfig` type was renamed to `RootServiceFactoryOptions`; + - The `PluginServiceFactoryConfig` type was renamed to `PluginServiceFactoryOptions` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-app-api@1.14.0 + +### Minor Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +### Patch Changes + +- db2e2d5: Updated config schema to support app.routes.bindings +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/integration@1.13.0 + +### Minor Changes + +- b5deed0: Add support for `token` for `bitbucketCloud` integration + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend@1.24.0 + +### Minor Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.0 + +### Minor Changes + +- cb32ca7: **BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. + + Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-common@0.8.0 + +### Minor Changes + +- f4085b8: **BREAKING**: Removed the deprecated and unused `token` option from `EvaluatorRequestOptions`. The `PermissionsClient` now has its own `PermissionClientRequestOptions` type that declares the `token` option instead. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.8.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: Updated the `ServerPermissionClient` to match the new `PermissionsService` interface, where the deprecated `token` option has been removed and the options are now required. + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- 28b2cfb: Fix invalid cross-reference in API Reference docs + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder@1.23.0 + +### Minor Changes + +- 52b6db0: Use virtualization with `EntityPicker` as done earlier with `MultiEntityPicker` to fix performance issues with large data sets. `VirtualizedListbox` extracted into reusable component. +- 3583ce5: Use virtualization with `MultiEntityPicker`. Fixes performance issues with large data sets. +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- 4d7e11f: enable resizing of the task log stream viewer +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 89c44b3: Support `catalogFilter` array on `OwnedEntityPicker` +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.0 + +### Minor Changes + +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- b9451dd: Updated `catalog:write` scaffolder action to show correct file path location in log message +- ff1bb4c: Added a documentation how to use checkpoints +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- 62d1fe3: Fix user entity not being fetched for scaffolder dry runner +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.14 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.12 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.0 + +### Minor Changes + +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.0 + +### Minor Changes + +- 70c4b36: Adds support for custom tag policies when creating GitHub environments. + +### Patch Changes + +- ccfc9d1: Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. + + Adding environment secrets now works as expected. + +- 141f366: Added action to enable GitHub Pages on a repo + +- 4410fed: Fixed issue with octokit call missing owner and repo when creating environment variables and secrets using github:environment:create action + +- dfaa28d: Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action + + Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-react@1.10.0 + +### Minor Changes + +- 354e68c: Improve validation error display text in scaffolder +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 4d7e11f: disables rendering of output box if no output is returned +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/app-defaults@1.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/backend-common@0.23.3 + +### Patch Changes + +- 8c09c97: Deprecate legacy status check factory, handler and types. +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- c964a3d: Add dependencies that are needed by cross-imports from backend-defaults +- b60db08: Fixing exporting of classes properly from new packages +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.15 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-node@0.1.22 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-search-common@1.2.13 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-openapi-utils@0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/errors@1.2.4 + +## @backstage/backend-tasks@0.5.27 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.4 + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- edf5cc3: The function `isDockerDisabledForTests` is deprecated and will no longer be exported in the near future as it should only be used internally. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- fce7887: Added mock for the Root Health Service in `mockServices`. +- 906c817: Updated `startTestBackend` and `ServiceFactoryTester` to only accept plain service factory or backend feature objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- 98ccf00: Internal refactor of `mockServices.httpAuth.factory` to allow it to still be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.26.11 + +### Patch Changes + +- 133464c: Added experimental support for dynamic frontend plugin builds, enabled via setting `EXPERIMENTAL_MODULE_FEDERATION` for the app build, and using the `frontend-dynamic-container` package role to create a container. Both of these are experimental and will change in the future. +- e2e320c: - remove unused dependencies `winston` and `yn` from the template of backend plugins; + - update `msw` to version `2.3.1` in the template of backend plugins; + starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2; +- 0540c5a: Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`. +- 7652db4: Only bootstrap global-agent if it's actually being used +- f0c0039: Fix issue with CLI that was preventing upgrading from 1.28 +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- a60d73b: Fix a few minor issues with the backend template that were causing failing linting checks in the main repo. +- 0510d98: Subpath export `package.json` should be of a unique name to avoid typescript resolution issues +- 4baac0c: The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. +- Updated dependencies + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.13.0 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/cli-node@0.2.7 + +### Patch Changes + +- 133464c: Added internal metadata for the new experimental `frontend-dynamic-container` role. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-components@0.14.9 + +### Patch Changes + +- d4ffdbb: Fixed bug where `` component with empty string as placeholder gave an error +- Updated dependencies + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.17-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.2-next.0 + +### Patch Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-plugin-api@0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/test-utils@1.5.7-next.0 + - @backstage/types@1.1.1 + +## @backstage/integration-react@1.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.13.0-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + +## @backstage/repo-tools@0.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.15-next.0 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/plugin-techdocs-node@1.12.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-api-docs@0.11.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-app-backend@0.3.70-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-app-node@0.1.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/config-loader@1.8.1 + +## @backstage/plugin-app-visualizer@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.2-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.13-next.0 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.16-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.2-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.14-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.2-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.14-next.0 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-auth-node@0.4.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-react@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/integration@1.13.0-next.0 + +## @backstage/plugin-catalog@1.21.1-next.0 + +### Patch Changes + +- d133eaa: Added small notes to AboutCard to discourage customizability PRs +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-catalog-backend@1.23.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/backend-openapi-utils@0.1.14-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-catalog-backend-module-aws@0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/backend-openapi-utils@0.1.14-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-backend-module-github@0.6.4-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.20-next.0 + +### Patch Changes + +- 8db30ad: The Gitlab configuration supports an additional optional boolean key `catalog.providers.gitlab..restrictUsersToGroup`. Setting this to `true` will make Backstage only import users from the group defined in the `group` key, instead of all users in the organisation (self-hosted) or of the root group (SaaS). It will default to false, keeping the original implementation intact, when not explicitly set. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.20-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-catalog-backend-module-ldap@0.6.2-next.0 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-logs@0.0.1-next.0 + +### Patch Changes + +- 97caf55: Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them. + + See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install + and configure the plugin. + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-scaffolder-common@1.5.3 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.2 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-catalog-graph@0.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-node@1.12.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-catalog-react@1.12.2-next.0 + +### Patch Changes + +- 2030962: Make EntityOwnerPicker display metadata.title or spec.profile.displayName for mode=only-owners instead of metadata.name +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-config-schema@0.1.57-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.10 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-devtools-backend@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.10 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-events-backend@0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + +## @backstage/plugin-events-backend-module-github@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.7-next.0 + +## @backstage/plugin-events-node@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + +## @backstage/plugin-home@0.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-home-react@0.1.15-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + +## @backstage/plugin-home-react@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-kubernetes@0.11.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-kubernetes-backend@0.18.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-kubernetes-node@0.1.15-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-kubernetes-cluster@0.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-kubernetes-node@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-kubernetes-react@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-notifications@0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-notifications-backend@0.3.2-next.0 + +### Patch Changes + +- d7b8ca5: Added an option to filter notifications by topic +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.4 + +## @backstage/plugin-notifications-backend-module-email@0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + +## @backstage/plugin-notifications-node@0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.4 + +## @backstage/plugin-org@0.6.27-next.0 + +### Patch Changes + +- c307ef4: Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`. + + Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property. + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-org-react@0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-permission-backend@0.5.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-permission-node@0.7.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-proxy-backend@0.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.13-next.0 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.11-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.11-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.11-next.0 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.11-next.0 + +### Patch Changes + +- 6a4ad4e: Instead of using hardcoded `targetBranch` now fetch the default branch from Bitbucket repository. + This prevents from errors when no `targetBranch` is provided and the default repository branch is different from `master`, for example: `main`. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.45-next.0 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.3-next.0 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/plugin-notifications-common@0.0.4 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.38-next.0 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.1.8-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.7-next.0 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.3 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/backend-test-utils@0.4.3-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-search@1.4.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend@1.5.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-openapi-utils@0.1.14-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-explore@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-pg@0.5.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-app-api@0.7.9-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-techdocs@0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/plugin-techdocs-node@1.12.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-node@1.2.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-react@1.7.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-signals@0.0.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-signals-backend@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.7-next.0 + +### Patch Changes + +- 8ac9ce5: Fixed a bug with the TechDocsReaderPageProvider not re-rendering when setShadowDom is called, meaning that the useShadowDom hooks were inconsistent. This issue caused the TextSize addon changes not to reapply during navigation. +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/test-utils@1.5.7-next.0 + +## @backstage/plugin-techdocs-backend@1.10.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.26-next.0 + - @backstage/plugin-techdocs-node@1.12.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-techdocs-node@1.12.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-techdocs-react@1.2.6-next.0 + +### Patch Changes + +- 8ac9ce5: Fixed a bug with the TechDocsReaderPageProvider not re-rendering when setShadowDom is called, meaning that the useShadowDom hooks were inconsistent. This issue caused the TextSize addon changes not to reapply during navigation. +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-user-settings@0.8.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.99-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.22.0-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/frontend-app-api@0.7.2-next.0 + - @backstage/cli@0.26.10-next.0 + - @backstage/plugin-org@0.6.27-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/plugin-scaffolder-react@1.10.0-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-graph@0.4.7-next.0 + - @backstage/plugin-catalog-import@0.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-devtools@0.1.16-next.0 + - @backstage/plugin-home@0.7.6-next.0 + - @backstage/plugin-kubernetes@0.11.12-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.0 + - @backstage/plugin-notifications@0.2.3-next.0 + - @backstage/plugin-search@1.4.13-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + +## example-app-next@0.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.22.0-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/frontend-app-api@0.7.2-next.0 + - @backstage/cli@0.26.10-next.0 + - @backstage/plugin-org@0.6.27-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/plugin-scaffolder-react@1.10.0-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.0 + - @backstage/plugin-app-visualizer@0.1.8-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-graph@0.4.7-next.0 + - @backstage/plugin-catalog-import@0.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-home@0.7.6-next.0 + - @backstage/plugin-kubernetes@0.11.12-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.0 + - @backstage/plugin-notifications@0.2.3-next.0 + - @backstage/plugin-search@1.4.13-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.8-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + +## app-next-example-plugin@0.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + +## example-backend@0.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-scaffolder-backend@1.23.0-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.0 + - @backstage/plugin-notifications-backend@0.3.2-next.0 + - @backstage/plugin-app-backend@0.3.70-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.7-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.4-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.39-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.19-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.8-next.0 + - @backstage/plugin-devtools-backend@0.3.7-next.0 + - @backstage/plugin-kubernetes-backend@0.18.2-next.0 + - @backstage/plugin-permission-backend@0.5.45-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.18-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-proxy-backend@0.5.2-next.0 + - @backstage/plugin-search-backend@1.5.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.27-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.26-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/plugin-signals-backend@0.1.7-next.0 + - @backstage/plugin-techdocs-backend@1.10.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-permission-common@0.7.14 + +## example-backend-legacy@0.2.100-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.38-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-backend@1.23.0-next.0 + - @backstage/plugin-app-backend@0.3.70-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.19-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.8-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-devtools-backend@0.3.7-next.0 + - @backstage/plugin-events-backend@0.3.8-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-kubernetes-backend@0.18.2-next.0 + - @backstage/plugin-permission-backend@0.5.45-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-proxy-backend@0.5.2-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.22-next.0 + - @backstage/plugin-search-backend@1.5.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.2-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.27-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.31-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.26-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/plugin-signals-backend@0.1.7-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/plugin-techdocs-backend@1.10.8-next.0 + - example-app@0.2.99-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-permission-common@0.7.14 + +## e2e-test@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.98-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/cli@0.26.10-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/test-utils@1.5.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + +## @internal/plugin-todo-list@1.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @internal/plugin-todo-list-backend@1.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 diff --git a/docs/releases/v1.29.0-next.1-changelog.md b/docs/releases/v1.29.0-next.1-changelog.md new file mode 100644 index 0000000000..34cd038f22 --- /dev/null +++ b/docs/releases/v1.29.0-next.1-changelog.md @@ -0,0 +1,1952 @@ +# Release v1.29.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.29.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.29.0-next.1) + +## @backstage/plugin-catalog-backend@1.24.0-next.1 + +### Minor Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-openapi-utils@0.1.15-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.0-next.1 + +### Minor Changes + +- cb32ca7: **BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. + + Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-scaffolder-react@1.10.0-next.1 + +### Minor Changes + +- 354e68c: Improve validation error display text in scaffolder + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + +## @backstage/app-defaults@1.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/backend-app-api@0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/backend-common@0.23.3-next.1 + +### Patch Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/backend-defaults@0.3.4-next.1 + +### Patch Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/backend-dynamic-feature-service@0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-backend@0.3.9-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/backend-openapi-utils@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + +## @backstage/backend-plugin-api@0.6.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/backend-tasks@0.5.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.4-next.1 + +### Patch Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/cli@0.26.11-next.1 + +### Patch Changes + +- 4baac0c: The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. +- Updated dependencies + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.13.0-next.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/core-app-api@1.13.1-next.1 + +### Patch Changes + +- db2e2d5: Updated config schema to support app.routes.bindings +- Updated dependencies + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.17-next.1 + +### Patch Changes + +- 780d994: Added `MyGroupsSidebarItem` to the sidebar in the `create-app` template +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.3-next.1 + - @backstage/test-utils@1.5.8-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.8-next.1 + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-api-docs@0.11.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-app-backend@0.3.71-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-app-node@0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config-loader@1.8.1 + +## @backstage/plugin-auth-backend@0.22.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3-next.1 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14-next.1 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5-next.1 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5-next.1 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5-next.1 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17-next.1 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3-next.1 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15-next.1 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.3-next.1 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.15-next.1 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## @backstage/plugin-auth-node@0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog@1.21.1-next.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-catalog-backend-module-aws@0.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-catalog-backend-module-azure@0.1.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.1.15-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-github@0.6.5-next.1 + +### Patch Changes + +- 9112efc: Adds support for `repository` events. + + The provider adds a subscription to the topic `github.repository`. + + Hereby, it supports events of type `repository` with actions + + - `archived` + - `deleted` + - `edited` + - `renamed` + - `transferred` + - `unarchived` + + Actions skipped as they don't require entity changes: + + - `created` + - `privatized` + - `publicized` + + If the config option `validateLocationsExist` is enabled, an API request + is necessary and will be executed. + This affects the actions `renamed`, `transferred`, and `unarchive` + of event type `repository`. + + Catalog entities related to the `GithubEntityProvider` instance will be adjusted + according to action and its meaning for them. + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.6.5-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.21-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-catalog-backend-module-logs@0.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-scaffolder-common@1.5.3 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.2 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-catalog-graph@0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-catalog-node@1.12.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/plugin-catalog-react@1.12.2-next.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + +## @backstage/plugin-devtools-backend@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.10 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/plugin-events-backend@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-module-azure@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-module-github@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-backend-test-utils@0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-events-node@0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + +## @backstage/plugin-home@0.7.7-next.1 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-home-react@0.1.15-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/theme@0.5.6 + +## @backstage/plugin-home-react@0.1.15-next.1 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-kubernetes@0.11.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-kubernetes-backend@0.18.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-kubernetes-common@0.8.0 + - @backstage/plugin-kubernetes-node@0.1.16-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/plugin-kubernetes-cluster@0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-kubernetes-node@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## @backstage/plugin-notifications@0.2.3-next.1 + +### Patch Changes + +- 3bf0697: The toolbar on the Notifications page is hidden when there are no listed notifications. +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-notifications-backend@0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## @backstage/plugin-notifications-backend-module-email@0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + +## @backstage/plugin-notifications-node@0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## @backstage/plugin-org@0.6.27-next.1 + +### Patch Changes + +- 5132d28: The `useGetEntities` hook could result in requests to `/api/catalog/entities` where the headers exceed the default maximum Node.js header size of 16KB. The hook logic has been adjusted to batch the requests. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-catalog-common@1.0.24 + +## @backstage/plugin-org-react@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-permission-backend@0.5.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## @backstage/plugin-permission-node@0.7.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-proxy-backend@0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder@1.22.1-next.1 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-scaffolder-react@1.10.0-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + +## @backstage/plugin-scaffolder-backend@1.23.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.14-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.12-next.1 + - @backstage/plugin-scaffolder-common@1.5.3 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12-next.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.1 + +### Patch Changes + +- 141f366: Added action to enable GitHub Pages on a repo + +- dfaa28d: Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action + + Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4-next.1 + +### Patch Changes + +- 2fb0eb8: Added support for passing `variables` to `gitlab:pipeline:trigger` +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node-test-utils@0.1.9-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-scaffolder-node@0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.3 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.9-next.1 + +### Patch Changes + +- 54429b5: Filename and imports correction for mockActionContext.ts +- Updated dependencies + - @backstage/backend-test-utils@0.4.4-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## @backstage/plugin-search@1.4.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend@1.5.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-openapi-utils@0.1.15-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-explore@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-pg@0.5.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-module-techdocs@0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.8-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-search-backend-node@1.2.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-signals-backend@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## @backstage/plugin-signals-node@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## @backstage/plugin-techdocs@1.10.7-next.1 + +### Patch Changes + +- 60caa92: Fix double scrollbar bug in reader +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/core-plugin-api@1.9.3 + - @backstage/test-utils@1.5.8-next.1 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## @backstage/plugin-techdocs-backend@1.10.9-next.1 + +### Patch Changes + +- 9ecf5fd: Adds extension point for publishers to the techdocs backend +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.8-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.27-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + +## @backstage/plugin-techdocs-node@1.12.8-next.1 + +### Patch Changes + +- 9ecf5fd: Adds extension point for publishers to the techdocs backend +- 4c4d077: Bumps default version of techdocs docker image to latest +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.12 + +## @backstage/plugin-user-settings@0.8.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.99-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-org@0.6.27-next.1 + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/plugin-notifications@0.2.3-next.1 + - @backstage/plugin-home@0.7.7-next.1 + - @backstage/plugin-scaffolder-react@1.10.0-next.1 + - @backstage/plugin-scaffolder@1.22.1-next.1 + - @backstage/cli@0.26.11-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.3-next.1 + - @backstage/plugin-api-docs@0.11.7-next.1 + - @backstage/plugin-catalog-graph@0.4.7-next.1 + - @backstage/plugin-catalog-import@0.12.1-next.1 + - @backstage/plugin-search@1.4.13-next.1 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.9-next.1 + - @backstage/plugin-kubernetes@0.11.12-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-devtools@0.1.16-next.0 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## example-app-next@0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-org@0.6.27-next.1 + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/plugin-notifications@0.2.3-next.1 + - @backstage/plugin-home@0.7.7-next.1 + - @backstage/plugin-scaffolder-react@1.10.0-next.1 + - @backstage/plugin-scaffolder@1.22.1-next.1 + - @backstage/cli@0.26.11-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.3-next.1 + - @backstage/plugin-api-docs@0.11.7-next.1 + - @backstage/plugin-catalog-graph@0.4.7-next.1 + - @backstage/plugin-catalog-import@0.12.1-next.1 + - @backstage/plugin-search@1.4.13-next.1 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.9-next.1 + - @backstage/plugin-kubernetes@0.11.12-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-app-visualizer@0.1.8-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## example-backend@0.0.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.9-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.1 + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.27-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-app-backend@0.3.71-next.1 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.8-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.40-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9-next.1 + - @backstage/plugin-devtools-backend@0.3.8-next.1 + - @backstage/plugin-kubernetes-backend@0.18.3-next.1 + - @backstage/plugin-notifications-backend@0.3.3-next.1 + - @backstage/plugin-permission-backend@0.5.46-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-proxy-backend@0.5.3-next.1 + - @backstage/plugin-scaffolder-backend@1.23.0-next.1 + - @backstage/plugin-search-backend@1.5.14-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.28-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-signals-backend@0.1.8-next.1 + +## example-backend-legacy@0.2.100-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.9-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4-next.1 + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.27-next.1 + - example-app@0.2.99-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-app-backend@0.3.71-next.1 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-devtools-backend@0.3.8-next.1 + - @backstage/plugin-events-backend@0.3.9-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-kubernetes-backend@0.18.3-next.1 + - @backstage/plugin-permission-backend@0.5.46-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-proxy-backend@0.5.3-next.1 + - @backstage/plugin-scaffolder-backend@1.23.0-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.39-next.1 + - @backstage/plugin-search-backend@1.5.14-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.3-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.28-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.32-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-signals-backend@0.1.8-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## e2e-test@0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.98-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/cli@0.26.11-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/test-utils@1.5.8-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## @internal/plugin-todo-list-backend@1.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 diff --git a/docs/releases/v1.29.0-next.2-changelog.md b/docs/releases/v1.29.0-next.2-changelog.md new file mode 100644 index 0000000000..f6269407cf --- /dev/null +++ b/docs/releases/v1.29.0-next.2-changelog.md @@ -0,0 +1,446 @@ +# Release v1.29.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.29.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.29.0-next.2) + +## @backstage/app-defaults@1.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/core-compat-api@0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/core-components@0.14.9-next.1 + +### Patch Changes + +- 99d672d: Modified the `Select` component to take in a `data-testid` parameter ensuring backwards compatibility with default value corresponding to previously hardcoded `data-testid` of "select". + +## @backstage/create-app@0.5.17-next.2 + +### Patch Changes + +- e90a2cd: Added the Catalog logs module to the `create-app` template + +## @backstage/dev-utils@1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/frontend-app-api@0.7.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/frontend-plugin-api@0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/frontend-test-utils@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/plugin-api-docs@0.11.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-app-visualizer@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/plugin-auth-react@0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-catalog@1.21.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-catalog-graph@0.4.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-catalog-import@0.12.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-catalog-react@1.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-config-schema@0.1.57-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-devtools@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-home@0.7.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-home-react@0.1.15-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-home-react@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-kubernetes@0.11.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-kubernetes-react@0.4.1-next.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-notifications@0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-org@0.6.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-org-react@0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-scaffolder@1.22.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-scaffolder-backend@1.23.0-next.2 + +### Patch Changes + +- ff1bb4c: Added a documentation how to use checkpoints + +## @backstage/plugin-scaffolder-react@1.10.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## @backstage/plugin-search@1.4.14-next.2 + +### Patch Changes + +- 1117aba: Update deps in search api extension to include fetch api +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-search-react@1.7.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## @backstage/plugin-signals@0.0.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-techdocs@1.10.7-next.2 + +### Patch Changes + +- 6fa652c: Improve default sorting of docs table +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## @backstage/plugin-techdocs-react@1.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## @backstage/plugin-user-settings@0.8.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## example-app@0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-devtools@0.1.16-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + +## example-app-next@0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-app-visualizer@0.1.8-next.1 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## app-next-example-plugin@0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## example-backend@0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + +## example-backend-legacy@0.2.100-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + - example-app@0.2.99-next.2 + +## e2e-test@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.2 + +## techdocs-cli-embedded-app@0.2.98-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## @internal/plugin-todo-list@1.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 diff --git a/docs/releases/v1.29.0.md b/docs/releases/v1.29.0.md new file mode 100644 index 0000000000..5f7ed1db19 --- /dev/null +++ b/docs/releases/v1.29.0.md @@ -0,0 +1,117 @@ +--- +id: v1.29.0 +title: v1.29.0 +description: Backstage Release v1.29.0 +--- + +These are the release notes for the v1.29.0 release of [Backstage](https://backstage.io/). + +A huge thanks to the whole team of maintainers and contributors as well as the amazing Backstage Community for the hard work in getting this release developed and done. + +## Highlights + +### BREAKING: Backend System deprecations and removals + +As part of the [work towards a stable 1.0 release of the new backend system](https://github.com/backstage/backstage/issues/24493), there are several new deprecations and breaking changes in the backend system packages: + +**Breaking**: + +- The deprecated `token` option has been removed from `PermissionsService`, and the request options are now required and must contain a `credentials` object. +- The deprecated `getPath` option has been removed from `httpRouterServiceFactory`, the plugin paths are now always `/api/`. +- It is no longer possible to pass service factory callbacks to the `defaultServiceFactories` option of `createSpecializedBackend`. + +**Deprecations**: + +- The ability to define options for service factories through `createServiceFactory` has been deprecated. See the [service architecture documentation](https://backstage.io/docs/backend-system/architecture/services) for more information on how to define customizable services. +- The ability to install backend features in callback form, i.e. `() => BackendFeature`, has been deprecated. This also includes other usages such as `startTestBackend`, and dynamically imported backend features. No manual changes should be needed for this change, as all backend feature creators have been updated to return `BackendFeature` instances directly. +- The `ServiceFactoryTest.get` method has been renamed to `ServiceFactoryTest.getSubject`, and is now deprecated. +- The following types have been renamed to use an `*Options` suffix instead: `ServiceRefConfig`, `RootServiceFactoryConfig`, `PluginServiceFactoryConfig`. +- Deprecated all exports related to the legacy status checker in `@backstage/backend-common`. +- The `isDockerDisabledForTests` function exported by `@backstage/backend-test-utils` has been deprecated. + +### Backend Health Service + +A new health service as been added to the new backend system. This service provides health check endpoints for the backend, and replaces `createStatusCheckRouter` from `@backstage/backend-common`. + +The service helps implement the new `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness` endpoints, which provide health checks for the entire backend instance. + +You can read more about this new service and how to customize it in the [Root Health Service documentation](https://backstage.io/docs/backend-system/core-services/root-health). + +### New Catalog Logs module + +This new `@backstage/plugin-catalog-backend-module-logs` module is a minimal module that will log any error events that are published by the catalog. This module is useful for making sure that catalog errors are visible in the logs, but you may want to replace it with a more customized solution if the resulting logs are too verbose. + +### Updates to the `@backstage/create-app` template + +New backstage projects created with `@backstage/create-app` will now include the Catalog Logs module for logging catalog error events, as well as support for the Postgres Search Engine. + +### Permission Policy deprecations + +The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + +- `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. +- `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + +Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +### Renaming the `setupRequestMockHandlers` test utility + +The `setupRequestMockHandlers` utility function exported by `@backstage/test-utils` and `@backstage/backend-test-utils` has been renamed to `registerMswTestHooks`. This is done to better reflect the context and the purpose of the function. The old name is deprecated and will be removed in a future release. + +### Catalog GitHub module support for `repository` events + +The GitHub provider module and `GithubEntityProvider` for the Catalog now supports event driven ingestion of repositories by subscribing to `repository` events from GitHub. This includes the actions `archived`, `deleted`, `edited`, `renamed`, `transferred`, and `unarchived`. This is in addition to the existing support for `push` events, which you can read more about in the integration documentation for [GitHub Discovery](https://backstage.io/docs/integrations/github/discovery#events-support). + +Contributed by [@pjungermann](https://github.com/pjungermann) in [#25360](https://github.com/backstage/backstage/pull/25360) + +### Catalog i18n support + +The Catalog plugin as well as the Catalog React library now support internationalization (i18n). This means that you can customize the messaging in the catalog, as well as add translations. You can read more about this in the [i18n documentation](https://backstage.io/docs/plugins/internationalization/). + +Contributed by [@mario-mui](https://github.com/mario-mui) in [#23392](https://github.com/backstage/backstage/pull/23392) + +### Route Binding configuration improvements + +It is now possible to explicitly remove default route bindings, for cases where you don't want a plugin route to be bound to any target at all: + +```yaml +app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false +``` + +### Scaffolder Fields performance improvements + +The `EntityPicker` and `MultiEntityPicker` fields have been updated to improve performance with large catalogs. Contributed by [@kmikko](https://github.com/kmikko) in [#25315](https://github.com/backstage/backstage/pull/25315), [#25380](https://github.com/backstage/backstage/pull/25380) + +### BREAKING: Catalog LDAP Module improvements + +The `@backstage/plugin-catalog-backend-module-ldap` module has been improved to support multiple or no declarations of both user and group configs. + +This change is breaking for `readLdapOrg` and `LdapProviderConfig`, which now both always accept arrays of `users` and `groups` configurations. + +Contributed by [@Jenson3210](https://github.com/Jenson3210) in [#25261](https://github.com/backstage/backstage/pull/25261) + +## Security Fixes + +This release does not contain any security fixes. + +## Upgrade path + +We recommend that you keep your Backstage project up to date with this latest release. For more guidance on how to upgrade, check out the documentation for [keeping Backstage updated](https://backstage.io/docs/getting-started/keeping-backstage-updated). + +## Links and References + +Below you can find a list of links and references to help you learn about and start using this new release. + +- [Backstage official website](https://backstage.io/), [documentation](https://backstage.io/docs/), and [getting started guide](https://backstage.io/docs/getting-started/) +- [GitHub repository](https://github.com/backstage/backstage) +- Backstage's [versioning and support policy](https://backstage.io/docs/overview/versioning-policy) +- [Community Discord](https://discord.gg/backstage-687207715902193673) for discussions and support +- [Changelog](https://github.com/backstage/backstage/tree/master/docs/releases/v1.29.0-changelog.md) +- Backstage [Demos](https://backstage.io/demos), [Blog](https://backstage.io/blog), [Roadmap](https://backstage.io/docs/overview/roadmap) and [Plugins](https://backstage.io/plugins) + +Sign up for our [newsletter](https://info.backstage.spotify.com/newsletter_subscribe) if you want to be informed about what is happening in the world of Backstage. diff --git a/docs/releases/v1.30.0-next.0-changelog.md b/docs/releases/v1.30.0-next.0-changelog.md new file mode 100644 index 0000000000..f3b7f2ef45 --- /dev/null +++ b/docs/releases/v1.30.0-next.0-changelog.md @@ -0,0 +1,2214 @@ +# Release v1.30.0-next.0 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.0](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.0) + +## @backstage/cli@0.27.0-next.0 + +### Minor Changes + +- 32a38e1: **BREAKING**: The lockfile (`yarn.lock`) dependency analysis and mutations have been removed from several commands. + + The `versions:bump` command will no longer attempt to bump and deduplicate dependencies by modifying the lockfile, it will only update `package.json` files. + + The `versions:check` command has been removed, since its only purpose was verification and mutation of the lockfile. We recommend using the `yarn dedupe` command instead, or the `yarn-deduplicate` package if you're using Yarn classic. + + The check that was built into the `package start` command has been removed, it will no longer warn about lockfile mismatches. + + The packages in the Backstage ecosystem handle package duplications much better now than when these CLI features were first introduced, so the need for these features has diminished. By removing them, we drastically reduce the integration between the Backstage CLI and Yarn, making it much easier to add support for other package managers in the future. + +### Patch Changes + +- 7eb08a6: Add frontend-dynamic-container role to eslint config factory +- fbc7819: Use ES2022 in CLI bundler +- e53074f: Updated default backend plugin to use `RootConfigService` instead of `Config`. This also removes the dependency on `@backstage/config` as it's no longer used. +- ee2b0e5: The experimental module federation build now has the ability to force the use of development versions of `react` and `react-dom` by setting the `FORCE_REACT_DEVELOPMENT` flag. +- 239dffc: Remove usage of deprecated functionality from @backstage/config-loader +- 0eedec3: Add support for dynamic plugins via the EXPERIMENTAL_MODULE_FEDERATION environment variable when running `yarn start`. +- dc4fb4f: Fix for `repo build --all` not properly detecting the experimental public entry point. +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/integration@1.14.0-next.0 + +### Minor Changes + +- 78c1329: Updated `GitlabUrlReader.readUrl` and `GitlabUrlReader.readTree` to accept a user-provided token, supporting both bearer and private tokens. + +### Patch Changes + +- c591670: Updated functions for `getHarnessEditContentsUrl`, `getHarnessFileContentsUrl`, `getHarnessArchiveUrl`, `getHarnessLatestCommitUrl` and `parseHarnessUrl` to handle account and org level urls +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-react@1.11.0-next.0 + +### Minor Changes + +- 8839381: Add scaffolder option to display object items in separate rows on review page + +### Patch Changes + +- d0e95a7: Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in `FormProps` +- 4670f06: support `ajv-errors` for scaffolder validation to allow for customizing the error messages +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + +## @backstage/app-defaults@1.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-permission-react@0.4.24 + +## @backstage/backend-app-api@0.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/backend-common@0.23.4-next.0 + +### Patch Changes + +- ba8571e: Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. + +- 7e13b7a: The remaining exports in the package have now been deprecated: + + - `cacheToPluginCacheManager` + - `createLegacyAuthAdapters` + - `LegacyCreateRouter` + - `legacyPlugin` + - `loggerToWinstonLogger` + - `makeLegacyPlugin` + + Users of these export should fully [migrate to the new backend system](https://backstage.io/docs/backend-system/building-backends/migrating). + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/backend-defaults@0.4.2-next.0 + +### Patch Changes + +- 4e79d19: The `createHealthRouter` utility that allows you to create a health check router is now exported via `@backstage/backend-defaults/rootHttpRouter`. +- 78c1329: Updated `GitlabUrlReader.readUrl` and `GitlabUrlReader.readTree` to accept a user-provided token, supporting both bearer and private tokens. +- 8e967da: Fixed the routing of the new health check service, the health endpoints should now properly be available at `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness`. +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-app-api@0.8.1-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/backend-dynamic-feature-service@0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-app-api@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-backend@0.3.10-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/backend-openapi-utils@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + +## @backstage/backend-plugin-api@0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + +## @backstage/backend-tasks@0.5.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.5-next.0 + +### Patch Changes + +- 4e79d19: The default services for `startTestBackend` and `ServiceFactoryTester` now includes the Root Health Service. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/backend-app-api@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/config-loader@1.8.2-next.0 + +### Patch Changes + +- 493feac: Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and + `ConfigSources.defaultForTargets`, which results in omission of a ConfigSource + for the default app-config.yaml configuration file if it's not present. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-app-api@1.14.1-next.0 + +### Patch Changes + +- 9a46a81: The request to delete the session cookie when running the app in protected mode is now done with a plain `fetch` rather than `FetchApi`. This fixes a bug where the app would immediately try to sign-in again when removing the cookie during logout. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-compat-api@0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/core-components@0.14.10-next.0 + +### Patch Changes + +- 678971a: Move the `Link` component to the `RoutedTabs` instead of the `HeaderTabs` component +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.18-next.0 + +### Patch Changes + +- c0a705d: Added the Kubernetes plugin to `create-app` +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.4-next.0 + +### Patch Changes + +- 7777b5f: Support icon overriding with the new `IconBundleBlueprint` API. +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-plugin-api@0.6.8-next.0 + +### Patch Changes + +- 4e53ad6: Introduce a new way to encapsulate extension kinds that replaces the extension creator pattern with `createExtensionBlueprint` + + This allows the creation of extension instances with the following pattern: + + ```tsx + // create the extension blueprint which is used to create instances + const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(params: { text: string }) { + return { + element:

{params.text}

, + }; + }, + }); + + // create an instance of the extension blueprint with params + const testExtension = EntityCardBlueprint.make({ + name: 'foo', + params: { + text: 'Hello World', + }, + }); + ``` + +- 9b89b82: The `ExtensionBoundary` now by default infers whether it's routable from whether it outputs a route path. + +- 7777b5f: Added a new `IconBundleBlueprint` that lets you create icon bundle extensions that can be installed in an App in order to override or add new app icons. + + ```tsx + import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + + const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, + }); + ``` + +- 31bfc44: Extension data references can now be defined in a way that encapsulates the ID string in the type, in addition to the data type itself. The old way of creating extension data references is deprecated and will be removed in a future release. + + For example, the following code: + + ```ts + export const myExtension = + createExtensionDataRef('my-plugin.my-data'); + ``` + + Should be updated to the following: + + ```ts + export const myExtension = createExtensionDataRef().with({ + id: 'my-plugin.my-data', + }); + ``` + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/frontend-app-api@0.7.4-next.0 + - @backstage/test-utils@1.5.9-next.0 + - @backstage/types@1.1.1 + +## @backstage/integration-react@1.1.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/repo-tools@0.9.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.9-next.0 + +## @backstage/test-utils@1.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-react@0.4.24 + +## @backstage/plugin-api-docs@0.11.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-react@0.4.24 + +## @backstage/plugin-app-backend@0.3.72-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-app-node@0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + +## @backstage/plugin-app-visualizer@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.0 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.0 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.0 + +### Patch Changes + +- 39f36a9: Updated the Microsoft authenticator to accurately define required scopes, but to also omit the required and additional scopes when requesting resource scopes. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## @backstage/plugin-auth-node@0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-react@0.1.5-next.0 + +### Patch Changes + +- aeac3e9: feat: Hide visibility of CookieAuthRedirect +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + +## @backstage/plugin-catalog@1.21.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-catalog-backend@1.24.1-next.0 + +### Patch Changes + +- a629fb2: Added setAllowedLocationTypes while introducing a new extension point called CatalogLocationsExtensionPoint +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + +## @backstage/plugin-catalog-backend-module-aws@0.3.18-next.0 + +### Patch Changes + +- ba8571e: Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. +- 90a7340: `AwsOrganizationCloudAccountProcessor` configuration field `roleArn` is deprecated in favor of new field `accountId` +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-kubernetes-common@0.8.1 + +## @backstage/plugin-catalog-backend-module-azure@0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-kubernetes-common@0.8.1 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-common@0.8.0 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + +## @backstage/plugin-catalog-backend-module-logs@0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.31-next.0 + +### Patch Changes + +- 58dff4d: Added option to ingest groups based on their group membership in Azure Entra ID +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.3 + - @backstage/plugin-permission-common@0.8.0 + +## @backstage/plugin-catalog-graph@0.4.8-next.0 + +### Patch Changes + +- fba7537: Memoize entity graph nodes when applying an `entityFilter` to prevent repeated redraws +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-catalog-common@1.0.25 + +## @backstage/plugin-catalog-node@1.12.5-next.0 + +### Patch Changes + +- a629fb2: Added setAllowedLocationTypes while introducing a new extension point called CatalogLocationsExtensionPoint +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/plugin-catalog-react@1.12.3-next.0 + +### Patch Changes + +- 31bfc44: Updated alpha definitions of extension data references. +- 9b89b82: Internal refactor to remove unnecessary `routable` prop in the implementation of the `createEntityContentExtension` alpha export. +- bebd569: Fix extra divider displayed on user list picker component +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-react@0.4.24 + +## @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-config-schema@0.1.58-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/plugin-permission-react@0.4.24 + +## @backstage/plugin-devtools-backend@0.3.9-next.0 + +### Patch Changes + +- 32a38e1: Removed unused code for lockfile analysis. +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/plugin-events-backend@0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.9-next.0 + +### Patch Changes + +- ba8571e: Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-module-azure@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-module-gerrit@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-module-github@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-backend-test-utils@0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-events-node@0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + +## @backstage/plugin-home@0.7.8-next.0 + +### Patch Changes + +- 31bfc44: Updated alpha definitions of extension data references. +- fdcc059: Fixed a bug on the WelcomeTitle component where the welcome message wasn't correct when the language was set to Spanish +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## @backstage/plugin-home-react@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-kubernetes@0.11.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/plugin-kubernetes-react@0.4.2-next.0 + +## @backstage/plugin-kubernetes-backend@0.18.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/plugin-kubernetes-node@0.1.17-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/plugin-kubernetes-cluster@0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/plugin-kubernetes-react@0.4.2-next.0 + +## @backstage/plugin-kubernetes-node@0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.1 + +## @backstage/plugin-kubernetes-react@0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.1 + +## @backstage/plugin-notifications@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-notifications-backend@0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + +## @backstage/plugin-notifications-backend-module-email@0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + +## @backstage/plugin-notifications-node@0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.9-next.0 + +## @backstage/plugin-org@0.6.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog-common@1.0.25 + +## @backstage/plugin-org-react@0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## @backstage/plugin-permission-node@0.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + +## @backstage/plugin-proxy-backend@0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder@1.23.1-next.0 + +### Patch Changes + +- bbd9f56: Cleaned up codebase of RepoUrlPicker +- b8600fe: Fix issue with `RepoUrlPicker` not refreshing the credentials for a different host +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/plugin-scaffolder-react@1.11.0-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + +## @backstage/plugin-scaffolder-backend@1.23.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.0 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.47-next.0 + +### Patch Changes + +- dae85df: Add examples for `fetch:cookiecutter` scaffolder action & improve related tests +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.0 + +### Patch Changes + +- fad1b90: Allow the `createGitlabProjectVariableAction` to use oauth tokens + +- aab708e: Added test cases for gitlab:issue:edit examples + +- ef742dc: Added test cases for gitlab:projectAccessToken:create example + +- 1ba4c2f: Added test cases for gitlab:pipeline:trigger examples + +- a6603e4: Add custom action for merge request: **auto** + + The **Auto** action selects the committed action between _create_ and _update_. + + The **Auto** action fetches files using the **/projects/repository/tree endpoint**. + After fetching, it checks if the file exists locally and in the repository. If it does, it chooses **update**; otherwise, it chooses **create**. + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.6-next.0 + +### Patch Changes + +- 6fc03c7: Add examples for notification:send scaffolder action & improve related tests +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.0 + +### Patch Changes + +- 449def7: Add examples for fetch:rails scaffolder action & improve related tests +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.0 + +## @backstage/plugin-scaffolder-node@0.4.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.4 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-test-utils@0.4.5-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## @backstage/plugin-search@1.4.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend@1.5.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend-module-explore@0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend-module-pg@0.5.33-next.0 + +### Patch Changes + +- 7251567: Removing `@backstage/backend-app-api` dependency +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-backend-module-techdocs@0.1.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-techdocs-node@1.12.9-next.0 + +## @backstage/plugin-search-backend-node@1.2.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-search-react@1.7.14-next.0 + +### Patch Changes + +- 31bfc44: Updated alpha definitions of extension data references. +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-signals@0.0.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + +## @backstage/plugin-signals-backend@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + +## @backstage/plugin-signals-node@0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## @backstage/plugin-techdocs@1.10.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.9-next.0 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-backend@1.10.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.0 + - @backstage/plugin-techdocs-node@1.12.9-next.0 + +## @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-node@1.12.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.13 + +## @backstage/plugin-techdocs-react@1.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-user-settings@0.8.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.100-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/plugin-home@0.7.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/plugin-scaffolder-react@1.11.0-next.0 + - @backstage/frontend-app-api@0.7.4-next.0 + - @backstage/plugin-catalog-graph@0.4.8-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.0 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-catalog-import@0.12.2-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-devtools@0.1.17-next.0 + - @backstage/plugin-kubernetes@0.11.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.15-next.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.10-next.0 + +## example-app-next@0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.0 + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/plugin-home@0.7.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/plugin-scaffolder-react@1.11.0-next.0 + - @backstage/frontend-app-api@0.7.4-next.0 + - @backstage/plugin-catalog-graph@0.4.8-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.0 + - @backstage/plugin-app-visualizer@0.1.9-next.0 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-catalog-import@0.12.2-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes@0.11.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.15-next.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.10-next.0 + +## app-next-example-plugin@0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + +## example-backend@0.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-devtools-backend@0.3.9-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-app-backend@0.3.72-next.0 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.0 + - @backstage/plugin-kubernetes-backend@0.18.4-next.0 + - @backstage/plugin-notifications-backend@0.3.4-next.0 + - @backstage/plugin-permission-backend@0.5.47-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-proxy-backend@0.5.4-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.0 + - @backstage/plugin-search-backend@1.5.15-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-signals-backend@0.1.9-next.0 + - @backstage/plugin-techdocs-backend@1.10.10-next.0 + +## example-backend-legacy@0.2.101-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/plugin-devtools-backend@0.3.9-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.0 + - example-app@0.2.100-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-app-backend@0.3.72-next.0 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.0 + - @backstage/plugin-events-backend@0.3.10-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-kubernetes-backend@0.18.4-next.0 + - @backstage/plugin-permission-backend@0.5.47-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-proxy-backend@0.5.4-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.0 + - @backstage/plugin-search-backend@1.5.15-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-signals-backend@0.1.9-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + - @backstage/plugin-techdocs-backend@1.10.10-next.0 + +## e2e-test@0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.9-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @internal/plugin-todo-list@1.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @internal/plugin-todo-list-backend@1.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 diff --git a/docs/releases/v1.30.0-next.1-changelog.md b/docs/releases/v1.30.0-next.1-changelog.md new file mode 100644 index 0000000000..1d93235740 --- /dev/null +++ b/docs/releases/v1.30.0-next.1-changelog.md @@ -0,0 +1,2175 @@ +# Release v1.30.0-next.1 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.1) + +## @backstage/core-app-api@1.14.2-next.0 + +### Patch Changes + +- 9a46a81: The request to delete the session cookie when running the app in protected mode is now done with a plain `fetch` rather than `FetchApi`. This fixes a bug where the app would immediately try to sign-in again when removing the cookie during logout. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - + +## @backstage/config-loader@1.9.0-next.1 + +### Minor Changes + +- 274428f: Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). + +### Patch Changes + +- 1edd6c2: The `env` option of `ConfigSources.default` now correctly allows undefined members. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications-backend-module-email@0.2.0-next.1 + +### Minor Changes + +- def53a7: **BREAKING** Following `NotificationTemplateRenderer` methods now return a Promise and **must** be awaited: `getSubject`, `getText` and `getHtml`. + + Required changes and example usage: + + ```diff + import { notificationsEmailTemplateExtensionPoint } from '@backstage/plugin-notifications-backend-module-email'; + import { Notification } from '@backstage/plugin-notifications-common'; + +import { getNotificationSubject, getNotificationTextContent, getNotificationHtmlContent } from 'my-notification-processing-library` + export const notificationsModuleEmailDecorator = createBackendModule({ + pluginId: 'notifications', + moduleId: 'email.templates', + register(reg) { + reg.registerInit({ + deps: { + emailTemplates: notificationsEmailTemplateExtensionPoint, + }, + async init({ emailTemplates }) { + emailTemplates.setTemplateRenderer({ + - getSubject(notification) { + + async getSubject(notification) { + - return `New notification from ${notification.source}`; + + const subject = await getNotificationSubject(notification); + + return `New notification from ${subject}`; + }, + - getText(notification) { + + async getText(notification) { + - return notification.content; + + const text = await getNotificationTextContent(notification); + + return text; + }, + - getHtml(notification) { + + async getHtml(notification) { + - return `

${notification.content}

`; + + const html = await getNotificationHtmlContent(notification); + + return html; + }, + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + +## @backstage/plugin-techdocs-common@0.1.0-next.0 + +### Minor Changes + +- 4698e1f: Initial release of the techdocs-common package. + +## @backstage/app-defaults@1.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/backend-app-api@0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/backend-common@0.23.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/backend-defaults@0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-app-api@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/backend-dynamic-feature-service@0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-app-api@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-app-node@0.1.23-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-backend@0.3.10-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/backend-openapi-utils@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/errors@1.2.4 + +## @backstage/backend-plugin-api@0.7.1-next.1 + +### Patch Changes + +- f011d1b: fix typo in `getPluginRequestToken` comments +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/backend-tasks@0.5.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-app-api@0.8.1-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/cli@0.27.0-next.1 + +### Patch Changes + +- e6e7d86: Switched the target from `'ES2022'` to `'es2022'` for better compatibility with older versions of `swc`. +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.8-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.18-next.1 + +### Patch Changes + +- 6c1081c: Updated dockerfile and `app-config.production.yaml` to make it easier to get started with example data +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.5-next.1 + +### Patch Changes + +- 3be9aeb: Added support for v2 extensions, which declare their inputs and outputs without using a data map. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-plugin-api@0.6.8-next.1 + +### Patch Changes + +- 3be9aeb: Extensions have been changed to be declared with an array of inputs and outputs, rather than a map of named data refs. This change was made to reduce confusion around the role of the input and output names, as well as enable more powerful APIs for overriding extensions. + + An extension that was previously declared like this: + + ```tsx + const exampleExtension = createExtension({ + name: 'example', + inputs: { + items: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( +
+ Example + {inputs.items.map(item => { + return
{item.output.element}
; + })} +
+ ), + }; + }, + }); + ``` + + Should be migrated to the following: + + ```tsx + const exampleExtension = createExtension({ + name: 'example', + inputs: { + items: createExtensionInput([coreExtensionData.reactElement]), + }, + output: [coreExtensionData.reactElement], + factory({ inputs }) { + return [ + coreExtensionData.reactElement( +
+ Example + {inputs.items.map(item => { + return
{item.get(coreExtensionData.reactElement)}
; + })} +
, + ), + ]; + }, + }); + ``` + +- 3fb421d: Added support to be able to define `zod` config schema in Blueprints, with built in schema merging from the Blueprint and the extension instances. + +- 6349099: Added config input type to the extensions + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.12-next.1 + +### Patch Changes + +- 3be9aeb: Added support for v2 extensions, which declare their inputs and outputs without using a data map. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.9-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## @backstage/plugin-api-docs@0.11.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-app-backend@0.3.72-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-app-node@0.1.23-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-app-node@0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + +## @backstage/plugin-app-visualizer@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.1 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.1 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.1 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.1 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.1 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.1 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.1 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.1 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.1 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.1 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-auth-node@0.4.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog@1.21.2-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend@1.24.1-next.1 + +### Patch Changes + +- 51240ee: Preserve default `allowedLocationTypes` when `setAllowedLocationTypes()` of `CatalogLocationsExtensionPoint` is not called. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-aws@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-azure@0.1.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.1 + +### Patch Changes + +- c7b14ed: Adds new optional `excludeRepos` configuration option to the Gitlab catalog provider. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-logs@0.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-catalog-common@1.0.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-graph@0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + +## @backstage/plugin-catalog-node@1.12.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-react@1.12.3-next.1 + +### Patch Changes + +- 7ca331c: Correct `EntityDisplayName`'s icon alignment with the text. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + +## @backstage/plugin-devtools@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-devtools-common@0.1.12-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-devtools-backend@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-devtools-common@0.1.12-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools-common@0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend@0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-module-azure@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-module-gerrit@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-module-github@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-module-gitlab@0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-backend-test-utils@0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-events-node@0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + +## @backstage/plugin-home@0.7.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## @backstage/plugin-kubernetes@0.11.13-next.1 + +### Patch Changes + +- e6c15cc: Adds support for Backstage's new frontend system, available via the `/alpha` sub-path export. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.1 + +## @backstage/plugin-kubernetes-backend@0.18.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-kubernetes-node@0.1.17-next.1 + +## @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.1 + +## @backstage/plugin-kubernetes-common@0.8.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-node@0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-react@0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications-backend@0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## @backstage/plugin-notifications-node@0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## @backstage/plugin-org@0.6.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-org-react@0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-permission-common@0.8.1-next.0 + +### Patch Changes + +- 137fa34: Add the MetadataResponseSerializedRule type from @backstage/plugin-permission-node, since this type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.8.1-next.1 + +### Patch Changes + +- 5cd9878: The MetadataResponseSerializedRule type has been moved to @backstage/plugin-permission-common, and should be imported from there going forward. To avoid an immediate breaking change, this type is still re-exported from this package, but is marked as deprecated and will be removed in a future release. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @backstage/plugin-permission-react@0.4.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-proxy-backend@0.5.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder@1.23.1-next.1 + +### Patch Changes + +- 896a22d: Fix helper text margin for scaffolder EntityNamePicker and EntityTagsPicker when using outlined text field +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.1-next.1 + +### Patch Changes + +- ef87e06: Fix scaffolder action `catalog:write` to write to directories that don't already exist +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.1 + +### Patch Changes + +- e3b64be: Added examples for publish:bitbucketServer action and improve its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.1 + +### Patch Changes + +- 24de005: Added test cases for publish:gitea examples +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.1 + +### Patch Changes + +- d21d307: Added examples for github:environment:create action and improve its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.1 + +## @backstage/plugin-scaffolder-common@1.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-test-utils@0.4.5-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## @backstage/plugin-scaffolder-react@1.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search@1.4.15-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search-backend@1.5.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-explore@0.1.29-next.1 + +### Patch Changes + +- 93fc1a0: Updated dependency `@backstage-community/plugin-explore-common` to `^0.0.4`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-pg@0.5.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.9-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-node@1.2.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-common@1.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-react@1.7.14-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-signals-backend@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## @backstage/plugin-signals-node@0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## @backstage/plugin-techdocs@1.10.8-next.1 + +### Patch Changes + +- 69bd940: Use annotation constants from new techdocs-common package. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-backend@1.10.10-next.1 + +### Patch Changes + +- 69bd940: Use annotation constants from new techdocs-common package. +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.9-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-techdocs-node@1.12.9-next.1 + +### Patch Changes + +- 69bd940: Use annotation constants from new techdocs-common package. +- 949083d: Update `patchMkdocsYmlPrebuild` to modify `repo_url` and `edit_uri` independently. +- Updated dependencies + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-user-settings@0.8.11-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.100-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/cli@0.27.0-next.1 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/plugin-user-settings@0.8.11-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-catalog-graph@0.4.8-next.1 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-devtools@0.1.17-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## example-app-next@0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/cli@0.27.0-next.1 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-user-settings@0.8.11-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-catalog-graph@0.4.8-next.1 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-app-visualizer@0.1.9-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## app-next-example-plugin@0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-components@0.14.10-next.0 + +## example-backend@0.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.1 + - @backstage/plugin-techdocs-backend@1.10.10-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-scaffolder-backend@1.23.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/plugin-app-backend@0.3.72-next.1 + - @backstage/plugin-devtools-backend@0.3.9-next.1 + - @backstage/plugin-proxy-backend@0.5.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.1 + - @backstage/plugin-kubernetes-backend@0.18.4-next.1 + - @backstage/plugin-permission-backend@0.5.47-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.1 + - @backstage/plugin-search-backend@1.5.15-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + - @backstage/plugin-notifications-backend@0.3.4-next.1 + - @backstage/plugin-signals-backend@0.1.9-next.1 + +## example-backend-legacy@0.2.101-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.10-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.1 + - @backstage/plugin-scaffolder-backend@1.23.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.1 + - example-app@0.2.100-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-app-backend@0.3.72-next.1 + - @backstage/plugin-devtools-backend@0.3.9-next.1 + - @backstage/plugin-proxy-backend@0.5.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-backend@0.18.4-next.1 + - @backstage/plugin-permission-backend@0.5.47-next.1 + - @backstage/plugin-search-backend@1.5.15-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + - @backstage/plugin-events-backend@0.3.10-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.1 + - @backstage/plugin-signals-backend@0.1.9-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## e2e-test@0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/cli@0.27.0-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @internal/plugin-todo-list-backend@1.0.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## @internal/plugin-todo-list-common@1.0.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 diff --git a/docs/releases/v1.30.0-next.2-changelog.md b/docs/releases/v1.30.0-next.2-changelog.md new file mode 100644 index 0000000000..cdf4164566 --- /dev/null +++ b/docs/releases/v1.30.0-next.2-changelog.md @@ -0,0 +1,156 @@ +# Release v1.30.0-next.2 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.2](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.2) + +## @backstage/cli@0.27.0-next.2 + +### Patch Changes + +- b2d97fd: Fixing loading of additional config files with new `ConfigSources` +- adabb40: New command now supports setting package license +- Updated dependencies + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.1 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.14.0-next.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/create-app@0.5.18-next.2 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/plugin-catalog-graph@0.4.8-next.2 + +### Patch Changes + +- 4a529c2: Use `entityPresentationApi` for the node title and the icon. +- Updated dependencies + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + +## example-app@0.2.100-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.2 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-devtools@0.1.17-next.1 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.11-next.1 + +## example-app-next@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.2 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-app-visualizer@0.1.9-next.1 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.11-next.1 + +## e2e-test@0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.2 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-techdocs-react@1.2.7-next.0 diff --git a/docs/releases/v1.30.0-next.3-changelog.md b/docs/releases/v1.30.0-next.3-changelog.md new file mode 100644 index 0000000000..4fa86da821 --- /dev/null +++ b/docs/releases/v1.30.0-next.3-changelog.md @@ -0,0 +1,2199 @@ +# Release v1.30.0-next.3 + +Upgrade Helper: [https://backstage.github.io/upgrade-helper/?to=1.30.0-next.3](https://backstage.github.io/upgrade-helper/?to=1.30.0-next.3) + +## @backstage/backend-plugin-api@0.8.0-next.2 + +### Minor Changes + +- 7c5f3b0: The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + + We're looking for ways to make it possible to augment services without the need to replace the entire service. + + Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + + ```diff + // @backstage/backend-defaults + + + export const urlReaderFactoriesServiceRef = createServiceRef({ + + id: 'core.urlReader.factories', + + scope: 'plugin', + + multiton: true, + + }); + + ... + + export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + + factories: urlReaderFactoriesServiceRef, + }, + - async factory({ config, logger }) { + + async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, + + factories, + }); + }, + }); + ``` + + With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + + ```ts + // packages/backend/index.ts + import { createServiceFactory } from '@backstage/backend-plugin-api'; + import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; + ... + + backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, + })); + + ... + + ``` + +### Patch Changes + +- 6061061: Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. + +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. + +- 8b13183: Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + + A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + + Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + + ```ts + const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, + }); + ``` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/frontend-plugin-api@0.7.0-next.2 + +### Minor Changes + +- 72754db: **BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + + This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. + +### Patch Changes + +- 210d066: Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-auth-node@0.5.0-next.2 + +### Minor Changes + +- 579afd0: **BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog@1.22.0-next.2 + +### Minor Changes + +- 6925dcb: Introduces the HasSubdomainsCard component that displays the subdomains of a given domain + +### Patch Changes + +- 604a504: The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder@1.24.0-next.2 + +### Minor Changes + +- 3fca643: Added field extension `RepoBranchPicker` that supports autocompletion for Bitbucket + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## @backstage/app-defaults@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## @backstage/backend-app-api@0.8.1-next.2 + +### Patch Changes + +- 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. +- 80a0737: Added configuration for the `packages` options to config schema +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-common@0.23.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-defaults@0.4.2-next.2 + +### Patch Changes + +- 0d16b52: Add access restrictions to the JWKS external access method config schema +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 7c5f3b0: Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. +- 1d5f298: Avoid excessive numbers of error listeners on cache clients +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-dynamic-feature-service@0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-openapi-utils@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/backend-tasks@0.5.28-next.2 + +### Patch Changes + +- ba9abf4: The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/backend-test-utils@0.4.5-next.2 + +### Patch Changes + +- 8b13183: Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. +- 7c5f3b0: Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/cli@0.27.0-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## @backstage/config-loader@1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/core-compat-api@0.2.8-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- 16cf96c: Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## @backstage/create-app@0.5.18-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## @backstage/dev-utils@1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## @backstage/frontend-app-api@0.7.5-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/frontend-test-utils@0.1.12-next.2 + +### Patch Changes + +- 8209449: Added new APIs for testing extensions +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/repo-tools@0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## @techdocs/cli@1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## @backstage/test-utils@1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## @backstage/plugin-api-docs@0.11.8-next.2 + +### Patch Changes + +- 4b6d2cb: Updated dependency `@graphiql/react` to `^0.23.0`. +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-app-backend@0.3.72-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-app-node@0.1.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + +## @backstage/plugin-app-visualizer@0.1.9-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-auth-backend@0.22.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## @backstage/plugin-auth-backend-module-pinniped-provider@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-auth-backend-module-vmware-cloud-provider@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + +### Patch Changes + +- 3fca643: Added method `listBranchesByRepository` to `BitbucketCloudClient` +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + +## @backstage/plugin-catalog-backend@1.24.1-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-aws@0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-azure@0.1.43-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.37-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-gcp@0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.40-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-github-org@0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-gitlab-org@0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.4.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-backend-module-ldap@0.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-logs@0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-puppetdb@0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-catalog-common@1.0.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/catalog-model@1.5.0 + +## @backstage/plugin-catalog-graph@0.4.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-import@0.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + +## @backstage/plugin-catalog-node@1.12.5-next.2 + +### Patch Changes + +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-catalog-react@1.12.3-next.2 + +### Patch Changes + +- 012e3eb: Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + +## @backstage/plugin-devtools@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## @backstage/plugin-devtools-backend@0.3.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-devtools-common@0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend@0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-aws-sqs@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-events-backend-module-azure@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-backend-module-github@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-module-gitlab@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-events-backend-test-utils@0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.2 + +## @backstage/plugin-events-node@0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + +## @backstage/plugin-home@0.7.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## @backstage/plugin-kubernetes@0.11.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## @backstage/plugin-kubernetes-backend@0.18.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 8c1aa06: Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. + This was already documented and supported by the plugin. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## @backstage/plugin-kubernetes-common@0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-node@0.1.17-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-kubernetes-react@0.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-notifications-backend@0.3.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-notifications-backend-module-email@0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-notifications-node@0.2.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-org@0.6.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-org-react@0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-permission-backend@0.5.47-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + +## @backstage/plugin-permission-common@0.8.1-next.1 + +### Patch Changes + +- df784fe: Add the MetadataResponse type from @backstage/plugin-permission-node, since this + type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-permission-node@0.8.1-next.2 + +### Patch Changes + +- df784fe: The MetadataResponse type has been moved to @backstage/plugin-permission-common + to match the recent move of MetadataResponseSerializedRule, and should be + imported from there going forward. To avoid an immediate breaking change, this + type is still re-exported from this package, but is marked as deprecated and + will be removed in a future release. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-permission-react@0.4.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## @backstage/plugin-proxy-backend@0.5.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend@1.23.1-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 3fca643: Added autocompletion support for resource `branches` +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-gcp@0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-notifications@0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-notifications-common@0.0.5 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 382e868: Added test cases for sentry:project:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-common@1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node@0.4.9-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/types@1.1.1 + +## @backstage/plugin-scaffolder-react@1.11.0-next.2 + +### Patch Changes + +- 072c00c: Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes +- 04759f2: Fix null check in `isJsonObject` utility function for scaffolder review state component +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search@1.4.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-search-backend@1.5.15-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-stack-overflow-collator@0.1.16-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## @backstage/plugin-search-backend-node@1.2.28-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## @backstage/plugin-search-common@1.2.14-next.1 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## @backstage/plugin-search-react@1.7.14-next.2 + +### Patch Changes + +- 3123c16: Fix package metadata +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## @backstage/plugin-signals-backend@0.1.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-signals-node@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## @backstage/plugin-techdocs@1.10.8-next.2 + +### Patch Changes + +- 67e76f2: TechDocs now supports the `mkdocs-redirects` plugin. Redirects defined using the `mkdocs-redirect` plugin will be handled automatically in TechDocs. Redirecting to external urls is not supported. In the case that an external redirect url is provided, TechDocs will redirect to the current documentation site home. +- bdc5471: Fixed issue where header styles were incorrectly generated when themes used CSS variables to define font size. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @backstage/plugin-techdocs-backend@1.10.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-techdocs-node@1.12.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-techdocs-common@0.1.0-next.0 + +## @backstage/plugin-user-settings@0.8.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## @backstage/plugin-user-settings-backend@0.2.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## example-app@0.2.100-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-devtools@0.1.17-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## example-app-next@0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-app-visualizer@0.1.9-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## app-next-example-plugin@0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + +## example-backend@0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-notifications-backend@0.3.4-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/catalog-model@1.5.0 + +## example-backend-legacy@0.2.101-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## e2e-test@0.2.19-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## techdocs-cli-embedded-app@0.2.99-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## @internal/plugin-todo-list-backend@1.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## @internal/plugin-todo-list-common@1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 diff --git a/docs/releases/v1.6.0-changelog.md b/docs/releases/v1.6.0-changelog.md index 0532243cbf..e9692f4f1b 100644 --- a/docs/releases/v1.6.0-changelog.md +++ b/docs/releases/v1.6.0-changelog.md @@ -248,8 +248,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` @@ -313,8 +312,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` diff --git a/docs/releases/v1.6.0-next.0-changelog.md b/docs/releases/v1.6.0-next.0-changelog.md index 5ff2311b79..fbe9caefde 100644 --- a/docs/releases/v1.6.0-next.0-changelog.md +++ b/docs/releases/v1.6.0-next.0-changelog.md @@ -10,8 +10,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` @@ -69,8 +68,7 @@ 1. First we need to add the @backstage/plugin-azure-devops package to your frontend app: - ```bash - # From your Backstage root directory + ```bash title="From your Backstage root directory" yarn add --cwd packages/app @backstage/plugin-azure-devops ``` diff --git a/docs/tooling/cli/03-commands.md b/docs/tooling/cli/03-commands.md index 234d0d04b7..a6652bd036 100644 --- a/docs/tooling/cli/03-commands.md +++ b/docs/tooling/cli/03-commands.md @@ -26,7 +26,6 @@ repo [command] Command that run across an entire package [command] Lifecycle scripts for individual packages migrate [command] Migration utilities versions:bump [options] Bump Backstage packages to the latest versions -versions:check [options] Check Backstage package versioning clean Delete cache directories [DEPRECATED] build-workspace [packages...] Builds a temporary dist workspace from the provided packages @@ -327,8 +326,7 @@ Options: ## versions\:bump Bump all `@backstage` packages to the latest versions. This checks for updates -in the package registry, and will update entries both in `yarn.lock` and -`package.json` files when necessary. +in the package registry, and will update entries `package.json` files when necessary. ```text Usage: backstage-cli versions:bump [options] @@ -339,23 +337,6 @@ Options: --release Bump to a specific Backstage release line or version (default: "main") ``` -## versions\:check - -Validate `@backstage` dependencies within the repo, making sure that there are -no duplicates of packages that might lead to breakages. - -By supplying the `--fix` flag the command will attempt to fix any conflict that -can be resolved by editing `yarn.lock`, but will not attempt to search for -remote updates or modify any `package.json` files. - -```text -Usage: backstage-cli versions:check [options] - -Options: - --fix Fix any auto-fixable versioning problems - -h, --help display help for command -``` - ## build-workspace Builds a mirror of the workspace using the packaged production version of each diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index 528d2c6325..db3f126de7 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -36,12 +36,15 @@ Please ensure the appropriate database drivers are installed in your `backend` package. If you intend to use both PostgreSQL and SQLite, you can install both of them. -```bash -# From your Backstage root directory -# install pg if you need PostgreSQL -yarn --cwd packages/backend add pg +Install pg if you need PostgreSQL: -# install SQLite 3 if you intend to set it as the client +```bash title="From your Backstage root directory" +yarn --cwd packages/backend add pg +``` + +Install SQLite 3 if you intend to set it as the client: + +```bash title="From your Backstage root directory" yarn --cwd packages/backend add better-sqlite3 ``` diff --git a/docs/tutorials/enable-public-entry.md b/docs/tutorials/enable-public-entry.md index 6282f66418..25d7382f13 100644 --- a/docs/tutorials/enable-public-entry.md +++ b/docs/tutorials/enable-public-entry.md @@ -100,3 +100,47 @@ With that, Backstage's cli and backend will detect public entry point and serve 5. Finally, as soon as you log in, you will be redirected to the main app home page (inspect the page and see that the protected bundle was served from the app backend after the redirect). That's it! + +## New Frontend System + +If your app uses the new frontend system, you can still use the public entry point feature. The `index-public-experimental.tsx` file does end up looking a bit different in this case: + +```tsx title="in packages/app/src/index-public-experimental.tsx" +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { CookieAuthRedirect } from '@backstage/plugin-auth-react'; +import { createApp } from '@backstage/frontend-app-api'; +import { + coreExtensionData, + createExtension, + createExtensionOverrides, + createSignInPageExtension, +} from '@backstage/frontend-plugin-api'; + +const signInPage = createSignInPageExtension({ + name: 'guest', + loader: async () => props => , +}); + +const authRedirectExtension = createExtension({ + namespace: 'app', + name: 'layout', + attachTo: { id: 'app/root', input: 'children' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ + element: , + }), +}); + +const app = createApp({ + features: [ + createExtensionOverrides({ + extensions: [signInPage, authRedirectExtension], + }), + ], +}); + +ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot()); +``` diff --git a/docs/tutorials/setup-opentelemetry.md b/docs/tutorials/setup-opentelemetry.md index 30dde79db8..faaeab49f4 100644 --- a/docs/tutorials/setup-opentelemetry.md +++ b/docs/tutorials/setup-opentelemetry.md @@ -33,6 +33,7 @@ const { } = require('@opentelemetry/auto-instrumentations-node'); const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); +// By default exports the metrics on localhost:9464/metrics const prometheus = new PrometheusExporter(); const sdk = new NodeSDK({ // You can add a traceExporter field here too diff --git a/docs/tutorials/switching-sqlite-postgres.md b/docs/tutorials/switching-sqlite-postgres.md index 2ef481dbf3..aec9ca2ee9 100644 --- a/docs/tutorials/switching-sqlite-postgres.md +++ b/docs/tutorials/switching-sqlite-postgres.md @@ -19,8 +19,7 @@ switch between database backends. First, add PostgreSQL to your `backend` package: -```bash -# From your Backstage root directory +```bash title="From your Backstage root directory" yarn --cwd packages/backend add pg ``` diff --git a/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx b/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx index e92c9f2265..c15b9fe208 100644 --- a/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx +++ b/microsite/blog/2022-12-19-backstage-wrapped-2022.mdx @@ -20,7 +20,7 @@ Last year we celebrated 75 [public adopters](https://github.com/backstage/backst ![Accelerated growth of Public Backstage Adopters, from January 2021–December 2022.](assets/2022-12-19/public-backstage-adopters-2022.png) -Not only are our adopters growing at an exponential rate, but so too are our contributors. Our [Discord server](https://discord.gg/backstage-687207715902193673-687207715902193673) is a perfect example of the worlds of our adopters and contributors melding. On average, at any given hour of the day or night, there are more than 500 people online available to talk and provide support on any Backstage topic. Discord discussions run a full gamut of topics – from technical support questions to best practices for onboarding engineers to Backstage – basically, anything and everything an adopter may be interested to know and learn. At the very beginning of 2022, we “only” had 4k Discord members. Today, we have a growing membership of 8.2k. +Not only are our adopters growing at an exponential rate, but so too are our contributors. Our [Discord server](https://discord.gg/backstage-687207715902193673) is a perfect example of the worlds of our adopters and contributors melding. On average, at any given hour of the day or night, there are more than 500 people online available to talk and provide support on any Backstage topic. Discord discussions run a full gamut of topics – from technical support questions to best practices for onboarding engineers to Backstage – basically, anything and everything an adopter may be interested to know and learn. At the very beginning of 2022, we “only” had 4k Discord members. Today, we have a growing membership of 8.2k. Isn’t it what we can call a great and vibrant community of enthusiasts? diff --git a/microsite/blog/2024-06-27-backstage-engineer-journey.mdx b/microsite/blog/2024-06-27-backstage-engineer-journey.mdx new file mode 100644 index 0000000000..b9bfc20e4e --- /dev/null +++ b/microsite/blog/2024-06-27-backstage-engineer-journey.mdx @@ -0,0 +1,135 @@ +--- +# prettier-ignore +title: "1,186 days with Backstage: A journey through the eyes of a developer" +author: Camila Loiola, Spotify +authorURL: https://github.com/camilaibs +authorImageURL: https://avatars.githubusercontent.com/u/6290749?v=4 +--- + +**TL;DR** There is no shortage of content that discusses the benefits that Backstage brings to organizations (and if you want to read more on that, you can just click [here](https://engineering.atspotify.com/category/backstage/), [here](https://backstage.io/blog), or [here](https://backstage.spotify.com/discover/blog/)). But I want to share what it did for me, a developer here at Spotify. And not just what it did, but how I’ve used Backstage to help me achieve goals at each stage of my journey at Spotify — from my first day as a brand new employee to today, my 1,186th day as a contributor. + +![Backstage Engineer Journey](assets/2024-06-27/backstage-engineer-journey.svg) + +{/* truncate */} + +## A new joiner with a need to get up to speed + +My first six months after joining Spotify, I was eager and determined to get up to speed so that I’d be able to start contributing as soon as possible. I set goals for myself and developed concrete activities to reach those goals: + +### Goal 1: Learn the internal process to develop, test, secure, and publish applications. + +Understanding a company’s practices and technologies is absolutely essential to getting started, no matter what company you join. + +At Spotify, we use a Backstage plugin called [TechDocs](https://backstage.io/docs/features/techdocs/) to document [Golden Paths](https://engineering.atspotify.com/2020/08/how-we-use-golden-paths-to-solve-fragmentation-in-our-software-ecosystem/) — tutorials that teach us the development process for each role at the company — for web, backend, mobile, data science development, and so on. + +With Backstage, I had an understanding of how to put things into production, even before I had my first task. I used the web Golden Path to guide me on setting up my environment; creating and structuring projects; and building, publishing, and monitoring production applications. + +> **Reflection:**
_“Is it easy for newly hired engineers to settle in and find information critical to getting started?”_ + +With Backstage, new hires can quickly and easily understand the nuances of their particular positions and be ready to deliver their first task to production. + +### Goal 2: Find and read the documentation and source code of the products my team maintains. + +Having familiarized myself with the ways of working, it was a good time to understand my specific team’s business domain. This meant spending time reading through documentation and source code to find out the value we provide to our customers. In the past, I would spend time with colleagues for knowledge transfer as opposed to learning on my own and consult my colleagues if needed. + +With the [Backstage Software Catalog](https://backstage.io/docs/features/software-catalog/), I could easily browse the components my team owns, find the source repo and documentation. + +> **Reflection:**
_“Are you able to accomplish this without asking a teammate for help?”_ + +Use Backstage to get to know the products that you will be working on, and use your peers' time to ask contextualized clarifying questions instead. + +### Goal 3: Figure out where my team fits within the company and with other teams. + +It was also crucial for me to build a network from the beginning so that I could explore collaboration opportunities. + +At Spotify, we use a Backstage plugin called Band Manager which displays information about each team and allows employees to see not only their own teams but also those within the same product area and company domain. + +> **Reflection:**
_“Are you going to need a one-on-one with your manager to ask this question?”_ + +Backstage helped me find the teams close to mine, and I took the opportunity to develop my network and talk to members from those teams to understand our boundaries and gather ideas to contribute. + +It’s pretty likely that you can relate to one or more of these examples, so use Backstage to support your goals from the very beginning! + +Now let’s move on to the middle of the journey, between six months and two years into life at the company. + +## Two years in — from contributor to leader + +After about two years of contributing to my team’s products, I began to look for opportunities to actually lead initiatives and create a new project. One of my objectives was to ensure I was upholding Spotify’s standards and choosing Spotify-approved technologies. + +My goal has changed from being a contributor to being the driver, i.e., from doing what I was told to do to defining what needed to be done. I “started driving initiatives” and “supporting my peers” in order to position myself as a reference in my technical area. + +### Goal 1: Quickly spin up new projects using the most appropriate technology stack. + +At Spotify, we expect engineers to use technologies approved by our [Tech Radar](https://backstage.io/blog/2020/05/14/tech-radar-plugin/#what-is-tech-radar) and follow the best quality standards. + +To do so, we use a plugin called [Scaffolder](https://backstage.io/docs/reference/plugin-scaffolder) (aka Software Templates), which allows engineers to create templates for setting up new projects. Templates automate actions such as creating repositories, allocating resources, registering in the catalog, configuring deployments, and connecting to other internal systems. + +> **Reflection:**
_“Do you need to create a ticket to get these things done before you even start coding? And is there a lot of time spent looking for boilerplate code to start a new project and connecting it to other internal systems?"_ + +Backstage enabled me to quickly create and publish a new backend that can deploy code to production with just a few clicks, so I could focus on building solutions for the business. I could use Backstage to easily build the right thing. Instead of spending time scaffolding projects, I could spend my time writing code. + +### Goal 2: Create products that have a healthy operational state and are easy to evolve. + +Keeping track of vulnerabilities and letting my team know about technical debts were other parts of my routine. + +With the [Soundcheck plugin](https://backstage.spotify.com/marketplace/spotify/plugin/soundcheck/), developers can easily configure checks to verify component health, such as reliability and other development and operational standards. We also have an internal Vulnerabilities plugin that displays fragilities in our components, so we can continuously prevent security issues. + +> **Reflection:**
_“Do you know which components owned by your team follows best practices and standards and which ones are lagging behind?”_ + +Backstage helped me gather and better understand our technical debts, enabling me to effectively prioritize during our team’s planning, visualize which entities were compliant to our team’s best practices, and identify which ones needed improvement. + +### Goal 3: Respond effectively to incidents within our components or their dependencies. + +If you’ve been with a company for around two years, you might have had to manage incidents in one way or another — this was certainly the case for me. A typical problem was to determine if the issue is with the service that I own or a downstream dependency. + +Spotify has adopted [PagerDuty](https://backstage.spotify.com/marketplace/pagerduty/plugin/pagerduty/) to manage on-call rotation and to track incident resolutions. We integrated the PagerDuty plugin in Backstage, so we see who is on call and if there is an incident for a particular component. + +> **Reflection:**
_“When a downstream dependency is failing, do you know who to contact?”_ + +In the event that one of our components starts failing, I check whether any of the systems we rely on is experiencing an incident, and Backstage again makes it easy for me to see the dependencies between systems and the status of those dependencies. + +## Today and beyond! + +With three-plus years of experience at Spotify, I’m taking on more and more responsibility by embracing challenges outside my team’s scope, coordinating cross-team collaboration, attending and facilitating courses for personal development and growth, and mentoring my peers. + +Backstage has allowed me to drive my pursuit of growth in a multitude of ways. + +### Goal 1: Stay in the flow and optimize my time management practices. + +At this point, finding time to focus — with all that I have on my plate — while also making time to learn new concepts is both fundamental and challenging for me. + +Spotify encourages its developers to manage their time in order to stay in the flow and to turn to the tools that help them prioritize effectively and focus on their work. + +> **Reflection:**
_“How do you surface the most important notifications and avoid non-emergency distractions?”_ + +Backstage has been instrumental in helping me stay in the flow by surfacing the most critical notifications and call to actions related to cost spikes, security vulnerabilities in my homepage. + +### Goal 2: Platformize our solutions and reuse code to boost my productivity. + +Reinventing the wheel is a common problem in software development and Spotify is no different. + +Autonomous teams tend to prioritize speed over reuse and develop the same solutions in different places (1) as it unblocks the delivery for their team and (2) there is no easy way to discover if a particular functionality has been developed in another part of the company and if it can be reused for their requirement. + +> **Reflection:**
_“Do you find yourself wondering if someone else already did it before starting a task?”_ + +With the [Backstage Search plugin](https://backstage.io/docs/features/search/), I can discover solutions to reuse, find inspiration, and also share what I’ve built for other developers to discover and use. + +### Goal 3: Grow my career and take steps toward the next level. + +I began looking for opportunities to gain new experiences and responsibilities while expanding my knowledge. + +With Spotify’s [Skill Exchange plugin](https://backstage.spotify.com/marketplace/spotify/plugin/skill-exchange/) for Backstage, engineers temporarily embed with a different team, allowing them to partake in work outside their day-to-day. The plugin also helps engineers connect with mentees or mentors that align with their skills profiles. + +> **Reflection:**
_“Are you able to find opportunities that match your current skill set?”_ + +With Backstage, I've applied for an embed to experience working with another team and eventually make a permanent move to this new team, calling it my new home. + +I am passionate about teaching and eager to share my knowledge to help others. Through Skill Exchange, I found a mentee, a colleague transitioning into engineering, and I am helping them take their first steps. + +## What’s next? + +Backstage is highly customizable, and with its extensive plugin ecosystem, engineers can use it strategically to achieve their goals. + +No matter what point in your journey you are, you can find value with [Backstage](http://backstage.io). It was, after all, made by engineers for engineers. 💚 + +Special thanks to [Stanley Nicholl](https://github.com/stanley-nicholl), [Patrik Oldsberg](https://github.com/Rugvip), [Vincenzo Scamporlino](https://github.com/vinzscam), [Mihai Tabara](https://github.com/MihaiTabara), [Emma Indal](https://github.com/emmaindal), [Raghunandan Balachandran](https://github.com/soapraj) for all of their support. diff --git a/microsite/blog/assets/2024-06-27/backstage-engineer-journey.svg b/microsite/blog/assets/2024-06-27/backstage-engineer-journey.svg new file mode 100644 index 0000000000..1e302206a5 --- /dev/null +++ b/microsite/blog/assets/2024-06-27/backstage-engineer-journey.svg @@ -0,0 +1,362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/microsite/data/plugins/azure-storage.yaml b/microsite/data/plugins/azure-storage.yaml index 655a6421ed..6e723c0eeb 100644 --- a/microsite/data/plugins/azure-storage.yaml +++ b/microsite/data/plugins/azure-storage.yaml @@ -4,9 +4,9 @@ author: Deepankumar authorUrl: https://github.com/deepan10 category: Infrastructure description: Explore Azure Storage Blobs in Backstage. -documentation: https://github.com/deepan10/backstage-plugins/tree/main/plugins/azure-storage +documentation: https://github.com/backstage/community-plugins/tree/main/workspaces/azure-storage-explorer/plugins/azure-storage iconUrl: /img/azure-storage-folder.png -npmPackageName: 'backstage-plugin-azure-storage' +npmPackageName: '@backstage-community/plugin-azure-storage-explorer' tags: - Azure - Azure Storage diff --git a/microsite/data/plugins/dora-metrics.yaml b/microsite/data/plugins/dora-metrics.yaml deleted file mode 100644 index 3f59902f46..0000000000 --- a/microsite/data/plugins/dora-metrics.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: DORA Metrics -author: Okay -authorUrl: https://www.okayhq.com -category: Metrics -description: Embed dashboards (like DORA metrics) in your team or service pages from any dev tools (including Github, Gitlab, Jira, Argo, CircleCI, Buildkite, Pagerduty, Rollbar, Sentry, etc). -documentation: https://github.com/OkayHQ/backstage-plugin -iconUrl: /img/okay.png -npmPackageName: '@okayhq/backstage-plugin' -addedDate: '2022-06-16' diff --git a/microsite/data/plugins/harness-chaos.yaml b/microsite/data/plugins/harness-chaos.yaml new file mode 100644 index 0000000000..e089b3d86b --- /dev/null +++ b/microsite/data/plugins/harness-chaos.yaml @@ -0,0 +1,11 @@ +title: Harness Chaos Engineering +author: harness.io +authorUrl: https://github.com/harness/backstage-plugins +category: Chaos Engineering +description: This plugin lets you view the status of Harness Chaos Engineering Resources and launch Chaos Experiments directly inside Backstage. +documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/harness-chaos +iconUrl: https://static.harness.io/ng-static/images/favicon.png +npmPackageName: '@harnessio/backstage-plugin-harness-chaos' +tags: + - resiliency +addedDate: '2024-06-25' diff --git a/microsite/data/plugins/harness-iacm.yaml b/microsite/data/plugins/harness-iacm.yaml new file mode 100644 index 0000000000..4463bab371 --- /dev/null +++ b/microsite/data/plugins/harness-iacm.yaml @@ -0,0 +1,13 @@ +--- +title: Harness Infrastructure as Code Management +author: harness.io +authorUrl: https://github.com/harness/backstage-plugins +category: Infrastructure +description: This plugin provides an overview of all the resources provisioned through information on active workspaces in your account +documentation: https://github.com/harness/backstage-plugins/tree/main/plugins/harness-iacm#harness-iacm-plugin +iconUrl: https://static.harness.io/ng-static/images/favicon.png +npmPackageName: '@harnessio/backstage-plugin-harness-iacm' +tags: + - infrastrucure-as-code + - resource-management +addedDate: '2024-07-03' diff --git a/microsite/data/plugins/infracost.yaml b/microsite/data/plugins/infracost.yaml new file mode 100644 index 0000000000..b6ead655d5 --- /dev/null +++ b/microsite/data/plugins/infracost.yaml @@ -0,0 +1,15 @@ +--- +title: VeeCode Infracost +author: VeeCode Platform +authorUrl: https://platform.vee.codes/ +category: FinOps +description: The Infracost plug-in works in conjunction with terraform and provides a graphical representation and cost estimate data for your entity at your respective provider. +documentation: https://platform.vee.codes/plugin/Infracost +iconUrl: https://veecode-platform.github.io/support/imgs/logo_5.svg +npmPackageName: '@veecode-platform/backstage-plugin-infracost' +tags: + - finops + - infracost + - estimate + - terraform +addedDate: '2024-07-25' diff --git a/microsite/data/plugins/relations.yaml b/microsite/data/plugins/relations.yaml new file mode 100644 index 0000000000..61ce052453 --- /dev/null +++ b/microsite/data/plugins/relations.yaml @@ -0,0 +1,10 @@ +--- +title: Relations +author: dweber019 +authorUrl: https://github.com/dweber019 +category: Catalog +description: This plugin enables you to configure additional relations between entities, which aren't supported out of box. +documentation: https://github.com/dweber019/backstage-plugins/tree/main/plugins/relations +iconUrl: https://raw.githubusercontent.com/dweber019/backstage-plugins/main/plugins/relations-backend/docs/pluginIcon.png +npmPackageName: '@dweber019/backstage-plugin-relations' +addedDate: '2024-05-15' diff --git a/microsite/data/plugins/tech-insights.yaml b/microsite/data/plugins/tech-insights.yaml index fbf96df3e6..3bd40d576a 100644 --- a/microsite/data/plugins/tech-insights.yaml +++ b/microsite/data/plugins/tech-insights.yaml @@ -6,7 +6,7 @@ category: Discovery description: Visualize, understand and optimize your team's tech health. documentation: https://roadie.io/backstage/plugins/tech-insights/ iconUrl: https://roadie.io/images/logos/tech-insights.png -npmPackageName: '@backstage/plugin-tech-insights' +npmPackageName: '@backstage-community/plugin-tech-insights' tags: - tech-insights - reporting diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index b1a6533ab9..67f6de4962 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -241,7 +241,7 @@ const config: Config = { position: 'left', }, { - to: 'docs/releases/v1.28.0', + to: 'docs/releases/v1.29.0', label: 'Releases', position: 'left', }, diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 075025afbb..ddccb14c91 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -1,6 +1,7 @@ { "releases": { "Release Notes": [ + "releases/v1.29.0", "releases/v1.28.0", "releases/v1.27.0", "releases/v1.26.0", @@ -120,7 +121,8 @@ "features/software-templates/writing-custom-step-layouts", "features/software-templates/authorizing-scaffolder-template-details", "features/software-templates/migrating-to-rjsf-v5", - "features/software-templates/migrating-from-v1beta2-to-v1beta3" + "features/software-templates/migrating-from-v1beta2-to-v1beta3", + "features/software-templates/dry-run-testing" ] }, { @@ -295,6 +297,7 @@ "conf/writing", "conf/defining" ], + "Notifications": ["notifications/index"], "Auth and identity": [ "auth/index", { @@ -392,6 +395,7 @@ "backend-system/architecture/plugins", "backend-system/architecture/extension-points", "backend-system/architecture/modules", + "backend-system/architecture/feature-loaders", "backend-system/architecture/naming-patterns" ] }, @@ -429,6 +433,7 @@ "backend-system/core-services/permissions", "backend-system/core-services/plugin-metadata", "backend-system/core-services/root-config", + "backend-system/core-services/root-health", "backend-system/core-services/root-http-router", "backend-system/core-services/root-lifecycle", "backend-system/core-services/root-logger", diff --git a/microsite/src/pages/community/index.tsx b/microsite/src/pages/community/index.tsx index c331638c78..a32f5edd13 100644 --- a/microsite/src/pages/community/index.tsx +++ b/microsite/src/pages/community/index.tsx @@ -96,6 +96,11 @@ const Community = () => { url: 'https://tanzu.vmware.com/developer-portal', logo: 'img/partner-logo-tanzubybroadcom.png', }, + { + name: 'StatusNeo', + url: 'https://statusneo.com/backstage', + logo: 'img/partner-logo-statusneo.png', + }, ]; //#endregion diff --git a/microsite/static/img/okay.png b/microsite/static/img/okay.png deleted file mode 100644 index 357219d9d2..0000000000 Binary files a/microsite/static/img/okay.png and /dev/null differ diff --git a/microsite/static/img/partner-logo-statusneo.png b/microsite/static/img/partner-logo-statusneo.png new file mode 100644 index 0000000000..61a676a220 Binary files /dev/null and b/microsite/static/img/partner-logo-statusneo.png differ diff --git a/mkdocs.yml b/mkdocs.yml index b9f6d41d46..3b0c7d9caf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,7 +20,7 @@ nav: - Support and community: 'overview/support.md' - Logo assets: 'overview/logos.md' - Getting Started: - - Installing a standalone server: 'getting-started/index.md' + - Creating your Backstage App: 'getting-started/index.md' - Configuring Backstage: - Database: 'getting-started/config/database.md' - Authentication: 'getting-started/config/authentication.md' @@ -64,6 +64,7 @@ nav: - Writing Custom Actions: 'features/software-templates/writing-custom-actions.md' - Writing Custom Step Layouts: 'features/software-templates/writing-custom-step-layouts.md' - Migrating from v1beta2 to v1beta3 templates: 'features/software-templates/migrating-from-v1beta2-to-v1beta3.md' + - Dry Run Testing: 'features/software-templates/dry-run-testing.md' - Backstage Search: - Overview: 'features/search/README.md' - Getting Started: 'features/search/getting-started.md' @@ -158,7 +159,7 @@ nav: - Azure EasyAuth: 'auth/microsoft/azure-easyauth.md' - Bitbucket: 'auth/bitbucket/provider.md' - Bitbucket Server: 'auth/bitbucketServer/provider.md' - - Cloudflare Access: 'auth/cloudflare/access.md' + - Cloudflare Access: 'auth/cloudflare/provider.md' - GitHub: 'auth/github/provider.md' - GitLab: 'auth/gitlab/provider.md' - Google: 'auth/google/provider.md' diff --git a/package.json b/package.json index 59150dedc9..d9b384fb7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "root", - "version": "1.28.0", + "version": "1.30.0-next.3", "private": true, "repository": { "type": "git", diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index dc43bdf646..410841fec9 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/app-defaults +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 1.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 1.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-permission-react@0.4.24 + +## 1.5.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 1.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 1.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-permission-react@0.4.23 + +## 1.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-permission-react@0.4.23 + ## 1.5.6 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index f7ed1ab5ea..284e832ae5 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/app-defaults", - "version": "1.5.6", + "version": "1.5.10-next.2", "description": "Provides the default wiring of a Backstage App", "backstage": { "role": "web-library" diff --git a/packages/app-next-example-plugin/CHANGELOG.md b/packages/app-next-example-plugin/CHANGELOG.md index 9b9603769f..62a7b18df0 100644 --- a/packages/app-next-example-plugin/CHANGELOG.md +++ b/packages/app-next-example-plugin/CHANGELOG.md @@ -1,5 +1,53 @@ # app-next-example-plugin +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + +## 0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-components@0.14.10-next.0 + +## 0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/frontend-plugin-api@0.6.7 + +## 0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## 0.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + ## 0.0.12 ### Patch Changes diff --git a/packages/app-next-example-plugin/api-report.md b/packages/app-next-example-plugin/api-report.md index c89a7b599b..962b1473a8 100644 --- a/packages/app-next-example-plugin/api-report.md +++ b/packages/app-next-example-plugin/api-report.md @@ -7,7 +7,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { default as React_2 } from 'react'; // @public (undocumented) -const examplePlugin: BackstagePlugin<{}, {}>; +const examplePlugin: BackstagePlugin<{}, {}, {}>; export default examplePlugin; // @public (undocumented) diff --git a/packages/app-next-example-plugin/package.json b/packages/app-next-example-plugin/package.json index c806e32620..ad179ebbe5 100644 --- a/packages/app-next-example-plugin/package.json +++ b/packages/app-next-example-plugin/package.json @@ -1,6 +1,6 @@ { "name": "app-next-example-plugin", - "version": "0.0.12", + "version": "0.0.14-next.2", "description": "Backstage internal example plugin", "backstage": { "role": "frontend-plugin", diff --git a/packages/app-next/CHANGELOG.md b/packages/app-next/CHANGELOG.md index 9f1eb9d444..6382efa811 100644 --- a/packages/app-next/CHANGELOG.md +++ b/packages/app-next/CHANGELOG.md @@ -1,5 +1,341 @@ # example-app-next +## 0.0.14-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-app-visualizer@0.1.9-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.2 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-app-visualizer@0.1.9-next.1 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.11-next.1 + +## 0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/cli@0.27.0-next.1 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-user-settings@0.8.11-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-catalog-graph@0.4.8-next.1 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-app-visualizer@0.1.9-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.0 + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/plugin-home@0.7.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/plugin-scaffolder-react@1.11.0-next.0 + - @backstage/frontend-app-api@0.7.4-next.0 + - @backstage/plugin-catalog-graph@0.4.8-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.0 + - @backstage/plugin-app-visualizer@0.1.9-next.0 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-catalog-import@0.12.2-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes@0.11.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.15-next.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.10-next.0 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.23.0 + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-app-api@0.7.3 + - @backstage/plugin-org@0.6.27 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-notifications@0.2.3 + - @backstage/plugin-home@0.7.7 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.14 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/app-defaults@1.5.8 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-api-docs@0.11.7 + - @backstage/plugin-app-visualizer@0.1.8 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-catalog-graph@0.4.7 + - @backstage/plugin-catalog-import@0.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6 + - @backstage/plugin-kubernetes@0.11.12 + - @backstage/plugin-kubernetes-cluster@0.0.13 + - @backstage/plugin-search-react@1.7.13 + - @backstage/plugin-signals@0.0.8 + - @backstage/plugin-user-settings@0.8.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-app-visualizer@0.1.8-next.1 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-org@0.6.27-next.1 + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/plugin-notifications@0.2.3-next.1 + - @backstage/plugin-home@0.7.7-next.1 + - @backstage/plugin-scaffolder-react@1.10.0-next.1 + - @backstage/plugin-scaffolder@1.22.1-next.1 + - @backstage/cli@0.26.11-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.3-next.1 + - @backstage/plugin-api-docs@0.11.7-next.1 + - @backstage/plugin-catalog-graph@0.4.7-next.1 + - @backstage/plugin-catalog-import@0.12.1-next.1 + - @backstage/plugin-search@1.4.13-next.1 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.9-next.1 + - @backstage/plugin-kubernetes@0.11.12-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-app-visualizer@0.1.8-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## 0.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.22.0-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/frontend-app-api@0.7.2-next.0 + - @backstage/cli@0.26.10-next.0 + - @backstage/plugin-org@0.6.27-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/plugin-scaffolder-react@1.10.0-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.0 + - @backstage/plugin-app-visualizer@0.1.8-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-graph@0.4.7-next.0 + - @backstage/plugin-catalog-import@0.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-home@0.7.6-next.0 + - @backstage/plugin-kubernetes@0.11.12-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.0 + - @backstage/plugin-notifications@0.2.3-next.0 + - @backstage/plugin-search@1.4.13-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.8-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + ## 0.0.12 ### Patch Changes diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 271040ecec..f6d0174ca1 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -43,6 +43,8 @@ app: - entity-card:org/ownership - entity-card:org/user-profile + - entity-content:kubernetes/kubernetes + # scmAuthExtension: >- # createScmAuthExtension({ # id: 'apis.scmAuth.addons.ghe', diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 1959f9430c..a12371a135 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -1,6 +1,6 @@ { "name": "example-app-next", - "version": "0.0.12", + "version": "0.0.14-next.3", "private": true, "repository": { "type": "git", diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index c911a01258..b57c680474 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -36,18 +36,14 @@ import { convertLegacyApp } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; import { Route } from 'react-router'; import { CatalogImportPage } from '@backstage/plugin-catalog-import'; -import { - createApiFactory, - configApiRef, - SignInPageProps, -} from '@backstage/core-plugin-api'; +import { createApiFactory, configApiRef } from '@backstage/core-plugin-api'; import { ScmAuth, ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { createSignInPageExtension } from '@backstage/frontend-plugin-api'; -import { SignInPage } from '@backstage/core-components'; +import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha'; +import { signInPageOverrides } from './overrides/SignInPage'; /* @@ -90,12 +86,6 @@ const homePageExtension = createExtension({ }, }); -const signInPage = createSignInPageExtension({ - name: 'guest', - loader: async () => (props: SignInPageProps) => - , -}); - const scmAuthExtension = createApiExtension({ factory: ScmAuth.createDefaultApiFactory(), }); @@ -121,13 +111,14 @@ const app = createApp({ userSettingsPlugin, homePlugin, appVisualizerPlugin, + kubernetesPlugin, + signInPageOverrides, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ homePageExtension, scmAuthExtension, scmIntegrationApi, - signInPage, notFoundErrorPage, ], }), diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 826040a6cb..095c992e18 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -47,9 +47,11 @@ const IndexPage = createPageExtension({ return (
op -
- Page 1 -
+ {page1Link && ( +
+ Page 1 +
+ )}
Home
@@ -82,10 +84,10 @@ const Page1 = createPageExtension({ return (

This is page 1

- Go back + {indexLink && Go back} Page 2 {/* Page 2 */} - Page X + {xLink && Page X}
Sub-page content: @@ -115,7 +117,7 @@ const ExternalPage = createPageExtension({ return (

This is page X

- Go back + {indexLink && Go back}
); }; diff --git a/packages/app-next/src/index-public-experimental.tsx b/packages/app-next/src/index-public-experimental.tsx new file mode 100644 index 0000000000..56bc698ffd --- /dev/null +++ b/packages/app-next/src/index-public-experimental.tsx @@ -0,0 +1,49 @@ +/* + * 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 React from 'react'; +import ReactDOM from 'react-dom/client'; +import { CookieAuthRedirect } from '@backstage/plugin-auth-react'; +import { createApp } from '@backstage/frontend-app-api'; +import { signInPageOverrides } from './overrides/SignInPage'; +import { + coreExtensionData, + createExtension, + createExtensionOverrides, +} from '@backstage/frontend-plugin-api'; + +const authRedirectExtension = createExtension({ + namespace: 'app', + name: 'layout', + attachTo: { id: 'app/root', input: 'children' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ + element: , + }), +}); + +const app = createApp({ + features: [ + signInPageOverrides, + createExtensionOverrides({ + extensions: [authRedirectExtension], + }), + ], +}); + +ReactDOM.createRoot(document.getElementById('root')!).render(app.createRoot()); diff --git a/packages/app-next/src/overrides/SignInPage.tsx b/packages/app-next/src/overrides/SignInPage.tsx new file mode 100644 index 0000000000..364a749293 --- /dev/null +++ b/packages/app-next/src/overrides/SignInPage.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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 React from 'react'; +import { SignInPage } from '@backstage/core-components'; +import { + createExtensionOverrides, + createSignInPageExtension, +} from '@backstage/frontend-plugin-api'; + +const signInPage = createSignInPageExtension({ + name: 'guest', + loader: async () => props => , +}); + +export const signInPageOverrides = createExtensionOverrides({ + extensions: [signInPage], +}); diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 3088708cf2..88e1134800 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,325 @@ # example-app +## 0.2.100-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.11.0-next.2 + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-search@1.4.15-next.2 + - @backstage/plugin-api-docs@0.11.8-next.2 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/plugin-scaffolder@1.24.0-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.3 + - @backstage/plugin-catalog-import@0.12.2-next.2 + - @backstage/plugin-org@0.6.28-next.2 + - @backstage/plugin-user-settings@0.8.11-next.2 + - @backstage/plugin-devtools@0.1.17-next.2 + - @backstage/plugin-home@0.7.9-next.2 + - @backstage/plugin-kubernetes@0.11.13-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.2 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.2.100-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.2 + - @backstage/plugin-catalog-graph@0.4.8-next.2 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-devtools@0.1.17-next.1 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.11-next.1 + +## 0.2.100-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/plugin-kubernetes@0.11.13-next.1 + - @backstage/cli@0.27.0-next.1 + - @backstage/plugin-scaffolder@1.23.1-next.1 + - @backstage/plugin-user-settings@0.8.11-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-search@1.4.15-next.1 + - @backstage/plugin-api-docs@0.11.8-next.1 + - @backstage/plugin-catalog-graph@0.4.8-next.1 + - @backstage/plugin-catalog-import@0.12.2-next.1 + - @backstage/plugin-home@0.7.9-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.1 + - @backstage/plugin-org@0.6.28-next.1 + - @backstage/plugin-scaffolder-react@1.11.0-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/plugin-devtools@0.1.17-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.2.100-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/plugin-home@0.7.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/plugin-scaffolder-react@1.11.0-next.0 + - @backstage/frontend-app-api@0.7.4-next.0 + - @backstage/plugin-catalog-graph@0.4.8-next.0 + - @backstage/plugin-scaffolder@1.23.1-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/plugin-auth-react@0.1.5-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-api-docs@0.11.8-next.0 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-catalog-import@0.12.2-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.7-next.0 + - @backstage/plugin-devtools@0.1.17-next.0 + - @backstage/plugin-kubernetes@0.11.13-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.14-next.0 + - @backstage/plugin-notifications@0.2.4-next.0 + - @backstage/plugin-org@0.6.28-next.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.15-next.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/plugin-signals@0.0.9-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.13-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + - @backstage/plugin-user-settings@0.8.10-next.0 + +## 0.2.99 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.23.0 + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-app-api@0.7.3 + - @backstage/plugin-org@0.6.27 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-notifications@0.2.3 + - @backstage/plugin-home@0.7.7 + - @backstage/plugin-scaffolder-react@1.10.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-search@1.4.14 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-search-common@1.2.13 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-api-docs@0.11.7 + - @backstage/plugin-auth-react@0.1.4 + - @backstage/plugin-catalog-graph@0.4.7 + - @backstage/plugin-catalog-import@0.12.1 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6 + - @backstage/plugin-devtools@0.1.16 + - @backstage/plugin-kubernetes@0.11.12 + - @backstage/plugin-kubernetes-cluster@0.0.13 + - @backstage/plugin-search-react@1.7.13 + - @backstage/plugin-signals@0.0.8 + - @backstage/plugin-user-settings@0.8.9 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-search@1.4.14-next.2 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.2 + - @backstage/plugin-auth-react@0.1.4-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-graph@0.4.7-next.2 + - @backstage/plugin-catalog-import@0.12.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.1 + - @backstage/plugin-devtools@0.1.16-next.1 + - @backstage/plugin-home@0.7.7-next.2 + - @backstage/plugin-kubernetes@0.11.12-next.2 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.2 + - @backstage/plugin-notifications@0.2.3-next.2 + - @backstage/plugin-org@0.6.27-next.2 + - @backstage/plugin-scaffolder@1.22.1-next.2 + - @backstage/plugin-scaffolder-react@1.10.0-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/plugin-signals@0.0.8-next.1 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.1 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + - @backstage/plugin-user-settings@0.8.9-next.2 + +## 0.2.99-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-org@0.6.27-next.1 + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/plugin-notifications@0.2.3-next.1 + - @backstage/plugin-home@0.7.7-next.1 + - @backstage/plugin-scaffolder-react@1.10.0-next.1 + - @backstage/plugin-scaffolder@1.22.1-next.1 + - @backstage/cli@0.26.11-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-app-api@0.7.3-next.1 + - @backstage/plugin-api-docs@0.11.7-next.1 + - @backstage/plugin-catalog-graph@0.4.7-next.1 + - @backstage/plugin-catalog-import@0.12.1-next.1 + - @backstage/plugin-search@1.4.13-next.1 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.9-next.1 + - @backstage/plugin-kubernetes@0.11.12-next.1 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-devtools@0.1.16-next.0 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## 0.2.99-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder@1.22.0-next.0 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/frontend-app-api@0.7.2-next.0 + - @backstage/cli@0.26.10-next.0 + - @backstage/plugin-org@0.6.27-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/plugin-scaffolder-react@1.10.0-next.0 + - @backstage/plugin-techdocs-module-addons-contrib@1.1.12-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-api-docs@0.11.7-next.0 + - @backstage/plugin-auth-react@0.1.4-next.0 + - @backstage/plugin-catalog-graph@0.4.7-next.0 + - @backstage/plugin-catalog-import@0.12.1-next.0 + - @backstage/plugin-catalog-unprocessed-entities@0.2.6-next.0 + - @backstage/plugin-devtools@0.1.16-next.0 + - @backstage/plugin-home@0.7.6-next.0 + - @backstage/plugin-kubernetes@0.11.12-next.0 + - @backstage/plugin-kubernetes-cluster@0.0.13-next.0 + - @backstage/plugin-notifications@0.2.3-next.0 + - @backstage/plugin-search@1.4.13-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/plugin-signals@0.0.8-next.0 + - @backstage/plugin-user-settings@0.8.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-search-common@1.2.12 + ## 0.2.98 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 6bbdd1f9ec..c4c0c18391 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.98", + "version": "0.2.100-next.3", "backstage": { "role": "frontend" }, diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md index a7804fc74e..c7c8883302 100644 --- a/packages/backend-app-api/CHANGELOG.md +++ b/packages/backend-app-api/CHANGELOG.md @@ -1,5 +1,124 @@ # @backstage/backend-app-api +## 0.8.1-next.2 + +### Patch Changes + +- 8b13183: Added support for the latest version of `BackendFeature`s from `@backstage/backend-plugin-api`, including feature loaders. +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 7c5f3b0: Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref. +- 80a0737: Added configuration for the `packages` options to config schema +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.8.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 0.8.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. +- f691c9b: **BREAKING**: Removed the ability to pass callback-form service factories through the `defaultServiceFactories` option of `createSpecializedBackend`. This is an immediate breaking change as usage of this function is expected to be very rare. + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 617a7d2: Internal refactor that avoids the use of service factory options. +- b60db08: Fixing exporting of classes properly from new packages +- 18b96b1: The ability to install backend features in callback form (`() => BackendFeature`) has been deprecated. This typically means that you need to update the installed features to use the latest version of `@backstage/backend-plugin-api`. If the feature is from a third-party package, please reach out to the package maintainer to update it. +- a63c4b6: Fixing issue with `MiddlewareFactory` deprecation wrapping +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.7.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 0.7.9-next.0 + +### Patch Changes + +- b60db08: Fixing exporting of classes properly from new packages +- a63c4b6: Fixing issue with `MiddlewareFactory` deprecation wrapping +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.6 ### Patch Changes diff --git a/packages/backend-app-api/api-report-alpha.md b/packages/backend-app-api/api-report-alpha.md index 488feedf2c..8616eec29a 100644 --- a/packages/backend-app-api/api-report-alpha.md +++ b/packages/backend-app-api/api-report-alpha.md @@ -4,12 +4,14 @@ ```ts import { FeatureDiscoveryService } from '@backstage/backend-plugin-api/alpha'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @alpha (undocumented) -export const featureDiscoveryServiceFactory: () => ServiceFactory< +export const featureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, - 'root' + 'root', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 7518f8e2cc..dea309ebb0 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -39,14 +39,19 @@ import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import type { Server } from 'node:http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; -import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserInfoService } from '@backstage/backend-plugin-api'; // @public @deprecated (undocumented) -export const authServiceFactory: () => ServiceFactory; +export const authServiceFactory: ServiceFactoryCompat< + AuthService, + 'plugin', + 'singleton', + undefined +>; // @public (undocumented) export interface Backend { @@ -54,9 +59,16 @@ export interface Backend { add( feature: | BackendFeature + | Promise<{ + default: BackendFeature; + }>, + ): void; + // @deprecated (undocumented) + add( + feature: | (() => BackendFeature) | Promise<{ - default: BackendFeature | (() => BackendFeature); + default: () => BackendFeature; }>, ): void; // (undocumented) @@ -66,7 +78,12 @@ export interface Backend { } // @public @deprecated (undocumented) -export const cacheServiceFactory: () => ServiceFactory; +export const cacheServiceFactory: ServiceFactoryCompat< + CacheService, + 'plugin', + 'singleton', + undefined +>; // Warning: (ae-forgotten-export) The symbol "createConfigSecretEnumerator_2" needs to be exported by the entry point index.d.ts // @@ -91,13 +108,15 @@ export function createSpecializedBackend( // @public (undocumented) export interface CreateSpecializedBackendOptions { // (undocumented) - defaultServiceFactories: ServiceFactoryOrFunction[]; + defaultServiceFactories: ServiceFactory[]; } // @public @deprecated (undocumented) -export const databaseServiceFactory: () => ServiceFactory< +export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated @@ -116,9 +135,11 @@ export class DefaultRootHttpRouter implements RootHttpRouterService { export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; // @public @deprecated (undocumented) -export const discoveryServiceFactory: () => ServiceFactory< +export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // Warning: (ae-forgotten-export) The symbol "ExtendedHttpServer_2" needs to be exported by the entry point index.d.ts @@ -141,20 +162,20 @@ export class HostDiscovery implements DiscoveryService { } // @public @deprecated (undocumented) -export const httpAuthServiceFactory: () => ServiceFactory< +export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, - 'plugin' + 'plugin', + 'singleton', + undefined >; -// Warning: (ae-forgotten-export) The symbol "HttpRouterFactoryOptions_2" needs to be exported by the entry point index.d.ts -// -// @public @deprecated (undocumented) -export type HttpRouterFactoryOptions = HttpRouterFactoryOptions_2; - // @public @deprecated -export const httpRouterServiceFactory: ( - options?: HttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory; +export const httpRouterServiceFactory: ServiceFactoryCompat< + HttpRouterService, + 'plugin', + 'singleton', + undefined +>; // Warning: (ae-forgotten-export) The symbol "HttpServerCertificateOptions_2" needs to be exported by the entry point index.d.ts // @@ -173,9 +194,12 @@ export type IdentityFactoryOptions = { }; // @public @deprecated (undocumented) -export const identityServiceFactory: ( - options?: IdentityFactoryOptions | undefined, -) => ServiceFactory; +export const identityServiceFactory: ServiceFactoryCompat< + IdentityService, + 'plugin', + 'singleton', + IdentityFactoryOptions +>; // Warning: (ae-forgotten-export) The symbol "LifecycleMiddlewareOptions_2" needs to be exported by the entry point index.d.ts // @@ -183,9 +207,11 @@ export const identityServiceFactory: ( export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; // @public @deprecated -export const lifecycleServiceFactory: () => ServiceFactory< +export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated @@ -199,9 +225,11 @@ export function loadBackendConfig(options: { }>; // @public @deprecated -export const loggerServiceFactory: () => ServiceFactory< +export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated (undocumented) @@ -226,9 +254,11 @@ export type MiddlewareFactoryErrorOptions = MiddlewareFactoryErrorOptions_2; export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; // @public @deprecated (undocumented) -export const permissionsServiceFactory: () => ServiceFactory< +export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // Warning: (ae-forgotten-export) The symbol "readCorsOptions_2" needs to be exported by the entry point index.d.ts @@ -255,9 +285,12 @@ export interface RootConfigFactoryOptions { } // @public @deprecated (undocumented) -export const rootConfigServiceFactory: ( - options?: RootConfigFactoryOptions | undefined, -) => ServiceFactory; +export const rootConfigServiceFactory: ServiceFactoryCompat< + RootConfigService, + 'root', + 'singleton', + RootConfigFactoryOptions +>; // Warning: (ae-forgotten-export) The symbol "RootHttpRouterConfigureContext_2" needs to be exported by the entry point index.d.ts // @@ -270,44 +303,57 @@ export type RootHttpRouterConfigureContext = RootHttpRouterConfigureContext_2; export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) -export const rootHttpRouterServiceFactory: ( +export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // @public @deprecated -export const rootLifecycleServiceFactory: () => ServiceFactory< +export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, - 'root' + 'root', + 'singleton', + undefined >; // @public @deprecated -export const rootLoggerServiceFactory: () => ServiceFactory< +export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, - 'root' + 'root', + 'singleton', + undefined >; // @public @deprecated (undocumented) -export const schedulerServiceFactory: () => ServiceFactory< +export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated (undocumented) -export const tokenManagerServiceFactory: () => ServiceFactory< +export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated (undocumented) -export const urlReaderServiceFactory: () => ServiceFactory< +export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated (undocumented) -export const userInfoServiceFactory: () => ServiceFactory< +export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index a23bca447b..e94da62b77 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -287,6 +287,7 @@ export interface Config { } >; }; + packages?: 'all' | { include?: string[]; exclude?: string[] }; }; /** Discovery options. */ diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 0c1c23d23d..e809d1c2f0 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-app-api", - "version": "0.7.6", + "version": "0.8.1-next.2", "description": "Core API used by Backstage backend apps", "backstage": { "role": "node-library" @@ -79,7 +79,7 @@ "minimatch": "^9.0.0", "minimist": "^1.2.5", "morgan": "^1.10.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "path-to-regexp": "^6.2.1", "selfsigned": "^2.0.0", diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 9d2b74fb43..0088e53f98 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -15,16 +15,7 @@ */ // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { - httpRouterServiceFactory as _httpRouterServiceFactory, - type HttpRouterFactoryOptions as _HttpRouterFactoryOptions, -} from '../../../../../backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory'; - -/** - * @public - * @deprecated Please import from `@backstage/backend-defaults/httpRouter` instead. - */ -export type HttpRouterFactoryOptions = _HttpRouterFactoryOptions; +import { httpRouterServiceFactory as _httpRouterServiceFactory } from '../../../../../backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory'; /** * HTTP route registration for plugins. diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts index 928a232131..bc12480ae8 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts @@ -15,6 +15,5 @@ */ export { httpRouterServiceFactory } from './httpRouterServiceFactory'; -export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory'; export { createLifecycleMiddleware } from './createLifecycleMiddleware'; export type { LifecycleMiddlewareOptions } from './createLifecycleMiddleware'; diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts index 7b889b3a0e..b16509cf96 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts @@ -22,7 +22,7 @@ describe('schedulerFactory', () => { it('creates sidecar database features', async () => { const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - const scheduler = await tester.get(); + const scheduler = await tester.getSubject(); await scheduler.scheduleTask({ id: 'task1', timeout: { seconds: 1 }, diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts index f603c6295e..dc52130ec6 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts @@ -21,7 +21,7 @@ describe('tokenManagerFactory', () => { it('should create a disabled manager without configuration', async () => { const tokenManager = await ServiceFactoryTester.from( tokenManagerServiceFactory, - ).get(); + ).getSubject(); await expect(tokenManager.authenticate('abc')).rejects.toThrow( 'no legacy keys are configured', diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index 3bbef5a4c0..cf8845fcec 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -24,6 +24,7 @@ import { createBackendPlugin, createBackendModule, createExtensionPoint, + createBackendFeatureLoader, } from '@backstage/backend-plugin-api'; import { BackendInitializer } from './BackendInitializer'; @@ -105,6 +106,128 @@ describe('BackendInitializer', () => { expect(factory3).not.toHaveBeenCalled(); }); + it('should discover features from feature loader', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + scope: 'plugin', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + + const pluginInit = jest.fn(async () => {}); + const moduleInit = jest.fn(async () => {}); + + const init = new BackendInitializer(baseFactories); + init.add( + createBackendFeatureLoader({ + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: factory1, + }); + yield createServiceFactory({ + service: ref2, + initialization: 'always', + deps: {}, + factory: factory2, + }); + yield createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + init: pluginInit, + }); + }, + }); + yield createBackendModule({ + pluginId: 'test', + moduleId: 'tester', + register(reg) { + reg.registerInit({ + deps: {}, + init: moduleInit, + }); + }, + }); + }, + }), + ); + await init.start(); + + expect(factory1).toHaveBeenCalled(); + expect(factory2).toHaveBeenCalled(); + expect(pluginInit).toHaveBeenCalled(); + expect(moduleInit).toHaveBeenCalled(); + }); + + it('should refuse to override already initialized services through loaded features', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref1, + deps: {}, + factory: () => ({ x: 1 }), + }), + ]); + init.add( + createBackendFeatureLoader({ + deps: { service1: ref1 }, + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: jest.fn(), + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + 'Unable to set service factory with id 1, service has already been instantiated', + ); + }); + + it('should refuse feature loader that depends on a plugin scoped service', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + }); + + const init = new BackendInitializer([ + ...baseFactories, + createServiceFactory({ + service: ref1, + deps: {}, + factory: () => ({ x: 1 }), + }), + ]); + init.add( + createBackendFeatureLoader({ + // @ts-expect-error + deps: { service1: ref1 }, + *loader() { + yield createServiceFactory({ + service: ref1, + deps: {}, + factory: jest.fn(), + }); + }, + }), + ); + await expect(init.start()).rejects.toThrow( + /^Feature loaders can only depend on root scoped services, but 'service1' is scoped to 'plugin'. Offending loader is created at '.*'$/, + ); + }); + it('should initialize plugin scoped services with eager initialization', async () => { const ref1 = createServiceRef<{ x: number }>({ id: '1', @@ -219,6 +342,62 @@ describe('BackendInitializer', () => { await init.start(); }); + it('should allow plugins and modules depend on multiton services', async () => { + expect.assertions(2); + + const multiServiceRef = createServiceRef({ + id: 'a', + multiton: true, + }); + const init = new BackendInitializer(baseFactories); + + init.add( + createServiceFactory({ + service: multiServiceRef, + deps: {}, + factory: () => 'x', + }), + ); + init.add( + createServiceFactory({ + service: multiServiceRef, + deps: {}, + factory: () => 'y', + }), + ); + + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: { multi: multiServiceRef }, + async init({ multi }) { + expect(multi).toEqual(['x', 'y']); + }, + }); + }, + }), + ); + + init.add( + createBackendModule({ + pluginId: 'test', + moduleId: 'test', + register(reg) { + reg.registerInit({ + deps: { multi: multiServiceRef }, + async init({ multi }) { + expect(multi).toEqual(['x', 'y']); + }, + }); + }, + }), + ); + + await init.start(); + }); + it('should forward errors when plugins fail to start', async () => { const init = new BackendInitializer([]); init.add( diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ddad94de14..c0da9eda62 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -25,8 +25,14 @@ import { } from '@backstage/backend-plugin-api'; import { ServiceOrExtensionPoint } from './types'; // Direct internal import to avoid duplication -// eslint-disable-next-line @backstage/no-forbidden-package-imports -import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import type { + InternalBackendFeature, + InternalBackendFeatureLoader, + InternalBackendRegistrations, +} from '../../../backend-plugin-api/src/wiring/types'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import type { InternalServiceFactory } from '../../../backend-plugin-api/src/services/system/types'; import { ForwardedError, ConflictError } from '@backstage/errors'; import { featureDiscoveryServiceRef } from '@backstage/backend-plugin-api/alpha'; import { DependencyGraph } from '../lib/DependencyGraph'; @@ -44,10 +50,11 @@ export interface BackendRegisterInit { export class BackendInitializer { #startPromise?: Promise; - #features = new Array(); + #registrations = new Array(); #extensionPoints = new Map(); #serviceRegistry: ServiceRegistry; #registeredFeatures = new Array>(); + #registeredFeatureLoaders = new Array(); constructor(defaultApiFactories: ServiceFactory[]) { this.#serviceRegistry = ServiceRegistry.create([...defaultApiFactories]); @@ -101,21 +108,12 @@ export class BackendInitializer { } #addFeature(feature: BackendFeature) { - if (feature.$$type !== '@backstage/BackendFeature') { - throw new Error( - `Failed to add feature, invalid type '${feature.$$type}'`, - ); - } - if (isServiceFactory(feature)) { this.#serviceRegistry.add(feature); - } else if (isInternalBackendFeature(feature)) { - if (feature.version !== 'v1') { - throw new Error( - `Failed to add feature, invalid version '${feature.version}'`, - ); - } - this.#features.push(feature); + } else if (isBackendFeatureLoader(feature)) { + this.#registeredFeatureLoaders.push(feature); + } else if (isBackendRegistrations(feature)) { + this.#registrations.push(feature); } else { throw new Error( `Failed to add feature, invalid feature ${JSON.stringify(feature)}`, @@ -170,14 +168,16 @@ export class BackendInitializer { this.#serviceRegistry.checkForCircularDeps(); } + await this.#applyBackendFeatureLoaders(this.#registeredFeatureLoaders); + // Initialize all root scoped services await this.#serviceRegistry.initializeEagerServicesWithScope('root'); const pluginInits = new Map(); const moduleInits = new Map>(); - // Enumerate all features - for (const feature of this.#features) { + // Enumerate all registrations + for (const feature of this.#registrations) { for (const r of feature.getRegistrations()) { const provides = new Set>(); @@ -205,7 +205,7 @@ export class BackendInitializer { consumes: new Set(Object.values(r.init.deps)), init: r.init, }); - } else { + } else if (r.type === 'module') { let modules = moduleInits.get(r.pluginId); if (!modules) { modules = new Map(); @@ -221,6 +221,8 @@ export class BackendInitializer { consumes: new Set(Object.values(r.init.deps)), init: r.init, }); + } else { + throw new Error(`Invalid registration type '${(r as any).type}'`); } } } @@ -383,16 +385,109 @@ export class BackendInitializer { throw new Error('Unexpected plugin lifecycle service implementation'); } + + async #applyBackendFeatureLoaders(loaders: InternalBackendFeatureLoader[]) { + for (const loader of loaders) { + const deps = new Map(); + const missingRefs = new Set(); + + for (const [name, ref] of Object.entries(loader.deps ?? {})) { + if (ref.scope !== 'root') { + throw new Error( + `Feature loaders can only depend on root scoped services, but '${name}' is scoped to '${ref.scope}'. Offending loader is ${loader.description}`, + ); + } + const impl = await this.#serviceRegistry.get( + ref as ServiceRef, + 'root', + ); + if (impl) { + deps.set(name, impl); + } else { + missingRefs.add(ref); + } + } + + if (missingRefs.size > 0) { + const missing = Array.from(missingRefs).join(', '); + throw new Error( + `No service available for the following ref(s): ${missing}, depended on by feature loader ${loader.description}`, + ); + } + + const result = await loader + .loader(Object.fromEntries(deps)) + .catch(error => { + throw new ForwardedError( + `Feature loader ${loader.description} failed`, + error, + ); + }); + + let didAddServiceFactory = false; + const newLoaders = new Array(); + + for await (const feature of result) { + if (isBackendFeatureLoader(feature)) { + newLoaders.push(feature); + } else { + didAddServiceFactory ||= isServiceFactory(feature); + this.#addFeature(feature); + } + } + + // Every time we add a new service factory we need to make sure that we don't have circular dependencies + if (didAddServiceFactory) { + this.#serviceRegistry.checkForCircularDeps(); + } + + // Apply loaders recursively, depth-first + if (newLoaders.length > 0) { + await this.#applyBackendFeatureLoaders(newLoaders); + } + } + } } -function isServiceFactory(feature: BackendFeature): feature is ServiceFactory { - return !!(feature as ServiceFactory).service; -} - -function isInternalBackendFeature( +function toInternalBackendFeature( feature: BackendFeature, -): feature is InternalBackendFeature { - return ( - typeof (feature as InternalBackendFeature).getRegistrations === 'function' - ); +): InternalBackendFeature { + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalBackendFeature; + if (internal.version !== 'v1') { + throw new Error( + `Invalid BackendFeature, bad version '${internal.version}'`, + ); + } + return internal; +} + +function isServiceFactory( + feature: BackendFeature, +): feature is InternalServiceFactory { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'service') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'service' in internal; +} + +function isBackendRegistrations( + feature: BackendFeature, +): feature is InternalBackendRegistrations { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'registrations') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'getRegistrations' in internal; +} + +function isBackendFeatureLoader( + feature: BackendFeature, +): feature is InternalBackendFeatureLoader { + return toInternalBackendFeature(feature).featureType === 'loader'; } diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 6e20dd98e8..0abd5605ae 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -48,22 +48,34 @@ function toInternalServiceFactory( return f; } -const pluginMetadataServiceFactory = createServiceFactory( - (options?: { pluginId: string }) => ({ +function createPluginMetadataServiceFactory(pluginId: string) { + return createServiceFactory({ service: coreServices.pluginMetadata, deps: {}, - factory: async () => ({ getId: () => options?.pluginId! }), - }), -); + factory: async () => ({ getId: () => pluginId }), + }); +} export class ServiceRegistry { static create(factories: Array): ServiceRegistry { - const registry = new ServiceRegistry(factories); + const factoryMap = new Map(); + for (const factory of factories) { + if (factory.service.multiton) { + const existing = factoryMap.get(factory.service.id) ?? []; + factoryMap.set( + factory.service.id, + existing.concat(toInternalServiceFactory(factory)), + ); + } else { + factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]); + } + } + const registry = new ServiceRegistry(factoryMap); registry.checkForCircularDeps(); return registry; } - readonly #providedFactories: Map; + readonly #providedFactories: Map; readonly #loadedDefaultFactories: Map< Function, Promise @@ -82,10 +94,8 @@ export class ServiceRegistry { readonly #addedFactoryIds = new Set(); readonly #instantiatedFactories = new Set(); - private constructor(factories: Array) { - this.#providedFactories = new Map( - factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), - ); + private constructor(factories: Map) { + this.#providedFactories = factories; this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); } @@ -93,17 +103,17 @@ export class ServiceRegistry { #resolveFactory( ref: ServiceRef, pluginId: string, - ): Promise | undefined { + ): Promise | undefined { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { - return Promise.resolve( - toInternalServiceFactory(pluginMetadataServiceFactory({ pluginId })), - ); + return Promise.resolve([ + toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId)), + ]); } let resolvedFactory: - | Promise - | InternalServiceFactory + | Promise + | InternalServiceFactory[] | undefined = this.#providedFactories.get(ref.id); const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; if (!resolvedFactory && !defaultFactory) { @@ -120,15 +130,18 @@ export class ServiceRegistry { ); this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); } - resolvedFactory = loadedFactory.catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the default factory loader threw an error, ${stringifyError( - error, - )}`, - ); - }); + resolvedFactory = loadedFactory.then( + factory => [factory], + error => { + throw new Error( + `Failed to instantiate service '${ + ref.id + }' because the default factory loader threw an error, ${stringifyError( + error, + )}`, + ); + }, + ); } return Promise.resolve(resolvedFactory); @@ -142,6 +155,9 @@ export class ServiceRegistry { if (this.#providedFactories.get(ref.id)) { return false; } + if (ref.multiton) { + return false; + } return !(ref as InternalServiceRef).__defaultFactory; }); @@ -156,13 +172,13 @@ export class ServiceRegistry { checkForCircularDeps(): void { const graph = DependencyGraph.fromIterable( - Array.from(this.#providedFactories).map( - ([serviceId, serviceFactory]) => ({ - value: serviceId, - provides: [serviceId], - consumes: Object.values(serviceFactory.deps).map(d => d.id), - }), - ), + Array.from(this.#providedFactories).map(([serviceId, factories]) => ({ + value: serviceId, + provides: [serviceId], + consumes: factories.flatMap(factory => + Object.values(factory.deps).map(d => d.id), + ), + })), ); const circularDependencies = Array.from(graph.detectCircularDependencies()); @@ -183,27 +199,36 @@ export class ServiceRegistry { ); } - if (this.#addedFactoryIds.has(factoryId)) { - throw new Error( - `Duplicate service implementations provided for ${factoryId}`, - ); - } - if (this.#instantiatedFactories.has(factoryId)) { throw new Error( `Unable to set service factory with id ${factoryId}, service has already been instantiated`, ); } - this.#addedFactoryIds.add(factoryId); - this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); + if (factory.service.multiton) { + const newFactories = ( + this.#providedFactories.get(factoryId) ?? [] + ).concat(toInternalServiceFactory(factory)); + this.#providedFactories.set(factoryId, newFactories); + } else { + if (this.#addedFactoryIds.has(factoryId)) { + throw new Error( + `Duplicate service implementations provided for ${factoryId}`, + ); + } + + this.#addedFactoryIds.add(factoryId); + this.#providedFactories.set(factoryId, [ + toInternalServiceFactory(factory), + ]); + } } async initializeEagerServicesWithScope( scope: 'root' | 'plugin', pluginId: string = 'root', ) { - for (const factory of this.#providedFactories.values()) { + for (const [factory] of this.#providedFactories.values()) { if (factory.service.scope === scope) { // Root-scoped services are eager by default, plugin-scoped are lazy by default if (scope === 'root' && factory.initialization !== 'lazy') { @@ -215,88 +240,112 @@ export class ServiceRegistry { } } - get(ref: ServiceRef, pluginId: string): Promise | undefined { + get( + ref: ServiceRef, + pluginId: string, + ): Promise | undefined { this.#instantiatedFactories.add(ref.id); - return this.#resolveFactory(ref, pluginId)?.then(factory => { - if (factory.service.scope === 'root') { - let existing = this.#rootServiceImplementations.get(factory); - if (!existing) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + const resolvedFactory = this.#resolveFactory(ref, pluginId); - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope !== 'root') { - throw new Error( - `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, - ); + if (!resolvedFactory) { + return ref.multiton + ? (Promise.resolve([]) as + | Promise + | undefined) + : undefined; + } + + return resolvedFactory + .then(factories => { + return Promise.all( + factories.map(factory => { + if (factory.service.scope === 'root') { + let existing = this.#rootServiceImplementations.get(factory); + if (!existing) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); + + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope !== 'root') { + throw new Error( + `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, + ); + } + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + + existing = Promise.all(rootDeps).then(entries => + factory.factory(Object.fromEntries(entries), undefined), + ); + this.#rootServiceImplementations.set(factory, existing); + } + return existing as Promise; } - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - existing = Promise.all(rootDeps).then(entries => - factory.factory(Object.fromEntries(entries), undefined), - ); - this.#rootServiceImplementations.set(factory, existing); - } - return existing as Promise; - } + let implementation = this.#implementations.get(factory); + if (!implementation) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let implementation = this.#implementations.get(factory); - if (!implementation) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope === 'root') { + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope === 'root') { - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - } + implementation = { + context: Promise.all(rootDeps) + .then(entries => + factory.createRootContext?.(Object.fromEntries(entries)), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, + ); + }), + byPlugin: new Map(), + }; - implementation = { - context: Promise.all(rootDeps) - .then(entries => - factory.createRootContext?.(Object.fromEntries(entries)), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, - ); - }), - byPlugin: new Map(), - }; + this.#implementations.set(factory, implementation); + } - this.#implementations.set(factory, implementation); - } + let result = implementation.byPlugin.get(pluginId) as Promise; + if (!result) { + const allDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let result = implementation.byPlugin.get(pluginId) as Promise; - if (!result) { - const allDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + const target = this.get(serviceRef, pluginId)!; + allDeps.push(target.then(impl => [name, impl])); + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - const target = this.get(serviceRef, pluginId)!; - allDeps.push(target.then(impl => [name, impl])); - } - - result = implementation.context - .then(context => - Promise.all(allDeps).then(entries => - factory.factory(Object.fromEntries(entries), context), - ), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, - ); - }); - implementation.byPlugin.set(pluginId, result); - } - - return result; - }); + result = implementation.context + .then(context => + Promise.all(allDeps).then(entries => + factory.factory(Object.fromEntries(entries), context), + ), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, + ); + }); + implementation.byPlugin.set(pluginId, result); + } + return result; + }), + ); + }) + .then(results => (ref.multiton ? results : results[0])); } } diff --git a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts index f5c4d8b152..fd449be541 100644 --- a/packages/backend-app-api/src/wiring/createSpecializedBackend.ts +++ b/packages/backend-app-api/src/wiring/createSpecializedBackend.ts @@ -24,13 +24,9 @@ import { Backend, CreateSpecializedBackendOptions } from './types'; export function createSpecializedBackend( options: CreateSpecializedBackendOptions, ): Backend { - const services = options.defaultServiceFactories.map(sf => - typeof sf === 'function' ? sf() : sf, - ); - const exists = new Set(); const duplicates = new Set(); - for (const { service } of services) { + for (const { service } of options.defaultServiceFactories) { if (exists.has(service.id)) { duplicates.add(service.id); } else { @@ -47,5 +43,5 @@ export function createSpecializedBackend( ); } - return new BackstageBackend(services); + return new BackstageBackend(options.defaultServiceFactories); } diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 027b2bf254..075eb58b3f 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -18,18 +18,25 @@ import { BackendFeature, ExtensionPoint, ServiceRef, - ServiceFactoryOrFunction, + ServiceFactory, } from '@backstage/backend-plugin-api'; /** * @public */ export interface Backend { + add(feature: BackendFeature | Promise<{ default: BackendFeature }>): void; + /** + * @deprecated The ability to add features defined as a callback is being + * removed. Please update the installed feature to no longer be defined as a + * callback, typically this means updating it to use the latest version of + * `@backstage/backend-plugin-api`. If the feature is from a third-party + * package, please reach out to the package maintainer to update it. + */ add( feature: - | BackendFeature | (() => BackendFeature) - | Promise<{ default: BackendFeature | (() => BackendFeature) }>, + | Promise<{ default: () => BackendFeature }>, ): void; start(): Promise; stop(): Promise; @@ -39,7 +46,7 @@ export interface Backend { * @public */ export interface CreateSpecializedBackendOptions { - defaultServiceFactories: ServiceFactoryOrFunction[]; + defaultServiceFactories: ServiceFactory[]; } /** diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 8d0ab8984c..5eadae4c0f 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,124 @@ # @backstage/backend-common +## 0.23.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.23.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.23.4-next.0 + +### Patch Changes + +- ba8571e: Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. +- 7e13b7a: The remaining exports in the package have now been deprecated: + + - `cacheToPluginCacheManager` + - `createLegacyAuthAdapters` + - `LegacyCreateRouter` + - `legacyPlugin` + - `loggerToWinstonLogger` + - `makeLegacyPlugin` + + Users of these export should fully [migrate to the new backend system](https://backstage.io/docs/backend-system/building-backends/migrating). + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.23.3 + +### Patch Changes + +- 8c09c97: Deprecate legacy status check factory, handler and types. +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- c964a3d: Add dependencies that are needed by cross-imports from backend-defaults +- b60db08: Fixing exporting of classes properly from new packages +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.23.3-next.1 + +### Patch Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.23.2-next.0 + +### Patch Changes + +- 8c09c97: Deprecate legacy status check factory, handler and types. +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- c964a3d: Add dependencies that are needed by cross-imports from backend-defaults +- b60db08: Fixing exporting of classes properly from new packages +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.23.0 ### Minor Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index de00ef6a5a..23ed13b62c 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -129,7 +129,7 @@ export class CacheManager extends CacheManager_2 {} // @public @deprecated (undocumented) export type CacheManagerOptions = CacheManagerOptions_2; -// @public +// @public @deprecated export function cacheToPluginCacheManager(cache: CacheService): { getClient(options?: CacheServiceOptions): CacheService; }; @@ -142,7 +142,7 @@ export interface ContainerRunner { runContainer(opts: RunContainerOptions): Promise; } -// @public +// @public @deprecated export function createLegacyAuthAdapters< TOptions extends { auth?: AuthService; @@ -184,7 +184,7 @@ export function createRootLogger( // @public @deprecated export function createServiceBuilder(_module: NodeModule): ServiceBuilder; -// @public +// @public @deprecated export function createStatusCheckRouter(options: { logger: LoggerService; path?: string; @@ -401,10 +401,10 @@ export type KubernetesContainerRunnerOptions = { timeoutMs?: number; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type LegacyCreateRouter = (deps: TEnv) => Promise; -// @public +// @public @deprecated export const legacyPlugin: ( name: string, createRouterImport: Promise<{ @@ -447,13 +447,13 @@ export function loadBackendConfig(options: { watch?: boolean; }): Promise; -// @public (undocumented) +// @public @deprecated (undocumented) export function loggerToWinstonLogger( logger: LoggerService, opts?: TransportStreamOptions, ): Logger; -// @public +// @public @deprecated export function makeLegacyPlugin< TEnv extends Record, TEnvTransforms extends { @@ -645,15 +645,15 @@ export type StaticAuthOptions = { logger?: LoggerService; }; -// @public +// @public @deprecated export type StatusCheck = () => Promise; -// @public +// @public @deprecated export function statusCheckHandler( options?: StatusCheckHandlerOptions, ): Promise; -// @public +// @public @deprecated export interface StatusCheckHandlerOptions { statusCheck?: StatusCheck; } diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index e993e1f21e..ccf44fba0f 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-common", - "version": "0.23.0", + "version": "0.23.4-next.2", "description": "Common functionality library for Backstage backends", "backstage": { "role": "node-library" @@ -102,7 +102,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "mysql2": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "p-limit": "^3.1.0", "path-to-regexp": "^6.2.1", @@ -137,7 +137,7 @@ "@types/webpack-env": "^1.15.2", "@types/yauzl": "^2.10.0", "aws-sdk-client-mock": "^4.0.0", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.0.0", "http-errors": "^2.0.0", "msw": "^1.0.0", "mysql2": "^3.0.0", diff --git a/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts b/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts index 0f591eac6c..15cd881649 100644 --- a/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts +++ b/packages/backend-common/src/compat/auth/createLegacyAuthAdapters.ts @@ -292,6 +292,7 @@ export class UserInfoCompat implements UserInfoService { /** * An adapter that ensures presence of the auth and/or httpAuth services. * @public + * @deprecated Migrate to use the new backend system and auth services instead. */ export function createLegacyAuthAdapters< TOptions extends { diff --git a/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts b/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts index 2654934804..2ae55b016f 100644 --- a/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts +++ b/packages/backend-common/src/compat/cache/cacheToPluginCacheManager.ts @@ -24,6 +24,7 @@ import { * old-backend plugin cache manager. * * @public + * @deprecated Migrate to use the new CacheService instead. */ export function cacheToPluginCacheManager(cache: CacheService): { getClient(options?: CacheServiceOptions): CacheService; diff --git a/packages/backend-common/src/compat/legacy/legacy.ts b/packages/backend-common/src/compat/legacy/legacy.ts index 57220a8430..4dc7d1a09a 100644 --- a/packages/backend-common/src/compat/legacy/legacy.ts +++ b/packages/backend-common/src/compat/legacy/legacy.ts @@ -18,6 +18,7 @@ import { AuthService, coreServices, createBackendPlugin, + HttpRouterService, ServiceRef, } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; @@ -27,6 +28,7 @@ import { TokenManager } from '../../deprecated'; /** * @public + * @deprecated Fully use the new backend system instead. */ export type LegacyCreateRouter = (deps: TEnv) => Promise; @@ -69,6 +71,7 @@ function wrapTokenManager(tokenManager: TokenManager, auth: AuthService) { * Creates a new custom plugin compatibility wrapper. * * @public + * @deprecated Fully use the new backend system instead. * @remarks * * Usually you can use {@link legacyPlugin} directly instead, but you might @@ -105,7 +108,10 @@ export function makeLegacyPlugin< return [key, transform(dep)]; } if (key === 'tokenManager') { - return [key, wrapTokenManager(dep as TokenManager, _auth)]; + return [ + key, + wrapTokenManager(dep as TokenManager, _auth as AuthService), + ]; } return [key, dep]; }), @@ -113,7 +119,7 @@ export function makeLegacyPlugin< const router = await createRouter( pluginEnv as TransformedEnv, ); - _router.use(router); + (_router as HttpRouterService).use(router); }, }); }, @@ -128,6 +134,7 @@ export function makeLegacyPlugin< * register it with the http router based on the plugin id. * * @public + * @deprecated Fully use the new backend system instead. * @remarks * * This is intended to be used by plugin authors to ease the transition to the diff --git a/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts b/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts index 4ba88358d2..83871f48e3 100644 --- a/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts +++ b/packages/backend-common/src/compat/logging/loggerToWinstonLogger.ts @@ -53,7 +53,10 @@ class BackstageLoggerTransport extends Transport { } } -/** @public */ +/** + * @public + * @deprecated Migrate to use the new LoggerService instead. + */ export function loggerToWinstonLogger( logger: LoggerService, opts?: TransportStreamOptions, diff --git a/packages/backend-common/src/deprecated/middleware/errorHandler.ts b/packages/backend-common/src/deprecated/middleware/errorHandler.ts index 3664ed0e0a..b4f07b9320 100644 --- a/packages/backend-common/src/deprecated/middleware/errorHandler.ts +++ b/packages/backend-common/src/deprecated/middleware/errorHandler.ts @@ -63,7 +63,7 @@ export type ErrorHandlerOptions = { * * @public * @returns An Express error request handler - * @deprecated Use {@link @backstage/backend-app-api#MiddlewareFactory.create.error} instead + * @deprecated Use {@link @backstage/backend-defaults/rootHttpRouter#MiddlewareFactory.create.error} instead */ export function errorHandler( options: ErrorHandlerOptions = {}, diff --git a/packages/backend-common/src/deprecated/middleware/index.ts b/packages/backend-common/src/deprecated/middleware/index.ts index 6c284cc6ec..f4f4fcde0b 100644 --- a/packages/backend-common/src/deprecated/middleware/index.ts +++ b/packages/backend-common/src/deprecated/middleware/index.ts @@ -17,3 +17,4 @@ export * from './errorHandler'; export * from './notFoundHandler'; export * from './requestLoggingHandler'; +export * from './statusCheckHandler'; diff --git a/packages/backend-common/src/middleware/statusCheckHandler.test.ts b/packages/backend-common/src/deprecated/middleware/statusCheckHandler.test.ts similarity index 100% rename from packages/backend-common/src/middleware/statusCheckHandler.test.ts rename to packages/backend-common/src/deprecated/middleware/statusCheckHandler.test.ts diff --git a/packages/backend-common/src/middleware/statusCheckHandler.ts b/packages/backend-common/src/deprecated/middleware/statusCheckHandler.ts similarity index 73% rename from packages/backend-common/src/middleware/statusCheckHandler.ts rename to packages/backend-common/src/deprecated/middleware/statusCheckHandler.ts index 4bd1a701c3..ebaf84c1b9 100644 --- a/packages/backend-common/src/middleware/statusCheckHandler.ts +++ b/packages/backend-common/src/deprecated/middleware/statusCheckHandler.ts @@ -21,6 +21,7 @@ import { NextFunction, Request, Response, RequestHandler } from 'express'; * {@link createStatusCheckRouter}. * * @public + * @deprecated Migrate to the {@link https://backstage.io/docs/backend-system/ | new backend system} and use the {@link https://backstage.io/docs/backend-system/core-services/root-health | Root Health Service} instead. */ export type StatusCheck = () => Promise; @@ -28,6 +29,7 @@ export type StatusCheck = () => Promise; * Options passed to {@link statusCheckHandler}. * * @public + * @deprecated Migrate to the {@link https://backstage.io/docs/backend-system/ | new backend system} and use the {@link https://backstage.io/docs/backend-system/core-services/root-health | Root Health Service} instead. */ export interface StatusCheckHandlerOptions { /** @@ -44,6 +46,7 @@ export interface StatusCheckHandlerOptions { * @public * @param options - An optional configuration object. * @returns An Express error request handler + * @deprecated Migrate to the {@link https://backstage.io/docs/backend-system/ | new backend system} and use the {@link https://backstage.io/docs/backend-system/core-services/root-health | Root Health Service} instead. */ export async function statusCheckHandler( options: StatusCheckHandlerOptions = {}, diff --git a/packages/backend-common/src/service/createStatusCheckRouter.test.ts b/packages/backend-common/src/deprecated/service/createStatusCheckRouter.test.ts similarity index 100% rename from packages/backend-common/src/service/createStatusCheckRouter.test.ts rename to packages/backend-common/src/deprecated/service/createStatusCheckRouter.test.ts diff --git a/packages/backend-common/src/service/createStatusCheckRouter.ts b/packages/backend-common/src/deprecated/service/createStatusCheckRouter.ts similarity index 85% rename from packages/backend-common/src/service/createStatusCheckRouter.ts rename to packages/backend-common/src/deprecated/service/createStatusCheckRouter.ts index 1a5953a748..fdece10ec5 100644 --- a/packages/backend-common/src/service/createStatusCheckRouter.ts +++ b/packages/backend-common/src/deprecated/service/createStatusCheckRouter.ts @@ -17,8 +17,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import Router from 'express-promise-router'; import express from 'express'; -import { errorHandler } from '../deprecated'; -import { statusCheckHandler, StatusCheck } from '../middleware'; +import { errorHandler, statusCheckHandler, StatusCheck } from '..'; /** * Creates a default status checking router, that you can add to your express @@ -31,6 +30,7 @@ import { statusCheckHandler, StatusCheck } from '../middleware'; * requests. * * @public + * @deprecated Migrate to the {@link https://backstage.io/docs/backend-system/ | new backend system} and use the {@link https://backstage.io/docs/backend-system/core-services/root-health | Root Health Service} instead. */ export async function createStatusCheckRouter(options: { logger: LoggerService; diff --git a/packages/backend-common/src/deprecated/service/index.ts b/packages/backend-common/src/deprecated/service/index.ts index a9fc43c22a..d01f25fad3 100644 --- a/packages/backend-common/src/deprecated/service/index.ts +++ b/packages/backend-common/src/deprecated/service/index.ts @@ -15,4 +15,5 @@ */ export { createServiceBuilder } from './createServiceBuilder'; +export { createStatusCheckRouter } from './createStatusCheckRouter'; export type { ServiceBuilder, RequestLoggingHandlerFactory } from './types'; diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index cb5e236888..ad5621b5a9 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -22,5 +22,3 @@ export * from './deprecated'; export * from './compat'; -export * from './middleware'; -export * from './service'; diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index b6cb827e8b..44b0a6b9b0 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,149 @@ # @backstage/backend-defaults +## 0.4.2-next.2 + +### Patch Changes + +- 0d16b52: Add access restrictions to the JWKS external access method config schema +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 7c5f3b0: Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service. +- 1d5f298: Avoid excessive numbers of error listeners on cache clients +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-app-api@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.4.2-next.0 + +### Patch Changes + +- 4e79d19: The `createHealthRouter` utility that allows you to create a health check router is now exported via `@backstage/backend-defaults/rootHttpRouter`. +- 78c1329: Updated `GitlabUrlReader.readUrl` and `GitlabUrlReader.readTree` to accept a user-provided token, supporting both bearer and private tokens. +- 8e967da: Fixed the routing of the new health check service, the health endpoints should now properly be available at `/.backstage/health/v1/readiness` and `/.backstage/health/v1/liveness`. +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-app-api@0.8.1-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 0.4.0 + +### Minor Changes + +- 1cb84d7: **BREAKING**: Removed the depreacted `getPath` option from `httpRouterServiceFactory`, as well as the `HttpRouterFactoryOptions` type. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- 419f387: Refactor of `rootHttpRouterServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- cb14a05: Repack the package to fix issues with typescript with named exports +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- e28af58: Refactor of `rootConfigServiceFactory` to allow it to be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config-loader@1.8.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.3.4-next.1 + +### Patch Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 0.3.3-next.0 + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- cb14a05: Repack the package to fix issues with typescript with named exports +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/backend-app-api@0.7.9-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/backend-dev-utils@0.1.4 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + ## 0.3.0 ### Minor Changes diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md index d64ce5bc2a..5ceca7f72e 100644 --- a/packages/backend-defaults/api-report-auth.md +++ b/packages/backend-defaults/api-report-auth.md @@ -4,10 +4,15 @@ ```ts import { AuthService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const authServiceFactory: () => ServiceFactory; +export const authServiceFactory: ServiceFactoryCompat< + AuthService, + 'plugin', + 'singleton', + undefined +>; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md index 0bfd7eb463..e8b6f075ab 100644 --- a/packages/backend-defaults/api-report-cache.md +++ b/packages/backend-defaults/api-report-cache.md @@ -7,7 +7,7 @@ import { CacheService } from '@backstage/backend-plugin-api'; import { CacheServiceOptions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export class CacheManager { @@ -25,7 +25,12 @@ export type CacheManagerOptions = { }; // @public -export const cacheServiceFactory: () => ServiceFactory; +export const cacheServiceFactory: ServiceFactoryCompat< + CacheService, + 'plugin', + 'singleton', + undefined +>; // @public (undocumented) export interface PluginCacheManager { diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md index 116830343a..8d204db025 100644 --- a/packages/backend-defaults/api-report-database.md +++ b/packages/backend-defaults/api-report-database.md @@ -8,7 +8,7 @@ import { DatabaseService } from '@backstage/backend-plugin-api'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PluginMetadataService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export class DatabaseManager implements LegacyRootDatabaseService { @@ -32,9 +32,11 @@ export type DatabaseManagerOptions = { }; // @public -export const databaseServiceFactory: () => ServiceFactory< +export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public @deprecated diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md index b434dab6fc..1b19a42aa5 100644 --- a/packages/backend-defaults/api-report-discovery.md +++ b/packages/backend-defaults/api-report-discovery.md @@ -5,12 +5,14 @@ ```ts import { Config } from '@backstage/config'; import { DiscoveryService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const discoveryServiceFactory: () => ServiceFactory< +export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md index 6ffe3d6539..a65a6e157d 100644 --- a/packages/backend-defaults/api-report-httpAuth.md +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -4,12 +4,14 @@ ```ts import { HttpAuthService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const httpAuthServiceFactory: () => ServiceFactory< +export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index a7ca2c8dd3..300e75a44d 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -7,22 +7,20 @@ import { HttpRouterService } from '@backstage/backend-plugin-api'; import { HumanDuration } from '@backstage/types'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export function createLifecycleMiddleware( options: LifecycleMiddlewareOptions, ): RequestHandler; -// @public (undocumented) -export interface HttpRouterFactoryOptions { - getPath?(pluginId: string): string; -} - // @public -export const httpRouterServiceFactory: ( - options?: HttpRouterFactoryOptions | undefined, -) => ServiceFactory; +export const httpRouterServiceFactory: ServiceFactoryCompat< + HttpRouterService, + 'plugin', + 'singleton', + undefined +>; // @public export interface LifecycleMiddlewareOptions { diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md index 4233584664..54ffb77fed 100644 --- a/packages/backend-defaults/api-report-lifecycle.md +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -4,12 +4,14 @@ ```ts import { LifecycleService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const lifecycleServiceFactory: () => ServiceFactory< +export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md index fa9018a40c..40ca4e21b4 100644 --- a/packages/backend-defaults/api-report-logger.md +++ b/packages/backend-defaults/api-report-logger.md @@ -4,12 +4,14 @@ ```ts import { LoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const loggerServiceFactory: () => ServiceFactory< +export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md index c436443ff1..eea11ed7c9 100644 --- a/packages/backend-defaults/api-report-permissions.md +++ b/packages/backend-defaults/api-report-permissions.md @@ -4,12 +4,14 @@ ```ts import { PermissionsService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const permissionsServiceFactory: () => ServiceFactory< +export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 317c451e1b..2935ad1128 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -26,9 +26,10 @@ export interface RootConfigFactoryOptions { } // @public (undocumented) -export const rootConfigServiceFactory: ( - options?: RootConfigFactoryOptions | undefined, -) => ServiceFactory; +export const rootConfigServiceFactory: (( + options?: RootConfigFactoryOptions, +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md new file mode 100644 index 0000000000..c7580cacd6 --- /dev/null +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -0,0 +1,18 @@ +## API Report File for "@backstage/backend-defaults" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { RootHealthService } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; + +// @public (undocumented) +export const rootHealthServiceFactory: ServiceFactoryCompat< + RootHealthService, + 'root', + 'singleton', + undefined +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index b753406202..3686f25a12 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// /// import { Config } from '@backstage/config'; @@ -17,10 +18,17 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; import { RequestListener } from 'http'; import { RootConfigService } from '@backstage/backend-plugin-api'; +import { RootHealthService } from '@backstage/backend-plugin-api'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; +import { Router } from 'express'; import type { Server } from 'node:http'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +// @public (undocumented) +export function createHealthRouter(options: { + health: RootHealthService; +}): Router; + // @public export function createHttpServer( listener: RequestListener, @@ -121,6 +129,8 @@ export interface RootHttpRouterConfigureContext { // (undocumented) config: RootConfigService; // (undocumented) + healthRouter: RequestHandler; + // (undocumented) lifecycle: LifecycleService; // (undocumented) logger: LoggerService; @@ -139,9 +149,10 @@ export type RootHttpRouterFactoryOptions = { }; // @public (undocumented) -export const rootHttpRouterServiceFactory: ( - options?: RootHttpRouterFactoryOptions | undefined, -) => ServiceFactory; +export const rootHttpRouterServiceFactory: (( + options?: RootHttpRouterFactoryOptions, +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md index 00eb9c1ebb..e522471d04 100644 --- a/packages/backend-defaults/api-report-rootLifecycle.md +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -4,12 +4,14 @@ ```ts import { RootLifecycleService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public -export const rootLifecycleServiceFactory: () => ServiceFactory< +export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, - 'root' + 'root', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md index 70dc0deee6..3878420192 100644 --- a/packages/backend-defaults/api-report-rootLogger.md +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -7,13 +7,15 @@ import { Format } from 'logform'; import { JsonObject } from '@backstage/types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; // @public -export const rootLoggerServiceFactory: () => ServiceFactory< +export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, - 'root' + 'root', + 'singleton', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index 58991ddca3..9953518e09 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -6,7 +6,7 @@ import { DatabaseService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; // @public export class DefaultSchedulerService { @@ -18,9 +18,11 @@ export class DefaultSchedulerService { } // @public -export const schedulerServiceFactory: () => ServiceFactory< +export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index c56cdba69b..f561c2cbf5 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -21,7 +21,8 @@ import { GitLabIntegration } from '@backstage/integration'; import { HarnessIntegration } from '@backstage/integration'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Readable } from 'stream'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeOptions } from '@backstage/backend-plugin-api'; import { UrlReaderServiceReadTreeResponse } from '@backstage/backend-plugin-api'; @@ -413,6 +414,13 @@ export type ReadUrlResponseFactoryFromStreamOptions = { lastModifiedAt?: Date; }; +// @public +export const urlReaderFactoriesServiceRef: ServiceRef< + ReaderFactory, + 'plugin', + 'multiton' +>; + // @public export type UrlReaderPredicateTuple = { predicate: (url: URL) => boolean; @@ -426,9 +434,11 @@ export class UrlReaders { } // @public -export const urlReaderServiceFactory: () => ServiceFactory< +export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md index d11fb69718..27f73d06e1 100644 --- a/packages/backend-defaults/api-report-userInfo.md +++ b/packages/backend-defaults/api-report-userInfo.md @@ -3,13 +3,15 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { UserInfoService } from '@backstage/backend-plugin-api'; // @public -export const userInfoServiceFactory: () => ServiceFactory< +export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-defaults/config.d.ts b/packages/backend-defaults/config.d.ts index ab1e443fc2..1c784bce0c 100644 --- a/packages/backend-defaults/config.d.ts +++ b/packages/backend-defaults/config.d.ts @@ -287,6 +287,46 @@ export interface Config { */ subjectPrefix?: string; }; + /** + * Restricts what types of access that are permitted for this access + * method. If no access restrictions are given, it'll have unlimited + * access. This access restriction applies for the framework level; + * individual plugins may have their own access control mechanisms + * on top of this. + */ + accessRestrictions?: Array<{ + /** + * Permit access to make requests to this plugin. + * + * Can be further refined by setting additional fields below. + */ + plugin: string; + /** + * If given, this method is limited to only performing actions + * with these named permissions in this plugin. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permission?: string | Array; + /** + * If given, this method is limited to only performing actions + * whose permissions have these attributes. + * + * Note that this only applies where permissions checks are + * enabled in the first place. Endpoints that are not protected by + * the permissions system at all, are not affected by this + * setting. + */ + permissionAttribute?: { + /** + * One of more of 'create', 'read', 'update', or 'delete'. + */ + action?: string | Array; + }; + }>; } >; }; diff --git a/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js new file mode 100644 index 0000000000..efbef1c76f --- /dev/null +++ b/packages/backend-defaults/migrations/scheduler/20240712211735_nullable_next_run.js @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.setNullable('next_run_start_at'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropNullable('next_run_start_at'); + }); +}; diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 4c34c7f317..cef9641de6 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-defaults", - "version": "0.3.0", + "version": "0.4.2-next.2", "description": "Backend defaults used by Backstage backend apps", "backstage": { "role": "node-library" @@ -30,6 +30,7 @@ "./logger": "./src/entrypoints/logger/index.ts", "./permissions": "./src/entrypoints/permissions/index.ts", "./rootConfig": "./src/entrypoints/rootConfig/index.ts", + "./rootHealth": "./src/entrypoints/rootHealth/index.ts", "./rootHttpRouter": "./src/entrypoints/rootHttpRouter/index.ts", "./rootLifecycle": "./src/entrypoints/rootLifecycle/index.ts", "./rootLogger": "./src/entrypoints/rootLogger/index.ts", @@ -72,6 +73,9 @@ "rootConfig": [ "src/entrypoints/rootConfig/index.ts" ], + "rootHealth": [ + "src/entrypoints/rootHealth/index.ts" + ], "rootHttpRouter": [ "src/entrypoints/rootHttpRouter/index.ts" ], @@ -139,7 +143,7 @@ "@types/express": "^4.17.6", "archiver": "^6.0.0", "base64-stream": "^1.0.0", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.0.0", "compression": "^1.7.4", "concat-stream": "^2.0.0", "cookie": "^0.6.0", @@ -161,7 +165,7 @@ "minimist": "^1.2.5", "morgan": "^1.10.0", "mysql2": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-forge": "^1.3.1", "p-limit": "^3.1.0", "path-to-regexp": "^6.2.1", diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index 9b3fa5d649..47fa09da49 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -30,6 +30,7 @@ import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { loggerServiceFactory } from '@backstage/backend-defaults/logger'; import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; import { rootConfigServiceFactory } from '@backstage/backend-defaults/rootConfig'; +import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth'; import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; import { rootLoggerServiceFactory } from '@backstage/backend-defaults/rootLogger'; @@ -39,25 +40,26 @@ import { userInfoServiceFactory } from '@backstage/backend-defaults/userInfo'; import { eventsServiceFactory } from '@backstage/plugin-events-node'; export const defaultServiceFactories = [ - authServiceFactory(), - cacheServiceFactory(), - rootConfigServiceFactory(), - databaseServiceFactory(), - discoveryServiceFactory(), - httpAuthServiceFactory(), - httpRouterServiceFactory(), - identityServiceFactory(), - lifecycleServiceFactory(), - loggerServiceFactory(), - permissionsServiceFactory(), - rootHttpRouterServiceFactory(), - rootLifecycleServiceFactory(), - rootLoggerServiceFactory(), - schedulerServiceFactory(), - tokenManagerServiceFactory(), - userInfoServiceFactory(), - urlReaderServiceFactory(), - eventsServiceFactory(), + authServiceFactory, + cacheServiceFactory, + rootConfigServiceFactory, + databaseServiceFactory, + discoveryServiceFactory, + httpAuthServiceFactory, + httpRouterServiceFactory, + identityServiceFactory, + lifecycleServiceFactory, + loggerServiceFactory, + permissionsServiceFactory, + rootHealthServiceFactory, + rootHttpRouterServiceFactory, + rootLifecycleServiceFactory, + rootLoggerServiceFactory, + schedulerServiceFactory, + tokenManagerServiceFactory, + userInfoServiceFactory, + urlReaderServiceFactory, + eventsServiceFactory, ]; /** diff --git a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts index 22357d0f49..f9ef4c9762 100644 --- a/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/authServiceFactory.test.ts @@ -17,7 +17,7 @@ import { ServiceFactoryTester, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { tokenManagerServiceFactory } from '@backstage/backend-app-api'; import { authServiceFactory } from './authServiceFactory'; @@ -68,7 +68,7 @@ const mockDeps = [ ]; describe('authServiceFactory', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.useRealTimers(); @@ -86,8 +86,8 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const searchAuth = await tester.get('search'); - const catalogAuth = await tester.get('catalog'); + const searchAuth = await tester.getSubject('search'); + const catalogAuth = await tester.getSubject('catalog'); const { token: searchToken } = await searchAuth.getPluginRequestToken({ onBehalfOf: await searchAuth.getOwnServiceCredentials(), @@ -117,8 +117,8 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const searchAuth = await tester.get('search'); - const catalogAuth = await tester.get('catalog'); + const searchAuth = await tester.getSubject('search'); + const catalogAuth = await tester.getSubject('catalog'); server.use( rest.get( @@ -163,7 +163,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); await expect( catalogAuth.getPluginRequestToken({ @@ -194,7 +194,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); const { token } = await catalogAuth.getPluginRequestToken({ onBehalfOf: { @@ -261,7 +261,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); const fullToken = 'eyJ0eXAiOiJ2bmQuYmFja3N0YWdlLnVzZXIiLCJhbGciOiJFUzI1NiIsImtpZCI6IjhkMDFjM2RiLTU2ZjktNDVmMC04NmRkLTA1YjNjODM1YjNkMyJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjcwMDcvYXBpL2F1dGgiLCJzdWIiOiJ1c2VyOmRldmVsb3BtZW50L2d1ZXN0IiwiZW50IjpbInVzZXI6ZGV2ZWxvcG1lbnQvZ3Vlc3QiLCJncm91cDpkZWZhdWx0L3RlYW0tYSJdLCJhdWQiOiJiYWNrc3RhZ2UiLCJpYXQiOjE3MTIwNzE3MTQsImV4cCI6MTcxMjA3NTMxNCwidWlwIjoiSmwxVEpycG9VUjR1NENjUE9nalJMeHpEMi1FMGZPR3ptSm81UWI2eS1aN19meG5oVVBEdWVWRE1CS0l6WF9pc0lvSDhlZm9EUFA5bG9aQnpPblB5Z2cifQ.1gVMq1ofO8PzRctu72D6c4IMqXuIabT79WdGEhW6vIrBRs_qhuWAa94Wvz_KYKpBTb2nxgzXJ5OeddeoYApMyQ'; @@ -318,9 +318,9 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const searchAuth = await tester.get('search'); - const catalogAuth = await tester.get('catalog'); - const permissionAuth = await tester.get('permission'); + const searchAuth = await tester.getSubject('search'); + const catalogAuth = await tester.getSubject('catalog'); + const permissionAuth = await tester.getSubject('permission'); /* Corresponding private key in case this test needs to be updated in the future: { @@ -431,7 +431,7 @@ describe('authServiceFactory', () => { dependencies: mockDeps, }); - const catalogAuth = await tester.get('catalog'); + const catalogAuth = await tester.getSubject('catalog'); await expect( catalogAuth.authenticate('limited-static-token'), @@ -450,7 +450,7 @@ describe('authServiceFactory', () => { }, }); - const scaffolderAuth = await tester.get('scaffolder'); + const scaffolderAuth = await tester.getSubject('scaffolder'); await expect( scaffolderAuth.authenticate('limited-static-token'), diff --git a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts index 559107c59c..39b01c46b6 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/ExternalTokenHandler.test.ts @@ -19,7 +19,7 @@ import { ExternalTokenHandler } from './ExternalTokenHandler'; import { TokenHandler } from './types'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { randomBytes } from 'crypto'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; @@ -81,7 +81,7 @@ class FakeTokenFactory { describe('ExternalTokenHandler', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); it('skips over inner handlers that do not match, and applies plugin restrictions', async () => { const handler1: TokenHandler = { diff --git a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts index 56930e4480..321e1823aa 100644 --- a/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/external/jwks.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -81,7 +81,7 @@ describe('JWKSHandler', () => { let mockSubject: string; const keyDurationSeconds = 5; - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { mockSubject = 'test_subject'; diff --git a/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts index 76bafb2a8f..e85ec8db63 100644 --- a/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts +++ b/packages/backend-defaults/src/entrypoints/auth/user/UserTokenHandler.test.ts @@ -18,7 +18,7 @@ import { JsonObject } from '@backstage/types'; import { UserTokenHandler } from './UserTokenHandler'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -64,7 +64,7 @@ async function createToken(options: { describe('UserTokenHandler', () => { let userTokenHandler: UserTokenHandler; - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.useRealTimers(); diff --git a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts index 89fabfe977..873bcf9648 100644 --- a/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts +++ b/packages/backend-defaults/src/entrypoints/cache/CacheManager.ts @@ -111,23 +111,10 @@ export class CacheManager { return { getClient: (defaultOptions = {}) => { const clientFactory = (options: CacheServiceOptions) => { - const concreteClient = this.getClientWithTtl( + return this.getClientWithTtl( pluginId, options.defaultTtl ?? this.defaultTtl, ); - - // Always provide an error handler to avoid stopping the process. - concreteClient.on('error', (err: Error) => { - // In all cases, just log the error. - this.logger?.error('Failed to create cache client', err); - - // Invoke any custom error handler if provided. - if (typeof this.errorHandler === 'function') { - this.errorHandler(err); - } - }); - - return concreteClient; }; return new DefaultCacheClient( @@ -149,11 +136,17 @@ export class CacheManager { return (pluginId, defaultTtl) => { if (!store) { store = new KeyvRedis(this.connection); + // Always provide an error handler to avoid stopping the process + store.on('error', (err: Error) => { + this.logger?.error('Failed to create redis cache client', err); + this.errorHandler?.(err); + }); } return new Keyv({ namespace: pluginId, ttl: defaultTtl, store, + emitErrors: false, useRedisSets: this.useRedisSets, }); }; @@ -165,10 +158,16 @@ export class CacheManager { return (pluginId, defaultTtl) => { if (!store) { store = new KeyvMemcache(this.connection); + // Always provide an error handler to avoid stopping the process + store.on('error', (err: Error) => { + this.logger?.error('Failed to create memcache cache client', err); + this.errorHandler?.(err); + }); } return new Keyv({ namespace: pluginId, ttl: defaultTtl, + emitErrors: false, store, }); }; @@ -180,6 +179,7 @@ export class CacheManager { new Keyv({ namespace: pluginId, ttl: defaultTtl, + emitErrors: false, store, }); } diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts index 58c074f985..a8b749939b 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.test.ts @@ -35,7 +35,7 @@ describe('httpRouterFactory', () => { dependencies: [rootHttpRouter.factory], }); - const router1 = await tester.get('test1'); + const router1 = await tester.getSubject('test1'); router1.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); expect(rootHttpRouter.use).toHaveBeenCalledWith( @@ -43,7 +43,7 @@ describe('httpRouterFactory', () => { expect.any(Function), ); - const router2 = await tester.get('test2'); + const router2 = await tester.getSubject('test2'); router2.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); expect(rootHttpRouter.use).toHaveBeenCalledWith( @@ -52,32 +52,6 @@ describe('httpRouterFactory', () => { ); }); - it('should use custom path generator', async () => { - const rootHttpRouter = mockServices.rootHttpRouter.mock(); - const tester = ServiceFactoryTester.from( - httpRouterServiceFactory({ - getPath: id => `/some/${id}/path`, - }), - { dependencies: [rootHttpRouter.factory] }, - ); - - const router1 = await tester.get('test1'); - router1.use(() => {}); - expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); - expect(rootHttpRouter.use).toHaveBeenCalledWith( - '/some/test1/path', - expect.any(Function), - ); - - const router2 = await tester.get('test2'); - router2.use(() => {}); - expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); - expect(rootHttpRouter.use).toHaveBeenCalledWith( - '/some/test2/path', - expect.any(Function), - ); - }); - describe('auth services', () => { const pluginSubject = createBackendPlugin({ pluginId: 'test', diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts index 4a0dec49e6..fb2646580b 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/httpRouterServiceFactory.ts @@ -26,16 +26,6 @@ import { createCredentialsBarrier } from './createCredentialsBarrier'; import { createAuthIntegrationRouter } from './createAuthIntegrationRouter'; import { createCookieAuthRefreshMiddleware } from './createCookieAuthRefreshMiddleware'; -/** - * @public - */ -export interface HttpRouterFactoryOptions { - /** - * A callback used to generate the path for each plugin, defaults to `/api/{pluginId}`. - */ - getPath?(pluginId: string): string; -} - /** * HTTP route registration for plugins. * @@ -45,57 +35,39 @@ export interface HttpRouterFactoryOptions { * * @public */ -export const httpRouterServiceFactory = createServiceFactory( - (options?: HttpRouterFactoryOptions) => ({ - service: coreServices.httpRouter, - initialization: 'always', - deps: { - plugin: coreServices.pluginMetadata, - config: coreServices.rootConfig, - logger: coreServices.logger, - lifecycle: coreServices.lifecycle, - rootHttpRouter: coreServices.rootHttpRouter, - auth: coreServices.auth, - httpAuth: coreServices.httpAuth, - }, - async factory({ - auth, +export const httpRouterServiceFactory = createServiceFactory({ + service: coreServices.httpRouter, + initialization: 'always', + deps: { + plugin: coreServices.pluginMetadata, + config: coreServices.rootConfig, + lifecycle: coreServices.lifecycle, + rootHttpRouter: coreServices.rootHttpRouter, + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + }, + async factory({ auth, httpAuth, config, plugin, rootHttpRouter, lifecycle }) { + const router = PromiseRouter(); + + rootHttpRouter.use(`/api/${plugin.getId()}`, router); + + const credentialsBarrier = createCredentialsBarrier({ httpAuth, config, - logger, - plugin, - rootHttpRouter, - lifecycle, - }) { - if (options?.getPath) { - logger.warn( - `DEPRECATION WARNING: The 'getPath' option for HttpRouterService is deprecated. The ability to reconfigure the '/api/' path prefix for plugins will be removed in the future.`, - ); - } - const getPath = options?.getPath ?? (id => `/api/${id}`); - const path = getPath(plugin.getId()); + }); - const router = PromiseRouter(); - rootHttpRouter.use(path, router); + router.use(createAuthIntegrationRouter({ auth })); + router.use(createLifecycleMiddleware({ lifecycle })); + router.use(credentialsBarrier.middleware); + router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth })); - const credentialsBarrier = createCredentialsBarrier({ - httpAuth, - config, - }); - - router.use(createAuthIntegrationRouter({ auth })); - router.use(createLifecycleMiddleware({ lifecycle })); - router.use(credentialsBarrier.middleware); - router.use(createCookieAuthRefreshMiddleware({ auth, httpAuth })); - - return { - use(handler: Handler): void { - router.use(handler); - }, - addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void { - credentialsBarrier.addAuthPolicy(policy); - }, - }; - }, - }), -); + return { + use(handler: Handler): void { + router.use(handler); + }, + addAuthPolicy(policy: HttpRouterServiceAuthPolicy): void { + credentialsBarrier.addAuthPolicy(policy); + }, + }; + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/httpRouter/index.ts b/packages/backend-defaults/src/entrypoints/httpRouter/index.ts index 928a232131..bc12480ae8 100644 --- a/packages/backend-defaults/src/entrypoints/httpRouter/index.ts +++ b/packages/backend-defaults/src/entrypoints/httpRouter/index.ts @@ -15,6 +15,5 @@ */ export { httpRouterServiceFactory } from './httpRouterServiceFactory'; -export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory'; export { createLifecycleMiddleware } from './createLifecycleMiddleware'; export type { LifecycleMiddlewareOptions } from './createLifecycleMiddleware'; diff --git a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts index 5752a9c634..63a7353241 100644 --- a/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootConfig/rootConfigServiceFactory.ts @@ -45,11 +45,10 @@ export interface RootConfigFactoryOptions { watch?: boolean; } -/** - * @public - */ -export const rootConfigServiceFactory = createServiceFactory( - (options?: RootConfigFactoryOptions) => ({ +export const rootConfigServiceFactoryWithOptions = ( + options?: RootConfigFactoryOptions, +) => + createServiceFactory({ service: coreServices.rootConfig, deps: {}, async factory() { @@ -61,5 +60,12 @@ export const rootConfigServiceFactory = createServiceFactory( console.log(`Loading config from ${source}`); return await ConfigSources.toConfig(source); }, - }), + })(); + +/** + * @public + */ +export const rootConfigServiceFactory = Object.assign( + rootConfigServiceFactoryWithOptions, + rootConfigServiceFactoryWithOptions(), ); diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/index.ts b/packages/backend-defaults/src/entrypoints/rootHealth/index.ts new file mode 100644 index 0000000000..35225b39c4 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHealth/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 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. + */ + +export { rootHealthServiceFactory } from './rootHealthServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts new file mode 100644 index 0000000000..0eee5c18c6 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.test.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2024 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 { mockServices } from '@backstage/backend-test-utils'; +import { DefaultRootHealthService } from './rootHealthServiceFactory'; + +describe('DefaultRootHealthService', () => { + describe('readiness', () => { + it(`should return a 500 response if the server hasn't started yet`, async () => { + const service = new DefaultRootHealthService({ + lifecycle: mockServices.rootLifecycle.mock(), + }); + await expect(service.getReadiness()).resolves.toEqual({ + status: 503, + payload: { + message: 'Backend has not started yet', + status: 'error', + }, + }); + }); + + it('should return 200 if the server has started', async () => { + let mockServerStartedFn = () => {}; + + const lifecycle = mockServices.rootLifecycle.mock({ + addStartupHook: jest.fn(fn => (mockServerStartedFn = fn)), + }); + + const service = new DefaultRootHealthService({ + lifecycle, + }); + + mockServerStartedFn(); + + await expect(service.getReadiness()).resolves.toEqual({ + status: 200, + payload: { status: 'ok' }, + }); + }); + + it(`should return a 500 response if the server has stopped`, async () => { + let mockServerStartedFn = () => {}; + let mockServerStoppedFn = () => {}; + + const lifecycle = mockServices.rootLifecycle.mock({ + addStartupHook: jest.fn(fn => (mockServerStartedFn = fn)), + addShutdownHook: jest.fn(fn => (mockServerStoppedFn = fn)), + }); + + const service = new DefaultRootHealthService({ + lifecycle, + }); + + mockServerStartedFn(); + mockServerStoppedFn(); + await expect(service.getReadiness()).resolves.toEqual({ + status: 503, + payload: { + message: 'Backend has not started yet', + status: 'error', + }, + }); + }); + }); + + describe('liveness', () => { + it('should return 200 if the server has started', async () => { + const service = new DefaultRootHealthService({ + lifecycle: mockServices.rootLifecycle.mock(), + }); + + await expect(service.getLiveness()).resolves.toEqual({ + status: 200, + payload: { status: 'ok' }, + }); + }); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts new file mode 100644 index 0000000000..c8b4ad5394 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHealth/rootHealthServiceFactory.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2024 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 { + RootHealthService, + RootLifecycleService, + coreServices, + createServiceFactory, +} from '@backstage/backend-plugin-api'; + +/** @internal */ +export class DefaultRootHealthService implements RootHealthService { + #isRunning = false; + + constructor(readonly options: { lifecycle: RootLifecycleService }) { + options.lifecycle.addStartupHook(() => { + this.#isRunning = true; + }); + options.lifecycle.addShutdownHook(() => { + this.#isRunning = false; + }); + } + + async getLiveness(): Promise<{ status: number; payload?: any }> { + return { status: 200, payload: { status: 'ok' } }; + } + + async getReadiness(): Promise<{ status: number; payload?: any }> { + if (!this.#isRunning) { + return { + status: 503, + payload: { message: 'Backend has not started yet', status: 'error' }, + }; + } + + return { status: 200, payload: { status: 'ok' } }; + } +} + +/** + * @public + */ +export const rootHealthServiceFactory = createServiceFactory({ + service: coreServices.rootHealth, + deps: { + lifecycle: coreServices.rootLifecycle, + }, + async factory({ lifecycle }) { + return new DefaultRootHealthService({ lifecycle }); + }, +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts new file mode 100644 index 0000000000..3772ab0fab --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/createHealthRouter.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 { RootHealthService } from '@backstage/backend-plugin-api'; +import Router from 'express-promise-router'; +import { Request, Response } from 'express'; + +/** + * @public + */ +export function createHealthRouter(options: { health: RootHealthService }) { + const router = Router(); + + router.get( + '/.backstage/health/v1/readiness', + async (_request: Request, response: Response) => { + const { status, payload } = await options.health.getReadiness(); + response.status(status).json(payload); + }, + ); + + router.get( + '/.backstage/health/v1/liveness', + async (_request: Request, response: Response) => { + const { status, payload } = await options.health.getLiveness(); + response.status(status).json(payload); + }, + ); + + return router; +} diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts index 9c2d96dab2..e9aee8a0e0 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/index.ts @@ -18,6 +18,7 @@ export { DefaultRootHttpRouter, type DefaultRootHttpRouterOptions, } from './DefaultRootHttpRouter'; +export { createHealthRouter } from './createHealthRouter'; export * from './http'; export { rootHttpRouterServiceFactory, diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts new file mode 100644 index 0000000000..dea0159dff --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright 2022 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 { + ServiceFactoryTester, + mockServices, +} from '@backstage/backend-test-utils'; +import { Express } from 'express'; +import request from 'supertest'; +import { rootHttpRouterServiceFactory } from './rootHttpRouterServiceFactory'; +import { coreServices } from '@backstage/backend-plugin-api'; + +describe('rootHttpRouterServiceFactory', () => { + it('should make the health endpoints available', async () => { + let app: Express | undefined = undefined; + + const tester = ServiceFactoryTester.from( + rootHttpRouterServiceFactory({ + configure(options) { + options.applyDefaults(); + app = options.app; + }, + }), + { + dependencies: [ + mockServices.rootConfig.factory({ + data: { + app: { baseUrl: 'http://localhost' }, + backend: { + baseUrl: 'http://localhost', + listen: { host: '', port: 0 }, + }, + }, + }), + ], + }, + ); + + // Trigger creation of the http service, accessing the app instance through the configure callback + await tester.getSubject(); + + await request(app!) + .get('/.backstage/health/v1/liveness') + .expect(200, { status: 'ok' }); + + await request(app!).get('/.backstage/health/v1/readiness').expect(503, { + message: 'Backend has not started yet', + status: 'error', + }); + + const lifecycle = await tester.getService(coreServices.rootLifecycle); + + await (lifecycle as any).startup(); // Trigger startup by calling the private startup method + + await request(app!).get('/.backstage/health/v1/readiness').expect(200, { + status: 'ok', + }); + + expect('test').toBe('test'); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index ea3dc42eb7..c3a97737e4 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -29,6 +29,7 @@ import { readHttpServerOptions, } from './http'; import { DefaultRootHttpRouter } from './DefaultRootHttpRouter'; +import { createHealthRouter } from './createHealthRouter'; /** * @public @@ -41,6 +42,7 @@ export interface RootHttpRouterConfigureContext { config: RootConfigService; logger: LoggerService; lifecycle: LifecycleService; + healthRouter: RequestHandler; applyDefaults: () => void; } @@ -67,16 +69,18 @@ function defaultConfigure({ applyDefaults }: RootHttpRouterConfigureContext) { applyDefaults(); } -/** @public */ -export const rootHttpRouterServiceFactory = createServiceFactory( - (options?: RootHttpRouterFactoryOptions) => ({ +const rootHttpRouterServiceFactoryWithOptions = ( + options?: RootHttpRouterFactoryOptions, +) => + createServiceFactory({ service: coreServices.rootHttpRouter, deps: { config: coreServices.rootConfig, rootLogger: coreServices.rootLogger, lifecycle: coreServices.rootLifecycle, + health: coreServices.rootHealth, }, - async factory({ config, rootLogger, lifecycle }) { + async factory({ config, rootLogger, lifecycle, health }) { const { indexPath, configure = defaultConfigure } = options ?? {}; const logger = rootLogger.child({ service: 'rootHttpRouter' }); const app = express(); @@ -84,6 +88,8 @@ export const rootHttpRouterServiceFactory = createServiceFactory( const router = DefaultRootHttpRouter.create({ indexPath }); const middleware = MiddlewareFactory.create({ config, logger }); const routes = router.handler(); + + const healthRouter = createHealthRouter({ health }); const server = await createHttpServer( app, readHttpServerOptions(config.getOptionalConfig('backend')), @@ -98,11 +104,13 @@ export const rootHttpRouterServiceFactory = createServiceFactory( config, logger, lifecycle, + healthRouter, applyDefaults() { app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); app.use(middleware.logging()); + app.use(healthRouter); app.use(routes); app.use(middleware.notFound()); app.use(middleware.error()); @@ -115,5 +123,10 @@ export const rootHttpRouterServiceFactory = createServiceFactory( return router; }, - }), + })(); + +/** @public */ +export const rootHttpRouterServiceFactory = Object.assign( + rootHttpRouterServiceFactoryWithOptions, + rootHttpRouterServiceFactoryWithOptions(), ); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts index c5c4c8708a..3e0782d4a3 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.test.ts @@ -347,6 +347,7 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + expect(parseDuration({ trigger: 'manual' })).toEqual('manual'); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts index 62b36e024c..f8d06baf56 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/PluginTaskSchedulerImpl.ts @@ -152,9 +152,12 @@ export class PluginTaskSchedulerImpl implements SchedulerService { export function parseDuration( frequency: SchedulerServiceTaskScheduleDefinition['frequency'], ): string { - if ('cron' in frequency) { + if (typeof frequency === 'object' && 'cron' in frequency) { return frequency.cron; } + if (typeof frequency === 'object' && 'trigger' in frequency) { + return frequency.trigger; + } const parsed = Duration.isDuration(frequency) ? frequency diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts index 1cafe00670..5db82f3ece 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.test.ts @@ -503,4 +503,32 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is not set for manually-triggered tasks, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'manual', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + const row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.next_run_start_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts index a1c4ec44fd..c5570a8e30 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskWorker.ts @@ -53,8 +53,8 @@ export class TaskWorker { ); let workCheckFrequency = this.workCheckFrequency; - const isCron = !settings?.cadence.startsWith('P'); - if (!isCron) { + const isDuration = settings?.cadence.startsWith('P'); + if (isDuration) { const cadence = Duration.fromISO(settings.cadence); if (cadence < workCheckFrequency) { workCheckFrequency = cadence; @@ -175,7 +175,9 @@ export class TaskWorker { // read it back again. taskSettingsV2Schema.parse(settings); - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let startAt: Knex.Raw | undefined; let nextStartAt: Knex.Raw | undefined; @@ -194,6 +196,9 @@ export class TaskWorker { nextStartAt = this.nextRunAtRaw(time); startAt ||= nextStartAt; + } else if (isManual) { + nextStartAt = this.knex.raw('null'); + startAt ||= nextStartAt; } else { startAt ||= this.knex.fn.now(); nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); @@ -317,7 +322,9 @@ export class TaskWorker { ticket: string, settings: TaskSettingsV2, ): Promise { - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let nextRun: Knex.Raw; if (isCron) { @@ -325,6 +332,8 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); nextRun = this.nextRunAtRaw(time); + } else if (isManual) { + nextRun = this.knex.raw('null'); } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts index 61e6c1d9a8..173c989543 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/types.ts @@ -40,6 +40,10 @@ function isValidCronFormat(c: string | undefined): boolean { } } +function isValidTrigger(t: string): boolean { + return t === 'manual'; +} + export const taskSettingsV1Schema = z.object({ version: z.literal(1), initialDelayDuration: z @@ -68,6 +72,11 @@ export const taskSettingsV2Schema = z.object({ cadence: z .string() .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidTrigger, { + message: "Invalid trigger, expecting 'manual'", + }), + ) .or( z.string().refine(isValidOptionalDurationString, { message: 'Invalid duration, expecting ISO Period', diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts index 7b889b3a0e..b16509cf96 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.test.ts @@ -22,7 +22,7 @@ describe('schedulerFactory', () => { it('creates sidecar database features', async () => { const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - const scheduler = await tester.get(); + const scheduler = await tester.getSubject(); await scheduler.scheduleTask({ id: 'task1', timeout: { seconds: 1 }, diff --git a/packages/backend-defaults/src/entrypoints/urlReader/index.ts b/packages/backend-defaults/src/entrypoints/urlReader/index.ts index cf927ea031..05271966c9 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/index.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/index.ts @@ -15,4 +15,7 @@ */ export * from './lib'; -export { urlReaderServiceFactory } from './urlReaderServiceFactory'; +export { + urlReaderServiceFactory, + urlReaderFactoriesServiceRef, +} from './urlReaderServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts index 7f02f1468f..5a2655ad1c 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsCodeCommitUrlReader.ts @@ -213,6 +213,7 @@ export class AwsCodeCommitUrlReader implements UrlReaderService { ); const codeCommit = new CodeCommitClient({ + customUserAgent: 'backstage-aws-codecommit-url-reader', region: region, credentials: credentials, }); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts index 27f60f5b77..324538add4 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AwsS3UrlReader.ts @@ -222,6 +222,7 @@ export class AwsS3UrlReader implements UrlReaderService { ); const s3 = new S3Client({ + customUserAgent: 'backstage-aws-s3-url-reader', region: region, credentials: credentials, endpoint: integration.config.endpoint, diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts index ce2385507b..f53a7b6c11 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/AzureUrlReader.test.ts @@ -26,7 +26,7 @@ import { import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -76,7 +76,7 @@ describe('AzureUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts index dc182af619..f5caade3fa 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketCloudUrlReader.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -56,7 +56,7 @@ describe('BitbucketCloudUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('readUrl', () => { it('should be able to readUrl via buffer without ETag', async () => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts index 2870138535..6af2858cde 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketServerUrlReader.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -51,7 +51,7 @@ const reader = new BitbucketServerUrlReader( describe('BitbucketServerUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('readTree', () => { const repoBuffer = fs.readFileSync( diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts index edd8d6d3fc..54880aaa75 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/BitbucketUrlReader.test.ts @@ -22,7 +22,7 @@ import { import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -104,7 +104,7 @@ describe('BitbucketUrlReader', () => { ); const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('readUrl', () => { it('should be able to readUrl via buffer without ETag', async () => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts index 38936806c6..f84422c564 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/FetchUrlReader.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -30,7 +30,7 @@ const fetchUrlReader = new FetchUrlReader(); describe('FetchUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts index 94627e074f..14ca4c018e 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GerritUrlReader.test.ts @@ -17,7 +17,7 @@ import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { NotModifiedError, NotFoundError } from '@backstage/errors'; @@ -92,7 +92,7 @@ const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { // eslint-disable-next-line jest/no-disabled-tests describe.skip('GerritUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { mockDir.clear(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts index 0b5983eefd..77c995c552 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GiteaUrlReader.test.ts @@ -16,7 +16,7 @@ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { GiteaIntegration, readGiteaConfig } from '@backstage/integration'; @@ -64,7 +64,7 @@ const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { describe('GiteaUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); afterAll(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts index f07ada0cc3..f1dd7c0dc6 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GithubUrlReader.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/integration'; import { createMockDirectory, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -74,7 +74,7 @@ const gheProcessor = new GithubUrlReader( describe('GithubUrlReader', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(mockDir.clear); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts index 46276fcdda..dd6a0970d2 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; import { rest } from 'msw'; @@ -72,7 +72,7 @@ describe('GitlabUrlReader', () => { beforeEach(mockDir.clear); const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('read', () => { beforeEach(() => { diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts index 615eeb20a2..35123da851 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/GitlabUrlReader.ts @@ -23,21 +23,21 @@ import { UrlReaderServiceSearchOptions, UrlReaderServiceSearchResponse, } from '@backstage/backend-plugin-api'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { + GitLabIntegration, + ScmIntegrations, getGitLabFileFetchUrl, getGitLabIntegrationRelativePath, getGitLabRequestOptions, - GitLabIntegration, - ScmIntegrations, } from '@backstage/integration'; -import fetch, { Response } from 'node-fetch'; import parseGitUrl from 'git-url-parse'; -import { Minimatch } from 'minimatch'; -import { Readable } from 'stream'; -import { NotFoundError, NotModifiedError } from '@backstage/errors'; -import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { trimEnd, trimStart } from 'lodash'; +import { Minimatch } from 'minimatch'; +import fetch, { Response } from 'node-fetch'; +import { Readable } from 'stream'; import { ReadUrlResponseFactory } from './ReadUrlResponseFactory'; +import { ReadTreeResponseFactory, ReaderFactory } from './types'; import { parseLastModified } from './util'; /** @@ -71,14 +71,14 @@ export class GitlabUrlReader implements UrlReaderService { url: string, options?: UrlReaderServiceReadUrlOptions, ): Promise { - const { etag, lastModifiedAfter, signal } = options ?? {}; + const { etag, lastModifiedAfter, signal, token } = options ?? {}; const builtUrl = await this.getGitlabFetchUrl(url); let response: Response; try { response = await fetch(builtUrl, { headers: { - ...getGitLabRequestOptions(this.integration.config).headers, + ...getGitLabRequestOptions(this.integration.config, token).headers, ...(etag && { 'If-None-Match': etag }), ...(lastModifiedAfter && { 'If-Modified-Since': lastModifiedAfter.toUTCString(), @@ -120,7 +120,7 @@ export class GitlabUrlReader implements UrlReaderService { url: string, options?: UrlReaderServiceReadTreeOptions, ): Promise { - const { etag, signal } = options ?? {}; + const { etag, signal, token } = options ?? {}; const { ref, full_name, filepath } = parseGitUrl(url); let repoFullName = full_name; @@ -147,7 +147,7 @@ export class GitlabUrlReader implements UrlReaderService { repoFullName, )}`, ).toString(), - getGitLabRequestOptions(this.integration.config), + getGitLabRequestOptions(this.integration.config, token), ); if (!projectGitlabResponse.ok) { const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`; @@ -175,7 +175,7 @@ export class GitlabUrlReader implements UrlReaderService { )}/repository/commits?${commitsReqParams.toString()}`, ).toString(), { - ...getGitLabRequestOptions(this.integration.config), + ...getGitLabRequestOptions(this.integration.config, token), // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can @@ -209,7 +209,7 @@ export class GitlabUrlReader implements UrlReaderService { repoFullName, )}/repository/archive?${archiveReqParams.toString()}`, { - ...getGitLabRequestOptions(this.integration.config), + ...getGitLabRequestOptions(this.integration.config, token), // TODO(freben): The signal cast is there because pre-3.x versions of // node-fetch have a very slightly deviating AbortSignal type signature. // The difference does not affect us in practice however. The cast can diff --git a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts index f1efbca108..004e1931d0 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/lib/HarnessUrlReader.test.ts @@ -16,7 +16,7 @@ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { HarnessIntegration, readHarnessConfig } from '@backstage/integration'; @@ -120,7 +120,7 @@ const handlers = [ describe('HarnessUrlReader', () => { const worker = setupServer(...handlers); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeAll(() => worker.listen({ onUnhandledRequest: 'bypass' })); afterAll(() => { jest.clearAllMocks(); diff --git a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index 810882edf8..2639e99d83 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -14,12 +14,36 @@ * limitations under the License. */ +import { ReaderFactory } from './lib'; import { UrlReaders } from './lib/UrlReaders'; import { coreServices, createServiceFactory, + createServiceRef, } from '@backstage/backend-plugin-api'; +/** + * @public + * A non-singleton reference to URL Reader factory services. + * + * @example + * Creating a service factory implementation for a Custom URL Reader. + * ```ts + * createServiceFactory({ + * service: urlReaderFactoriesServiceRef, + * deps: {}, + * async factory() { + * return CustomUrlReader.factory; + * }, + * }); + * ``` + */ +export const urlReaderFactoriesServiceRef = createServiceRef({ + id: 'core.urlReader.factories', + scope: 'plugin', + multiton: true, +}); + /** * Reading content from external systems. * @@ -34,11 +58,13 @@ export const urlReaderServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, + factories: urlReaderFactoriesServiceRef, }, - async factory({ config, logger }) { + async factory({ config, logger, factories }) { return UrlReaders.default({ config, logger, + factories, }); }, }); diff --git a/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts b/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts index b63b24fb18..692d179ee2 100644 --- a/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts +++ b/packages/backend-defaults/src/entrypoints/userInfo/DefaultUserInfoService.test.ts @@ -17,7 +17,7 @@ import { BackstageUserPrincipal } from '@backstage/backend-plugin-api'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { JsonObject } from '@backstage/types'; import { SignJWT, base64url, importJWK } from 'jose'; @@ -28,7 +28,7 @@ import { DefaultUserInfoService } from './DefaultUserInfoService'; describe('DefaultUserInfoService', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const mockPublicKey = { kty: 'EC', diff --git a/packages/backend-defaults/src/migrations.test.ts b/packages/backend-defaults/src/migrations.test.ts new file mode 100644 index 0000000000..579c32e626 --- /dev/null +++ b/packages/backend-defaults/src/migrations.test.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2024 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 { Knex } from 'knex'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import fs from 'fs'; + +const migrationsDir = `${__dirname}/../migrations/scheduler`; +const migrationsFiles = fs.readdirSync(migrationsDir).sort(); + +async function migrateUpOnce(knex: Knex): Promise { + await knex.migrate.up({ directory: migrationsDir }); +} + +async function migrateDownOnce(knex: Knex): Promise { + await knex.migrate.down({ directory: migrationsDir }); +} + +async function migrateUntilBefore(knex: Knex, target: string): Promise { + const index = migrationsFiles.indexOf(target); + if (index === -1) { + throw new Error(`Migration ${target} not found`); + } + for (let i = 0; i < index; i++) { + await migrateUpOnce(knex); + } +} + +jest.setTimeout(60_000); + +describe('migrations', () => { + const databases = TestDatabases.create(); + + it.each(databases.eachSupportedId())( + '20210928160613_init.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20210928160613_init.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.fn.now(), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: expect.anything(), + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + // This looks odd - you might expect a .toThrow at the end but that + // actually is flaky for some reason specifically on sqlite when + // performing multiple runs in sequence + await expect(knex('backstage_backend_tasks__tasks')).rejects.toEqual( + expect.anything(), + ); + + await knex.destroy(); + }, + ); + + it.each(databases.eachSupportedId())( + '20240712211735_nullable_next_run.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20240712211735_nullable_next_run.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: null, + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + await expect( + knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }), + ).rejects.toEqual(expect.anything()); + + await knex.destroy(); + }, + ); +}); diff --git a/packages/backend-dynamic-feature-service/CHANGELOG.md b/packages/backend-dynamic-feature-service/CHANGELOG.md index 4363805410..14111873a1 100644 --- a/packages/backend-dynamic-feature-service/CHANGELOG.md +++ b/packages/backend-dynamic-feature-service/CHANGELOG.md @@ -1,5 +1,162 @@ # @backstage/backend-dynamic-feature-service +## 0.2.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-app-api@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-app-node@0.1.23-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-backend@0.3.10-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-app-api@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-backend@0.3.10-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-search-common@1.2.13 + +## 0.2.15 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/backend-common@0.23.3 + - @backstage/cli-node@0.2.7 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-node@0.1.22 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-search-common@1.2.13 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-backend@0.3.9-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-search-common@1.2.12 + +## 0.2.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/backend-app-api@0.7.9-next.0 + - @backstage/plugin-app-node@0.1.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-events-backend@0.3.8-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-search-common@1.2.12 + ## 0.2.11 ### Patch Changes diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 20db7db71b..97dea60170 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -28,7 +28,7 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { Router } from 'express'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TaskRunner } from '@backstage/backend-tasks'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; @@ -114,18 +114,22 @@ export interface DynamicPluginsFactoryOptions { } // @public (undocumented) -export const dynamicPluginsFeatureDiscoveryServiceFactory: () => ServiceFactory< +export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, - 'root' + 'root', + 'singleton', + undefined >; // @public (undocumented) export const dynamicPluginsFrontendSchemas: BackendFeatureCompat; // @public (undocumented) -export const dynamicPluginsRootLoggerServiceFactory: () => ServiceFactory< +export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, - 'root' + 'root', + 'singleton', + undefined >; // @public (undocumented) @@ -142,19 +146,26 @@ export interface DynamicPluginsSchemasService { } // @public (undocumented) -export const dynamicPluginsSchemasServiceFactory: ( - options?: DynamicPluginsSchemasOptions | undefined, -) => ServiceFactory; +export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< + DynamicPluginsSchemasService, + 'root', + 'singleton', + DynamicPluginsSchemasOptions +>; // @public (undocumented) -export const dynamicPluginsServiceFactory: ( - options?: DynamicPluginsFactoryOptions | undefined, -) => ServiceFactory; +export const dynamicPluginsServiceFactory: ServiceFactoryCompat< + DynamicPluginProvider, + 'root', + 'singleton', + DynamicPluginsFactoryOptions +>; // @public (undocumented) export const dynamicPluginsServiceRef: ServiceRef< DynamicPluginProvider, - 'root' + 'root', + 'singleton' >; // @public (undocumented) diff --git a/packages/backend-dynamic-feature-service/package.json b/packages/backend-dynamic-feature-service/package.json index 91a7d96ce1..d6ac21a3ae 100644 --- a/packages/backend-dynamic-feature-service/package.json +++ b/packages/backend-dynamic-feature-service/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-dynamic-feature-service", "description": "Backstage dynamic feature service", - "version": "0.2.11", + "version": "0.2.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-legacy/CHANGELOG.md b/packages/backend-legacy/CHANGELOG.md index cfe705fcad..a601890055 100644 --- a/packages/backend-legacy/CHANGELOG.md +++ b/packages/backend-legacy/CHANGELOG.md @@ -1,5 +1,264 @@ # example-backend-legacy +## 0.2.101-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.2 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-events-backend@0.3.10-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.2.101-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.10-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.1 + - @backstage/plugin-scaffolder-backend@1.23.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.1 + - example-app@0.2.100-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-app-backend@0.3.72-next.1 + - @backstage/plugin-devtools-backend@0.3.9-next.1 + - @backstage/plugin-proxy-backend@0.5.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-backend@0.18.4-next.1 + - @backstage/plugin-permission-backend@0.5.47-next.1 + - @backstage/plugin-search-backend@1.5.15-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + - @backstage/plugin-events-backend@0.3.10-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.1 + - @backstage/plugin-signals-backend@0.1.9-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## 0.2.101-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.40-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/plugin-devtools-backend@0.3.9-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.33-next.0 + - example-app@0.2.100-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-app-backend@0.3.72-next.0 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.0 + - @backstage/plugin-events-backend@0.3.10-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-kubernetes-backend@0.18.4-next.0 + - @backstage/plugin-permission-backend@0.5.47-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-proxy-backend@0.5.4-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.24-next.0 + - @backstage/plugin-search-backend@1.5.15-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.4-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-signals-backend@0.1.9-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + - @backstage/plugin-techdocs-backend@1.10.10-next.0 + +## 0.2.100 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend@1.23.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-backend@0.5.46 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-techdocs-backend@1.10.9 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.39 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-search-backend@1.5.14 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-backend@0.3.71 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-devtools-backend@0.3.8 + - @backstage/plugin-events-backend@0.3.9 + - @backstage/plugin-kubernetes-backend@0.18.3 + - @backstage/plugin-proxy-backend@0.5.3 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.3 + - @backstage/plugin-search-backend-module-explore@0.1.28 + - @backstage/plugin-search-backend-module-pg@0.5.32 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-signals-backend@0.1.8 + - @backstage/plugin-signals-node@0.1.8 + - example-app@0.2.99 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.2.100-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + - example-app@0.2.99-next.2 + +## 0.2.100-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.9-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4-next.1 + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.27-next.1 + - example-app@0.2.99-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-app-backend@0.3.71-next.1 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-devtools-backend@0.3.8-next.1 + - @backstage/plugin-events-backend@0.3.9-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-kubernetes-backend@0.18.3-next.1 + - @backstage/plugin-permission-backend@0.5.46-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-proxy-backend@0.5.3-next.1 + - @backstage/plugin-scaffolder-backend@1.23.0-next.1 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.23-next.1 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.39-next.1 + - @backstage/plugin-search-backend@1.5.14-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.3-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.28-next.1 + - @backstage/plugin-search-backend-module-pg@0.5.32-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-signals-backend@0.1.8-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## 0.2.100-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.3-next.0 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.38-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-backend@1.23.0-next.0 + - @backstage/plugin-app-backend@0.3.70-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.19-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.8-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-devtools-backend@0.3.7-next.0 + - @backstage/plugin-events-backend@0.3.8-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-kubernetes-backend@0.18.2-next.0 + - @backstage/plugin-permission-backend@0.5.45-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-proxy-backend@0.5.2-next.0 + - @backstage/plugin-scaffolder-backend-module-confluence-to-markdown@0.2.22-next.0 + - @backstage/plugin-search-backend@1.5.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + - @backstage/plugin-search-backend-module-elasticsearch@1.5.2-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.27-next.0 + - @backstage/plugin-search-backend-module-pg@0.5.31-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.26-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/plugin-signals-backend@0.1.7-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/plugin-techdocs-backend@1.10.8-next.0 + - example-app@0.2.99-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-permission-common@0.7.14 + ## 0.2.99 ### Patch Changes diff --git a/packages/backend-legacy/package.json b/packages/backend-legacy/package.json index 1ee3a9bb31..a650924720 100644 --- a/packages/backend-legacy/package.json +++ b/packages/backend-legacy/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-legacy", - "version": "0.2.99", + "version": "0.2.101-next.2", "backstage": { "role": "backend" }, @@ -66,7 +66,7 @@ "@gitbeaker/node": "^35.1.0", "@octokit/rest": "^19.0.3", "azure-devops-node-api": "^12.0.0", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.0.0", "dockerode": "^4.0.0", "example-app": "link:../app", "express": "^4.17.1", diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md index d490618301..682036d984 100644 --- a/packages/backend-openapi-utils/CHANGELOG.md +++ b/packages/backend-openapi-utils/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/backend-openapi-utils +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/errors@1.2.4 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/errors@1.2.4 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/errors@1.2.4 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/errors@1.2.4 + ## 0.1.12 ### Patch Changes diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json index 8d4130c95d..35db691c25 100644 --- a/packages/backend-openapi-utils/package.json +++ b/packages/backend-openapi-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-openapi-utils", "description": "OpenAPI typescript support.", - "version": "0.1.12", + "version": "0.1.16-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index bbe2fb68e1..d404d05b33 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,248 @@ # @backstage/backend-plugin-api +## 0.8.0-next.2 + +### Minor Changes + +- 7c5f3b0: The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref. + + We're looking for ways to make it possible to augment services without the need to replace the entire service. + + Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets: + + ```diff + // @backstage/backend-defaults + + + export const urlReaderFactoriesServiceRef = createServiceRef({ + + id: 'core.urlReader.factories', + + scope: 'plugin', + + multiton: true, + + }); + + ... + + export const urlReaderServiceFactory = createServiceFactory({ + service: coreServices.urlReader, + deps: { + config: coreServices.rootConfig, + logger: coreServices.logger, + + factories: urlReaderFactoriesServiceRef, + }, + - async factory({ config, logger }) { + + async factory({ config, logger, factories }) { + return UrlReaders.default({ + config, + logger, + + factories, + }); + }, + }); + ``` + + With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like: + + ```ts + // packages/backend/index.ts + import { createServiceFactory } from '@backstage/backend-plugin-api'; + import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults'; + ... + + backend.add(createServiceFactory({ + service: urlReaderFactoriesServiceRef, + deps: {}, + async factory() { + return CustomUrlReader.factory; + }, + })); + + ... + + ``` + +### Patch Changes + +- 6061061: Added `createBackendFeatureLoader`, which can be used to create an installable backend feature that can in turn load in additional backend features in a dynamic way. +- ba9abf4: The `SchedulerService` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `SchedulerService.triggerTask`. +- 8b13183: Added `createBackendFeatureLoader`, which can be used to programmatically select and install backend features. + + A feature loader can return an list of features to be installed, for example in the form on an `Array` or other for of iterable, which allows for the loader to be defined as a generator function. Both synchronous and asynchronous loaders are supported. + + Additionally, a loader can depend on services in its implementation, with the restriction that it can only depend on root-scoped services, and it may not override services that have already been instantiated. + + ```ts + const searchLoader = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + *loader({ config }) { + // Example of a custom config flag to enable search + if (config.getOptionalString('customFeatureToggle.search')) { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + } + }, + }); + ``` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.7.1-next.1 + +### Patch Changes + +- f011d1b: fix typo in `getPluginRequestToken` comments +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + +## 0.7.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: The `PermissionsService` no longer supports passing the deprecated `token` option, and the request options are now required. + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- 062c01c: Deprecated the ability to define options for service factories through `createServiceFactory`. In the future all service factories will return a plain `ServiceFactory` object, rather than allowing users to pass options to the factory. To allow for customization of a service implementation one can instead export one or a few building blocks that allows for simple re-implementation of the service instead. + + For example, instead of: + + ```ts + export const fooServiceFactory = createServiceFactory( + (options?: { bar: string }) => ({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }), + ); + ``` + + We instead encourage service implementations to provide an easy to use API for re-implementing the service for advanced use-cases: + + ```ts + /** @public */ + export class DefaultFooService implements FooService { + static create(options: { bar: string; logger: LoggerService }) { + return new DefaultFooService(options.logger, options.bar ?? 'default'); + } + + private constructor( + private readonly logger: string, + private readonly bar: string, + ) {} + + // The rest of the implementation + } + ``` + + A user that wishes to customize the service can then easily do so by defining their own factory: + + ```ts + export const customFooServiceFactory = createServiceFactory({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return DefaultFooService.create({ logger, bar: 'baz' }); + }, + }); + ``` + + This is of course more verbose than the previous solution where the factory could be customized through `fooServiceFactory({ bar: 'baz' })`, but this is a simplified which in practice should be using static configuration instead. + + In cases where the old options patterns significantly improves the usability of the service factory, the old pattern can still be implemented like this: + + ```ts + const fooServiceFactoryWithOptions = (options?: { bar: string }) => + createServiceFactory({ + service: fooServiceRef, + deps: { logger: coreServices.logger }, + factory({ logger }) { + return { + // Implementation of the foo service using the `bar` option. + }; + }, + }); + + export const fooServiceFactory = Object.assign( + fooServiceFactoryWithOptions, + fooServiceFactoryWithOptions(), + ); + ``` + + This change is being made because the ability to define an options callback encourages bad design of services factories. When possible, a service should be configurable through static configuration, and the existence of options may discourage that. More importantly though, the existing options do not work well with the dependency injection system of services, which is a problem for callbacks an other more advanced options. This lead to a bad pattern where only a few explicit dependencies where made available in callbacks, rather than providing an API that allowed simple re-implementation of the service with full access to dependency injection. + + A separate benefit of this change is that it simplifies the TypeScript types in a way that allows TypeScript to provide a much better error message when a service factory doesn't properly implement the service interface. + +- fe47a3e: All service config types were renamed to option types in order to standardize frontend and backend `create*` function signatures: + + - The `ServiceRefConfig` type was renamed to`ServiceRefOptions`; + - The `RootServiceFactoryConfig` type was renamed to `RootServiceFactoryOptions`; + - The `PluginServiceFactoryConfig` type was renamed to `PluginServiceFactoryOptions` + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.6.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + +## 0.6.21-next.0 + +### Patch Changes + +- 53ced70: Added a new Root Health Service which adds new endpoints for health checks. +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + ## 0.6.19 ### Patch Changes diff --git a/packages/backend-plugin-api/api-report-alpha.md b/packages/backend-plugin-api/api-report-alpha.md index 81b378a671..0f4348336a 100644 --- a/packages/backend-plugin-api/api-report-alpha.md +++ b/packages/backend-plugin-api/api-report-alpha.md @@ -17,7 +17,8 @@ export interface FeatureDiscoveryService { // @alpha export const featureDiscoveryServiceRef: ServiceRef< FeatureDiscoveryService, - 'root' + 'root', + 'singleton' >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index 258cdefe1b..319ddf0bc6 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -9,6 +9,7 @@ import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common' import { AuthorizePermissionResponse } from '@backstage/plugin-permission-common'; import { Config } from '@backstage/config'; import { Duration } from 'luxon'; +import { EvaluatorRequestOptions } from '@backstage/plugin-permission-common'; import { Handler } from 'express'; import { HumanDuration } from '@backstage/types'; import { IdentityApi } from '@backstage/plugin-auth-node'; @@ -81,14 +82,12 @@ export interface BackendModuleRegistrationPoints { ): void; // (undocumented) registerInit< - Deps extends { - [name in string]: unknown; + TDeps extends { + [name in string]: ServiceRef | ExtensionPoint; }, >(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -104,14 +103,12 @@ export interface BackendPluginRegistrationPoints { ): void; // (undocumented) registerInit< - Deps extends { - [name in string]: unknown; + TDeps extends { + [name in string]: ServiceRef; }, >(options: { - deps: { - [name in keyof Deps]: ServiceRef; - }; - init(deps: Deps): Promise; + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -188,27 +185,73 @@ export type CacheServiceSetOptions = { // @public export namespace coreServices { - const auth: ServiceRef; - const userInfo: ServiceRef; - const cache: ServiceRef; - const rootConfig: ServiceRef; - const database: ServiceRef; - const discovery: ServiceRef; - const httpAuth: ServiceRef; - const httpRouter: ServiceRef; - const lifecycle: ServiceRef; - const logger: ServiceRef; - const permissions: ServiceRef; - const pluginMetadata: ServiceRef; - const rootHttpRouter: ServiceRef; - const rootLifecycle: ServiceRef; - const rootLogger: ServiceRef; - const scheduler: ServiceRef; + const auth: ServiceRef; + const userInfo: ServiceRef; + const cache: ServiceRef; + const rootConfig: ServiceRef; + const database: ServiceRef; + const discovery: ServiceRef; + const rootHealth: ServiceRef; + const httpAuth: ServiceRef; + const httpRouter: ServiceRef; + const lifecycle: ServiceRef; + const logger: ServiceRef; + const permissions: ServiceRef; + const pluginMetadata: ServiceRef< + PluginMetadataService, + 'plugin', + 'singleton' + >; + const rootHttpRouter: ServiceRef; + const rootLifecycle: ServiceRef; + const rootLogger: ServiceRef; + const scheduler: ServiceRef; const // @deprecated - tokenManager: ServiceRef; - const urlReader: ServiceRef; + tokenManager: ServiceRef; + const urlReader: ServiceRef; const // @deprecated - identity: ServiceRef; + identity: ServiceRef; +} + +// @public +export function createBackendFeatureLoader< + TDeps extends { + [name in string]: unknown; + }, +>(options: CreateBackendFeatureLoaderOptions): BackendFeature; + +// @public +export interface CreateBackendFeatureLoaderOptions< + TDeps extends { + [name in string]: unknown; + }, +> { + // (undocumented) + deps?: { + [name in keyof TDeps]: ServiceRef; + }; + // (undocumented) + loader(deps: TDeps): + | Iterable< + | BackendFeature + | Promise<{ + default: BackendFeature; + }> + > + | Promise< + Iterable< + | BackendFeature + | Promise<{ + default: BackendFeature; + }> + > + > + | AsyncIterable< + | BackendFeature + | { + default: BackendFeature; + } + >; } // @public @@ -249,64 +292,92 @@ export interface CreateExtensionPointOptions { // @public export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( - config: RootServiceFactoryConfig, -): () => ServiceFactory; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; -// @public +// @public @deprecated export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( - config: (options?: TOpts) => RootServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; - -// @public -export function createServiceFactory< - TService, - TImpl extends TService, - TDeps extends { - [name in string]: ServiceRef; - }, - TContext = undefined, - TOpts extends object | undefined = undefined, ->( - config: PluginServiceFactoryConfig, -): () => ServiceFactory; - -// @public -export function createServiceFactory< - TService, - TImpl extends TService, - TDeps extends { - [name in string]: ServiceRef; - }, - TContext = undefined, - TOpts extends object | undefined = undefined, ->( - config: ( + options: ( options?: TOpts, - ) => PluginServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; + +// @public +export function createServiceFactory< + TService, + TInstances extends 'singleton' | 'multiton', + TImpl extends TService, + TDeps extends { + [name in string]: ServiceRef; + }, + TContext = undefined, + TOpts extends object | undefined = undefined, +>( + options: PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; + +// @public @deprecated +export function createServiceFactory< + TService, + TInstances extends 'singleton' | 'multiton', + TImpl extends TService, + TDeps extends { + [name in string]: ServiceRef; + }, + TContext = undefined, + TOpts extends object | undefined = undefined, +>( + options: ( + options?: TOpts, + ) => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public export function createServiceRef( - config: ServiceRefConfig, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - config: ServiceRefConfig, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; // @public export interface DatabaseService { @@ -420,31 +491,41 @@ export interface LoggerService { export interface PermissionsService extends PermissionEvaluator { authorize( requests: AuthorizePermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; authorizeConditional( requests: QueryPermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; } // @public -export type PermissionsServiceRequestOptions = - | { - token?: string; - } - | { - credentials: BackstageCredentials; - }; +export interface PermissionsServiceRequestOptions + extends EvaluatorRequestOptions { + // (undocumented) + credentials: BackstageCredentials; +} // @public export interface PluginMetadataService { getId(): string; } -// @public (undocumented) -export interface PluginServiceFactoryConfig< +// @public @deprecated (undocumented) +export type PluginServiceFactoryConfig< TService, + TInstances extends 'singleton' | 'multiton', + TContext, + TImpl extends TService, + TDeps extends { + [name in string]: ServiceRef; + }, +> = PluginServiceFactoryOptions; + +// @public (undocumented) +export interface PluginServiceFactoryOptions< + TService, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { @@ -464,7 +545,7 @@ export interface PluginServiceFactoryConfig< ): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -500,6 +581,18 @@ export function resolveSafeChildPath(base: string, path: string): string; // @public export interface RootConfigService extends Config {} +// @public (undocumented) +export interface RootHealthService { + getLiveness(): Promise<{ + status: number; + payload?: JsonValue; + }>; + getReadiness(): Promise<{ + status: number; + payload?: JsonValue; + }>; +} + // @public export interface RootHttpRouterService { use(path: string, handler: Handler): void; @@ -511,9 +604,20 @@ export interface RootLifecycleService extends LifecycleService {} // @public export interface RootLoggerService extends LoggerService {} -// @public (undocumented) -export interface RootServiceFactoryConfig< +// @public @deprecated (undocumented) +export type RootServiceFactoryConfig< TService, + TInstances extends 'singleton' | 'multiton', + TImpl extends TService, + TDeps extends { + [name in string]: ServiceRef; + }, +> = RootServiceFactoryOptions; + +// @public (undocumented) +export interface RootServiceFactoryOptions< + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -525,7 +629,7 @@ export interface RootServiceFactoryConfig< factory(deps: ServiceRefsToInstances): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -574,7 +678,10 @@ export interface SchedulerServiceTaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + | { + trigger: 'manual'; + }; initialDelay?: Duration | HumanDuration; scope?: 'global' | 'local'; timeout: Duration | HumanDuration; @@ -587,7 +694,14 @@ export interface SchedulerServiceTaskScheduleDefinitionConfig { cron: string; } | string - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { + trigger: 'manual'; + }; initialDelay?: string | HumanDuration; scope?: 'global' | 'local'; timeout: string | HumanDuration; @@ -606,34 +720,67 @@ export type SearchResponseFile = UrlReaderServiceSearchResponseFile; export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > extends BackendFeature { // (undocumented) - service: ServiceRef; + service: ServiceRef; } -// @public +// @public @deprecated (undocumented) +export interface ServiceFactoryCompat< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', + TOpts extends object | undefined = undefined, +> extends ServiceFactory { + // @deprecated (undocumented) + ( + ...options: undefined extends TOpts ? [] : [options?: TOpts] + ): ServiceFactory; +} + +// @public @deprecated export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); // @public export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > = { id: string; scope: TScope; + multiton?: TInstances extends 'multiton' ? true : false; T: TService; $$type: '@backstage/ServiceRef'; }; +// @public @deprecated (undocumented) +export type ServiceRefConfig< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', +> = ServiceRefOptions; + // @public (undocumented) -export interface ServiceRefConfig { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', +> { // (undocumented) - defaultFactory?: ( + defaultFactory?( service: ServiceRef, - ) => Promise; + ): Promise; + // @deprecated (undocumented) + defaultFactory?( + service: ServiceRef, + ): Promise<() => ServiceFactory>; // (undocumented) id: string; // (undocumented) + multiton?: TInstances extends 'multiton' ? true : false; + // (undocumented) scope?: TScope; } diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index e8538cece7..61f8b9353d 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-plugin-api", - "version": "0.6.19", + "version": "0.8.0-next.2", "description": "Core API used by Backstage backend plugins", "backstage": { "role": "node-library" diff --git a/packages/backend-plugin-api/src/deprecated.ts b/packages/backend-plugin-api/src/deprecated.ts new file mode 100644 index 0000000000..0d02f9ed7e --- /dev/null +++ b/packages/backend-plugin-api/src/deprecated.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2024 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 { + ServiceRef, + ServiceRefOptions, + RootServiceFactoryOptions, + PluginServiceFactoryOptions, +} from './services'; + +/** + * @public + * @deprecated Use {@link ServiceRefOptions} instead + */ +export type ServiceRefConfig< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', +> = ServiceRefOptions; + +/** + * @public + * @deprecated Use {@link RootServiceFactoryOptions} instead + */ +export type RootServiceFactoryConfig< + TService, + TInstances extends 'singleton' | 'multiton', + TImpl extends TService, + TDeps extends { [name in string]: ServiceRef }, +> = RootServiceFactoryOptions; + +/** + * @public + * @deprecated Use {@link PluginServiceFactoryOptions} instead + */ +export type PluginServiceFactoryConfig< + TService, + TInstances extends 'singleton' | 'multiton', + TContext, + TImpl extends TService, + TDeps extends { [name in string]: ServiceRef }, +> = PluginServiceFactoryOptions; diff --git a/packages/backend-plugin-api/src/index.ts b/packages/backend-plugin-api/src/index.ts index e6e57db1dc..eb554db25d 100644 --- a/packages/backend-plugin-api/src/index.ts +++ b/packages/backend-plugin-api/src/index.ts @@ -24,3 +24,4 @@ export * from './services'; export type { BackendFeature, BackendFeatureCompat } from './types'; export * from './paths'; export * from './wiring'; +export * from './deprecated'; diff --git a/packages/backend-plugin-api/src/services/definitions/AuthService.ts b/packages/backend-plugin-api/src/services/definitions/AuthService.ts index dce5e9be3b..1e0cf6e56b 100644 --- a/packages/backend-plugin-api/src/services/definitions/AuthService.ts +++ b/packages/backend-plugin-api/src/services/definitions/AuthService.ts @@ -208,7 +208,7 @@ export interface AuthService { * * @remarks * - * This method should be called before each request. Do not cold on to the + * This method should be called before each request. Do not hold on to the * issued token and reuse it for future calls. */ getPluginRequestToken(options: { diff --git a/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts b/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts index c5046a988b..a25081d17a 100644 --- a/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts +++ b/packages/backend-plugin-api/src/services/definitions/PermissionsService.ts @@ -17,6 +17,7 @@ import { AuthorizePermissionRequest, AuthorizePermissionResponse, + EvaluatorRequestOptions, PermissionEvaluator, QueryPermissionRequest, QueryPermissionResponse, @@ -24,18 +25,14 @@ import { import { BackstageCredentials } from './AuthService'; /** - * Options for {@link @backstage/plugin-permission-common#PermissionEvaluator} requests. + * Options for {@link PermissionsService} requests. * * @public */ -export type PermissionsServiceRequestOptions = - | { - /** @deprecated use the `credentials` option instead. */ - token?: string; - } - | { - credentials: BackstageCredentials; - }; +export interface PermissionsServiceRequestOptions + extends EvaluatorRequestOptions { + credentials: BackstageCredentials; +} /** * Permission system integration for authorization of user/service actions. @@ -59,7 +56,7 @@ export interface PermissionsService extends PermissionEvaluator { */ authorize( requests: AuthorizePermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; /** @@ -79,6 +76,6 @@ export interface PermissionsService extends PermissionEvaluator { */ authorizeConditional( requests: QueryPermissionRequest[], - options?: PermissionsServiceRequestOptions, + options: PermissionsServiceRequestOptions, ): Promise; } diff --git a/packages/backend-plugin-api/src/services/definitions/RootHealthService.ts b/packages/backend-plugin-api/src/services/definitions/RootHealthService.ts new file mode 100644 index 0000000000..e6c56e8654 --- /dev/null +++ b/packages/backend-plugin-api/src/services/definitions/RootHealthService.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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 { JsonValue } from '@backstage/types'; + +/** + * @public + */ +export interface RootHealthService { + /** + * Get the liveness status of the backend. + */ + getLiveness(): Promise<{ status: number; payload?: JsonValue }>; + /** + * Get the readiness status of the backend. + */ + getReadiness(): Promise<{ status: number; payload?: JsonValue }>; +} diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts index 7b20487af4..875e24786f 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.test.ts @@ -16,7 +16,6 @@ import { ConfigReader } from '@backstage/config'; import { HumanDuration } from '@backstage/types'; -import { Duration } from 'luxon'; import { readSchedulerServiceTaskScheduleDefinitionFromConfig } from './SchedulerService'; describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => { @@ -35,7 +34,7 @@ describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => { const result = readSchedulerServiceTaskScheduleDefinitionFromConfig(config); expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *'); - expect(result.timeout).toEqual(Duration.fromISO('PT3M')); + expect(result.timeout).toEqual({ minutes: 3 }); expect((result.initialDelay as HumanDuration).minutes).toEqual(20); expect(result.scope).toBe('global'); }); @@ -51,7 +50,7 @@ describe('readSchedulerServiceTaskScheduleDefinitionFromConfig', () => { const result = readSchedulerServiceTaskScheduleDefinitionFromConfig(config); expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *'); - expect(result.timeout).toEqual(Duration.fromISO('PT3M')); + expect(result.timeout).toEqual({ minutes: 3 }); expect(result.initialDelay).toBeUndefined(); expect(result.scope).toBeUndefined(); }); diff --git a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts index 91edf27ad6..a8b84b9084 100644 --- a/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SchedulerService.ts @@ -96,7 +96,8 @@ export interface SchedulerServiceTaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -193,7 +194,12 @@ export interface SchedulerServiceTaskScheduleDefinitionConfig { cron: string; } | string - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -348,27 +354,33 @@ export interface SchedulerService { getScheduledTasks(): Promise; } -function readDuration(config: Config, key: string): Duration | HumanDuration { +function readDuration(config: Config, key: string): HumanDuration { if (typeof config.get(key) === 'string') { const value = config.getString(key); const duration = Duration.fromISO(value); if (!duration.isValid) { throw new Error(`Invalid duration: ${value}`); } - return duration; + return duration.toObject(); } return readDurationFromConfig(config, { key }); } -function readCronOrDuration( +function readFrequency( config: Config, key: string, -): { cron: string } | Duration | HumanDuration { +): { cron: string } | HumanDuration | { trigger: 'manual' } { const value = config.get(key); if (typeof value === 'object' && (value as { cron?: string }).cron) { return value as { cron: string }; } + if ( + typeof value === 'object' && + (value as { trigger?: string }).trigger === 'manual' + ) { + return { trigger: 'manual' }; + } return readDuration(config, key); } @@ -383,7 +395,7 @@ function readCronOrDuration( export function readSchedulerServiceTaskScheduleDefinitionFromConfig( config: Config, ): SchedulerServiceTaskScheduleDefinition { - const frequency = readCronOrDuration(config, 'frequency'); + const frequency = readFrequency(config, 'frequency'); const timeout = readDuration(config, 'timeout'); const initialDelay = config.has('initialDelay') diff --git a/packages/backend-plugin-api/src/services/definitions/coreServices.ts b/packages/backend-plugin-api/src/services/definitions/coreServices.ts index b0518fb9d4..33d1da2903 100644 --- a/packages/backend-plugin-api/src/services/definitions/coreServices.ts +++ b/packages/backend-plugin-api/src/services/definitions/coreServices.ts @@ -102,6 +102,13 @@ export namespace coreServices { import('./DiscoveryService').DiscoveryService >({ id: 'core.discovery' }); + /** + * The service reference for the plugin scoped {@link RootHealthService}. + */ + export const rootHealth = createServiceRef< + import('./RootHealthService').RootHealthService + >({ id: 'core.rootHealth', scope: 'root' }); + /** * Authentication of HTTP requests. * diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index 6f30c5f980..5e30d444fd 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -32,6 +32,7 @@ export type { export type { RootConfigService } from './RootConfigService'; export type { DatabaseService } from './DatabaseService'; export type { DiscoveryService } from './DiscoveryService'; +export type { RootHealthService } from './RootHealthService'; export type { HttpRouterService, HttpRouterServiceAuthPolicy, diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 2b91758cbc..c2d3b9c6fc 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -16,10 +16,11 @@ export type { ServiceRef, - ServiceRefConfig, + ServiceRefOptions, ServiceFactory, - PluginServiceFactoryConfig, - RootServiceFactoryConfig, + PluginServiceFactoryOptions, + RootServiceFactoryOptions, ServiceFactoryOrFunction, + ServiceFactoryCompat, } from './types'; export { createServiceRef, createServiceFactory } from './types'; diff --git a/packages/backend-plugin-api/src/services/system/types.test.ts b/packages/backend-plugin-api/src/services/system/types.test.ts index adf097761b..52322f998b 100644 --- a/packages/backend-plugin-api/src/services/system/types.test.ts +++ b/packages/backend-plugin-api/src/services/system/types.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createServiceFactory, createServiceRef } from './types'; +import { + InternalServiceFactory, + createServiceFactory, + createServiceRef, +} from './types'; const ref = createServiceRef({ id: 'x' }); const rootDep = createServiceRef({ id: 'y', scope: 'root' }); @@ -36,6 +40,10 @@ describe('createServiceFactory', () => { }, }); expect(metaFactory).toEqual(expect.any(Function)); + expect(metaFactory().$$type).toBe('@backstage/BackendFeature'); + expect((metaFactory() as InternalServiceFactory).featureType).toBe( + 'service', + ); expect(metaFactory().service).toBe(ref); // @ts-expect-error @@ -324,6 +332,24 @@ describe('createServiceFactory', () => { metaFactory(); }); + it('should support old service refs without a multiton field', () => { + const oldPluginDep = pluginDep as Omit; // Old refs don't have a multiton field + const metaFactory = createServiceFactory({ + service: ref, + deps: { + plugin: oldPluginDep, + }, + async factory({ plugin }) { + const plugin1: boolean = plugin; + // @ts-expect-error + const plugin2: number = plugin; + unused(plugin1, plugin2); + return 'x'; + }, + }); + expect(metaFactory).toEqual(expect.any(Function)); + }); + it('should only allow objects as options', () => { // @ts-expect-error const metaFactory = createServiceFactory((_opts: string) => ({ diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 13252ba6e4..5bca592768 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -24,6 +24,7 @@ import { BackendFeature } from '../../types'; export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > = { id: string; @@ -38,6 +39,8 @@ export type ServiceRef< */ scope: TScope; + multiton?: TInstances extends 'multiton' ? true : false; + /** * Utility for getting the type of the service, using `typeof serviceRef.T`. * Attempting to actually read this value will result in an exception. @@ -51,16 +54,37 @@ export type ServiceRef< export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', > extends BackendFeature { - service: ServiceRef; + service: ServiceRef; +} + +/** + * @public + * @deprecated This type exists only as a helper for old code that relied on `createServiceFactory` to return `() => ServiceFactory` instead of `ServiceFactory`. You should remove the `()` parentheses at the end of your usages. This type will be removed in a future release. + */ +export interface ServiceFactoryCompat< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', + TOpts extends object | undefined = undefined, +> extends ServiceFactory { + /** + * @deprecated Callable service factories will be removed in a future release, please re-implement the service factory using the available APIs instead. If no options are being passed, you can simply remove the trailing `()`. + */ + ( + ...options: undefined extends TOpts ? [] : [options?: TOpts] + ): ServiceFactory; } /** @internal */ export interface InternalServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', -> extends ServiceFactory { + TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton', +> extends ServiceFactory { version: 'v1'; + featureType: 'service'; initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; createRootContext?(deps: { [key in string]: unknown }): Promise; @@ -73,17 +97,29 @@ export interface InternalServiceFactory< /** * Represents either a {@link ServiceFactory} or a function that returns one. * + * @deprecated The support for service factory functions is deprecated and will be removed. * @public */ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ -export interface ServiceRefConfig { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton', +> { id: string; scope?: TScope; - defaultFactory?: ( + multiton?: TInstances extends 'multiton' ? true : false; + defaultFactory?( service: ServiceRef, - ) => Promise; + ): Promise; + /** + * @deprecated The defaultFactory must return a plain `ServiceFactory` object, support for returning a function will be removed. + */ + defaultFactory?( + service: ServiceRef, + ): Promise<() => ServiceFactory>; } /** @@ -92,8 +128,8 @@ export interface ServiceRefConfig { * @public */ export function createServiceRef( - config: ServiceRefConfig, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create root scoped services. @@ -101,24 +137,46 @@ export function createServiceRef( * @public */ export function createServiceRef( - config: ServiceRefConfig, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; + +/** + * Creates a new service definition. This overload is used to create plugin scoped services. + * + * @public + */ export function createServiceRef( - config: ServiceRefConfig, -): ServiceRef { - const { id, scope = 'plugin', defaultFactory } = config; + options: ServiceRefOptions, +): ServiceRef; + +/** + * Creates a new service definition. This overload is used to create root scoped services. + * + * @public + */ +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; +export function createServiceRef< + TService, + TInstances extends 'singleton' | 'multiton', +>( + options: ServiceRefOptions, +): ServiceRef { + const { id, scope = 'plugin', multiton = false, defaultFactory } = options; return { id, scope, + multiton, get T(): TService { throw new Error(`tried to read ServiceRef.T of ${this}`); }, toString() { - return `serviceRef{${config.id}}`; + return `serviceRef{${options.id}}`; }, $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, - } as ServiceRef & { + } as ServiceRef & { __defaultFactory?: ( service: ServiceRef, ) => Promise | (() => ServiceFactory)>; @@ -130,12 +188,17 @@ type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, TScope extends 'root' | 'plugin' = 'root' | 'plugin', > = { - [key in keyof T as T[key]['scope'] extends TScope ? key : never]: T[key]['T']; + [key in keyof T as T[key]['scope'] extends TScope + ? key + : never]: T[key]['multiton'] extends true | undefined + ? Array + : T[key]['T']; }; /** @public */ -export interface RootServiceFactoryConfig< - TService, +export interface RootServiceFactoryOptions< + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { @@ -150,14 +213,15 @@ export interface RootServiceFactoryConfig< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; } /** @public */ -export interface PluginServiceFactoryConfig< +export interface PluginServiceFactoryOptions< TService, + TInstances extends 'singleton' | 'multiton', TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, @@ -173,7 +237,7 @@ export interface PluginServiceFactoryConfig< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; createRootContext?( deps: ServiceRefsToInstances, @@ -188,97 +252,152 @@ export interface PluginServiceFactoryConfig< * Creates a root scoped service factory without options. * * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - config: RootServiceFactoryConfig, -): () => ServiceFactory; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a root scoped service factory with optional options. * + * @deprecated The ability to define options for service factories is deprecated + * and will be removed. Please use the non-callback form of createServiceFactory + * and provide an API that allows for a simple re-implementation of the service + * factory instead. * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - config: (options?: TOpts) => RootServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; + options: ( + options?: TOpts, + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory without options. * * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, TOpts extends object | undefined = undefined, >( - config: PluginServiceFactoryConfig, -): () => ServiceFactory; + options: PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory with optional options. * + * @deprecated The ability to define options for service factories is deprecated + * and will be removed. Please use the non-callback form of createServiceFactory + * and provide an API that allows for a simple re-implementation of the service + * factory instead. * @public - * @param config - The service factory configuration. + * @param options - The service factory configuration. */ export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, TOpts extends object | undefined = undefined, >( - config: ( + options: ( options?: TOpts, - ) => PluginServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; + ) => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; export function createServiceFactory< TService, + TInstances extends 'singleton' | 'multiton', TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext, TOpts extends object | undefined = undefined, >( - config: - | RootServiceFactoryConfig - | PluginServiceFactoryConfig - | ((options: TOpts) => RootServiceFactoryConfig) + options: + | RootServiceFactoryOptions + | PluginServiceFactoryOptions | (( options: TOpts, - ) => PluginServiceFactoryConfig) - | (() => RootServiceFactoryConfig) - | (() => PluginServiceFactoryConfig), -): (options: TOpts) => ServiceFactory { - const configCallback = typeof config === 'function' ? config : () => config; + ) => RootServiceFactoryOptions) + | (( + options: TOpts, + ) => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >) + | (() => RootServiceFactoryOptions) + | (() => PluginServiceFactoryOptions< + TService, + TInstances, + TContext, + TImpl, + TDeps + >), +): ServiceFactoryCompat< + TService, + 'root' | 'plugin', + 'singleton' | 'multiton', + TOpts +> { + const configCallback = + typeof options === 'function' ? options : () => options; const factory = ( - options: TOpts, + o?: TOpts, ): InternalServiceFactory => { - const anyConf = configCallback(options); + const anyConf = configCallback(o!); if (anyConf.service.scope === 'root') { - const c = anyConf as RootServiceFactoryConfig; + const c = anyConf as RootServiceFactoryOptions< + TService, + TInstances, + TImpl, + TDeps + >; return { $$type: '@backstage/BackendFeature', version: 'v1', + featureType: 'service', service: c.service, initialization: c.initialization, deps: c.deps, - factory: async (deps: TDeps) => c.factory(deps), + factory: async (deps: ServiceRefsToInstances) => + c.factory(deps), }; } - const c = anyConf as PluginServiceFactoryConfig< + const c = anyConf as PluginServiceFactoryOptions< TService, + TInstances, TContext, TImpl, TDeps @@ -286,20 +405,26 @@ export function createServiceFactory< return { $$type: '@backstage/BackendFeature', version: 'v1', + featureType: 'service', service: c.service, initialization: c.initialization, ...('createRootContext' in c ? { - createRootContext: async (deps: TDeps) => - c?.createRootContext?.(deps), + createRootContext: async ( + deps: ServiceRefsToInstances, + ) => c?.createRootContext?.(deps), } : {}), deps: c.deps, - factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), + factory: async (deps: ServiceRefsToInstances, ctx: TContext) => + c.factory(deps, ctx), }; }; - factory.$$type = '@backstage/BackendFeatureFactory'; - - return factory; + // This constructs the `ServiceFactoryCompat` type, which is both a plain + // factory object as well as a function that can be called to construct a + // factory, potentially with options. In the future only the plain factory + // form will be supported, but for now we need to allow callers to call the + // factory too. + return Object.assign(factory, factory(undefined as TOpts)); } diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts new file mode 100644 index 0000000000..37fc079551 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.test.ts @@ -0,0 +1,195 @@ +/* + * Copyright 2022 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 { coreServices, createServiceFactory } from '../services'; +import { InternalServiceFactory } from '../services/system/types'; +import { BackendFeature } from '../types'; +import { createBackendFeatureLoader } from './createBackendFeatureLoader'; +import { createBackendPlugin } from './createBackendPlugin'; +import { InternalBackendFeatureLoader } from './types'; + +describe('createBackendFeatureLoader', () => { + it('should create an empty feature loader', () => { + const result = createBackendFeatureLoader({ + deps: {}, + loader: () => [], + }) as InternalBackendFeatureLoader; + + expect(result.$$type).toEqual('@backstage/BackendFeature'); + expect(result.version).toEqual('v1'); + expect(result.featureType).toEqual('loader'); + expect(result.deps).toEqual({}); + expect(result.loader).toEqual(expect.any(Function)); + expect(result.description).toMatch(/^created at '.*'$/); + }); + + it('should create a feature loader that loads a few features', async () => { + const result = createBackendFeatureLoader({ + deps: { + config: coreServices.rootConfig, + }, + loader({ config: _unused }) { + return [ + createBackendPlugin({ + pluginId: 'x', + register() {}, + })(), + createServiceFactory({ + service: coreServices.pluginMetadata, + deps: {}, + factory: () => ({ getId: () => 'fake-id' }), + })(), + // Dynamic import format + Promise.resolve({ + default: createBackendPlugin({ + pluginId: 'y', + register() {}, + })(), + }), + ]; + }, + }) as InternalBackendFeatureLoader; + + expect(result.$$type).toEqual('@backstage/BackendFeature'); + expect(result.version).toEqual('v1'); + expect(result.featureType).toEqual('loader'); + + const results = await result.loader({ config: {} }); + expect(results.length).toBe(3); + const [pluginX, serviceFactory, pluginY] = results; + expect(pluginX.$$type).toBe('@backstage/BackendFeature'); + expect(serviceFactory.$$type).toBe('@backstage/BackendFeature'); + expect(pluginY.$$type).toBe('@backstage/BackendFeature'); + expect((serviceFactory as InternalServiceFactory).service.id).toBe( + coreServices.pluginMetadata.id, + ); + }); + + it('should support multiple output formats', async () => { + const feature = createBackendPlugin({ pluginId: 'x', register() {} })(); + const dynamicFeature = Promise.resolve({ default: feature }); + + async function extractResult(f: BackendFeature) { + const internal = f as InternalBackendFeatureLoader; + return internal.loader({}); + } + + await expect( + extractResult( + createBackendFeatureLoader({ + loader() { + return [feature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async loader() { + return [feature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + *loader() { + yield feature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async *loader() { + yield feature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + loader() { + return [dynamicFeature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async loader() { + return [dynamicFeature]; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + *loader() { + yield dynamicFeature; + }, + }), + ), + ).resolves.toEqual([feature]); + + await expect( + extractResult( + createBackendFeatureLoader({ + async *loader() { + yield dynamicFeature; + }, + }), + ), + ).resolves.toEqual([feature]); + }); + + it('should only allow dependencies on root scoped services', () => { + createBackendFeatureLoader({ + deps: { + rootLogger: coreServices.rootLogger, + }, + loader: () => [], + }); + createBackendFeatureLoader({ + deps: { + // @ts-expect-error + logger: coreServices.logger, + }, + loader: () => [], + }); + createBackendFeatureLoader({ + deps: { + rootLogger: coreServices.rootLogger, + // @ts-expect-error + logger: coreServices.logger, + }, + loader: () => [], + }); + expect('test').toBe('test'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts new file mode 100644 index 0000000000..cfa3e96c65 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendFeatureLoader.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2024 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 { ServiceRef } from '../services'; +import { BackendFeature } from '../types'; +import { describeParentCallSite } from './describeParentCallSite'; +import { InternalBackendFeatureLoader } from './types'; + +/** + * @public + * Options for creating a new backend feature loader. + */ +export interface CreateBackendFeatureLoaderOptions< + TDeps extends { [name in string]: unknown }, +> { + deps?: { + [name in keyof TDeps]: ServiceRef; + }; + loader( + deps: TDeps, + ): + | Iterable> + | Promise>> + | AsyncIterable; +} + +/** + * @public + * Creates a new backend feature loader. + */ +export function createBackendFeatureLoader< + TDeps extends { [name in string]: unknown }, +>(options: CreateBackendFeatureLoaderOptions): BackendFeature { + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + featureType: 'loader', + description: `created at '${describeParentCallSite()}'`, + deps: options.deps, + async loader(deps: TDeps) { + const it = await options.loader(deps); + const result = new Array(); + for await (const item of it) { + if ('$$type' in item && item.$$type === '@backstage/BackendFeature') { + result.push(item); + } else if ('default' in item) { + result.push(item.default); + } else { + throw new Error(`Invalid item "${item}"`); + } + } + return result; + }, + } as InternalBackendFeatureLoader; +} diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts similarity index 52% rename from packages/backend-plugin-api/src/wiring/factories.test.ts rename to packages/backend-plugin-api/src/wiring/createBackendModule.test.ts index 6d5c1f39cd..3426d5a90c 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.test.ts @@ -14,59 +14,10 @@ * limitations under the License. */ -import { - createBackendModule, - createBackendPlugin, - createExtensionPoint, -} from './factories'; -import { InternalBackendFeature } from './types'; - -describe('createExtensionPoint', () => { - it('should create an ExtensionPoint', () => { - const extensionPoint = createExtensionPoint({ id: 'x' }); - expect(extensionPoint).toBeDefined(); - expect(extensionPoint.id).toBe('x'); - expect(() => extensionPoint.T).not.toThrow(); - expect(String(extensionPoint)).toBe('extensionPoint{x}'); - }); -}); - -describe('createBackendPlugin', () => { - it('should create a BackendPlugin', () => { - const result = createBackendPlugin({ - pluginId: 'x', - register(r) { - r.registerInit({ deps: {}, async init() {} }); - }, - }); - - // legacy form - const legacy = result() as unknown as InternalBackendFeature; - expect(legacy.$$type).toEqual('@backstage/BackendFeature'); - expect(legacy.version).toEqual('v1'); - expect(legacy.getRegistrations).toEqual(expect.any(Function)); - - // new form - const plugin = result as unknown as InternalBackendFeature; - expect(plugin.$$type).toEqual('@backstage/BackendFeature'); - expect(plugin.version).toEqual('v1'); - expect(plugin.getRegistrations).toEqual(expect.any(Function)); - expect(plugin.getRegistrations()).toEqual([ - { - type: 'plugin', - pluginId: 'x', - extensionPoints: [], - init: { - deps: expect.any(Object), - func: expect.any(Function), - }, - }, - ]); - - // @ts-expect-error - expect(plugin({ a: 'a' })).toBeDefined(); - }); -}); +import { createServiceRef } from '../services'; +import { createBackendModule } from './createBackendModule'; +import { createExtensionPoint } from './createExtensionPoint'; +import { InternalBackendRegistrations } from './types'; describe('createBackendModule', () => { it('should create a BackendModule', () => { @@ -79,13 +30,13 @@ describe('createBackendModule', () => { }); // legacy form - const legacy = result() as unknown as InternalBackendFeature; + const legacy = result() as unknown as InternalBackendRegistrations; expect(legacy.$$type).toEqual('@backstage/BackendFeature'); expect(legacy.version).toEqual('v1'); expect(legacy.getRegistrations).toEqual(expect.any(Function)); // new form - const module = result as unknown as InternalBackendFeature; + const module = result as unknown as InternalBackendRegistrations; expect(module.$$type).toEqual('@backstage/BackendFeature'); expect(module.version).toEqual('v1'); expect(module.getRegistrations).toEqual(expect.any(Function)); @@ -105,4 +56,35 @@ describe('createBackendModule', () => { // @ts-expect-error expect(module({ a: 'a' })).toBeDefined(); }); + + it('should be able to depend on all types of dependencies', () => { + const extensionPoint = createExtensionPoint({ id: 'point' }); + const singleServiceRef = createServiceRef({ id: 'single' }); + const multiServiceRef = createServiceRef({ + id: 'multi', + multiton: true, + }); + + const plugin = createBackendModule({ + pluginId: 'x', + moduleId: 'y', + register(r) { + r.registerInit({ + deps: { + point: extensionPoint, + single: singleServiceRef, + multi: multiServiceRef, + }, + async init({ point, single, multi }) { + const a: string = point; + const b: string = single; + const c: string[] = multi; + expect([a, b, c]).toBe('unused'); + }, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); }); diff --git a/packages/backend-plugin-api/src/wiring/createBackendModule.ts b/packages/backend-plugin-api/src/wiring/createBackendModule.ts new file mode 100644 index 0000000000..56f041606e --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendModule.ts @@ -0,0 +1,107 @@ +/* + * Copyright 2022 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 { BackendFeatureCompat } from '../types'; +import { + BackendModuleRegistrationPoints, + InternalBackendModuleRegistration, + InternalBackendPluginRegistration, +} from './types'; + +/** + * The configuration options passed to {@link createBackendModule}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateBackendModuleOptions { + /** + * Should exactly match the `id` of the plugin that the module extends. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + + /** + * The ID of this module, used to identify the module and ensure that it is not installed twice. + */ + moduleId: string; + register(reg: BackendModuleRegistrationPoints): void; +} + +/** + * Creates a new backend module for a given plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export function createBackendModule( + options: CreateBackendModuleOptions, +): BackendFeatureCompat { + function getRegistrations() { + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendModuleRegistration['init'] | undefined = undefined; + + options.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error('registerExtensionPoint called after registerInit'); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, + ); + } + + return [ + { + type: 'module', + pluginId: options.pluginId, + moduleId: options.moduleId, + extensionPoints, + init, + }, + ]; + } + + function backendFeatureCompatWrapper() { + return backendFeatureCompatWrapper; + } + + Object.assign(backendFeatureCompatWrapper, { + $$type: '@backstage/BackendFeature' as const, + version: 'v1', + getRegistrations, + }); + + return backendFeatureCompatWrapper as BackendFeatureCompat; +} diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts new file mode 100644 index 0000000000..f4a0877b85 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright 2022 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 { createServiceRef } from '../services'; +import { createBackendPlugin } from './createBackendPlugin'; +import { createExtensionPoint } from './createExtensionPoint'; +import { InternalBackendRegistrations } from './types'; + +describe('createBackendPlugin', () => { + it('should create a BackendPlugin', () => { + const result = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, + }); + + // legacy form + const legacy = result() as unknown as InternalBackendRegistrations; + expect(legacy.$$type).toEqual('@backstage/BackendFeature'); + expect(legacy.version).toEqual('v1'); + expect(legacy.getRegistrations).toEqual(expect.any(Function)); + + // new form + const plugin = result as unknown as InternalBackendRegistrations; + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + expect(plugin.version).toEqual('v1'); + expect(plugin.getRegistrations).toEqual(expect.any(Function)); + expect(plugin.getRegistrations()).toEqual([ + { + type: 'plugin', + pluginId: 'x', + extensionPoints: [], + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); + + // @ts-expect-error + expect(plugin({ a: 'a' })).toBeDefined(); + }); + + it('should be able to depend on all compatible dependencies', () => { + const singleServiceRef = createServiceRef({ id: 'single' }); + const multiServiceRef = createServiceRef({ + id: 'multi', + multiton: true, + }); + + const plugin = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ + deps: { + single: singleServiceRef, + multi: multiServiceRef, + }, + async init({ single, multi }) { + const a: string = single; + const b: string[] = multi; + expect([a, b]).toBe('unused'); + }, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); + + it('should not be able to depend on extension points', () => { + const extensionPoint = createExtensionPoint({ id: 'point' }); + + const plugin = createBackendPlugin({ + pluginId: 'x', + register(r) { + r.registerInit({ + deps: { + // @ts-expect-error + point: extensionPoint, + }, + async init() {}, + }); + }, + }); + + expect(plugin.$$type).toEqual('@backstage/BackendFeature'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts b/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts new file mode 100644 index 0000000000..f5984ef24b --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createBackendPlugin.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2022 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 { BackendFeatureCompat } from '../types'; +import { + BackendPluginRegistrationPoints, + InternalBackendPluginRegistration, +} from './types'; + +/** + * The configuration options passed to {@link createBackendPlugin}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateBackendPluginOptions { + /** + * The ID of this plugin. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + pluginId: string; + register(reg: BackendPluginRegistrationPoints): void; +} + +/** + * Creates a new backend plugin. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export function createBackendPlugin( + options: CreateBackendPluginOptions, +): BackendFeatureCompat { + function getRegistrations() { + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendPluginRegistration['init'] | undefined = undefined; + + options.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error('registerExtensionPoint called after registerInit'); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${options.pluginId}`, + ); + } + + return [ + { + type: 'plugin', + pluginId: options.pluginId, + extensionPoints, + init, + }, + ]; + } + + function backendFeatureCompatWrapper() { + return backendFeatureCompatWrapper; + } + + Object.assign(backendFeatureCompatWrapper, { + $$type: '@backstage/BackendFeature' as const, + version: 'v1', + getRegistrations, + }); + + return backendFeatureCompatWrapper as BackendFeatureCompat; +} diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts new file mode 100644 index 0000000000..825063e845 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.test.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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 { createExtensionPoint } from './createExtensionPoint'; + +describe('createExtensionPoint', () => { + it('should create an ExtensionPoint', () => { + const extensionPoint = createExtensionPoint({ id: 'x' }); + expect(extensionPoint).toBeDefined(); + expect(extensionPoint.id).toBe('x'); + expect(() => extensionPoint.T).not.toThrow(); + expect(String(extensionPoint)).toBe('extensionPoint{x}'); + }); +}); diff --git a/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts new file mode 100644 index 0000000000..9dbae249ea --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/createExtensionPoint.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 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 { ExtensionPoint } from './types'; + +/** + * The configuration options passed to {@link createExtensionPoint}. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ +export interface CreateExtensionPointOptions { + /** + * The ID of this extension point. + * + * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} + */ + id: string; +} + +/** + * Creates a new backend extension point. + * + * @public + * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} + */ +export function createExtensionPoint( + options: CreateExtensionPointOptions, +): ExtensionPoint { + return { + id: options.id, + get T(): T { + if (process.env.NODE_ENV === 'test') { + // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken + return null as T; + } + throw new Error(`tried to read ExtensionPoint.T of ${this}`); + }, + toString() { + return `extensionPoint{${options.id}}`; + }, + $$type: '@backstage/ExtensionPoint', + }; +} diff --git a/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts new file mode 100644 index 0000000000..35603e33b0 --- /dev/null +++ b/packages/backend-plugin-api/src/wiring/describeParentCallSite.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 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. + */ + +// Single re-export to avoid doing this import in multiple places, but still +// avoid duplicate declarations because this one is a bit tricky. +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +export { describeParentCallSite } from '../../../frontend-plugin-api/src/routing/describeParentCallSite'; diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts deleted file mode 100644 index 81e2247152..0000000000 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2022 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 { BackendFeatureCompat } from '../types'; -import { - BackendModuleRegistrationPoints, - BackendPluginRegistrationPoints, - ExtensionPoint, - InternalBackendModuleRegistration, - InternalBackendPluginRegistration, -} from './types'; - -/** - * The configuration options passed to {@link createExtensionPoint}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateExtensionPointOptions { - /** - * The ID of this extension point. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - id: string; -} - -/** - * Creates a new backend extension point. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/extension-points | The architecture of extension points} - */ -export function createExtensionPoint( - options: CreateExtensionPointOptions, -): ExtensionPoint { - return { - id: options.id, - get T(): T { - if (process.env.NODE_ENV === 'test') { - // Avoid throwing errors so tests asserting extensions' properties cannot be easily broken - return null as T; - } - throw new Error(`tried to read ExtensionPoint.T of ${this}`); - }, - toString() { - return `extensionPoint{${options.id}}`; - }, - $$type: '@backstage/ExtensionPoint', - }; -} - -/** - * The configuration options passed to {@link createBackendPlugin}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateBackendPluginOptions { - /** - * The ID of this plugin. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - pluginId: string; - register(reg: BackendPluginRegistrationPoints): void; -} - -/** - * Creates a new backend plugin. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/plugins | The architecture of plugins} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export function createBackendPlugin( - options: CreateBackendPluginOptions, -): BackendFeatureCompat { - function getRegistrations() { - const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = - []; - let init: InternalBackendPluginRegistration['init'] | undefined = undefined; - - options.register({ - registerExtensionPoint(ext, impl) { - if (init) { - throw new Error('registerExtensionPoint called after registerInit'); - } - extensionPoints.push([ext, impl]); - }, - registerInit(regInit) { - if (init) { - throw new Error('registerInit must only be called once'); - } - init = { - deps: regInit.deps, - func: regInit.init, - }; - }, - }); - - if (!init) { - throw new Error( - `registerInit was not called by register in ${options.pluginId}`, - ); - } - - return [ - { - type: 'plugin', - pluginId: options.pluginId, - extensionPoints, - init, - }, - ]; - } - - function backendFeatureCompatWrapper() { - return backendFeatureCompatWrapper; - } - - Object.assign(backendFeatureCompatWrapper, { - $$type: '@backstage/BackendFeature' as const, - version: 'v1', - getRegistrations, - }); - - return backendFeatureCompatWrapper as BackendFeatureCompat; -} - -/** - * The configuration options passed to {@link createBackendModule}. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export interface CreateBackendModuleOptions { - /** - * Should exactly match the `id` of the plugin that the module extends. - * - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ - pluginId: string; - - /** - * The ID of this module, used to identify the module and ensure that it is not installed twice. - */ - moduleId: string; - register(reg: BackendModuleRegistrationPoints): void; -} - -/** - * Creates a new backend module for a given plugin. - * - * @public - * @see {@link https://backstage.io/docs/backend-system/architecture/modules | The architecture of modules} - * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} - */ -export function createBackendModule( - options: CreateBackendModuleOptions, -): BackendFeatureCompat { - function getRegistrations() { - const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = - []; - let init: InternalBackendModuleRegistration['init'] | undefined = undefined; - - options.register({ - registerExtensionPoint(ext, impl) { - if (init) { - throw new Error('registerExtensionPoint called after registerInit'); - } - extensionPoints.push([ext, impl]); - }, - registerInit(regInit) { - if (init) { - throw new Error('registerInit must only be called once'); - } - init = { - deps: regInit.deps, - func: regInit.init, - }; - }, - }); - - if (!init) { - throw new Error( - `registerInit was not called by register in ${options.moduleId} module for ${options.pluginId}`, - ); - } - - return [ - { - type: 'module', - pluginId: options.pluginId, - moduleId: options.moduleId, - extensionPoints, - init, - }, - ]; - } - - function backendFeatureCompatWrapper() { - return backendFeatureCompatWrapper; - } - - Object.assign(backendFeatureCompatWrapper, { - $$type: '@backstage/BackendFeature' as const, - version: 'v1', - getRegistrations, - }); - - return backendFeatureCompatWrapper as BackendFeatureCompat; -} diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index 5197cec050..95eea6c3d4 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -14,17 +14,17 @@ * limitations under the License. */ -import type { - CreateBackendPluginOptions, - CreateBackendModuleOptions, - CreateExtensionPointOptions, -} from './factories'; +import { type CreateBackendModuleOptions } from './createBackendModule'; +import { type CreateBackendPluginOptions } from './createBackendPlugin'; +import { type CreateExtensionPointOptions } from './createExtensionPoint'; +export { createBackendModule } from './createBackendModule'; +export { createBackendPlugin } from './createBackendPlugin'; +export { createExtensionPoint } from './createExtensionPoint'; export { - createBackendModule, - createBackendPlugin, - createExtensionPoint, -} from './factories'; + createBackendFeatureLoader, + type CreateBackendFeatureLoaderOptions, +} from './createBackendFeatureLoader'; export type { BackendModuleRegistrationPoints, diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 55d43ccb7c..1e8c3910a3 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ServiceRef } from '../services/system/types'; +import { InternalServiceFactory, ServiceRef } from '../services/system/types'; import { BackendFeature } from '../types'; /** @@ -36,6 +36,17 @@ export type ExtensionPoint = { $$type: '@backstage/ExtensionPoint'; }; +/** @ignore */ +type DepsToInstances< + TDeps extends { + [key in string]: ServiceRef | ExtensionPoint; + }, +> = { + [key in keyof TDeps]: TDeps[key] extends ServiceRef + ? Array + : TDeps[key]['T']; +}; + /** * The callbacks passed to the `register` method of a backend plugin. * @@ -46,11 +57,13 @@ export interface BackendPluginRegistrationPoints { ref: ExtensionPoint, impl: TExtensionPoint, ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef; - }; - init(deps: Deps): Promise; + registerInit< + TDeps extends { + [name in string]: ServiceRef; + }, + >(options: { + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } @@ -64,17 +77,20 @@ export interface BackendModuleRegistrationPoints { ref: ExtensionPoint, impl: TExtensionPoint, ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; + registerInit< + TDeps extends { + [name in string]: ServiceRef | ExtensionPoint; + }, + >(options: { + deps: TDeps; + init(deps: DepsToInstances): Promise; }): void; } /** @internal */ -export interface InternalBackendFeature extends BackendFeature { +export interface InternalBackendRegistrations extends BackendFeature { version: 'v1'; + featureType: 'registrations'; getRegistrations(): Array< InternalBackendPluginRegistration | InternalBackendModuleRegistration >; @@ -102,3 +118,20 @@ export interface InternalBackendModuleRegistration { func(deps: Record): Promise; }; } + +/** + * @public + */ +export interface InternalBackendFeatureLoader extends BackendFeature { + version: 'v1'; + featureType: 'loader'; + description: string; + deps: Record>; + loader(deps: Record): Promise; +} + +/** @internal */ +export type InternalBackendFeature = + | InternalBackendRegistrations + | InternalBackendFeatureLoader + | InternalServiceFactory; diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 9624df483b..1a5879f14e 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/backend-tasks +## 0.5.28-next.2 + +### Patch Changes + +- ba9abf4: The `PluginTaskScheduler` now allows tasks with `frequency: { trigger: 'manual' }`. This means that the task will not be scheduled, but rather run only when manually triggered with `PluginTaskScheduler.triggerTask`. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.5.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.5.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.5.27 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.5.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.5.26-next.0 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.5.24 ### Patch Changes diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 0d5ced044a..c0f9da3a01 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -62,7 +62,14 @@ export interface TaskScheduleDefinition { cron: string; } | Duration - | HumanDuration_2; + | HumanDuration_2 + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { + trigger: 'manual'; + }; initialDelay?: Duration | HumanDuration_2; scope?: 'global' | 'local'; timeout: Duration | HumanDuration_2; diff --git a/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js new file mode 100644 index 0000000000..efbef1c76f --- /dev/null +++ b/packages/backend-tasks/migrations/20240712211735_nullable_next_run.js @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.setNullable('next_run_start_at'); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = async function down(knex) { + await knex + .delete() + .from('backstage_backend_tasks__tasks') + .where({ next_run_start_at: null }); + await knex.schema.alterTable('backstage_backend_tasks__tasks', table => { + table.dropNullable('next_run_start_at'); + }); +}; diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 843a78706d..3f482b38e6 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.5.24", + "version": "0.5.28-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-tasks/src/migrations.test.ts b/packages/backend-tasks/src/migrations.test.ts index 82208da975..7583948f71 100644 --- a/packages/backend-tasks/src/migrations.test.ts +++ b/packages/backend-tasks/src/migrations.test.ts @@ -81,4 +81,43 @@ describe('migrations', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + '20240712211735_nullable_next_run.js, %p', + async databaseId => { + const knex = await databases.init(databaseId); + + await migrateUntilBefore(knex, '20240712211735_nullable_next_run.js'); + await migrateUpOnce(knex); + + await knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }); + + await expect(knex('backstage_backend_tasks__tasks')).resolves.toEqual([ + { + id: 'test', + settings_json: '{}', + next_run_start_at: null, + current_run_ticket: null, + current_run_started_at: null, + current_run_expires_at: null, + }, + ]); + + await migrateDownOnce(knex); + + await expect( + knex('backstage_backend_tasks__tasks').insert({ + id: 'test', + settings_json: '{}', + next_run_start_at: knex.raw('null'), + }), + ).rejects.toEqual(expect.anything()); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts index 77965632e9..eca899098f 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.test.ts @@ -345,6 +345,7 @@ describe('PluginTaskManagerImpl', () => { expect(parseDuration({ milliseconds: 5000 })).toEqual('PT5S'); expect(parseDuration(Duration.fromMillis(5000))).toEqual('PT5S'); expect(parseDuration({ cron: '1 * * * *' })).toEqual('1 * * * *'); + expect(parseDuration({ trigger: 'manual' })).toEqual('manual'); }); }); }); diff --git a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts index b1647f4edf..0ccc77a6c1 100644 --- a/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts +++ b/packages/backend-tasks/src/tasks/PluginTaskSchedulerImpl.ts @@ -152,6 +152,9 @@ export function parseDuration( if ('cron' in frequency) { return frequency.cron; } + if ('trigger' in frequency) { + return frequency.trigger; + } const parsed = Duration.isDuration(frequency) ? frequency diff --git a/packages/backend-tasks/src/tasks/TaskWorker.test.ts b/packages/backend-tasks/src/tasks/TaskWorker.test.ts index 10ebfde5b4..829d7754f8 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.test.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.test.ts @@ -503,4 +503,32 @@ describe('TaskWorker', () => { await knex.destroy(); }, ); + + it.each(databases.eachSupportedId())( + 'next_run_start_at is not set for manually-triggered tasks, %p', + async databaseId => { + const knex = await databases.init(databaseId); + await migrateBackendTasks(knex); + + const fn = jest.fn( + async () => new Promise(resolve => setTimeout(resolve, 50)), + ); + + const initialSettings: TaskSettingsV2 = { + version: 2, + cadence: 'manual', + timeoutAfterDuration: 'PT1M', + }; + + const worker = new TaskWorker('task99', fn, knex, logger); + await worker.persistTask(initialSettings); + await worker.tryClaimTask('ticket', initialSettings); + await worker.tryReleaseTask('ticket', initialSettings); + + const row = (await knex(DB_TASKS_TABLE))[0]; + expect(row.next_run_start_at).toBeNull(); + + await knex.destroy(); + }, + ); }); diff --git a/packages/backend-tasks/src/tasks/TaskWorker.ts b/packages/backend-tasks/src/tasks/TaskWorker.ts index 57cbf2203e..a50b988522 100644 --- a/packages/backend-tasks/src/tasks/TaskWorker.ts +++ b/packages/backend-tasks/src/tasks/TaskWorker.ts @@ -52,8 +52,8 @@ export class TaskWorker { ); let workCheckFrequency = this.workCheckFrequency; - const isCron = !settings?.cadence.startsWith('P'); - if (!isCron) { + const isDuration = settings?.cadence.startsWith('P'); + if (isDuration) { const cadence = Duration.fromISO(settings.cadence); if (cadence < workCheckFrequency) { workCheckFrequency = cadence; @@ -174,7 +174,9 @@ export class TaskWorker { // read it back again. taskSettingsV2Schema.parse(settings); - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let startAt: Knex.Raw | undefined; let nextStartAt: Knex.Raw | undefined; @@ -193,6 +195,9 @@ export class TaskWorker { nextStartAt = this.nextRunAtRaw(time); startAt ||= nextStartAt; + } else if (isManual) { + nextStartAt = this.knex.raw('null'); + startAt ||= nextStartAt; } else { startAt ||= this.knex.fn.now(); nextStartAt = nowPlus(Duration.fromISO(settings.cadence), this.knex); @@ -316,7 +321,9 @@ export class TaskWorker { ticket: string, settings: TaskSettingsV2, ): Promise { - const isCron = !settings?.cadence.startsWith('P'); + const isManual = settings?.cadence === 'manual'; + const isDuration = settings?.cadence.startsWith('P'); + const isCron = !isManual && !isDuration; let nextRun: Knex.Raw; if (isCron) { @@ -324,6 +331,8 @@ export class TaskWorker { this.logger.debug(`task: ${this.taskId} will next occur around ${time}`); nextRun = this.nextRunAtRaw(time); + } else if (isManual) { + nextRun = this.knex.raw('null'); } else { const dt = Duration.fromISO(settings.cadence).as('seconds'); this.logger.debug( diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts index c52d59b016..adeb134611 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.test.ts @@ -16,7 +16,6 @@ import { ConfigReader } from '@backstage/config'; import { HumanDuration } from '@backstage/types'; -import { Duration } from 'luxon'; import { readTaskScheduleDefinitionFromConfig } from './readTaskScheduleDefinitionFromConfig'; describe('readTaskScheduleDefinitionFromConfig', () => { @@ -35,7 +34,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => { const result = readTaskScheduleDefinitionFromConfig(config); expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *'); - expect(result.timeout).toEqual(Duration.fromISO('PT3M')); + expect(result.timeout).toEqual({ minutes: 3 }); expect((result.initialDelay as HumanDuration).minutes).toEqual(20); expect(result.scope).toBe('global'); }); @@ -51,7 +50,7 @@ describe('readTaskScheduleDefinitionFromConfig', () => { const result = readTaskScheduleDefinitionFromConfig(config); expect((result.frequency as { cron: string }).cron).toBe('0 30 * * * *'); - expect(result.timeout).toEqual(Duration.fromISO('PT3M')); + expect(result.timeout).toEqual({ minutes: 3 }); expect(result.initialDelay).toBeUndefined(); expect(result.scope).toBeUndefined(); }); diff --git a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts index 5a173f246b..85099d82db 100644 --- a/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts +++ b/packages/backend-tasks/src/tasks/readTaskScheduleDefinitionFromConfig.ts @@ -19,27 +19,33 @@ import { HumanDuration } from '@backstage/types'; import { TaskScheduleDefinition } from './types'; import { Duration } from 'luxon'; -function readDuration(config: Config, key: string): Duration | HumanDuration { +function readDuration(config: Config, key: string): HumanDuration { if (typeof config.get(key) === 'string') { const value = config.getString(key); const duration = Duration.fromISO(value); if (!duration.isValid) { throw new Error(`Invalid duration: ${value}`); } - return duration; + return duration.toObject(); } return readDurationFromConfig(config, { key }); } -function readCronOrDuration( +function readFrequency( config: Config, key: string, -): { cron: string } | Duration | HumanDuration { +): { cron: string } | Duration | HumanDuration | { trigger: 'manual' } { const value = config.get(key); if (typeof value === 'object' && (value as { cron?: string }).cron) { return value as { cron: string }; } + if ( + typeof value === 'object' && + (value as { trigger?: string }).trigger === 'manual' + ) { + return { trigger: 'manual' }; + } return readDuration(config, key); } @@ -56,7 +62,7 @@ function readCronOrDuration( export function readTaskScheduleDefinitionFromConfig( config: Config, ): TaskScheduleDefinition { - const frequency = readCronOrDuration(config, 'frequency'); + const frequency = readFrequency(config, 'frequency'); const timeout = readDuration(config, 'timeout'); const initialDelay = config.has('initialDelay') diff --git a/packages/backend-tasks/src/tasks/types.ts b/packages/backend-tasks/src/tasks/types.ts index e6a31873be..a0bdbbaa64 100644 --- a/packages/backend-tasks/src/tasks/types.ts +++ b/packages/backend-tasks/src/tasks/types.ts @@ -100,7 +100,12 @@ export interface TaskScheduleDefinition { cron: string; } | Duration - | HumanDuration; + | HumanDuration + /** + * This task will only run when manually triggered with the `triggerTask` method; no automatic + * scheduling. This is useful for locking of global tasks that should not be run concurrently. + */ + | { trigger: 'manual' }; /** * The maximum amount of time that a single task invocation can take, before @@ -373,6 +378,10 @@ function isValidCronFormat(c: string | undefined): boolean { } } +function isValidTrigger(t: string): boolean { + return t === 'manual'; +} + export const taskSettingsV1Schema = z.object({ version: z.literal(1), initialDelayDuration: z @@ -401,6 +410,11 @@ export const taskSettingsV2Schema = z.object({ cadence: z .string() .refine(isValidCronFormat, { message: 'Invalid cron' }) + .or( + z.string().refine(isValidTrigger, { + message: "Invalid trigger, expecting 'manual'", + }), + ) .or( z.string().refine(isValidOptionalDurationString, { message: 'Invalid duration, expecting ISO Period', diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index 4bc1108ead..cbca6bdcab 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,102 @@ # @backstage/backend-test-utils +## 0.4.5-next.2 + +### Patch Changes + +- 8b13183: Internal updates to support latest version of `BackendFeauture`s from `@backstage/backend-plugin-api`. +- 7c5f3b0: Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-app-api@0.8.1-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-app-api@0.8.1-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.4.5-next.0 + +### Patch Changes + +- 4e79d19: The default services for `startTestBackend` and `ServiceFactoryTester` now includes the Root Health Service. +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/backend-app-api@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.4.4 + +### Patch Changes + +- 2f99178: The `ServiceFactoryTest.get` method was deprecated and the `ServiceFactoryTest.getSubject` should be used instead. The `getSubject` method has the same behavior, but has a better method name to indicate that the service instance returned is the subject currently being tested. +- edf5cc3: The function `isDockerDisabledForTests` is deprecated and will no longer be exported in the near future as it should only be used internally. +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- fce7887: Added mock for the Root Health Service in `mockServices`. +- 906c817: Updated `startTestBackend` and `ServiceFactoryTester` to only accept plain service factory or backend feature objects, no longer supporting the callback form. This lines up with the changes to `@backstage/backend-plugin-api` and should not require any code changes. +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- 98ccf00: Internal refactor of `mockServices.httpAuth.factory` to allow it to still be constructed with options, but without declaring options via `createServiceFactory`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/backend-app-api@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.4-next.1 + +### Patch Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 +- Updated dependencies + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/backend-app-api@0.7.10-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.4.3-next.0 + +### Patch Changes + +- fce7887: Added mock for the Root Health Service in `mockServices`. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/backend-app-api@0.7.9-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.0 ### Minor Changes diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 019d398ea6..584fedb1f5 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -22,7 +22,6 @@ import { EventsService } from '@backstage/plugin-events-node'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { HttpAuthService } from '@backstage/backend-plugin-api'; -import { HttpRouterFactoryOptions } from '@backstage/backend-defaults/httpRouter'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; import { JsonObject } from '@backstage/types'; @@ -32,12 +31,14 @@ import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; +import { RootHealthService } from '@backstage/backend-plugin-api'; import { RootHttpRouterFactoryOptions } from '@backstage/backend-defaults/rootHttpRouter'; import { RootHttpRouterService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; @@ -54,7 +55,7 @@ export interface CreateMockDirectoryOptions { mockOsTmpDir?: boolean; } -// @public (undocumented) +// @public @deprecated (undocumented) export function isDockerDisabledForTests(): boolean; // @public (undocumented) @@ -155,7 +156,12 @@ export namespace mockServices { // (undocumented) export namespace auth { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + AuthService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -164,7 +170,12 @@ export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + CacheService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -173,7 +184,12 @@ export namespace mockServices { // (undocumented) export namespace database { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + DatabaseService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -184,7 +200,12 @@ export namespace mockServices { // (undocumented) export namespace discovery { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + DiscoveryService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -193,7 +214,12 @@ export namespace mockServices { // (undocumented) export namespace events { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + EventsService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -205,13 +231,10 @@ export namespace mockServices { }): HttpAuthService; // (undocumented) export namespace httpAuth { - const factory: ( - options?: - | { - defaultCredentials?: BackstageCredentials | undefined; - } - | undefined, - ) => ServiceFactory; + const factory: ((options?: { + defaultCredentials?: BackstageCredentials; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -220,9 +243,12 @@ export namespace mockServices { // (undocumented) export namespace httpRouter { const // (undocumented) - factory: ( - options?: HttpRouterFactoryOptions | undefined, - ) => ServiceFactory; + factory: ServiceFactoryCompat< + HttpRouterService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -233,7 +259,12 @@ export namespace mockServices { // (undocumented) export namespace identity { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + IdentityService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -242,7 +273,12 @@ export namespace mockServices { // (undocumented) export namespace lifecycle { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + LifecycleService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -251,7 +287,12 @@ export namespace mockServices { // (undocumented) export namespace logger { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + LoggerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -260,7 +301,12 @@ export namespace mockServices { // (undocumented) export namespace permissions { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + PermissionsService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -275,16 +321,40 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - factory: ( - options?: Options | undefined, - ) => ServiceFactory; + factory: ServiceFactory< + RootConfigService, + 'root', + 'singleton' | 'multiton' + > & + (( + options?: Options | undefined, + ) => ServiceFactory< + RootConfigService, + 'root', + 'singleton' | 'multiton' + >); + } + // (undocumented) + export namespace rootHealth { + const // (undocumented) + factory: ServiceFactoryCompat< + RootHealthService, + 'root', + 'singleton', + undefined + >; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; } // (undocumented) export namespace rootHttpRouter { const // (undocumented) - factory: ( + factory: (( options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory; + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -293,7 +363,12 @@ export namespace mockServices { // (undocumented) export namespace rootLifecycle { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + RootLifecycleService, + 'root', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -308,9 +383,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - factory: ( - options?: Options | undefined, - ) => ServiceFactory; + factory: ServiceFactory & + (( + options?: Options | undefined, + ) => ServiceFactory); const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -319,7 +395,12 @@ export namespace mockServices { // (undocumented) export namespace scheduler { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + SchedulerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -330,7 +411,12 @@ export namespace mockServices { // (undocumented) export namespace tokenManager { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + TokenManagerService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -339,7 +425,12 @@ export namespace mockServices { // (undocumented) export namespace urlReader { const // (undocumented) - factory: () => ServiceFactory; + factory: ServiceFactoryCompat< + UrlReaderService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -350,7 +441,12 @@ export namespace mockServices { ): UserInfoService; // (undocumented) export namespace userInfo { - const factory: () => ServiceFactory; + const factory: ServiceFactoryCompat< + UserInfoService, + 'plugin', + 'singleton', + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -359,25 +455,46 @@ export namespace mockServices { } // @public -export class ServiceFactoryTester { - static from( - subject: - | ServiceFactory - | (() => ServiceFactory), +export function registerMswTestHooks(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + +// @public +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', +> { + static from< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ): ServiceFactoryTester; + ): ServiceFactoryTester; + // @deprecated get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; - getService( - service: ServiceRef, + ): Promise; + getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetInstances extends 'singleton' | 'multiton' = 'singleton', + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; + getSubject( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise; } // @public export interface ServiceFactoryTesterOptions { - dependencies?: Array ServiceFactory)>; + dependencies?: Array; } // @public (undocumented) @@ -391,7 +508,7 @@ export type ServiceMock = { : TService[Key]; }; -// @public +// @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { listen: (t: any) => void; close: () => void; @@ -422,9 +539,8 @@ export interface TestBackendOptions { // (undocumented) features?: Array< | BackendFeature - | (() => BackendFeature) | Promise<{ - default: BackendFeature | (() => BackendFeature); + default: BackendFeature; }> >; } diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 9db94acc3e..44373e733f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/backend-test-utils", - "version": "0.4.0", + "version": "0.4.5-next.2", "description": "Test helpers library for Backstage backends", "backstage": { "role": "node-library" @@ -56,7 +56,7 @@ "@keyv/memcache": "^1.3.5", "@keyv/redis": "^2.5.3", "@types/keyv": "^4.2.0", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.0.0", "cookie": "^0.6.0", "express": "^4.17.1", "fs-extra": "^11.0.0", diff --git a/packages/backend-test-utils/src/deprecated.ts b/packages/backend-test-utils/src/deprecated.ts index fad7523367..9790580a9c 100644 --- a/packages/backend-test-utils/src/deprecated.ts +++ b/packages/backend-test-utils/src/deprecated.ts @@ -14,10 +14,32 @@ * limitations under the License. */ +import { registerMswTestHooks } from './msw'; import { CreateMockDirectoryOptions } from './filesystem'; +import { isDockerDisabledForTests as _isDockerDisabledForTests } from './util'; /** * @public * @deprecated Use `CreateMockDirectoryOptions` from `@backstage/backend-test-utils` instead. */ export type MockDirectoryOptions = CreateMockDirectoryOptions; + +/** + * @public + * @deprecated Use `registerMswTestHooks` from `@backstage/backend-test-utils` instead. + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void { + registerMswTestHooks(worker); +} + +/** + * @public + * @deprecated This is an internal function and will no longer be exported from this package. + */ +export function isDockerDisabledForTests(): boolean { + return _isDockerDisabledForTests(); +} diff --git a/packages/backend-test-utils/src/index.ts b/packages/backend-test-utils/src/index.ts index a9feacbacd..4124a861f4 100644 --- a/packages/backend-test-utils/src/index.ts +++ b/packages/backend-test-utils/src/index.ts @@ -26,4 +26,3 @@ export * from './database'; export * from './msw'; export * from './filesystem'; export * from './next'; -export * from './util'; diff --git a/packages/backend-test-utils/src/msw/index.ts b/packages/backend-test-utils/src/msw/index.ts index 29cbdf854a..fa6a37445f 100644 --- a/packages/backend-test-utils/src/msw/index.ts +++ b/packages/backend-test-utils/src/msw/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { setupRequestMockHandlers } from './setupRequestMockHandlers'; +export { registerMswTestHooks } from './registerMswTestHooks'; diff --git a/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts b/packages/backend-test-utils/src/msw/registerMswTestHooks.ts similarity index 94% rename from packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts rename to packages/backend-test-utils/src/msw/registerMswTestHooks.ts index 625ef0e0fc..4e26c38321 100644 --- a/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts +++ b/packages/backend-test-utils/src/msw/registerMswTestHooks.ts @@ -19,7 +19,7 @@ * @public * @param worker - service worker */ -export function setupRequestMockHandlers(worker: { +export function registerMswTestHooks(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 24571b634c..8d36fec50e 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -24,6 +24,7 @@ import { httpRouterServiceFactory } from '@backstage/backend-defaults/httpRouter import { lifecycleServiceFactory } from '@backstage/backend-defaults/lifecycle'; import { loggerServiceFactory } from '@backstage/backend-defaults/logger'; import { permissionsServiceFactory } from '@backstage/backend-defaults/permissions'; +import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth'; import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; import { schedulerServiceFactory } from '@backstage/backend-defaults/scheduler'; @@ -69,21 +70,28 @@ function createLoggerMock() { } /** @internal */ -function simpleFactory< +function simpleFactoryWithOptions< TService, TScope extends 'root' | 'plugin', TOptions extends [options?: object] = [], >( ref: ServiceRef, factory: (...options: TOptions) => TService, -): (...options: TOptions) => ServiceFactory { - return createServiceFactory((options: unknown) => ({ - service: ref as ServiceRef, - deps: {}, - async factory() { - return (factory as any)(options); - }, - })) as (...options: TOptions) => ServiceFactory; +): ServiceFactory & + ((...options: TOptions) => ServiceFactory) { + const factoryWithOptions = (...options: TOptions) => + createServiceFactory({ + service: ref as ServiceRef, + deps: {}, + async factory() { + return factory(...options); + }, + })(); + return Object.assign( + factoryWithOptions, + factoryWithOptions(...([undefined] as unknown as TOptions)), + ) as ServiceFactory & + ((...options: TOptions) => ServiceFactory); } /** @public */ @@ -133,7 +141,10 @@ export namespace mockServices { export namespace rootConfig { export type Options = { data?: JsonObject }; - export const factory = simpleFactory(coreServices.rootConfig, rootConfig); + export const factory = simpleFactoryWithOptions( + coreServices.rootConfig, + rootConfig, + ); } export function rootLogger(options?: rootLogger.Options): LoggerService { @@ -144,7 +155,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; - export const factory = simpleFactory(coreServices.rootLogger, rootLogger); + export const factory = simpleFactoryWithOptions( + coreServices.rootLogger, + rootLogger, + ); export const mock = simpleMock(coreServices.rootLogger, () => ({ child: jest.fn(), debug: jest.fn(), @@ -167,10 +181,11 @@ export namespace mockServices { }; } export namespace tokenManager { - export const factory = simpleFactory( - coreServices.tokenManager, - tokenManager, - ); + export const factory = createServiceFactory({ + service: coreServices.tokenManager, + deps: {}, + factory: () => tokenManager(), + }); export const mock = simpleMock(coreServices.tokenManager, () => ({ authenticate: jest.fn(), getToken: jest.fn(), @@ -181,7 +196,11 @@ export namespace mockServices { return new MockIdentityService(); } export namespace identity { - export const factory = simpleFactory(coreServices.identity, identity); + export const factory = createServiceFactory({ + service: coreServices.identity, + deps: {}, + factory: () => identity(), + }); export const mock = simpleMock(coreServices.identity, () => ({ getIdentity: jest.fn(), })); @@ -269,15 +288,10 @@ export namespace mockServices { ); } export namespace httpAuth { - /** - * Creates a mock service factory for the `HttpAuthService`. - * - * By default all requests without credentials are treated as requests from - * the default mock user principal. This behavior can be configured with the - * `defaultCredentials` option. - */ - export const factory = createServiceFactory( - (options?: { defaultCredentials?: BackstageCredentials }) => ({ + const factoryWithOptions = (options?: { + defaultCredentials?: BackstageCredentials; + }) => + createServiceFactory({ service: coreServices.httpAuth, deps: { plugin: coreServices.pluginMetadata }, factory: ({ plugin }) => @@ -285,7 +299,17 @@ export namespace mockServices { plugin.getId(), options?.defaultCredentials ?? mockCredentials.user(), ), - }), + })(); + /** + * Creates a mock service factory for the `HttpAuthService`. + * + * By default all requests without credentials are treated as requests from + * the default mock user principal. This behavior can be configured with the + * `defaultCredentials` option. + */ + export const factory = Object.assign( + factoryWithOptions, + factoryWithOptions(), ); export const mock = simpleMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), @@ -344,6 +368,14 @@ export namespace mockServices { })); } + export namespace rootHealth { + export const factory = rootHealthServiceFactory; + export const mock = simpleMock(coreServices.rootHealth, () => ({ + getLiveness: jest.fn(), + getReadiness: jest.fn(), + })); + } + export namespace httpRouter { export const factory = httpRouterServiceFactory; export const mock = simpleMock(coreServices.httpRouter, () => ({ diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts index 14dd427ecf..5970f6ea52 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts @@ -56,34 +56,34 @@ describe('ServiceFactoryTester', () => { it('should test a root service factory', async () => { const tester = ServiceFactoryTester.from(rootFactory); - await expect(tester.get()).resolves.toBe('root'); + await expect(tester.getSubject()).resolves.toBe('root'); }); it('should test a plugin service factory', async () => { const tester = ServiceFactoryTester.from(pluginFactory); - await expect(tester.get('x')).resolves.toBe('x-plugin'); - await expect(tester.get('y')).resolves.toBe('y-plugin'); - await expect(tester.get('z')).resolves.toBe('z-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('x-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-plugin'); + await expect(tester.getSubject('z')).resolves.toBe('z-plugin'); }); it('should test a plugin service factory with root context', async () => { const tester = ServiceFactoryTester.from(sharedPluginFactory); - await expect(tester.get('x')).resolves.toBe('x-1-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester.get('z')).resolves.toBe('z-3-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('x-1-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester.getSubject('z')).resolves.toBe('z-3-plugin'); const tester2 = ServiceFactoryTester.from(sharedPluginFactory); - await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); - await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester2.get('x')).resolves.toBe('x-3-plugin'); - await expect(tester2.get('x')).resolves.toBe('x-3-plugin'); - await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); - await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); + await expect(tester2.getSubject('z')).resolves.toBe('z-1-plugin'); + await expect(tester2.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester2.getSubject('x')).resolves.toBe('x-3-plugin'); + await expect(tester2.getSubject('x')).resolves.toBe('x-3-plugin'); + await expect(tester2.getSubject('y')).resolves.toBe('y-2-plugin'); + await expect(tester2.getSubject('z')).resolves.toBe('z-1-plugin'); }); it('should use dependencies', async () => { @@ -96,7 +96,7 @@ describe('ServiceFactoryTester', () => { { dependencies: [rootFactory, pluginFactory()] }, ); - await expect(tester.get('x')).resolves.toBe('root, x-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('root, x-plugin'); }); it('should use dependencies with root context', async () => { @@ -109,11 +109,11 @@ describe('ServiceFactoryTester', () => { { dependencies: [sharedPluginFactory(), pluginFactory] }, ); - await expect(tester.get('x')).resolves.toBe('x-1-plugin, x-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(tester.get('z')).resolves.toBe('z-3-plugin, z-plugin'); + await expect(tester.getSubject('x')).resolves.toBe('x-1-plugin, x-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.getSubject('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.getSubject('z')).resolves.toBe('z-3-plugin, z-plugin'); }); it('should prioritize the subject implementation', async () => { @@ -126,7 +126,7 @@ describe('ServiceFactoryTester', () => { { dependencies: [rootFactory] }, ); - await expect(tester.get()).resolves.toBe('other-root'); + await expect(tester.getSubject()).resolves.toBe('other-root'); }); it('should throw on missing dependencies', async () => { @@ -138,7 +138,7 @@ describe('ServiceFactoryTester', () => { }), ); - await expect(tester.get('x')).rejects.toThrow( + await expect(tester.getSubject('x')).rejects.toThrow( "Failed to instantiate service 'b' for 'x' because the following dependent services are missing: 'a'", ); }); diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index 0555ab10a4..e8530f89bf 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -35,7 +35,7 @@ export interface ServiceFactoryTesterOptions { * If a service factory is provided for a service that already has a default * implementation, the provided factory will override the default. */ - dependencies?: Array ServiceFactory)>; + dependencies?: Array; } /** @@ -43,8 +43,12 @@ export interface ServiceFactoryTesterOptions { * * @public */ -export class ServiceFactoryTester { - readonly #subject: ServiceRef; +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', +> { + readonly #subject: ServiceRef; readonly #registry: ServiceRegistry; /** @@ -54,31 +58,41 @@ export class ServiceFactoryTester { * @param options - Additional options * @returns A new tester instance for the provided subject. */ - static from( - subject: - | ServiceFactory - | (() => ServiceFactory), + static from< + TService, + TScope extends 'root' | 'plugin', + TInstances extends 'singleton' | 'multiton' = 'singleton', + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ) { - const subjectFactory = typeof subject === 'function' ? subject() : subject; + ): ServiceFactoryTester { const registry = ServiceRegistry.create([ ...defaultServiceFactories, - ...(options?.dependencies?.map(f => - typeof f === 'function' ? f() : f, - ) ?? []), - subjectFactory, + ...(options?.dependencies ?? []), + subject, ]); - return new ServiceFactoryTester(subjectFactory.service, registry); + return new ServiceFactoryTester(subject.service, registry); } private constructor( - subject: ServiceRef, + subject: ServiceRef, registry: ServiceRegistry, ) { this.#subject = subject; this.#registry = registry; } + /** + * Returns the service instance for the subject. + * + * @deprecated Use `getSubject` instead. + */ + async get( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise { + return this.getSubject(...args); + } + /** * Returns the service instance for the subject. * @@ -89,11 +103,12 @@ export class ServiceFactoryTester { * * By default the plugin ID 'test' is used. */ - async get( + async getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; - return this.#registry.get(this.#subject, pluginId ?? 'test')!; + const instance = this.#registry.get(this.#subject, pluginId ?? 'test')!; + return instance; } /** @@ -103,10 +118,14 @@ export class ServiceFactoryTester { * * A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used. */ - async getService( - service: ServiceRef, + async getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetInstances extends 'singleton' | 'multiton' = 'singleton', + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; const instance = await this.#registry.get(service, pluginId ?? 'test'); if (instance === undefined) { diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index ecba34290d..a01d4bebf5 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -242,6 +242,15 @@ describe('TestBackend', () => { expect(res.body).toEqual({ message: 'pong' }); }); + it('should expose health check endpoints', async () => { + const { server } = await startTestBackend({ features: [] }); + + const res = await request(server).get('/.backstage/health/v1/liveness'); + + expect(res.status).toEqual(200); + expect(res.body).toEqual({ status: 'ok' }); + }); + it('should provide extension point implementations', async () => { expect.assertions(3); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index f23883ba3e..cf98c25e4a 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -36,7 +36,11 @@ import { ConfigReader } from '@backstage/config'; import express from 'express'; // Direct internal import to avoid duplication // eslint-disable-next-line @backstage/no-forbidden-package-imports -import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; +import { + InternalBackendFeature, + InternalBackendRegistrations, +} from '@backstage/backend-plugin-api/src/wiring/types'; +import { createHealthRouter } from '@backstage/backend-defaults/rootHttpRouter'; /** @public */ export interface TestBackendOptions { @@ -48,11 +52,7 @@ export interface TestBackendOptions { ]; }, ]; - features?: Array< - | BackendFeature - | (() => BackendFeature) - | Promise<{ default: BackendFeature | (() => BackendFeature) }> - >; + features?: Array>; } /** @public */ @@ -77,6 +77,7 @@ export const defaultServiceFactories = [ mockServices.lifecycle.factory(), mockServices.logger.factory(), mockServices.permissions.factory(), + mockServices.rootHealth.factory(), mockServices.rootLifecycle.factory(), mockServices.rootLogger.factory(), mockServices.scheduler.factory(), @@ -96,7 +97,7 @@ function createPluginsForOrphanModules(features: Array) { const modulePluginIds = new Set(); for (const feature of features) { - if (isInternalBackendFeature(feature)) { + if (isInternalBackendRegistrations(feature)) { const registrations = feature.getRegistrations(); for (const registration of registrations) { if (registration.type === 'plugin') { @@ -139,18 +140,7 @@ function createExtensionPointTestModules( } const registrations = features.flatMap(feature => { - if (feature.$$type !== '@backstage/BackendFeature') { - throw new Error( - `Failed to add feature, invalid type '${feature.$$type}'`, - ); - } - - if (isInternalBackendFeature(feature)) { - if (feature.version !== 'v1') { - throw new Error( - `Failed to add feature, invalid version '${feature.version}'`, - ); - } + if (isInternalBackendRegistrations(feature)) { return feature.getRegistrations(); } return []; @@ -255,15 +245,18 @@ export async function startTestBackend( config: coreServices.rootConfig, lifecycle: coreServices.rootLifecycle, rootLogger: coreServices.rootLogger, + health: coreServices.rootHealth, }, - async factory({ config, lifecycle, rootLogger }) { + async factory({ config, lifecycle, rootLogger, health }) { const router = DefaultRootHttpRouter.create(); const logger = rootLogger.child({ service: 'rootHttpRouter' }); const app = express(); const middleware = MiddlewareFactory.create({ config, logger }); + const healthRouter = createHealthRouter({ health }); + app.use(healthRouter); app.use(router.handler()); app.use(middleware.notFound()); app.use(middleware.error()); @@ -360,10 +353,28 @@ function registerTestHooks() { registerTestHooks(); -function isInternalBackendFeature( +function toInternalBackendFeature( feature: BackendFeature, -): feature is InternalBackendFeature { - return ( - typeof (feature as InternalBackendFeature).getRegistrations === 'function' - ); +): InternalBackendFeature { + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error(`Invalid BackendFeature, bad type '${feature.$$type}'`); + } + const internal = feature as InternalBackendFeature; + if (internal.version !== 'v1') { + throw new Error( + `Invalid BackendFeature, bad version '${internal.version}'`, + ); + } + return internal; +} + +function isInternalBackendRegistrations( + feature: BackendFeature, +): feature is InternalBackendRegistrations { + const internal = toInternalBackendFeature(feature); + if (internal.featureType === 'registrations') { + return true; + } + // Backwards compatibility for v1 registrations that use duck typing + return 'getRegistrations' in internal; } diff --git a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts index 05eeabd91a..ca7cc95425 100644 --- a/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts +++ b/packages/backend-test-utils/src/util/isDockerDisabledForTests.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -/** @public */ export function isDockerDisabledForTests() { // If we are not running in continuous integration, the default is to skip // the (relatively heavy, long running) docker based tests. If you want to diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index a606e77894..8fbce11752 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,234 @@ # example-backend +## 0.0.29-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-backend@1.23.1-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.2 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.2 + - @backstage/plugin-notifications-backend@0.3.4-next.2 + - @backstage/plugin-kubernetes-backend@0.18.4-next.2 + - @backstage/plugin-permission-backend@0.5.47-next.2 + - @backstage/plugin-devtools-backend@0.3.9-next.2 + - @backstage/plugin-techdocs-backend@1.10.10-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-signals-backend@0.1.9-next.2 + - @backstage/plugin-proxy-backend@0.5.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-app-backend@0.3.72-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-search-backend-node@1.2.28-next.2 + - @backstage/plugin-search-backend@1.5.15-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.2 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.2 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.2 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.2 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/catalog-model@1.5.0 + +## 0.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.1 + - @backstage/plugin-techdocs-backend@1.10.10-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-scaffolder-backend@1.23.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/plugin-app-backend@0.3.72-next.1 + - @backstage/plugin-devtools-backend@0.3.9-next.1 + - @backstage/plugin-proxy-backend@0.5.4-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.1 + - @backstage/plugin-kubernetes-backend@0.18.4-next.1 + - @backstage/plugin-permission-backend@0.5.47-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.1 + - @backstage/plugin-search-backend@1.5.15-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + - @backstage/plugin-search-backend-node@1.2.28-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + - @backstage/plugin-notifications-backend@0.3.4-next.1 + - @backstage/plugin-signals-backend@0.1.9-next.1 + +## 0.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-devtools-backend@0.3.9-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-app-backend@0.3.72-next.0 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.9-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.6-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.41-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.10-next.0 + - @backstage/plugin-kubernetes-backend@0.18.4-next.0 + - @backstage/plugin-notifications-backend@0.3.4-next.0 + - @backstage/plugin-permission-backend@0.5.47-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.20-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-proxy-backend@0.5.4-next.0 + - @backstage/plugin-scaffolder-backend@1.23.1-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.0 + - @backstage/plugin-search-backend@1.5.15-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.29-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.28-next.0 + - @backstage/plugin-search-backend-node@1.2.28-next.0 + - @backstage/plugin-signals-backend@0.1.9-next.0 + - @backstage/plugin-techdocs-backend@1.10.10-next.0 + +## 0.0.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend@1.23.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-backend@0.5.46 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-techdocs-backend@1.10.9 + - @backstage/plugin-notifications-backend@0.3.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-search-backend@1.5.14 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-app-backend@0.3.71 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.8 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5 + - @backstage/plugin-catalog-backend-module-openapi@0.1.40 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9 + - @backstage/plugin-devtools-backend@0.3.8 + - @backstage/plugin-kubernetes-backend@0.18.3 + - @backstage/plugin-proxy-backend@0.5.3 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-search-backend-module-explore@0.1.28 + - @backstage/plugin-search-backend-module-techdocs@0.1.27 + - @backstage/plugin-search-backend-node@1.2.27 + - @backstage/plugin-signals-backend@0.1.8 + - @backstage/catalog-model@1.5.0 + +## 0.0.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend@1.23.0-next.2 + +## 0.0.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-backend@1.10.9-next.1 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.1 + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/plugin-search-backend-module-techdocs@0.1.27-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-app-backend@0.3.71-next.1 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.8-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.5-next.1 + - @backstage/plugin-catalog-backend-module-openapi@0.1.40-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.9-next.1 + - @backstage/plugin-devtools-backend@0.3.8-next.1 + - @backstage/plugin-kubernetes-backend@0.18.3-next.1 + - @backstage/plugin-notifications-backend@0.3.3-next.1 + - @backstage/plugin-permission-backend@0.5.46-next.1 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.19-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-proxy-backend@0.5.3-next.1 + - @backstage/plugin-scaffolder-backend@1.23.0-next.1 + - @backstage/plugin-search-backend@1.5.14-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + - @backstage/plugin-search-backend-module-explore@0.1.28-next.1 + - @backstage/plugin-search-backend-node@1.2.27-next.1 + - @backstage/plugin-signals-backend@0.1.8-next.1 + +## 0.0.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-scaffolder-backend@1.23.0-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.0 + - @backstage/plugin-notifications-backend@0.3.2-next.0 + - @backstage/plugin-app-backend@0.3.70-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-guest-provider@0.1.7-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-backend-module-backstage-openapi@0.2.4-next.0 + - @backstage/plugin-catalog-backend-module-openapi@0.1.39-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.19-next.0 + - @backstage/plugin-catalog-backend-module-unprocessed@0.4.8-next.0 + - @backstage/plugin-devtools-backend@0.3.7-next.0 + - @backstage/plugin-kubernetes-backend@0.18.2-next.0 + - @backstage/plugin-permission-backend@0.5.45-next.0 + - @backstage/plugin-permission-backend-module-allow-all-policy@0.1.18-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-proxy-backend@0.5.2-next.0 + - @backstage/plugin-search-backend@1.5.13-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + - @backstage/plugin-search-backend-module-explore@0.1.27-next.0 + - @backstage/plugin-search-backend-module-techdocs@0.1.26-next.0 + - @backstage/plugin-search-backend-node@1.2.26-next.0 + - @backstage/plugin-signals-backend@0.1.7-next.0 + - @backstage/plugin-techdocs-backend@1.10.8-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-permission-common@0.7.14 + ## 0.0.27 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index 1a0c493a38..cafe89a527 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.0.27", + "version": "0.0.29-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index a4acd19cf2..952f3315c4 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -15,13 +15,24 @@ */ import { createBackend } from '@backstage/backend-defaults'; +import { createBackendFeatureLoader } from '@backstage/backend-plugin-api'; const backend = createBackend(); +// An example of how to group together and load multiple features. You can also +// access root-scoped services by adding `deps`. +const searchLoader = createBackendFeatureLoader({ + *loader() { + yield import('@backstage/plugin-search-backend/alpha'); + yield import('@backstage/plugin-search-backend-module-catalog/alpha'); + yield import('@backstage/plugin-search-backend-module-explore/alpha'); + yield import('@backstage/plugin-search-backend-module-techdocs/alpha'); + }, +}); + backend.add(import('@backstage/plugin-auth-backend')); backend.add(import('./authModuleGithubProvider')); backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); - backend.add(import('@backstage/plugin-app-backend/alpha')); backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); backend.add( @@ -37,13 +48,10 @@ backend.add(import('@backstage/plugin-permission-backend/alpha')); backend.add(import('@backstage/plugin-proxy-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend/alpha')); backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); -backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-explore/alpha')); -backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); backend.add( import('@backstage/plugin-catalog-backend-module-backstage-openapi'), ); -backend.add(import('@backstage/plugin-search-backend/alpha')); +backend.add(searchLoader); backend.add(import('@backstage/plugin-techdocs-backend/alpha')); backend.add(import('@backstage/plugin-signals-backend')); backend.add(import('@backstage/plugin-notifications-backend')); diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 2d91fa44a5..de941e2502 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -123,6 +123,7 @@ interface DomainEntityV1alpha1 extends Entity { spec: { owner: string; subdomainOf?: string; + type?: string; }; } export { DomainEntityV1alpha1 as DomainEntity }; @@ -454,6 +455,7 @@ interface SystemEntityV1alpha1 extends Entity { spec: { owner: string; domain?: string; + type?: string; }; } export { SystemEntityV1alpha1 as SystemEntity }; diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml index 598f8500aa..02370d9a14 100644 --- a/packages/catalog-model/examples/domains/artists-domain.yaml +++ b/packages/catalog-model/examples/domains/artists-domain.yaml @@ -13,3 +13,4 @@ metadata: spec: owner: team-a subdomainOf: audio + type: product-group diff --git a/packages/catalog-model/examples/domains/playback-domain.yaml b/packages/catalog-model/examples/domains/playback-domain.yaml index 58b5afe56e..e058ebf0a2 100644 --- a/packages/catalog-model/examples/domains/playback-domain.yaml +++ b/packages/catalog-model/examples/domains/playback-domain.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: user:frank.tiernan subdomainOf: audio + type: product-group diff --git a/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml b/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml index 8de3c00880..17fe21ba69 100644 --- a/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml +++ b/packages/catalog-model/examples/systems/artist-engagement-portal-system.yaml @@ -8,3 +8,4 @@ metadata: spec: owner: team-a domain: artists + type: service diff --git a/packages/catalog-model/examples/systems/audio-playback-system.yaml b/packages/catalog-model/examples/systems/audio-playback-system.yaml index 7430ae2ff5..f1edb7f17d 100644 --- a/packages/catalog-model/examples/systems/audio-playback-system.yaml +++ b/packages/catalog-model/examples/systems/audio-playback-system.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: team-c domain: playback + type: feature-set diff --git a/packages/catalog-model/examples/systems/podcast-system.yaml b/packages/catalog-model/examples/systems/podcast-system.yaml index 47a2f7ac9f..77f53925c1 100644 --- a/packages/catalog-model/examples/systems/podcast-system.yaml +++ b/packages/catalog-model/examples/systems/podcast-system.yaml @@ -6,3 +6,4 @@ metadata: spec: owner: team-b domain: playback + type: feature-set diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts index 3c1e7f0a6d..61408b7e80 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.test.ts @@ -32,6 +32,7 @@ describe('DomainV1alpha1Validator', () => { spec: { owner: 'me', subdomainOf: 'parent-domain', + type: 'domain-type', }, }; }); @@ -84,4 +85,19 @@ describe('DomainV1alpha1Validator', () => { (entity as any).spec.subdomainOf = ''; await expect(validator.check(entity)).rejects.toThrow(/subdomainOf/); }); + + it('accepts missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts index 1fe3dfb522..765329abb4 100644 --- a/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/DomainEntityV1alpha1.ts @@ -33,6 +33,7 @@ export interface DomainEntityV1alpha1 extends Entity { spec: { owner: string; subdomainOf?: string; + type?: string; }; } diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts index fd78633a89..828fe7a648 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.test.ts @@ -32,6 +32,7 @@ describe('SystemV1alpha1Validator', () => { spec: { owner: 'me', domain: 'domain', + type: 'system-type', }, }; }); @@ -84,4 +85,19 @@ describe('SystemV1alpha1Validator', () => { (entity as any).spec.domain = ''; await expect(validator.check(entity)).rejects.toThrow(/domain/); }); + + it('accepts missing type', async () => { + delete (entity as any).spec.type; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong type', async () => { + (entity as any).spec.type = 7; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); + + it('rejects empty type', async () => { + (entity as any).spec.type = ''; + await expect(validator.check(entity)).rejects.toThrow(/type/); + }); }); diff --git a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts index 88d405ec5b..37ecee8f6c 100644 --- a/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/SystemEntityV1alpha1.ts @@ -33,6 +33,7 @@ export interface SystemEntityV1alpha1 extends Entity { spec: { owner: string; domain?: string; + type?: string; }; } diff --git a/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json index 7d0d7e25ff..e4a6c29dd1 100644 --- a/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/Domain.v1alpha1.schema.json @@ -12,7 +12,8 @@ }, "spec": { "owner": "artist-relations-team", - "subdomainOf": "audio" + "subdomainOf": "audio", + "type": "product-group" } } ], @@ -45,6 +46,12 @@ "description": "An entity reference to another domain of which the domain is a part.", "examples": ["audio"], "minLength": 1 + }, + "type": { + "type": "string", + "description": "The type of domain. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their catalog hierarchy.", + "examples": ["product-group", "bundle"], + "minLength": 1 } } } diff --git a/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json index 2cdbc37076..f91fe751dc 100644 --- a/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/System.v1alpha1.schema.json @@ -12,7 +12,8 @@ }, "spec": { "owner": "artist-relations-team", - "domain": "artists" + "domain": "artists", + "type": "service" } } ], @@ -45,6 +46,12 @@ "description": "An entity reference to the domain that the system belongs to.", "examples": ["artists"], "minLength": 1 + }, + "type": { + "type": "string", + "description": "The type of system. There is currently no enforced set of values for this field, so it is left up to the adopting organization to choose a nomenclature that matches their catalog hierarchy.", + "examples": ["product", "service", "feature-set"], + "minLength": 1 } } } diff --git a/packages/cli-node/CHANGELOG.md b/packages/cli-node/CHANGELOG.md index dcf9073277..21995e4812 100644 --- a/packages/cli-node/CHANGELOG.md +++ b/packages/cli-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/cli-node +## 0.2.7 + +### Patch Changes + +- 133464c: Added internal metadata for the new experimental `frontend-dynamic-container` role. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.6 ### Patch Changes diff --git a/packages/cli-node/package.json b/packages/cli-node/package.json index 56cf50a096..c727918510 100644 --- a/packages/cli-node/package.json +++ b/packages/cli-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli-node", - "version": "0.2.6", + "version": "0.2.7", "description": "Node.js library for Backstage CLIs", "backstage": { "role": "node-library" diff --git a/packages/cli-node/src/roles/PackageRoles.ts b/packages/cli-node/src/roles/PackageRoles.ts index 401568ad6d..70b62e2479 100644 --- a/packages/cli-node/src/roles/PackageRoles.ts +++ b/packages/cli-node/src/roles/PackageRoles.ts @@ -58,6 +58,11 @@ const packageRoleInfos: PackageRoleInfo[] = [ platform: 'web', output: ['types', 'esm'], }, + { + role: 'frontend-dynamic-container' as PackageRole, // experimental + platform: 'web', + output: ['bundle'], + }, { role: 'backend-plugin', platform: 'node', diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index ecbed0abdc..887445aac7 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,163 @@ # @backstage/cli +## 0.27.0-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.27.0-next.2 + +### Patch Changes + +- b2d97fd: Fixing loading of additional config files with new `ConfigSources` +- adabb40: New command now supports setting package license +- Updated dependencies + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.9.0-next.1 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.14.0-next.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.27.0-next.1 + +### Patch Changes + +- e6e7d86: Switched the target from `'ES2022'` to `'es2022'` for better compatibility with older versions of `swc`. +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.27.0-next.0 + +### Minor Changes + +- 32a38e1: **BREAKING**: The lockfile (`yarn.lock`) dependency analysis and mutations have been removed from several commands. + + The `versions:bump` command will no longer attempt to bump and deduplicate dependencies by modifying the lockfile, it will only update `package.json` files. + + The `versions:check` command has been removed, since its only purpose was verification and mutation of the lockfile. We recommend using the `yarn dedupe` command instead, or the `yarn-deduplicate` package if you're using Yarn classic. + + The check that was built into the `package start` command has been removed, it will no longer warn about lockfile mismatches. + + The packages in the Backstage ecosystem handle package duplications much better now than when these CLI features were first introduced, so the need for these features has diminished. By removing them, we drastically reduce the integration between the Backstage CLI and Yarn, making it much easier to add support for other package managers in the future. + +### Patch Changes + +- 7eb08a6: Add frontend-dynamic-container role to eslint config factory +- fbc7819: Use ES2022 in CLI bundler +- e53074f: Updated default backend plugin to use `RootConfigService` instead of `Config`. This also removes the dependency on `@backstage/config` as it's no longer used. +- ee2b0e5: The experimental module federation build now has the ability to force the use of development versions of `react` and `react-dom` by setting the `FORCE_REACT_DEVELOPMENT` flag. +- 239dffc: Remove usage of deprecated functionality from @backstage/config-loader +- 0eedec3: Add support for dynamic plugins via the EXPERIMENTAL_MODULE_FEDERATION environment variable when running `yarn start`. +- dc4fb4f: Fix for `repo build --all` not properly detecting the experimental public entry point. +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.26.11 + +### Patch Changes + +- 133464c: Added experimental support for dynamic frontend plugin builds, enabled via setting `EXPERIMENTAL_MODULE_FEDERATION` for the app build, and using the `frontend-dynamic-container` package role to create a container. Both of these are experimental and will change in the future. +- e2e320c: - remove unused dependencies `winston` and `yn` from the template of backend plugins; + - update `msw` to version `2.3.1` in the template of backend plugins; + starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2; +- 0540c5a: Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`. +- 7652db4: Only bootstrap global-agent if it's actually being used +- f0c0039: Fix issue with CLI that was preventing upgrading from 1.28 +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- a60d73b: Fix a few minor issues with the backend template that were causing failing linting checks in the main repo. +- 0510d98: Subpath export `package.json` should be of a unique name to avoid typescript resolution issues +- 4baac0c: The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. +- Updated dependencies + - @backstage/cli-node@0.2.7 + - @backstage/integration@1.13.0 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.26.11-next.1 + +### Patch Changes + +- 4baac0c: The `backendPlugin` and `backendModule` factory now includes a step for automatically adding the new backend plugin/module to the `index.ts` file of the backend. +- Updated dependencies + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/integration@1.13.0-next.0 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + +## 0.26.10-next.0 + +### Patch Changes + +- e2e320c: - remove unused dependencies `winston` and `yn` from the template of backend plugins; + - update `msw` to version `2.3.1` in the template of backend plugins; + starting with v1 and switching later to v2 is tedious and not straight forward; it's easier to start with v2; +- 0540c5a: Updated the scaffolding output message for `plugin-common` in `backstage-cli`. Now, when executing `backstage-cli new` to create a new `plugin-common` package, the output message accurately reflects the action by displaying `Creating common plugin package...` instead of the previous, less accurate `Creating backend plugin...`. +- 7652db4: Only bootstrap global-agent if it's actually being used +- f0c0039: Fix issue with CLI that was preventing upgrading from 1.28 +- d228862: Update default backend plugin created by the cli to use non-deprecated error handling middleware +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- a60d73b: Fix a few minor issues with the backend template that were causing failing linting checks in the main repo. +- 0510d98: Subpath export `package.json` should be of a unique name to avoid typescript resolution issues +- Updated dependencies + - @backstage/integration@1.13.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/eslint-plugin@0.1.8 + - @backstage/release-manifests@0.0.11 + - @backstage/types@1.1.1 + ## 0.26.7 ### Patch Changes diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index 01b0f7ddec..980d305c12 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -22,7 +22,6 @@ Commands: package [command] migrate [command] versions:bump [options] - versions:check [options] versions:migrate [options] clean build-workspace [options] [packages...] @@ -194,6 +193,7 @@ Options: --scope --npm-registry --baseVersion + --license --no-private -h, --help ``` @@ -607,16 +607,6 @@ Options: -h, --help ``` -### `backstage-cli versions:check` - -``` -Usage: backstage-cli versions:check [options] - -Options: - --fix - -h, --help -``` - ### `backstage-cli versions:migrate` ``` diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index 6bc200cade..96d8478ec0 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -201,6 +201,7 @@ function createConfigForRole(dir, role, extraConfig = {}) { case 'frontend': case 'frontend-plugin': case 'frontend-plugin-module': + case 'frontend-dynamic-container': return createConfig(dir, { ...extraConfig, extends: [ diff --git a/packages/cli/package.json b/packages/cli/package.json index 573a3ae030..ba45b8f514 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/cli", - "version": "0.26.7", + "version": "0.27.0-next.3", "description": "CLI for developing Backstage plugins and apps", "backstage": { "role": "cli" @@ -53,6 +53,7 @@ "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", "@manypkg/get-packages": "^1.1.3", + "@module-federation/enhanced": "^0.1.19", "@octokit/graphql": "^5.0.0", "@octokit/graphql-schema": "^13.7.0", "@octokit/oauth-app": "^4.2.0", @@ -90,7 +91,7 @@ "css-loader": "^6.5.1", "ctrlc-windows": "^2.1.0", "diff": "^5.0.0", - "esbuild": "^0.20.0", + "esbuild": "^0.21.0", "esbuild-loader": "^4.0.0", "eslint": "^8.6.0", "eslint-config-prettier": "^9.0.0", @@ -120,7 +121,7 @@ "lodash": "^4.17.21", "mini-css-extract-plugin": "^2.4.2", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-libs-browser": "^2.2.1", "npm-packlist": "^5.0.0", "ora": "^5.3.0", diff --git a/packages/cli/src/commands/build/buildFrontend.ts b/packages/cli/src/commands/build/buildFrontend.ts index 6d7ca2e6a9..f29e0bdd38 100644 --- a/packages/cli/src/commands/build/buildFrontend.ts +++ b/packages/cli/src/commands/build/buildFrontend.ts @@ -16,7 +16,7 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; -import { buildBundle } from '../../lib/bundler'; +import { buildBundle, getModuleFederationOptions } from '../../lib/bundler'; import { getEnvironmentParallelism } from '../../lib/parallel'; import { loadCliConfig } from '../../lib/config'; @@ -24,16 +24,22 @@ interface BuildAppOptions { targetDir: string; writeStats: boolean; configPaths: string[]; + isModuleFederationRemote?: true; } export async function buildFrontend(options: BuildAppOptions) { const { targetDir, writeStats, configPaths } = options; const { name } = await fs.readJson(resolvePath(targetDir, 'package.json')); + await buildBundle({ targetDir, entry: 'src/index', parallelism: getEnvironmentParallelism(), statsJsonEnabled: writeStats, + moduleFederation: getModuleFederationOptions( + name, + options.isModuleFederationRemote, + ), ...(await loadCliConfig({ args: configPaths, fromPackage: name, diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index f15ae27ee0..c0ad3c912b 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -22,6 +22,7 @@ import { paths } from '../../lib/paths'; import { buildFrontend } from './buildFrontend'; import { buildBackend } from './buildBackend'; import { isValidUrl } from '../../lib/urls'; +import chalk from 'chalk'; export async function command(opts: OptionValues): Promise { const role = await findRoleFromCommand(opts); @@ -49,6 +50,21 @@ export async function command(opts: OptionValues): Promise { }); } + // experimental + if ((role as string) === 'frontend-dynamic-container') { + console.log( + chalk.yellow( + `⚠️ WARNING: The 'frontend-dynamic-container' package role is experimental and will receive immediate breaking changes in the future.`, + ), + ); + return buildFrontend({ + targetDir: paths.targetDir, + configPaths: [], + writeStats: Boolean(opts.stats), + isModuleFederationRemote: true, + }); + } + const roleInfo = PackageRoles.getRoleInfo(role); const outputs = new Set(); diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 7fcb0aef1a..f8d7e54e97 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -19,16 +19,16 @@ import { promisify } from 'util'; import chalk from 'chalk'; import inquirer, { Answers, Question } from 'inquirer'; import { exec as execCb } from 'child_process'; -import { resolve as resolvePath, join as joinPath } from 'path'; +import { join as joinPath, resolve as resolvePath } from 'path'; import camelCase from 'lodash/camelCase'; import upperFirst from 'lodash/upperFirst'; import os from 'os'; import { OptionValues } from 'commander'; import { assertError } from '@backstage/errors'; import { - parseOwnerIds, addCodeownersEntry, getCodeownersFilePath, + parseOwnerIds, } from '../../lib/codeowners'; import { paths } from '../../lib/paths'; import { Task, templatingTask } from '../../lib/tasks'; @@ -266,6 +266,7 @@ export default async (opts: OptionValues) => { const { version: pluginVersion } = isMonoRepo ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) : { version: '0.1.0' }; + const license = opts.license ?? 'Apache-2.0'; let lockfile: Lockfile | undefined; try { @@ -299,6 +300,7 @@ export default async (opts: OptionValues) => { name, privatePackage, npmRegistry, + license, }, createPackageVersionProvider(lockfile), isMonoRepo, diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 25a3a4e26c..7c3192fb2e 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -254,6 +254,10 @@ export function registerCommands(program: Command) { '--baseVersion ', 'The version to use for any new packages (default: 0.1.0)', ) + .option( + '--license ', + 'The license to use for any new packages (default: Apache-2.0)', + ) .option('--no-private', 'Do not mark new packages as private') .action(lazy(() => import('./new/new').then(m => m.default))); @@ -396,7 +400,7 @@ export function registerCommands(program: Command) { .action(lazy(() => import('./versions/bump').then(m => m.default))); program - .command('versions:check') + .command('versions:check', { hidden: true }) .option('--fix', 'Fix any auto-fixable versioning problems') .description('Check Backstage package versioning') .action(lazy(() => import('./versions/lint').then(m => m.default))); diff --git a/packages/cli/src/commands/new/new.ts b/packages/cli/src/commands/new/new.ts index 61e27af3c5..2733b0be9b 100644 --- a/packages/cli/src/commands/new/new.ts +++ b/packages/cli/src/commands/new/new.ts @@ -70,11 +70,14 @@ export default async (opts: OptionValues) => { return dir; } + const license = opts.license ?? 'Apache-2.0'; + let modified = false; try { await factory.create(options, { isMonoRepo: await isMonoRepo(), defaultVersion, + license, scope: opts.scope?.replace(/^@/, ''), npmRegistry: opts.npmRegistry, private: Boolean(opts.private), diff --git a/packages/cli/src/commands/start/command.ts b/packages/cli/src/commands/start/command.ts index 2f2af95d85..aa46b717bb 100644 --- a/packages/cli/src/commands/start/command.ts +++ b/packages/cli/src/commands/start/command.ts @@ -15,6 +15,7 @@ */ import { OptionValues } from 'commander'; +import { PackageRole } from '@backstage/cli-node'; import { findRoleFromCommand } from '../../lib/role'; import { startBackend, startBackendPlugin } from './startBackend'; import { startFrontend } from './startFrontend'; @@ -47,6 +48,13 @@ export async function command(opts: OptionValues): Promise { case 'frontend-plugin': case 'frontend-plugin-module': return startFrontend({ entry: 'dev/index', ...options }); + case 'frontend-dynamic-container' as PackageRole: // experimental + return startFrontend({ + entry: 'src/index', + ...options, + skipOpenBrowser: true, + isModuleFederationRemote: true, + }); default: throw new Error( `Start command is not supported for package role '${role}'`, diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts index a809473788..20faf4a9ff 100644 --- a/packages/cli/src/commands/start/startFrontend.ts +++ b/packages/cli/src/commands/start/startFrontend.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { serveBundle } from '../../lib/bundler'; +import { readJson } from 'fs-extra'; +import { getModuleFederationOptions, serveBundle } from '../../lib/bundler'; +import { paths } from '../../lib/paths'; interface StartAppOptions { verifyVersions?: boolean; @@ -22,14 +24,23 @@ interface StartAppOptions { checksEnabled: boolean; configPaths: string[]; + skipOpenBrowser?: boolean; + isModuleFederationRemote?: boolean; } export async function startFrontend(options: StartAppOptions) { + const { name } = await readJson(paths.resolveTarget('package.json')); + const waitForExit = await serveBundle({ entry: options.entry, checksEnabled: options.checksEnabled, configPaths: options.configPaths, verifyVersions: options.verifyVersions, + skipOpenBrowser: options.skipOpenBrowser, + moduleFederation: getModuleFederationOptions( + name, + options.isModuleFederationRemote, + ), }); await waitForExit(); diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 0dfe2f61c3..cc48b91fd8 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -18,15 +18,11 @@ import fs from 'fs-extra'; import { Command } from 'commander'; import * as runObj from '../../lib/run'; import bump, { bumpBackstageJsonVersion, createVersionFinder } from './bump'; -import { - setupRequestMockHandlers, - withLogCollector, -} from '@backstage/test-utils'; +import { registerMswTestHooks, withLogCollector } from '@backstage/test-utils'; import { YarnInfoInspectData } from '../../lib/versioning/packages'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { NotFoundError } from '@backstage/errors'; -import { Lockfile } from '../../lib/versioning/Lockfile'; import { MockDirectory, createMockDirectory, @@ -120,17 +116,6 @@ const lockfileMock = `${HEADER} version "1.0.3" `; -// This is the lockfile that we produce to unlock versions before we run yarn install -const lockfileMockResult = `${HEADER} -"@backstage/core@^1.0.5": - version "1.0.6" - dependencies: - "@backstage/core-api" "^1.0.6" - -"@backstage/theme@^1.0.0": - version "1.0.0" -`; - // Avoid flakes by comparing sorted log lines. File system access is async, which leads to the log line order being indeterministic const expectLogsToMatch = ( recievedLogs: String[], @@ -156,7 +141,7 @@ describe('bump', () => { }); const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('should bump backstage dependencies', async () => { mockDir.setContent({ @@ -207,11 +192,7 @@ describe('bump', () => { 'Using default pattern glob @backstage/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', - 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', - 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', - 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', 'bumping @backstage/core in a to ^1.0.6', 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/theme in b to ^2.0.0', @@ -223,9 +204,8 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(mockFetchPackageInfo).toHaveBeenCalledTimes(3); + expect(mockFetchPackageInfo).toHaveBeenCalledTimes(2); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); - expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core-api'); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme'); expect(runObj.run).toHaveBeenCalledTimes(1); @@ -235,12 +215,6 @@ describe('bump', () => { expect.any(Object), ); - const lockfileContents = await fs.readFile( - mockDir.resolve('yarn.lock'), - 'utf8', - ); - expect(lockfileContents).toBe(lockfileMockResult); - const packageA = await fs.readJson( mockDir.resolve('packages/a/package.json'), ); @@ -315,11 +289,7 @@ describe('bump', () => { 'Using default pattern glob @backstage/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', - 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', - 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', - 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', 'bumping @backstage/core in a to ^1.0.6', 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/theme in b to ^2.0.0', @@ -331,9 +301,8 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(mockFetchPackageInfo).toHaveBeenCalledTimes(3); + expect(mockFetchPackageInfo).toHaveBeenCalledTimes(2); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); - expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core-api'); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme'); expect(runObj.run).not.toHaveBeenCalledWith( @@ -342,12 +311,6 @@ describe('bump', () => { expect.any(Object), ); - const lockfileContents = await fs.readFile( - mockDir.resolve('yarn.lock'), - 'utf8', - ); - expect(lockfileContents).toBe(lockfileMockResult); - const packageA = await fs.readJson( mockDir.resolve('packages/a/package.json'), ); @@ -428,12 +391,7 @@ describe('bump', () => { 'Using default pattern glob @backstage/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', - 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', - 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', - 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', 'bumping @backstage/theme in b to ^5.0.0', 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/core in a to ^1.0.6', @@ -446,9 +404,8 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(mockFetchPackageInfo).toHaveBeenCalledTimes(2); + expect(mockFetchPackageInfo).toHaveBeenCalledTimes(1); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); - expect(mockFetchPackageInfo).not.toHaveBeenCalledWith('@backstage/theme'); expect(runObj.run).toHaveBeenCalledTimes(1); expect(runObj.run).toHaveBeenCalledWith( @@ -457,12 +414,6 @@ describe('bump', () => { expect.any(Object), ); - const lockfileContents = await fs.readFile( - mockDir.resolve('yarn.lock'), - 'utf8', - ); - expect(lockfileContents).toBe(lockfileMockResult); - const packageA = await fs.readJson( mockDir.resolve('packages/a/package.json'), ); @@ -632,12 +583,7 @@ describe('bump', () => { 'Using default pattern glob @backstage/*', 'Checking for updates of @backstage/core', 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', - 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', - 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', - 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', 'bumping @backstage/theme in b to ^5.0.0', 'bumping @backstage/core in b to ^1.0.6', 'bumping @backstage/core in a to ^1.0.6', @@ -661,21 +607,6 @@ describe('bump', () => { "@backstage-extra/custom-two@^1.0.0": version "1.0.0" -`; - const customLockfileMockResult = `${HEADER} -"@backstage-extra/custom-two@^1.0.0": - version "1.0.0" - -"@backstage-extra/custom@^1.1.0": - version "1.1.0" - -"@backstage/core@^1.0.5": - version "1.0.6" - dependencies: - "@backstage/core-api" "^1.0.6" - -"@backstage/theme@^1.0.0": - version "1.0.0" `; mockDir.setContent({ 'yarn.lock': customLockfileMock, @@ -734,12 +665,7 @@ describe('bump', () => { 'Checking for updates of @backstage-extra/custom', 'Checking for updates of @backstage-extra/custom-two', 'Checking for updates of @backstage/theme', - 'Checking for updates of @backstage/core-api', 'Some packages are outdated, updating', - 'unlocking @backstage/core@^1.0.3 ~> 1.0.6', - 'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0', - 'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7', - 'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7', 'bumping @backstage/core in a to ^1.0.6', 'bumping @backstage-extra/custom in a to ^1.1.0', 'bumping @backstage-extra/custom-two in a to ^2.0.0', @@ -757,7 +683,7 @@ describe('bump', () => { 'Version bump complete!', ]); - expect(mockFetchPackageInfo).toHaveBeenCalledTimes(5); + expect(mockFetchPackageInfo).toHaveBeenCalledTimes(4); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/core'); expect(mockFetchPackageInfo).toHaveBeenCalledWith('@backstage/theme'); @@ -768,12 +694,6 @@ describe('bump', () => { expect.any(Object), ); - const lockfileContents = await fs.readFile( - mockDir.resolve('yarn.lock'), - 'utf8', - ); - expect(lockfileContents).toEqual(customLockfileMockResult); - const packageA = await fs.readJson( mockDir.resolve('packages/a/package.json'), ); @@ -801,7 +721,7 @@ describe('bump', () => { it('should ignore not found packages', async () => { mockDir.setContent({ - 'yarn.lock': lockfileMockResult, + 'yarn.lock': lockfileMock, 'package.json': JSON.stringify({ workspaces: { packages: ['packages/*'], @@ -851,21 +771,11 @@ describe('bump', () => { 'Checking for updates of @backstage/theme', 'Package info not found, ignoring package @backstage/core', 'Package info not found, ignoring package @backstage/theme', - 'Checking for updates of @backstage/core', - 'Checking for updates of @backstage/theme', - 'Package info not found, ignoring package @backstage/core', - 'Package info not found, ignoring package @backstage/theme', 'All Backstage packages are up to date!', ]); expect(runObj.run).toHaveBeenCalledTimes(0); - const lockfileContents = await fs.readFile( - mockDir.resolve('yarn.lock'), - 'utf8', - ); - expect(lockfileContents).toBe(lockfileMockResult); - const packageA = await fs.readJson( mockDir.resolve('packages/a/package.json'), ); @@ -886,83 +796,6 @@ describe('bump', () => { }, }); }); - - // eslint-disable-next-line jest/expect-expect - it('should log duplicates', async () => { - jest.spyOn(Lockfile.prototype, 'analyze').mockReturnValue({ - invalidRanges: [], - newVersions: [], - newRanges: [ - { - name: '@backstage/backend-app-api', - oldRange: '^1.0.0', - newRange: '^2.0.0', - oldVersion: '1.0.0', - newVersion: '2.0.0', - }, - ], - }); - mockDir.setContent({ - 'yarn.lock': `${HEADER} -"@backstage/backend-app-api@^1.0.0": - version "1.0.0" -`, - 'package.json': JSON.stringify({ - workspaces: { - packages: ['packages/*'], - }, - }), - packages: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - dependencies: { - '@backstage/backend-app-api': '^1.0.0', - }, - }), - }, - }, - }); - - jest.spyOn(runObj, 'run').mockResolvedValue(undefined); - worker.use( - rest.get( - 'https://versions.backstage.io/v1/tags/main/manifest.json', - (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - packages: [ - { - name: '@backstage/backend-app-api', - version: '2.0.0', - }, - ], - }), - ), - ), - ); - const { log: logs } = await withLogCollector(['log', 'warn'], async () => { - await bump({ pattern: null, release: 'main' } as unknown as Command); - }); - expectLogsToMatch(logs, [ - 'Using default pattern glob @backstage/*', - 'Checking for updates of @backstage/backend-app-api', - 'Checking for updates of @backstage/backend-app-api', - 'Some packages are outdated, updating', - 'bumping @backstage/backend-app-api in a to ^2.0.0', - 'Running yarn install to install new versions', - 'Checking for moved packages to the @backstage-community namespace...', - '⚠️ The following packages may have breaking changes:', - ' @backstage/backend-app-api : 1.0.0 ~> 2.0.0', - ' https://github.com/backstage/backstage/blob/master/packages/backend-app-api/CHANGELOG.md', - 'Version bump complete!', - ' ⚠️ Warning! ⚠️', - ' The below package(s) have incompatible duplicate installations, likely due to a bad dependency in a plugin.', - ' You can investigate this by running `yarn why `, and report the issue to the plugin maintainers.', - ' @backstage/backend-app-api', - ]); - }); }); describe('bumpBackstageJsonVersion', () => { diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index d6d5f424ba..5b981d3890 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -24,7 +24,6 @@ import fs from 'fs-extra'; import chalk from 'chalk'; import ora from 'ora'; import semver from 'semver'; -import { minimatch } from 'minimatch'; import { OptionValues } from 'commander'; import { isError, NotFoundError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; @@ -36,7 +35,6 @@ import { Lockfile, YarnInfoInspectData, } from '../../lib/versioning'; -import { forbiddenDuplicatesFilter } from './lint'; import { BACKSTAGE_JSON } from '@backstage/cli-common'; import { runParallelWorkers } from '../../lib/parallel'; import { @@ -44,7 +42,6 @@ import { getManifestByVersion, ReleaseManifest, } from '@backstage/release-manifests'; -import { PackageGraph } from '@backstage/cli-node'; import { migrateMovedPackages } from './migrate'; function shouldUseGlobalAgent(): boolean { @@ -125,8 +122,6 @@ export default async (opts: OptionValues) => { // Next check with the package registry to see which dependency ranges we need to bump const versionBumps = new Map(); - // Track package versions that we want to remove from yarn.lock in order to trigger a bump - const unlocked = Array<{ name: string; range: string; target: string }>(); await runParallelWorkers({ parallelismFactor: 4, @@ -157,72 +152,13 @@ export default async (opts: OptionValues) => { }, }); - const filter = (name: string) => minimatch(name, pattern); - - // Check for updates of transitive backstage dependencies - await runParallelWorkers({ - parallelismFactor: 4, - items: lockfile.keys(), - async worker(name) { - // Only check @backstage packages and friends, we don't want this to do a full update of all deps - if (!filter(name)) { - return; - } - - let target: string; - try { - target = await findTargetVersion(name); - } catch (error) { - if (isError(error) && error.name === 'NotFoundError') { - console.log(`Package info not found, ignoring package ${name}`); - return; - } - throw error; - } - - for (const entry of lockfile.get(name) ?? []) { - // Ignore lockfile entries that don't satisfy the version range, since - // these can't cause the package to be locked to an older version - if (!semver.satisfies(target, entry.range)) { - continue; - } - // Unlock all entries that are within range but on the old version - unlocked.push({ name, range: entry.range, target }); - } - }, - }); - - console.log(); - // Write all discovered version bumps to package.json in this repo - if (versionBumps.size === 0 && unlocked.length === 0) { + if (versionBumps.size === 0) { console.log(chalk.green('All Backstage packages are up to date!')); } else { console.log(chalk.yellow('Some packages are outdated, updating')); console.log(); - if (unlocked.length > 0) { - const removed = new Set(); - for (const { name, range, target } of unlocked) { - // Don't bother removing lockfile entries if they're already on the correct version - const existingEntry = lockfile.get(name)?.find(e => e.range === range); - if (existingEntry?.version === target) { - continue; - } - const key = JSON.stringify({ name, range }); - if (!removed.has(key)) { - removed.add(key); - console.log( - `${chalk.magenta('unlocking')} ${name}@${chalk.yellow( - range, - )} ~> ${chalk.yellow(target)}`, - ); - lockfile.remove(name, range); - } - } - await lockfile.save(lockfilePath); - } - const breakingUpdates = new Map(); await runParallelWorkers({ parallelismFactor: 4, @@ -334,38 +270,6 @@ export default async (opts: OptionValues) => { } console.log(); - - // Finally we make sure the new lockfile doesn't have any duplicates - const dedupLockfile = await Lockfile.load(lockfilePath); - - const result = dedupLockfile.analyze({ - filter, - localPackages: PackageGraph.fromPackages( - await PackageGraph.listTargetPackages(), - ), - }); - - const forbiddenNewRanges = result.newRanges.filter(({ name }) => - forbiddenDuplicatesFilter(name), - ); - if (forbiddenNewRanges.length > 0) { - console.log(chalk.yellow(' ⚠️ Warning! ⚠️')); - console.log(); - console.log( - chalk.yellow( - ' The below package(s) have incompatible duplicate installations, likely due to a bad dependency in a plugin.', - ), - ); - console.log( - chalk.yellow( - ' You can investigate this by running `yarn why `, and report the issue to the plugin maintainers.', - ), - ); - console.log(); - for (const { name } of forbiddenNewRanges) { - console.log(chalk.yellow(` ${name}`)); - } - } }; export function createStrictVersionFinder(options: { diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts index bfb2a752a0..66b5fc04c4 100644 --- a/packages/cli/src/commands/versions/lint.ts +++ b/packages/cli/src/commands/versions/lint.ts @@ -14,116 +14,8 @@ * limitations under the License. */ -import { OptionValues } from 'commander'; -import { Lockfile } from '../../lib/versioning'; -import { paths } from '../../lib/paths'; -import partition from 'lodash/partition'; -import { PackageGraph } from '@backstage/cli-node'; - -// Packages that we try to avoid duplicates for -const INCLUDED = [/^@backstage\//]; - -export const includedFilter = (name: string) => - INCLUDED.some(pattern => pattern.test(name)); - -// Packages that are not allowed to have any duplicates -const FORBID_DUPLICATES = [/^@backstage\/\w+-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$/, - // Duplicates of libraries are OK - // TODO(Rugvip): Check this using package role instead - /^@backstage\/plugin-.*-react$/, - /^@backstage\/plugin-.*-node$/, - /^@backstage\/plugin-.*-common$/, -]; - -export const forbiddenDuplicatesFilter = (name: string) => - FORBID_DUPLICATES.some(pattern => pattern.test(name)) && - !ALLOW_DUPLICATES.some(pattern => pattern.test(name)); - -export default async (cmd: OptionValues) => { - const fix = Boolean(cmd.fix); - - let success = true; - - const lockfilePath = paths.resolveTargetRoot('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); - const result = lockfile.analyze({ - filter: includedFilter, - localPackages: PackageGraph.fromPackages( - await PackageGraph.listTargetPackages(), - ), - }); - - logArray( - result.invalidRanges, - "The following packages versions are invalid and can't be analyzed:", - e => ` ${e.name} @ ${e.range}`, +export default async () => { + throw new Error( + 'This command has been removed, please consider alternatives such as `yarn dedupe` instead.', ); - - if (fix) { - lockfile.replaceVersions(result.newVersions); - await lockfile.save(lockfilePath); - } else { - const [newVersionsForbidden, newVersionsAllowed] = partition( - result.newVersions, - ({ name }) => forbiddenDuplicatesFilter(name), - ); - if (newVersionsForbidden.length && !fix) { - success = false; - } - - logArray( - newVersionsForbidden, - 'The following packages must be deduplicated, this can be done automatically with --fix', - e => - ` ${e.name} @ ${e.range} bumped from ${e.oldVersion} to ${e.newVersion}`, - ); - logArray( - newVersionsAllowed, - 'The following packages can be deduplicated, this can be done automatically with --fix', - e => - ` ${e.name} @ ${e.range} bumped from ${e.oldVersion} to ${e.newVersion}`, - ); - } - - const [newRangesForbidden, newRangesAllowed] = partition( - result.newRanges, - ({ name }) => forbiddenDuplicatesFilter(name), - ); - if (newRangesForbidden.length) { - success = false; - } - - logArray( - newRangesForbidden, - 'The following packages must be deduplicated by updating dependencies in package.json', - e => ` ${e.name} @ ${e.oldRange} should be changed to ${e.newRange}`, - ); - logArray( - newRangesAllowed, - 'The following packages can be deduplicated by updating dependencies in package.json', - e => ` ${e.name} @ ${e.oldRange} should be changed to ${e.newRange}`, - ); - - if (!success) { - throw new Error('Failed versioning check'); - } }; - -function logArray(arr: T[], header: string, each: (item: T) => string) { - if (arr.length === 0) { - return; - } - - console.log(header); - console.log(); - for (const e of arr) { - console.log(each(e)); - } - console.log(); -} diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 232657a3f2..0aa91a90a6 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -42,41 +42,50 @@ export async function buildBundle(options: BuildOptions) { const paths = resolveBundlingPaths(options); const publicPaths = await resolveOptionalBundlingPaths({ + targetDir: options.targetDir, entry: 'src/index-public-experimental', dist: 'dist/public', }); - const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ - config: options.fullConfig, - targetPath: paths.targetPath, - }); - const commonConfigOptions = { ...options, checksEnabled: false, isDev: false, getFrontendAppConfigs: () => options.frontendAppConfigs, }; - const configs = [ - await createConfig(paths, { - ...commonConfigOptions, - additionalEntryPoints: detectedModulesEntryPoint, - appMode: publicPaths ? 'protected' : 'public', - }), - ]; - if (publicPaths) { - console.log( - chalk.yellow( - `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, - ), - ); + const configs = []; + + if (options.moduleFederation?.mode === 'remote') { + // Package detection is disabled for remote bundles + configs.push(await createConfig(paths, commonConfigOptions)); + } else { + const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ + config: options.fullConfig, + targetPath: paths.targetPath, + }); + configs.push( - await createConfig(publicPaths, { + await createConfig(paths, { ...commonConfigOptions, - appMode: 'public', + additionalEntryPoints: detectedModulesEntryPoint, + appMode: publicPaths ? 'protected' : 'public', }), ); + + if (publicPaths) { + console.log( + chalk.yellow( + `⚠️ WARNING: The app /public entry point is an experimental feature that may receive immediate breaking changes.`, + ), + ); + configs.push( + await createConfig(publicPaths, { + ...commonConfigOptions, + appMode: 'public', + }), + ); + } } const isCi = yn(process.env.CI, { default: false }); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 53db7f3597..da7b378fbd 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -14,8 +14,13 @@ * limitations under the License. */ -import { BackendBundlingOptions, BundlingOptions } from './types'; -import { posix as posixPath, resolve as resolvePath } from 'path'; +import { + BackendBundlingOptions, + BundlingOptions, + ModuleFederationOptions, +} from './types'; +import { posix as posixPath, resolve as resolvePath, dirname } from 'path'; +import chalk from 'chalk'; import webpack, { ProvidePlugin } from 'webpack'; import { BackstagePackage } from '@backstage/cli-node'; @@ -24,15 +29,17 @@ import { Config } from '@backstage/config'; import ESLintPlugin from 'eslint-webpack-plugin'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; +import { ModuleFederationPlugin } from '@module-federation/enhanced/webpack'; import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin'; import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin'; import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin'; +import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; import { paths as cliPaths } from '../../lib/paths'; import fs from 'fs-extra'; import { getPackages } from '@manypkg/get-packages'; import { isChildPath } from '@backstage/cli-common'; import nodeExternals from 'webpack-node-externals'; -import { optimization } from './optimization'; +import { optimization as optimizationConfig } from './optimization'; import pickBy from 'lodash/pickBy'; import { readEntryPoints } from '../entryPoints'; import { runPlain } from '../run'; @@ -43,15 +50,42 @@ import { hasReactDomClient } from './hasReactDomClient'; const BUILD_CACHE_ENV_VAR = 'BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE'; -export function resolveBaseUrl(config: Config): URL { +export function resolveBaseUrl( + config: Config, + moduleFederation?: ModuleFederationOptions, +): URL { const baseUrl = config.getOptionalString('app.baseUrl'); + + const defaultBaseUrl = + moduleFederation?.mode === 'remote' + ? `http://localhost:${process.env.PORT ?? '3000'}` + : 'http://localhost:3000'; + try { - return new URL(baseUrl ?? '/', 'http://localhost:3000'); + return new URL(baseUrl ?? '/', defaultBaseUrl); } catch (error) { throw new Error(`Invalid app.baseUrl, ${error}`); } } +export function resolveEndpoint( + config: Config, + moduleFederation?: ModuleFederationOptions, +): { + host: string; + port: number; +} { + const url = resolveBaseUrl(config, moduleFederation); + + return { + host: config.getOptionalString('app.listen.host') ?? url.hostname, + port: + config.getOptionalNumber('app.listen.port') ?? + Number(url.port) ?? + (url.protocol === 'https:' ? 443 : 80), + }; +} + async function readBuildInfo() { const timestamp = Date.now(); @@ -92,7 +126,13 @@ export async function createConfig( paths: BundlingPaths, options: BundlingOptions, ): Promise { - const { checksEnabled, isDev, frontendConfig, publicSubPath = '' } = options; + const { + checksEnabled, + isDev, + frontendConfig, + moduleFederation, + publicSubPath = '', + } = options; const { plugins, loaders } = transforms(options); // Any package that is part of the monorepo but outside the monorepo root dir need @@ -100,12 +140,29 @@ export async function createConfig( const { packages } = await getPackages(cliPaths.targetDir); const externalPkgs = packages.filter(p => !isChildPath(paths.root, p.dir)); - const validBaseUrl = resolveBaseUrl(frontendConfig); + const validBaseUrl = resolveBaseUrl(frontendConfig, moduleFederation); let publicPath = validBaseUrl.pathname.replace(/\/$/, ''); if (publicSubPath) { publicPath = `${publicPath}${publicSubPath}`.replace('//', '/'); } + if (isDev) { + const { host, port } = resolveEndpoint( + options.frontendConfig, + options.moduleFederation, + ); + + plugins.push( + new ReactRefreshPlugin({ + overlay: { + sockProtocol: 'ws', + sockHost: host, + sockPort: port, + }, + }), + ); + } + if (checksEnabled) { plugins.push( new ForkTsCheckerWebpackPlugin({ @@ -123,23 +180,88 @@ export async function createConfig( // to remove this eventually! plugins.push( new ProvidePlugin({ - process: 'process/browser', + process: require.resolve('process/browser'), Buffer: ['buffer', 'Buffer'], }), ); - plugins.push( - new HtmlWebpackPlugin({ - meta: { - 'backstage-app-mode': options?.appMode ?? 'public', - }, - template: paths.targetHtml, - templateParameters: { - publicPath, - config: frontendConfig, - }, - }), - ); + if (options.moduleFederation?.mode !== 'remote') { + plugins.push( + new HtmlWebpackPlugin({ + meta: { + 'backstage-app-mode': options?.appMode ?? 'public', + }, + template: paths.targetHtml, + templateParameters: { + publicPath, + config: frontendConfig, + }, + }), + ); + } + + if (options.moduleFederation) { + const isRemote = options.moduleFederation?.mode === 'remote'; + + plugins.push( + new ModuleFederationPlugin({ + ...(isRemote && { + filename: 'remoteEntry.js', + exposes: { + '.': paths.targetEntry, + }, + }), + name: options.moduleFederation.name, + runtime: false, + shared: { + // React + react: { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + 'react-dom': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + // React Router + 'react-router': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + 'react-router-dom': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + // MUI v4 + '@material-ui/core/styles': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + '@material-ui/styles': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + // MUI v5 + '@mui/material/styles/': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + '@emotion/react': { + singleton: true, + requiredVersion: '*', + eager: !isRemote, + }, + }, + }), + ); + } const buildInfo = await readBuildInfo(); plugins.push( @@ -166,12 +288,53 @@ export async function createConfig( require.resolve('react-refresh'), ]; + const mode = isDev ? 'development' : 'production'; + const optimization = optimizationConfig(options); + + if ( + mode === 'production' && + process.env.EXPERIMENTAL_MODULE_FEDERATION && + process.env.FORCE_REACT_DEVELOPMENT + ) { + console.log( + chalk.yellow( + `⚠️ WARNING: Forcing react and react-dom into development mode. This build should not be used in production.`, + ), + ); + + const reactPackageDirs = [ + `${dirname(require.resolve('react/package.json'))}/`, + `${dirname(require.resolve('react-dom/package.json'))}/`, + ]; + + // Don't define process.env.NODE_ENV with value matching config.mode. If we + // don't set this to false, webpack will define the value of + // process.env.NODE_ENV for us, and the definition below will be ignored. + optimization.nodeEnv = false; + + // Instead, provide a custom definition which always uses "development" if + // the module is part of `react` or `react-dom`, and `config.mode` otherwise. + plugins.push( + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': webpack.DefinePlugin.runtimeValue( + ({ module }) => { + if (reactPackageDirs.some(val => module.resource.startsWith(val))) { + return '"development"'; + } + + return `"${mode}"`; + }, + ), + }), + ); + } + const withCache = yn(process.env[BUILD_CACHE_ENV_VAR], { default: false }); return { - mode: isDev ? 'development' : 'production', + mode, profile: false, - optimization: optimization(options), + optimization, bail: false, performance: { hints: false, // we check the gzip size instead @@ -211,8 +374,10 @@ export async function createConfig( rules: loaders, }, output: { + uniqueName: options.moduleFederation?.name, path: paths.targetDist, - publicPath: `${publicPath}/`, + publicPath: + options.moduleFederation?.mode === 'remote' ? 'auto' : `${publicPath}/`, filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', chunkFilename: isDev ? '[name].chunk.js' diff --git a/packages/cli/src/lib/bundler/index.ts b/packages/cli/src/lib/bundler/index.ts index a3b584efc4..2219784dc9 100644 --- a/packages/cli/src/lib/bundler/index.ts +++ b/packages/cli/src/lib/bundler/index.ts @@ -16,4 +16,5 @@ export { serveBackend } from './backend'; export { buildBundle } from './bundle'; +export { getModuleFederationOptions } from './moduleFederation'; export { serveBundle } from './server'; diff --git a/packages/cli/src/lib/bundler/moduleFederation.ts b/packages/cli/src/lib/bundler/moduleFederation.ts new file mode 100644 index 0000000000..ae3f9ab048 --- /dev/null +++ b/packages/cli/src/lib/bundler/moduleFederation.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 chalk from 'chalk'; +import { ModuleFederationOptions } from './types'; + +export function getModuleFederationOptions( + name: string, + isModuleFederationRemote?: boolean, +): ModuleFederationOptions | undefined { + if ( + !isModuleFederationRemote && + !process.env.EXPERIMENTAL_MODULE_FEDERATION + ) { + return undefined; + } + + console.log( + chalk.yellow( + `⚠️ WARNING: Module federation is experimental and will receive immediate breaking changes in the future.`, + ), + ); + + return { + mode: isModuleFederationRemote ? 'remote' : 'host', + // The default output mode requires the name to be a usable as a code + // symbol, there might be better options here but for now we need to + // sanitize the name. + name: name.replaceAll('@', '').replaceAll('/', '__').replaceAll('-', '_'), + }; +} diff --git a/packages/cli/src/lib/bundler/optimization.ts b/packages/cli/src/lib/bundler/optimization.ts index 88a0343099..cf240b4791 100644 --- a/packages/cli/src/lib/bundler/optimization.ts +++ b/packages/cli/src/lib/bundler/optimization.ts @@ -28,8 +28,15 @@ export const optimization = ( minimize: !isDev, minimizer: [ new EsbuildPlugin({ - target: 'es2019', + target: 'ES2022', format: 'iife', + exclude: 'remoteEntry.js', + }), + // Avoid iife wrapping of module federation remote entry as it breaks the variable assignment + new EsbuildPlugin({ + target: 'ES2022', + format: undefined, + include: 'remoteEntry.js', }), ], runtimeChunk: 'single', diff --git a/packages/cli/src/lib/bundler/server.ts b/packages/cli/src/lib/bundler/server.ts index 1da15cd37e..259f3263eb 100644 --- a/packages/cli/src/lib/bundler/server.ts +++ b/packages/cli/src/lib/bundler/server.ts @@ -14,23 +14,16 @@ * limitations under the License. */ -import { PackageGraph } from '@backstage/cli-node'; import { AppConfig } from '@backstage/config'; import chalk from 'chalk'; import fs from 'fs-extra'; -import uniq from 'lodash/uniq'; import openBrowser from 'react-dev-utils/openBrowser'; import webpack from 'webpack'; import WebpackDevServer from 'webpack-dev-server'; -import { - forbiddenDuplicatesFilter, - includedFilter, -} from '../../commands/versions/lint'; import { paths as libPaths } from '../../lib/paths'; import { loadCliConfig } from '../config'; -import { Lockfile } from '../versioning'; -import { createConfig, resolveBaseUrl } from './config'; +import { createConfig, resolveBaseUrl, resolveEndpoint } from './config'; import { createDetectedModulesEntryPoint } from './packageDetection'; import { resolveBundlingPaths, resolveOptionalBundlingPaths } from './paths'; import { ServeOptions } from './types'; @@ -41,38 +34,6 @@ export async function serveBundle(options: ServeOptions) { const targetPkg = await fs.readJson(paths.targetPackageJson); if (options.verifyVersions) { - const lockfile = await Lockfile.load( - libPaths.resolveTargetRoot('yarn.lock'), - ); - const result = lockfile.analyze({ - filter: includedFilter, - localPackages: PackageGraph.fromPackages( - await PackageGraph.listTargetPackages(), - ), - }); - const problemPackages = [...result.newVersions, ...result.newRanges] - .map(({ name }) => name) - .filter(forbiddenDuplicatesFilter); - - if (problemPackages.length > 1) { - console.log( - chalk.yellow( - `⚠️ Some of the following packages may be outdated or have duplicate installations: - - ${uniq(problemPackages).join(', ')} - `, - ), - ); - console.log( - chalk.yellow( - `⚠️ This can be resolved using the following command: - - yarn backstage-cli versions:check --fix - `, - ), - ); - } - if ( targetPkg.dependencies?.['react-router']?.includes('beta') || targetPkg.dependencies?.['react-router-dom']?.includes('beta') @@ -130,14 +91,11 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be } const { frontendConfig, fullConfig } = cliConfig; - const url = resolveBaseUrl(frontendConfig); - - const host = - frontendConfig.getOptionalString('app.listen.host') || url.hostname; - const port = - frontendConfig.getOptionalNumber('app.listen.port') || - Number(url.port) || - (url.protocol === 'https:' ? 443 : 80); + const url = resolveBaseUrl(frontendConfig, options.moduleFederation); + const { host, port } = resolveEndpoint( + frontendConfig, + options.moduleFederation, + ); const detectedModulesEntryPoint = await createDetectedModulesEntryPoint({ config: fullConfig, @@ -162,6 +120,7 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be const config = await createConfig(paths, { ...commonConfigOptions, additionalEntryPoints: detectedModulesEntryPoint, + moduleFederation: options.moduleFederation, }); if (process.env.EXPERIMENTAL_VITE) { @@ -232,14 +191,17 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be directory: paths.targetPublic, } : undefined, - historyApiFallback: { - // Paths with dots should still use the history fallback. - // See https://github.com/facebookincubator/create-react-app/issues/387. - disableDotRule: true, + historyApiFallback: + options.moduleFederation?.mode === 'remote' + ? false + : { + // Paths with dots should still use the history fallback. + // See https://github.com/facebookincubator/create-react-app/issues/387. + disableDotRule: true, - // The index needs to be rewritten relative to the new public path, including subroutes. - index: `${config.output?.publicPath}index.html`, - }, + // The index needs to be rewritten relative to the new public path, including subroutes. + index: `${config.output?.publicPath}index.html`, + }, server: url.protocol === 'https:' ? { @@ -256,7 +218,13 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be // When the dev server is behind a proxy, the host and public hostname differ allowedHosts: [url.hostname], client: { - webSocketURL: 'auto://0.0.0.0:0/ws', + webSocketURL: { hostname: host, port }, + }, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': + 'X-Requested-With, content-type, Authorization', }, }, compiler, @@ -278,7 +246,9 @@ DEPRECATION WARNING: React Router Beta is deprecated and support for it will be } }); - openBrowser(url.href); + if (!options.skipOpenBrowser) { + openBrowser(url.href); + } const waitForExit = async () => { for (const signal of ['SIGINT', 'SIGTERM'] as const) { diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index da1eec4e11..302b2fbe68 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -17,7 +17,6 @@ import { ModuleOptions, WebpackPluginInstance } from 'webpack'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import { svgrTemplate } from '../svgrTemplate'; -import ReactRefreshPlugin from '@pmmmwh/react-refresh-webpack-plugin'; type Transforms = { loaders: ModuleOptions['rules']; @@ -58,7 +57,7 @@ export const transforms = (options: TransformOptions): Transforms => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'es2022', externalHelpers: !isBackend, parser: { syntax: 'typescript', @@ -86,7 +85,7 @@ export const transforms = (options: TransformOptions): Transforms => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'es2022', externalHelpers: !isBackend, parser: { syntax: 'ecmascript', @@ -119,7 +118,7 @@ export const transforms = (options: TransformOptions): Transforms => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'es2022', externalHelpers: !isBackend, parser: { syntax: 'ecmascript', @@ -193,15 +192,7 @@ export const transforms = (options: TransformOptions): Transforms => { const plugins = new Array(); - if (isDev) { - if (!isBackend) { - plugins.push( - new ReactRefreshPlugin({ - overlay: { sockProtocol: 'ws' }, - }), - ); - } - } else { + if (!isDev) { plugins.push( new MiniCssExtractPlugin({ filename: 'static/[name].[contenthash:8].css', diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index 85a641465f..f8e228c5c4 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -18,6 +18,13 @@ import { AppConfig, Config } from '@backstage/config'; import { BundlingPathsOptions } from './paths'; import { ConfigSchema } from '@backstage/config-loader'; +export type ModuleFederationOptions = { + // Unique name for this module federation bundle + name: string; + // Whether this is a host or remote bundle + mode: 'host' | 'remote'; +}; + export type BundlingOptions = { checksEnabled: boolean; isDev: boolean; @@ -29,12 +36,15 @@ export type BundlingOptions = { publicSubPath?: string; // Mode that the app is running in, 'protected' or 'public', default is 'public' appMode?: string; + moduleFederation?: ModuleFederationOptions; }; export type ServeOptions = BundlingPathsOptions & { checksEnabled: boolean; configPaths: string[]; verifyVersions?: boolean; + skipOpenBrowser?: boolean; + moduleFederation?: ModuleFederationOptions; }; export type BuildOptions = BundlingPathsOptions & { @@ -46,6 +56,7 @@ export type BuildOptions = BundlingPathsOptions & { frontendConfig: Config; frontendAppConfigs: AppConfig[]; fullConfig: Config; + moduleFederation?: ModuleFederationOptions; }; export type BackendBundlingOptions = { diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 0c657b640e..aab16a8670 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -14,14 +14,9 @@ * limitations under the License. */ -import { - ConfigTarget, - loadConfig, - loadConfigSchema, -} from '@backstage/config-loader'; +import { ConfigSources, loadConfigSchema } from '@backstage/config-loader'; import { AppConfig, ConfigReader } from '@backstage/config'; import { paths } from './paths'; -import { isValidUrl } from './urls'; import { getPackages } from '@manypkg/get-packages'; import { PackageGraph } from '@backstage/cli-node'; @@ -37,13 +32,6 @@ type Options = { }; export async function loadCliConfig(options: Options) { - const configTargets: ConfigTarget[] = []; - options.args.forEach(arg => { - if (!isValidUrl(arg)) { - configTargets.push({ path: paths.resolveTarget(arg) }); - } - }); - // Consider all packages in the monorepo when loading in config const { packages } = await getPackages(paths.targetDir); @@ -75,32 +63,62 @@ export async function loadCliConfig(options: Options) { noUndeclaredProperties: options.strict, }); - const { appConfigs } = await loadConfig({ - experimentalEnvFunc: options.mockEnv + const source = ConfigSources.default({ + allowMissingDefaultConfig: true, + substitutionFunc: options.mockEnv ? async name => process.env[name] || 'x' : undefined, - configRoot: paths.targetRoot, - configTargets: configTargets, - watch: options.watch && { - onChange(newAppConfigs) { - const newFrontendAppConfigs = schema.process(newAppConfigs, { - visibility: options.fullVisibility - ? ['frontend', 'backend', 'secret'] - : ['frontend'], - withFilteredKeys: options.withFilteredKeys, - withDeprecatedKeys: options.withDeprecatedKeys, - ignoreSchemaErrors: !options.strict, - }); - options.watch?.(newFrontendAppConfigs); - }, - }, + watch: Boolean(options.watch), + rootDir: paths.targetRoot, + argv: options.args.flatMap(t => ['--config', paths.resolveTarget(t)]), }); + const appConfigs = await new Promise((resolve, reject) => { + async function loadConfigReaderLoop() { + let loaded = false; + + try { + const abortController = new AbortController(); + for await (const { configs } of source.readConfigData({ + signal: abortController.signal, + })) { + if (loaded) { + const newFrontendAppConfigs = schema.process(configs, { + visibility: options.fullVisibility + ? ['frontend', 'backend', 'secret'] + : ['frontend'], + withFilteredKeys: options.withFilteredKeys, + withDeprecatedKeys: options.withDeprecatedKeys, + ignoreSchemaErrors: !options.strict, + }); + options.watch?.(newFrontendAppConfigs); + } else { + resolve(configs); + loaded = true; + + if (!options.watch) { + abortController.abort(); + } + } + } + } catch (error) { + if (loaded) { + console.error(`Failed to reload configuration, ${error}`); + } else { + reject(error); + } + } + } + loadConfigReaderLoop(); + }); + + const configurationLoadedMessage = appConfigs.length + ? `Loaded config from ${appConfigs.map(c => c.context).join(', ')}` + : `No configuration files found, running without config`; + // printing to stderr to not clobber stdout in case the cli command // outputs structured data (e.g. as config:schema does) - process.stderr.write( - `Loaded config from ${appConfigs.map(c => c.context).join(', ')}\n`, - ); + process.stderr.write(`${configurationLoadedMessage}\n`); try { const frontendAppConfigs = schema.process(appConfigs, { diff --git a/packages/cli/src/lib/new/factories/backendModule.test.ts b/packages/cli/src/lib/new/factories/backendModule.test.ts index 78ef5f96bb..59b5403239 100644 --- a/packages/cli/src/lib/new/factories/backendModule.test.ts +++ b/packages/cli/src/lib/new/factories/backendModule.test.ts @@ -26,6 +26,14 @@ import { import { backendModule } from './backendModule'; import { createMockDirectory } from '@backstage/backend-test-utils'; +const backendIndexTsContent = ` +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.start(); +`; + describe('backendModule factory', () => { const mockDir = createMockDirectory(); @@ -44,6 +52,9 @@ describe('backendModule factory', () => { packages: { backend: { 'package.json': JSON.stringify({}), + src: { + 'index.ts': backendIndexTsContent, + }, }, }, plugins: {}, @@ -68,6 +79,7 @@ describe('backendModule factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); @@ -86,8 +98,20 @@ describe('backendModule factory', () => { 'Installing:', `moving plugins${sep}test-backend-module-tester-two`, 'backend adding dependency', + 'backend adding module', ]); + await expect( + fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'), + ).resolves.toBe(` +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('backstage-plugin-test-backend-module-tester-two')); +backend.start(); +`); + await expect( fs.readJson(mockDir.resolve('packages/backend/package.json')), ).resolves.toEqual({ diff --git a/packages/cli/src/lib/new/factories/backendModule.ts b/packages/cli/src/lib/new/factories/backendModule.ts index 698146a229..de759538b1 100644 --- a/packages/cli/src/lib/new/factories/backendModule.ts +++ b/packages/cli/src/lib/new/factories/backendModule.ts @@ -20,7 +20,7 @@ import camelCase from 'lodash/camelCase'; import { paths } from '../../paths'; import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { CreateContext, createFactory } from '../types'; -import { addPackageDependency, Task } from '../../tasks'; +import { addPackageDependency, addToBackend, Task } from '../../tasks'; import { moduleIdIdPrompt, ownerPrompt, @@ -75,6 +75,7 @@ export const backendModule = createFactory({ packageVersion: ctx.defaultVersion, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, + license: ctx.license, }, }); @@ -91,6 +92,10 @@ export const backendModule = createFactory({ }); } + await addToBackend(name, { + type: 'module', + }); + if (options.owner) { await addCodeownersEntry(`/plugins/${dirName}`, options.owner); } diff --git a/packages/cli/src/lib/new/factories/backendPlugin.test.ts b/packages/cli/src/lib/new/factories/backendPlugin.test.ts index c5930592a3..3b90e2b0dd 100644 --- a/packages/cli/src/lib/new/factories/backendPlugin.test.ts +++ b/packages/cli/src/lib/new/factories/backendPlugin.test.ts @@ -26,6 +26,14 @@ import { import { backendPlugin } from './backendPlugin'; import { createMockDirectory } from '@backstage/backend-test-utils'; +const backendIndexTsContent = ` +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.start(); +`; + describe('backendPlugin factory', () => { const mockDir = createMockDirectory(); @@ -44,6 +52,9 @@ describe('backendPlugin factory', () => { packages: { backend: { 'package.json': JSON.stringify({}), + src: { + 'index.ts': backendIndexTsContent, + }, }, }, plugins: {}, @@ -67,6 +78,7 @@ describe('backendPlugin factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); @@ -89,6 +101,7 @@ describe('backendPlugin factory', () => { 'Installing:', `moving plugins${sep}test-backend`, 'backend adding dependency', + 'backend adding plugin', ]); await expect( @@ -99,6 +112,17 @@ describe('backendPlugin factory', () => { }, }); + await expect( + fs.readFile(mockDir.resolve('packages/backend/src/index.ts'), 'utf8'), + ).resolves.toBe(` +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('backstage-plugin-test-backend')); +backend.start(); +`); + expect(Task.forCommand).toHaveBeenCalledTimes(2); expect(Task.forCommand).toHaveBeenCalledWith('yarn install', { cwd: mockDir.resolve('plugins/test-backend'), diff --git a/packages/cli/src/lib/new/factories/backendPlugin.ts b/packages/cli/src/lib/new/factories/backendPlugin.ts index 09ea98fbe7..9129a66075 100644 --- a/packages/cli/src/lib/new/factories/backendPlugin.ts +++ b/packages/cli/src/lib/new/factories/backendPlugin.ts @@ -20,7 +20,7 @@ import camelCase from 'lodash/camelCase'; import { paths } from '../../paths'; import { addCodeownersEntry, getCodeownersFilePath } from '../../codeowners'; import { CreateContext, createFactory } from '../types'; -import { addPackageDependency, Task } from '../../tasks'; +import { addPackageDependency, addToBackend, Task } from '../../tasks'; import { ownerPrompt, pluginIdPrompt } from './common/prompts'; import { executePluginPackageTemplate } from './common/tasks'; import { resolvePackageName } from './common/util'; @@ -64,6 +64,7 @@ export const backendPlugin = createFactory({ pluginVersion: ctx.defaultVersion, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, + license: ctx.license, }, }); @@ -78,6 +79,10 @@ export const backendPlugin = createFactory({ }, ); }); + + await addToBackend(name, { + type: 'plugin', + }); } if (options.owner) { diff --git a/packages/cli/src/lib/new/factories/frontendPlugin.test.ts b/packages/cli/src/lib/new/factories/frontendPlugin.test.ts index cca79dea2a..55e569a15f 100644 --- a/packages/cli/src/lib/new/factories/frontendPlugin.test.ts +++ b/packages/cli/src/lib/new/factories/frontendPlugin.test.ts @@ -80,6 +80,7 @@ describe('frontendPlugin factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); @@ -174,6 +175,7 @@ const router = ( defaultVersion: '1.0.0', markAsModified: () => {}, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); await expect( diff --git a/packages/cli/src/lib/new/factories/frontendPlugin.ts b/packages/cli/src/lib/new/factories/frontendPlugin.ts index 1058c879c1..41907d9d3a 100644 --- a/packages/cli/src/lib/new/factories/frontendPlugin.ts +++ b/packages/cli/src/lib/new/factories/frontendPlugin.ts @@ -67,6 +67,7 @@ export const frontendPlugin = createFactory({ pluginVersion: ctx.defaultVersion, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/factories/nodeLibraryPackage.test.ts b/packages/cli/src/lib/new/factories/nodeLibraryPackage.test.ts index e3673d21ba..69999761e9 100644 --- a/packages/cli/src/lib/new/factories/nodeLibraryPackage.test.ts +++ b/packages/cli/src/lib/new/factories/nodeLibraryPackage.test.ts @@ -64,6 +64,7 @@ describe('nodeLibraryPackage factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); @@ -134,6 +135,7 @@ describe('nodeLibraryPackage factory', () => { defaultVersion: '1.0.0', markAsModified: () => {}, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(Task.forCommand).toHaveBeenCalledTimes(2); diff --git a/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts b/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts index 687caa7cd5..3d9029a01d 100644 --- a/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts +++ b/packages/cli/src/lib/new/factories/nodeLibraryPackage.ts @@ -61,6 +61,7 @@ export const nodeLibraryPackage = createFactory({ pluginVersion: ctx.defaultVersion, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/factories/pluginCommon.test.ts b/packages/cli/src/lib/new/factories/pluginCommon.test.ts index d85e490510..8f01401435 100644 --- a/packages/cli/src/lib/new/factories/pluginCommon.test.ts +++ b/packages/cli/src/lib/new/factories/pluginCommon.test.ts @@ -62,12 +62,13 @@ describe('pluginCommon factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); expectLogsToMatch(output, [ - 'Creating backend plugin backstage-plugin-test-common', + 'Creating common plugin package backstage-plugin-test-common', 'Checking Prerequisites:', `availability plugins${sep}test-common`, 'creating temp dir', diff --git a/packages/cli/src/lib/new/factories/pluginCommon.ts b/packages/cli/src/lib/new/factories/pluginCommon.ts index 560386c618..eb17c70a5c 100644 --- a/packages/cli/src/lib/new/factories/pluginCommon.ts +++ b/packages/cli/src/lib/new/factories/pluginCommon.ts @@ -46,7 +46,7 @@ export const pluginCommon = createFactory({ }); Task.log(); - Task.log(`Creating backend plugin ${chalk.cyan(name)}`); + Task.log(`Creating common plugin package ${chalk.cyan(name)}`); const targetDir = ctx.isMonoRepo ? paths.resolveTargetRoot('plugins', suffix) @@ -61,6 +61,7 @@ export const pluginCommon = createFactory({ privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, pluginVersion: ctx.defaultVersion, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/factories/pluginNode.test.ts b/packages/cli/src/lib/new/factories/pluginNode.test.ts index d8d9ea33ab..b6aad0a129 100644 --- a/packages/cli/src/lib/new/factories/pluginNode.test.ts +++ b/packages/cli/src/lib/new/factories/pluginNode.test.ts @@ -62,6 +62,7 @@ describe('pluginNode factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); diff --git a/packages/cli/src/lib/new/factories/pluginNode.ts b/packages/cli/src/lib/new/factories/pluginNode.ts index c7f9ba6f6a..8eaab3cadb 100644 --- a/packages/cli/src/lib/new/factories/pluginNode.ts +++ b/packages/cli/src/lib/new/factories/pluginNode.ts @@ -61,6 +61,7 @@ export const pluginNode = createFactory({ privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, pluginVersion: ctx.defaultVersion, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/factories/pluginWeb.test.ts b/packages/cli/src/lib/new/factories/pluginWeb.test.ts index ff4211d8fa..20fbbbbdb7 100644 --- a/packages/cli/src/lib/new/factories/pluginWeb.test.ts +++ b/packages/cli/src/lib/new/factories/pluginWeb.test.ts @@ -62,6 +62,7 @@ describe('pluginWeb factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); diff --git a/packages/cli/src/lib/new/factories/pluginWeb.ts b/packages/cli/src/lib/new/factories/pluginWeb.ts index dc8cb73dce..6635b0b0e1 100644 --- a/packages/cli/src/lib/new/factories/pluginWeb.ts +++ b/packages/cli/src/lib/new/factories/pluginWeb.ts @@ -61,6 +61,7 @@ export const pluginWeb = createFactory({ privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, pluginVersion: ctx.defaultVersion, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts index d83c2ce3f7..c4b9d78fc1 100644 --- a/packages/cli/src/lib/new/factories/scaffolderModule.test.ts +++ b/packages/cli/src/lib/new/factories/scaffolderModule.test.ts @@ -62,6 +62,7 @@ describe('scaffolderModule factory', () => { modified = true; }, createTemporaryDirectory: (name: string) => fs.mkdtemp(name), + license: 'Apache-2.0', }); expect(modified).toBe(true); diff --git a/packages/cli/src/lib/new/factories/scaffolderModule.ts b/packages/cli/src/lib/new/factories/scaffolderModule.ts index 07f67ce78c..4c279a49d4 100644 --- a/packages/cli/src/lib/new/factories/scaffolderModule.ts +++ b/packages/cli/src/lib/new/factories/scaffolderModule.ts @@ -78,6 +78,7 @@ export const scaffolderModule = createFactory({ privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, pluginVersion: ctx.defaultVersion, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts b/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts index b0c8461189..691f0b9f1c 100644 --- a/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts +++ b/packages/cli/src/lib/new/factories/webLibraryPackage.test.ts @@ -64,6 +64,7 @@ describe('webLibraryPackage factory', () => { modified = true; }, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(modified).toBe(true); @@ -134,6 +135,7 @@ describe('webLibraryPackage factory', () => { defaultVersion: '1.0.0', markAsModified: () => {}, createTemporaryDirectory: () => fs.mkdtemp('test'), + license: 'Apache-2.0', }); expect(Task.forCommand).toHaveBeenCalledTimes(2); diff --git a/packages/cli/src/lib/new/factories/webLibraryPackage.ts b/packages/cli/src/lib/new/factories/webLibraryPackage.ts index 45e1085ff3..3c63127d7d 100644 --- a/packages/cli/src/lib/new/factories/webLibraryPackage.ts +++ b/packages/cli/src/lib/new/factories/webLibraryPackage.ts @@ -61,6 +61,7 @@ export const webLibraryPackage = createFactory({ pluginVersion: ctx.defaultVersion, privatePackage: ctx.private, npmRegistry: ctx.npmRegistry, + license: ctx.license, }, }); diff --git a/packages/cli/src/lib/new/types.ts b/packages/cli/src/lib/new/types.ts index 6a894bf9ef..534f99f94f 100644 --- a/packages/cli/src/lib/new/types.ts +++ b/packages/cli/src/lib/new/types.ts @@ -27,6 +27,8 @@ export interface CreateContext { isMonoRepo: boolean; /** The default version to use for new packages */ defaultVersion: string; + /** License to use for new packages */ + license: string; /** Creates a temporary directory. This will always be deleted after creation is done. */ createTemporaryDirectory(name: string): Promise; diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 851f962c62..ffff2892fb 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -23,6 +23,7 @@ import { basename, dirname } from 'path'; import recursive from 'recursive-readdir'; import { exec as execCb } from 'child_process'; import { assertError } from '@backstage/errors'; +import { paths } from './paths'; const exec = promisify(execCb); @@ -196,3 +197,37 @@ export async function addPackageDependency( throw new Error(`Failed to add package dependencies, ${error}`); } } + +export async function addToBackend( + name: string, + options: { + type: 'plugin' | 'module'; + }, +) { + if (await fs.pathExists(paths.resolveTargetRoot('packages/backend'))) { + await Task.forItem('backend', `adding ${options.type}`, async () => { + const backendFilePath = paths.resolveTargetRoot( + 'packages/backend/src/index.ts', + ); + if (!(await fs.pathExists(backendFilePath))) { + return; + } + + const content = await fs.readFile(backendFilePath, 'utf8'); + const lines = content.split('\n'); + const backendAddLine = `backend.add(import('${name}'));`; + + const backendStartIndex = lines.findIndex(line => + line.match(/backend.start/), + ); + + if (backendStartIndex !== -1) { + const [indentation] = lines[backendStartIndex].match(/^\s*/)!; + lines.splice(backendStartIndex, 0, `${indentation}${backendAddLine}`); + + const newContent = lines.join('\n'); + await fs.writeFile(backendFilePath, newContent, 'utf8'); + } + }); + } +} diff --git a/packages/cli/src/lib/versioning/Lockfile.test.ts b/packages/cli/src/lib/versioning/Lockfile.test.ts index a4267acdb7..d6996b9143 100644 --- a/packages/cli/src/lib/versioning/Lockfile.test.ts +++ b/packages/cli/src/lib/versioning/Lockfile.test.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { BackstagePackage } from '@backstage/cli-node'; import { Lockfile } from './Lockfile'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -47,34 +45,6 @@ b@^2: version "2.0.0" `; -const mockADedup = `${LEGACY_HEADER} -a@^1: - version "1.0.1" - resolved "https://my-registry/a-1.0.01.tgz#abc123" - integrity sha512-xyz - dependencies: - b "^2" - -b@2.0.x, b@^2: - version "2.0.1" -`; - -const mockB = `${LEGACY_HEADER} -"@s/a@*", "@s/a@1 || 2", "@s/a@^1": - version "1.0.1" - -"@s/a@^2.0.x": - version "2.0.0" -`; - -const mockBDedup = `${LEGACY_HEADER} -"@s/a@*", "@s/a@1 || 2", "@s/a@^2.0.x": - version "2.0.0" - -"@s/a@^1": - version "1.0.1" -`; - describe('Lockfile', () => { const mockDir = createMockDirectory(); @@ -93,75 +63,6 @@ describe('Lockfile', () => { ]); expect(lockfile.toString()).toBe(mockA); }); - - it('should deduplicate and save mockA', async () => { - mockDir.setContent({ - 'yarn.lock': mockA, - }); - - const lockfilePath = mockDir.resolve('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); - const result = lockfile.analyze({ localPackages: new Map() }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [], - newVersions: [ - { - name: 'b', - range: '^2', - oldVersion: '2.0.0', - newVersion: '2.0.1', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockA); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockADedup); - - await expect(fs.readFile(lockfilePath, 'utf8')).resolves.toBe(mockA); - await expect(lockfile.save(lockfilePath)).resolves.toBeUndefined(); - await expect(fs.readFile(lockfilePath, 'utf8')).resolves.toBe(mockADedup); - }); - - it('should deduplicate mockB', async () => { - mockDir.setContent({ - 'yarn.lock': mockB, - }); - - const lockfile = await Lockfile.load(mockDir.resolve('yarn.lock')); - const result = lockfile.analyze({ localPackages: new Map() }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [ - { - name: '@s/a', - oldRange: '^1', - newRange: '^2.0.x', - oldVersion: '1.0.1', - newVersion: '2.0.0', - }, - ], - newVersions: [ - { - name: '@s/a', - range: '*', - oldVersion: '1.0.1', - newVersion: '2.0.0', - }, - { - name: '@s/a', - range: '1 || 2', - oldVersion: '1.0.1', - newVersion: '2.0.0', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockB); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockBDedup); - }); }); const mockANew = `${MODERN_HEADER} @@ -179,51 +80,6 @@ b@^2: version: 2.0.0 `; -const mockANewDedup = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 2.0.1 - -b@^2: - version: 2.0.1 -`; - -const mockANewLocal = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 0.0.0-use.local - -b@^2: - version: 2.0.0 -`; - -const mockANewLocalDedup = `${MODERN_HEADER} -a@^1: - version: 1.0.1 - dependencies: - b: ^2 - integrity: sha512-xyz - resolved: "https://my-registry/a-1.0.01.tgz#abc123" - -"b@2.0.x, b@^2.0.1": - version: 0.0.0-use.local - -b@^2: - version: 0.0.0-use.local -`; - describe('New Lockfile', () => { const mockDir = createMockDirectory(); @@ -243,79 +99,4 @@ describe('New Lockfile', () => { ]); expect(lockfile.toString()).toBe(mockANew); }); - - it('should deduplicate and save mockANew', async () => { - mockDir.setContent({ - 'yarn.lock': mockANew, - }); - - const lockfilePath = mockDir.resolve('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); - const result = lockfile.analyze({ localPackages: new Map() }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [], - newVersions: [ - { - name: 'b', - range: '^2', - oldVersion: '2.0.0', - newVersion: '2.0.1', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockANew); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockANewDedup); - - await expect(fs.readFile(lockfilePath, 'utf8')).resolves.toBe(mockANew); - await expect(lockfile.save(lockfilePath)).resolves.toBeUndefined(); - await expect(fs.readFile(lockfilePath, 'utf8')).resolves.toBe( - mockANewDedup, - ); - }); - - it('should deduplicate and save mockANewLocal', async () => { - mockDir.setContent({ - 'yarn.lock': mockANewLocal, - }); - - const lockfilePath = mockDir.resolve('yarn.lock'); - const lockfile = await Lockfile.load(lockfilePath); - const result = lockfile.analyze({ - localPackages: new Map([ - [ - 'b', - { - packageJson: { version: '2.0.1' }, - } as BackstagePackage, - ], - ]), - }); - expect(result).toEqual({ - invalidRanges: [], - newRanges: [], - newVersions: [ - { - name: 'b', - range: '^2', - oldVersion: '2.0.0', - newVersion: '0.0.0-use.local', - }, - ], - }); - - expect(lockfile.toString()).toBe(mockANewLocal); - lockfile.replaceVersions(result.newVersions); - expect(lockfile.toString()).toBe(mockANewLocalDedup); - - await expect(fs.readFile(lockfilePath, 'utf8')).resolves.toBe( - mockANewLocal, - ); - await expect(lockfile.save(lockfilePath)).resolves.toBeUndefined(); - await expect(fs.readFile(lockfilePath, 'utf8')).resolves.toBe( - mockANewLocalDedup, - ); - }); }); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index 50d0a77cfd..047cf5aefc 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -15,10 +15,8 @@ */ import fs from 'fs-extra'; -import semver from 'semver'; import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; -import { BackstagePackage } from '@backstage/cli-node'; const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/; @@ -39,35 +37,6 @@ type LockfileQueryEntry = { dataKey: string; }; -/** Entries that have an invalid version range, for example an npm tag */ -type AnalyzeResultInvalidRange = { - name: string; - range: string; -}; - -/** Entries that can be deduplicated by bumping to an existing higher version */ -type AnalyzeResultNewVersion = { - name: string; - range: string; - oldVersion: string; - newVersion: string; -}; - -/** Entries that would need a dependency update in package.json to be deduplicated */ -type AnalyzeResultNewRange = { - name: string; - oldRange: string; - newRange: string; - oldVersion: string; - newVersion: string; -}; - -type AnalyzeResult = { - invalidRanges: AnalyzeResultInvalidRange[]; - newVersions: AnalyzeResultNewVersion[]; - newRanges: AnalyzeResultNewRange[]; -}; - // the new yarn header is handled out of band of the parsing // https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746 const NEW_HEADER = `${[ @@ -153,186 +122,6 @@ export class Lockfile { return this.packages.keys(); } - /** Analyzes the lockfile to identify possible actions and warnings for the entries */ - analyze(options: { - filter?: (name: string) => boolean; - localPackages: Map; - }): AnalyzeResult { - const { filter, localPackages } = options; - const result: AnalyzeResult = { - invalidRanges: [], - newVersions: [], - newRanges: [], - }; - - for (const [name, allEntries] of this.packages) { - if (filter && !filter(name)) { - continue; - } - - // Get rid of and signal any invalid ranges upfront - const invalid = allEntries.filter( - e => !semver.validRange(e.range) && !e.range.startsWith('workspace:'), - ); - result.invalidRanges.push( - ...invalid.map(({ range }) => ({ name, range })), - ); - - // Grab all valid entries, if there aren't at least 2 different valid ones we're done - const entries = allEntries.filter(e => semver.validRange(e.range)); - if (entries.length < 2) { - continue; - } - - // Find all versions currently in use - const versions = Array.from(new Set(entries.map(e => e.version))) - .map(v => { - // Translate workspace:^ references to the actual version - if (v === '0.0.0-use.local') { - const local = localPackages.get(name); - if (!local) { - throw new Error(`No local package found for ${name}`); - } - if (!local.packageJson.version) { - throw new Error(`No version found for local package ${name}`); - } - return { - entryVersion: v, - actualVersion: local.packageJson.version, - }; - } - return { entryVersion: v, actualVersion: v }; - }) - .sort((v1, v2) => semver.rcompare(v1.actualVersion, v2.actualVersion)); - - // If we're not using at least 2 different versions we're done - if (versions.length < 2) { - continue; - } - - // TODO(Rugvip): Support bumping into workspace ranges too - const acceptedVersions = new Set(); - for (const { version, range } of entries) { - // Finds the highest matching version from the the known versions - // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one - const acceptedVersion = versions.find(v => - semver.satisfies(v.actualVersion, range), - ); - if (!acceptedVersion) { - throw new Error( - `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`, - ); - } - - if (acceptedVersion.entryVersion !== version) { - result.newVersions.push({ - name, - range, - newVersion: acceptedVersion.entryVersion, - oldVersion: version, - }); - } - - acceptedVersions.add(acceptedVersion.actualVersion); - } - - // If all ranges were able to accept the same version, we're done - if (acceptedVersions.size === 1) { - continue; - } - - // Find the max version that we may want bump older packages to - const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0]; - // Find all existing ranges that satisfy the new max version, and pick the one that - // results in the highest minimum allowed version, usually being the more specific one - const maxEntry = entries - .filter(e => semver.satisfies(maxVersion, e.range)) - .map(e => ({ e, min: semver.minVersion(e.range) })) - .filter(p => p.min) - .sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e; - if (!maxEntry) { - throw new Error( - `No entry found that satisfies max version '${maxVersion}'`, - ); - } - - // Find all entries that don't satisfy the max version - for (const { version, range } of entries) { - if (semver.satisfies(maxVersion, range)) { - continue; - } - - result.newRanges.push({ - name, - oldRange: range, - newRange: maxEntry.range, - oldVersion: version, - newVersion: maxVersion, - }); - } - } - - return result; - } - - remove(name: string, range: string): boolean { - const query = `${name}@${range}`; - const existed = Boolean(this.data[query]); - delete this.data[query]; - - const newEntries = this.packages.get(name)?.filter(e => e.range !== range); - if (newEntries) { - this.packages.set(name, newEntries); - } - - return existed; - } - - /** Modifies the lockfile by bumping packages to the suggested versions */ - replaceVersions(results: AnalyzeResultNewVersion[]) { - for (const { name, range, oldVersion, newVersion } of results) { - const query = `${name}@${range}`; - - // Update the backing data - const entryData = this.data[query]; - if (!entryData) { - throw new Error(`No entry data for ${query}`); - } - if (entryData.version !== oldVersion) { - throw new Error( - `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, - ); - } - - // Modifying the data in the entry is not enough, we need to reference an existing version object - const matchingEntry = Object.entries(this.data).find( - ([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion, - ); - if (!matchingEntry) { - throw new Error( - `No matching entry found for ${name} at version ${newVersion}`, - ); - } - this.data[query] = matchingEntry[1]; - - // Update our internal data structure - const entry = this.packages.get(name)?.find(e => e.range === range); - if (!entry) { - throw new Error(`No entry data for ${query}`); - } - if (entry.version !== oldVersion) { - throw new Error( - `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, - ); - } - entry.version = newVersion; - } - } - - async save(path: string) { - await fs.writeFile(path, this.toString(), 'utf8'); - } - toString() { return this.legacy ? legacyStringifyLockfile(this.data) diff --git a/packages/cli/templates/default-backend-module/package.json.hbs b/packages/cli/templates/default-backend-module/package.json.hbs index 07eee73d50..db8b8749b3 100644 --- a/packages/cli/templates/default-backend-module/package.json.hbs +++ b/packages/cli/templates/default-backend-module/package.json.hbs @@ -4,7 +4,7 @@ "version": "{{packageVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/default-backend-plugin/README.md.hbs index 366ed27104..d519c3f459 100644 --- a/packages/cli/templates/default-backend-plugin/README.md.hbs +++ b/packages/cli/templates/default-backend-plugin/README.md.hbs @@ -11,4 +11,4 @@ start` in the root directory, and then navigating to [/{{id}}/health](http://loc You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs index 6b7aa915aa..b05d1a844b 100644 --- a/packages/cli/templates/default-backend-plugin/package.json.hbs +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -3,7 +3,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} @@ -31,21 +31,19 @@ "@backstage/backend-common": "{{versionQuery '@backstage/backend-common'}}", "@backstage/backend-defaults": "{{versionQuery '@backstage/backend-defaults'}}", "@backstage/backend-plugin-api": "{{versionQuery '@backstage/backend-plugin-api'}}", - "@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'}}", - "node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}", - "yn": "{{versionQuery 'yn' '4.0.0'}}" + "node-fetch": "{{versionQuery 'node-fetch' '2.6.7'}}" }, "devDependencies": { + "@backstage/backend-test-utils": "{{versionQuery '@backstage/backend-test-utils'}}", "@backstage/cli": "{{versionQuery '@backstage/cli'}}", "@backstage/plugin-auth-backend": "{{versionQuery '@backstage/plugin-auth-backend'}}", "@backstage/plugin-auth-backend-module-guest-provider": "{{versionQuery '@backstage/plugin-auth-backend-module-guest-provider'}}", + "@types/express": "{{versionQuery '@types/express' '4.17.6'}}", "@types/supertest": "{{versionQuery '@types/supertest' '2.0.12'}}", "supertest": "{{versionQuery 'supertest' '6.2.4'}}", - "msw": "{{versionQuery 'msw' '1.0.0'}}" + "msw": "{{versionQuery 'msw' '2.3.1'}}" }, "files": [ "dist" diff --git a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs index 773bd4354f..bf04b43afe 100644 --- a/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/default-backend-plugin/src/plugin.ts.hbs @@ -16,14 +16,17 @@ export const {{pluginVar}} = createBackendPlugin({ deps: { httpRouter: coreServices.httpRouter, logger: coreServices.logger, + config: coreServices.rootConfig, }, async init({ httpRouter, logger, + config, }) { httpRouter.use( await createRouter({ logger, + config, }), ); httpRouter.addAuthPolicy({ diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts index 4f5079744e..ed19d50d2b 100644 --- a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts +++ b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts @@ -1,4 +1,4 @@ -import { getVoidLogger } from '@backstage/backend-common'; +import { mockServices } from '@backstage/backend-test-utils'; import express from 'express'; import request from 'supertest'; @@ -9,7 +9,8 @@ describe('createRouter', () => { beforeAll(async () => { const router = await createRouter({ - logger: getVoidLogger(), + logger: mockServices.logger.mock(), + config: mockServices.rootConfig(), }); app = express().use(router); }); diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.ts b/packages/cli/templates/default-backend-plugin/src/service/router.ts index ff7134e59d..ced654aa82 100644 --- a/packages/cli/templates/default-backend-plugin/src/service/router.ts +++ b/packages/cli/templates/default-backend-plugin/src/service/router.ts @@ -1,16 +1,17 @@ -import { errorHandler } from '@backstage/backend-common'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter'; +import { LoggerService, RootConfigService } from '@backstage/backend-plugin-api'; import express from 'express'; import Router from 'express-promise-router'; export interface RouterOptions { logger: LoggerService; + config: RootConfigService; } export async function createRouter( options: RouterOptions, ): Promise { - const { logger } = options; + const { logger, config } = options; const router = Router(); router.use(express.json()); @@ -20,6 +21,8 @@ export async function createRouter( response.json({ status: 'ok' }); }); - router.use(errorHandler()); + const middleware = MiddlewareFactory.create({ logger, config }); + + router.use(middleware.error()); return router; } diff --git a/packages/cli/templates/default-common-plugin-package/package.json.hbs b/packages/cli/templates/default-common-plugin-package/package.json.hbs index 992599ef6d..689c3e6f53 100644 --- a/packages/cli/templates/default-common-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-common-plugin-package/package.json.hbs @@ -4,7 +4,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/default-node-plugin-package/package.json.hbs b/packages/cli/templates/default-node-plugin-package/package.json.hbs index 45becd1bea..d58f95b3b2 100644 --- a/packages/cli/templates/default-node-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-node-plugin-package/package.json.hbs @@ -4,7 +4,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index c833c125f6..b4034fd742 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -3,7 +3,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 9db62feebc..c79977f301 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -4,14 +4,14 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { screen } from '@testing-library/react'; import { - setupRequestMockHandlers, + registerMswTestHooks, renderInTestApp, } from '@backstage/test-utils'; describe('ExampleComponent', () => { const server = setupServer(); // Enable sane handlers for network requests - setupRequestMockHandlers(server); + registerMswTestHooks(server); // setup mock response beforeEach(() => { diff --git a/packages/cli/templates/default-react-plugin-package/package.json.hbs b/packages/cli/templates/default-react-plugin-package/package.json.hbs index 79f3430f46..bea066630f 100644 --- a/packages/cli/templates/default-react-plugin-package/package.json.hbs +++ b/packages/cli/templates/default-react-plugin-package/package.json.hbs @@ -4,7 +4,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/node-library-package/package.json.hbs b/packages/cli/templates/node-library-package/package.json.hbs index a6ede31893..d56a51bcd6 100644 --- a/packages/cli/templates/node-library-package/package.json.hbs +++ b/packages/cli/templates/node-library-package/package.json.hbs @@ -3,7 +3,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/scaffolder-module/package.json.hbs b/packages/cli/templates/scaffolder-module/package.json.hbs index 0ebe5dd5f4..df4bf352f4 100644 --- a/packages/cli/templates/scaffolder-module/package.json.hbs +++ b/packages/cli/templates/scaffolder-module/package.json.hbs @@ -4,7 +4,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/cli/templates/web-library-package/package.json.hbs b/packages/cli/templates/web-library-package/package.json.hbs index 271417a525..64a693dd4c 100644 --- a/packages/cli/templates/web-library-package/package.json.hbs +++ b/packages/cli/templates/web-library-package/package.json.hbs @@ -3,7 +3,7 @@ "version": "{{pluginVersion}}", "main": "src/index.ts", "types": "src/index.ts", - "license": "Apache-2.0", + "license": "{{license}}", {{#if privatePackage}} "private": {{privatePackage}}, {{/if}} diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index fb048e7237..78f2123970 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/config-loader +## 1.9.0-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.9.0-next.1 + +### Minor Changes + +- 274428f: Add configuration key to File and Remote `ConfigSource`s that enables configuration of parsing logic. Previously limited to yaml, these `ConfigSource`s now allow for a multitude of parsing options (e.g. JSON). + +### Patch Changes + +- 1edd6c2: The `env` option of `ConfigSources.default` now correctly allows undefined members. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.8.2-next.0 + +### Patch Changes + +- 493feac: Add boolean `allowMissingDefaultConfig` option to `ConfigSources.default` and + `ConfigSources.defaultForTargets`, which results in omission of a ConfigSource + for the default app-config.yaml configuration file if it's not present. +- Updated dependencies + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 1.8.1 ### Patch Changes diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index bc8f217bbb..e2741d609c 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -21,6 +21,7 @@ export type AsyncConfigSourceGenerator = AsyncGenerator< // @public export interface BaseConfigSourcesOptions { + allowMissingDefaultConfig?: boolean; // (undocumented) remote?: Pick; // (undocumented) @@ -87,7 +88,7 @@ export interface ConfigSourcesDefaultOptions extends BaseConfigSourcesOptions { // (undocumented) argv?: string[]; // (undocumented) - env?: Record; + env?: Record; } // @public @@ -141,6 +142,7 @@ export class FileConfigSource implements ConfigSource { // @public export interface FileConfigSourceOptions { + parser?: Parser; path: string; substitutionFunc?: EnvFunc; watch?: boolean; @@ -218,6 +220,11 @@ export interface MutableConfigSourceOptions { data?: JsonObject; } +// @public +export type Parser = ({ contents }: { contents: string }) => Promise<{ + result?: JsonObject; +}>; + // @public export interface ReadConfigDataOptions { // (undocumented) @@ -242,6 +249,7 @@ export class RemoteConfigSource implements ConfigSource { // @public export interface RemoteConfigSourceOptions { + parser?: Parser; reloadInterval?: HumanDuration; substitutionFunc?: EnvFunc; url: string; diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 28718b8290..a47a17c12f 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/config-loader", - "version": "1.8.1", + "version": "1.9.0-next.2", "description": "Config loading functionality used by Backstage backend, and CLI", "backstage": { "role": "node-library" @@ -49,7 +49,7 @@ "json-schema-traverse": "^1.0.0", "lodash": "^4.17.21", "minimist": "^1.2.5", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "typescript-json-schema": "^0.63.0", "yaml": "^2.0.0" }, diff --git a/packages/config-loader/src/sources/ConfigSources.test.ts b/packages/config-loader/src/sources/ConfigSources.test.ts index 4b14572666..e19ac1d51f 100644 --- a/packages/config-loader/src/sources/ConfigSources.test.ts +++ b/packages/config-loader/src/sources/ConfigSources.test.ts @@ -69,13 +69,18 @@ describe('ConfigSources', () => { }); it('should create default sources for targets', () => { + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockImplementation(path => { + return path === `${root}app-config.yaml`; + }); + expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), ), ).toEqual([{ name: 'FileConfigSource', path: `${root}app-config.yaml` }]); - const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockReturnValue(true); + fsSpy.mockReturnValue(true); + expect( mergeSources( ConfigSources.defaultForTargets({ rootDir: '/', targets: [] }), @@ -159,6 +164,10 @@ describe('ConfigSources', () => { }); it('should create a default source', () => { + const fsSpy = jest.spyOn(fs, 'pathExistsSync').mockImplementation(path => { + return path === `${root}app-config.yaml`; + }); + expect( mergeSources( ConfigSources.default({ @@ -184,6 +193,8 @@ describe('ConfigSources', () => { { name: 'FileConfigSource', path: resolvePath('b.yaml') }, { name: 'EnvConfigSource', env: { HOME: '/' } }, ]); + + fsSpy.mockRestore(); }); it('should merge sources', () => { diff --git a/packages/config-loader/src/sources/ConfigSources.ts b/packages/config-loader/src/sources/ConfigSources.ts index ecff39da2d..37c1c6b087 100644 --- a/packages/config-loader/src/sources/ConfigSources.ts +++ b/packages/config-loader/src/sources/ConfigSources.ts @@ -74,6 +74,11 @@ export interface BaseConfigSourcesOptions { watch?: boolean; rootDir?: string; remote?: Pick; + /** + * Allow the default app-config.yaml to be missing, in which case the source + * will not be created. + */ + allowMissingDefaultConfig?: boolean; /** * A custom substitution function that overrides the default one. @@ -102,7 +107,7 @@ export interface ConfigSourcesDefaultForTargetsOptions */ export interface ConfigSourcesDefaultOptions extends BaseConfigSourcesOptions { argv?: string[]; - env?: Record; + env?: Record; } /** @@ -177,14 +182,19 @@ export class ConfigSources { if (argSources.length === 0) { const defaultPath = resolvePath(rootDir, 'app-config.yaml'); const localPath = resolvePath(rootDir, 'app-config.local.yaml'); + const alwaysIncludeDefaultConfigSource = + !options.allowMissingDefaultConfig; + + if (alwaysIncludeDefaultConfigSource || fs.pathExistsSync(defaultPath)) { + argSources.push( + FileConfigSource.create({ + watch: options.watch, + path: defaultPath, + substitutionFunc: options.substitutionFunc, + }), + ); + } - argSources.push( - FileConfigSource.create({ - watch: options.watch, - path: defaultPath, - substitutionFunc: options.substitutionFunc, - }), - ); if (fs.pathExistsSync(localPath)) { argSources.push( FileConfigSource.create({ diff --git a/packages/config-loader/src/sources/FileConfigSource.test.ts b/packages/config-loader/src/sources/FileConfigSource.test.ts index 61cdeed272..4ed31ccee0 100644 --- a/packages/config-loader/src/sources/FileConfigSource.test.ts +++ b/packages/config-loader/src/sources/FileConfigSource.test.ts @@ -59,6 +59,19 @@ describe('FileConfigSource', () => { ]); }); + it('should read a config file with optional parser', async () => { + const tmp = await tmpFiles({ 'a.json': JSON.stringify({ a: 1 }) }); + + const source = FileConfigSource.create({ + path: tmp.resolve('a.json'), + parser: async ({ contents }) => ({ result: JSON.parse(contents) }), + }); + + await expect(readN(source, 1)).resolves.toEqual([ + [{ data: { a: 1 }, context: 'a.json', path: tmp.resolve('a.json') }], + ]); + }); + it('should watch config files', async () => { const tmp = await tmpFiles({ 'a.yaml': 'a: 1' }); diff --git a/packages/config-loader/src/sources/FileConfigSource.ts b/packages/config-loader/src/sources/FileConfigSource.ts index 74266de227..c0697c1180 100644 --- a/packages/config-loader/src/sources/FileConfigSource.ts +++ b/packages/config-loader/src/sources/FileConfigSource.ts @@ -17,16 +17,17 @@ import chokidar, { FSWatcher } from 'chokidar'; import fs from 'fs-extra'; import { basename, dirname, isAbsolute, resolve as resolvePath } from 'path'; -import yaml from 'yaml'; import { AsyncConfigSourceGenerator, ConfigSource, ConfigSourceData, SubstitutionFunc, + Parser, ReadConfigDataOptions, } from './types'; import { createConfigTransformer } from './transform'; import { NotFoundError } from '@backstage/errors'; +import { parseYamlContent } from './utils'; /** * Options for {@link FileConfigSource.create}. @@ -48,6 +49,11 @@ export interface FileConfigSourceOptions { * A substitution function to use instead of the default environment substitution. */ substitutionFunc?: SubstitutionFunc; + + /** + * A content parsing function to transform string content to configuration values. + */ + parser?: Parser; } async function readFile(path: string): Promise { @@ -95,11 +101,13 @@ export class FileConfigSource implements ConfigSource { readonly #path: string; readonly #substitutionFunc?: SubstitutionFunc; readonly #watch?: boolean; + readonly #parser: Parser; private constructor(options: FileConfigSourceOptions) { this.#path = options.path; this.#substitutionFunc = options.substitutionFunc; this.#watch = options.watch ?? true; + this.#parser = options.parser ?? parseYamlContent; } // Work is duplicated across each read, in practice that should not @@ -154,12 +162,12 @@ export class FileConfigSource implements ConfigSource { watchedPaths.push(this.#path); } - const content = await readFile(this.#path); - if (content === undefined) { + const contents = await readFile(this.#path); + if (contents === undefined) { throw new NotFoundError(`Config file "${this.#path}" does not exist`); } - const parsed = yaml.parse(content); - if (parsed === null) { + const { result: parsed } = await this.#parser({ contents }); + if (parsed === undefined) { return []; } try { diff --git a/packages/config-loader/src/sources/RemoteConfigSource.test.ts b/packages/config-loader/src/sources/RemoteConfigSource.test.ts index 6082c05e65..372dc91823 100644 --- a/packages/config-loader/src/sources/RemoteConfigSource.test.ts +++ b/packages/config-loader/src/sources/RemoteConfigSource.test.ts @@ -15,14 +15,14 @@ */ import { rest } from 'msw'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; import { RemoteConfigSource } from './RemoteConfigSource'; import { readN } from './__testUtils__/testUtils'; describe('RemoteConfigSource', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('should load config from a remote URL', async () => { worker.use( @@ -59,6 +59,45 @@ app: ]); }); + it('should load and parse config from a remote URL', async () => { + worker.use( + rest.get('http://localhost/config.json', (_req, res, ctx) => + res( + ctx.body( + JSON.stringify({ + app: { + title: 'Example App', + substituted: 'x', + escaped: '$${VALUE}', + }, + }), + ), + ), + ), + ); + + const source = RemoteConfigSource.create({ + url: 'http://localhost/config.json', + substitutionFunc: async () => 'x', + parser: async ({ contents }) => ({ result: JSON.parse(contents) }), + }); + + await expect(readN(source, 1)).resolves.toEqual([ + [ + { + context: 'http://localhost/config.json', + data: { + app: { + title: 'Example App', + substituted: 'x', + escaped: '${VALUE}', + }, + }, + }, + ], + ]); + }); + it('should reload config from a remote URL', async () => { let fetched = false; diff --git a/packages/config-loader/src/sources/RemoteConfigSource.ts b/packages/config-loader/src/sources/RemoteConfigSource.ts index 10a64ee635..1492cd4d28 100644 --- a/packages/config-loader/src/sources/RemoteConfigSource.ts +++ b/packages/config-loader/src/sources/RemoteConfigSource.ts @@ -22,14 +22,15 @@ import { } from '@backstage/types'; import isEqual from 'lodash/isEqual'; import fetch from 'node-fetch'; -import yaml from 'yaml'; import { ConfigTransformer, createConfigTransformer } from './transform'; import { AsyncConfigSourceGenerator, ConfigSource, SubstitutionFunc, ReadConfigDataOptions, + Parser, } from './types'; +import { parseYamlContent } from './utils'; const DEFAULT_RELOAD_INTERVAL = { seconds: 60 }; @@ -55,6 +56,11 @@ export interface RemoteConfigSourceOptions { * A substitution function to use instead of the default environment substitution. */ substitutionFunc?: SubstitutionFunc; + + /** + * A content parsing function to transform string content to configuration values. + */ + parser?: Parser; } /** @@ -84,6 +90,7 @@ export class RemoteConfigSource implements ConfigSource { readonly #url: string; readonly #reloadIntervalMs: number; readonly #transformer: ConfigTransformer; + readonly #parser: Parser; private constructor(options: RemoteConfigSourceOptions) { this.#url = options.url; @@ -93,6 +100,7 @@ export class RemoteConfigSource implements ConfigSource { this.#transformer = createConfigTransformer({ substitutionFunc: options.substitutionFunc, }); + this.#parser = options.parser ?? parseYamlContent; } async *readConfigData( @@ -135,11 +143,21 @@ export class RemoteConfigSource implements ConfigSource { throw await ResponseError.fromResponse(res); } - const content = await res.text(); - const data = await this.#transformer(yaml.parse(content)); - if (data === null) { + const contents = await res.text(); + const { result: rawData } = await this.#parser({ contents }); + if (rawData === undefined) { + /** + * This error message is/was coupled to the implementation and with refactoring it is no longer truly accurate + * This behavior is also inconsistent with {@link FileConfigSource}, which doesn't error on unparseable or empty + * content + * + * Preserving to not make a breaking change + */ throw new Error('configuration data is null'); - } else if (typeof data !== 'object') { + } + + const data = await this.#transformer(rawData); + if (typeof data !== 'object') { throw new Error('configuration data is not an object'); } else if (Array.isArray(data)) { throw new Error( diff --git a/packages/config-loader/src/sources/index.ts b/packages/config-loader/src/sources/index.ts index a25f689b40..3bf8e71990 100644 --- a/packages/config-loader/src/sources/index.ts +++ b/packages/config-loader/src/sources/index.ts @@ -38,4 +38,5 @@ export type { ConfigSourceData, ReadConfigDataOptions, AsyncConfigSourceGenerator, + Parser, } from './types'; diff --git a/packages/config-loader/src/sources/types.ts b/packages/config-loader/src/sources/types.ts index dec6f415f2..abdf2f5c8e 100644 --- a/packages/config-loader/src/sources/types.ts +++ b/packages/config-loader/src/sources/types.ts @@ -15,6 +15,7 @@ */ import { AppConfig } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /** * The data returned by {@link ConfigSource.readConfigData}. @@ -89,3 +90,18 @@ export interface ConfigSource { * @public */ export type SubstitutionFunc = (name: string) => Promise; + +/** + * A custom function to be used for parsing configuration content. + * + * @remarks + * + * The default parsing function will parse configuration content as yaml. + * + * @public + */ +export type Parser = ({ + contents, +}: { + contents: string; +}) => Promise<{ result?: JsonObject }>; diff --git a/packages/config-loader/src/sources/utils.ts b/packages/config-loader/src/sources/utils.ts index 9978234195..b948cc47b4 100644 --- a/packages/config-loader/src/sources/utils.ts +++ b/packages/config-loader/src/sources/utils.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { Parser } from './types'; +import yaml from 'yaml'; + /** @internal */ export interface SimpleDeferred { promise: Promise; @@ -55,3 +58,9 @@ export async function waitOrAbort( signals.forEach(s => s.addEventListener('abort', onAbort)); }); } + +/** @internal */ +export const parseYamlContent: Parser = async ({ contents }) => { + const parsed = yaml.parse(contents); + return { result: parsed === null ? undefined : parsed }; +}; diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index 7d65065bf5..26b9eb20fe 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,74 @@ # @backstage/core-app-api +## 1.14.1-next.0 + +### Patch Changes + +- 9a46a81: The request to delete the session cookie when running the app in protected mode is now done with a plain `fetch` rather than `FetchApi`. This fixes a bug where the app would immediately try to sign-in again when removing the cookie during logout. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.14.0 + +### Minor Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +### Patch Changes + +- db2e2d5: Updated config schema to support app.routes.bindings +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.13.1-next.1 + +### Patch Changes + +- db2e2d5: Updated config schema to support app.routes.bindings +- Updated dependencies + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.13.0-next.0 + +### Minor Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 1.12.6 ### Patch Changes diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts index 22f241fbcf..1a7ccb666f 100644 --- a/packages/core-app-api/config.d.ts +++ b/packages/core-app-api/config.d.ts @@ -65,6 +65,18 @@ export interface Config { }>; }>; }; + + routes?: { + /** + * Maps external route references to regular route references. Both the + * key and the value is expected to be on the form `.`. + * If the value is `false`, the route will be disabled even if it has a + * default mapping. + * + * @deepVisibility frontend + */ + bindings?: { [externalRouteRefId: string]: string | false }; + }; }; /** diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index ea953c4ebe..6a107e0a87 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": "1.12.6", + "version": "1.14.2-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts index 92e2abffb7..941ec90d66 100644 --- a/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts +++ b/packages/core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy.ts @@ -153,8 +153,9 @@ export class AppIdentityProxy implements IdentityApi { // It is fine if we do NOT worry yet about deleting cookies for OTHER backends like techdocs const appBaseUrl = await ctx.discoveryApi.getBaseUrl('app'); try { - await ctx.fetchApi.fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, { + await fetch(`${appBaseUrl}/.backstage/auth/v1/cookie`, { method: 'DELETE', + credentials: 'include', }); } catch { // Ignore the error for those who use static serving of the frontend diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts index b6d59bd3f2..0143aa2ded 100644 --- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts @@ -19,13 +19,13 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import { ConfigReader } from '@backstage/config'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; describe('MicrosoftAuth', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const microsoftAuth = MicrosoftAuth.create({ configApi: new ConfigReader(undefined), oauthRequestApi: new MockOAuthApi(), diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 85b6bcd684..01277a09c7 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -19,9 +19,12 @@ import { MockAnalyticsApi, renderWithEffects, withLogCollector, + registerMswTestHooks, } from '@backstage/test-utils'; -import { screen, waitFor, act } from '@testing-library/react'; +import { screen, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { setupServer } from 'msw/node'; +import { rest } from 'msw'; import React, { PropsWithChildren, ReactNode } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { @@ -51,6 +54,9 @@ import { } from '@backstage/core-plugin-api/alpha'; describe('Integration Test', () => { + const server = setupServer(); + registerMswTestHooks(server); + const noOpAnalyticsApi = createApiFactory( analyticsApiRef, new NoOpAnalyticsApi(), @@ -847,6 +853,17 @@ describe('Integration Test', () => { }); it('should clear app cookie when the user logs out', async () => { + const logoutSignal = jest.fn(); + server.use( + rest.delete( + 'http://localhost:7007/app/.backstage/auth/v1/cookie', + (_req, res, ctx) => { + logoutSignal(); + return res(ctx.status(200)); + }, + ), + ); + const meta = global.document.createElement('meta'); meta.name = 'backstage-app-mode'; meta.content = 'protected'; @@ -901,12 +918,7 @@ describe('Integration Test', () => { await userEvent.click(screen.getByText('Sign Out')); }); - await waitFor(() => - expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7007/app/.backstage/auth/v1/cookie', - { method: 'DELETE' }, - ), - ); + expect(logoutSignal).toHaveBeenCalled(); global.document.head.removeChild(meta); }); diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts index 9dfbbfd452..7d5c649a34 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.test.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.test.ts @@ -194,4 +194,35 @@ describe('collectRouteIds', () => { 'test.extRef': extRef, }); }); + + it('can disable external routes that have defaults', () => { + const source = createExternalRouteRef({ + id: 'test', + defaultTarget: 'test.target1', + }); + const target1 = createRouteRef({ id: 'test' }); + const plugin = createPlugin({ + id: 'test', + routes: { target1 }, + externalRoutes: { source }, + }); + + // resolves normally with no config + let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [ + plugin, + ]); + + expect(result.get(source)).toBe(target1); + + // can be disabled + result = resolveRouteBindings( + () => {}, + new MockConfigApi({ + app: { routes: { bindings: { 'test.source': false } } }, + }), + [plugin], + ); + + expect(result.get(source)).toBe(undefined); + }); }); diff --git a/packages/core-app-api/src/app/resolveRouteBindings.ts b/packages/core-app-api/src/app/resolveRouteBindings.ts index 55e1139b29..07dfea43ee 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.ts @@ -102,11 +102,12 @@ export function resolveRouteBindings( const bindings = config .getOptionalConfig('app.routes.bindings') ?.get(); + const disabledExternalRefs = new Set(); if (bindings) { for (const [externalRefId, targetRefId] of Object.entries(bindings)) { - if (typeof targetRefId !== 'string' || targetRefId === '') { + if (!isValidTargetRefId(targetRefId)) { throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`, + `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`, ); } @@ -116,23 +117,27 @@ export function resolveRouteBindings( `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, ); } - if (result.has(externalRef)) { - continue; - } - const targetRef = routesById.routes.get(targetRefId); - if (!targetRef) { - throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, - ); - } - result.set(externalRef, targetRef); + if (targetRefId === false) { + disabledExternalRefs.add(externalRef); + + result.delete(externalRef); + } else if (!result.has(externalRef)) { + const targetRef = routesById.routes.get(targetRefId); + if (!targetRef) { + throw new Error( + `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, + ); + } + + result.set(externalRef, targetRef); + } } } // Finally fall back to attempting to map defaults, at lowest priority for (const externalRef of routesById.externalRoutes.values()) { - if (!result.has(externalRef)) { + if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) { const defaultRefId = 'getDefaultTarget' in externalRef ? (externalRef.getDefaultTarget as () => string | undefined)() @@ -148,3 +153,15 @@ export function resolveRouteBindings( return result; } + +function isValidTargetRefId(value: unknown): value is string | false { + if (value === false) { + return true; + } + + if (typeof value === 'string' && value) { + return true; + } + + return false; +} diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 4273ea99ee..60c4a3b8d9 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -18,7 +18,7 @@ import { DefaultAuthConnector } from './DefaultAuthConnector'; import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; import { UrlPatternDiscovery } from '../../apis'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { ConfigReader } from '@backstage/config'; @@ -53,7 +53,7 @@ const defaultOptions = { describe('DefaultAuthConnector', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.resetAllMocks(); diff --git a/packages/core-compat-api/CHANGELOG.md b/packages/core-compat-api/CHANGELOG.md index b51c51eed4..34c134f731 100644 --- a/packages/core-compat-api/CHANGELOG.md +++ b/packages/core-compat-api/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/core-compat-api +## 0.2.8-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- 16cf96c: Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## 0.2.8-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## 0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## 0.2.7 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + +## 0.2.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/version-bridge@1.0.8 + ## 0.2.6 ### Patch Changes diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index be80ca50e9..8d177b4dab 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -37,12 +37,24 @@ export function convertLegacyRouteRef( ): SubRouteRef_2; // @public -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: ExternalRouteRef, -): ExternalRouteRef_2; +export function convertLegacyRouteRef( + ref: ExternalRouteRef, +): ExternalRouteRef_2; + +// @public +export function convertLegacyRouteRef( + ref: RouteRef_2, +): RouteRef; + +// @public +export function convertLegacyRouteRef( + ref: SubRouteRef_2, +): SubRouteRef; + +// @public +export function convertLegacyRouteRef( + ref: ExternalRouteRef_2, +): ExternalRouteRef; // @public export function convertLegacyRouteRefs< @@ -75,8 +87,8 @@ export type ToNewRouteRef = ? RouteRef_2 : T extends SubRouteRef ? SubRouteRef_2 - : T extends ExternalRouteRef - ? ExternalRouteRef_2 + : T extends ExternalRouteRef + ? ExternalRouteRef_2 : never; // (No @packageDocumentation comment for this package) diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index d275332f0b..0434b206d9 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-compat-api", - "version": "0.2.6", + "version": "0.2.8-next.2", "backstage": { "role": "web-library" }, @@ -44,6 +44,7 @@ "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", + "@backstage/test-utils": "workspace:^", "@oriflame/backstage-plugin-score-card": "^0.8.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^15.0.0" diff --git a/packages/core-compat-api/src/collectLegacyRoutes.tsx b/packages/core-compat-api/src/collectLegacyRoutes.tsx index b9f4e5528c..3523b0a40b 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.tsx @@ -101,7 +101,7 @@ function visitRouteChildren(options: { parentExtensionId: string; context: { pluginId: string; - extensions: ExtensionDefinition[]; + extensions: ExtensionDefinition[]; getUniqueName: () => string; discoverPlugin: (plugin: LegacyBackstagePlugin) => void; }; @@ -154,7 +154,7 @@ export function collectLegacyRoutes( ): BackstagePlugin[] { const pluginExtensions = new Map< LegacyBackstagePlugin, - ExtensionDefinition[] + ExtensionDefinition[] >(); const getUniqueName = (() => { diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 288f3f900b..4f9ba3a836 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -50,7 +50,9 @@ const legacyPluginStore = getOrCreateGlobalSingleton( () => new WeakMap(), ); -function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin { +export function toLegacyPlugin( + plugin: NewBackstagePlugin, +): LegacyBackstagePlugin { let legacy = legacyPluginStore.get(plugin); if (legacy) { return legacy; diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 5544862aee..9c646d5b00 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -14,10 +14,155 @@ * limitations under the License. */ -import React from 'react'; +import { + ApiHolder, + ApiRef, + AppContext, + useApp, +} from '@backstage/core-plugin-api'; +import { + AnyRouteRefParams, + ComponentRef, + ComponentsApi, + CoreErrorBoundaryFallbackProps, + CoreNotFoundErrorPageProps, + CoreProgressProps, + ExternalRouteRef, + IconComponent, + IconsApi, + RouteFunc, + RouteRef, + RouteResolutionApi, + RouteResolutionApiResolveOptions, + SubRouteRef, + componentsApiRef, + coreComponentRefs, + iconsApiRef, + routeResolutionApiRef, +} from '@backstage/frontend-plugin-api'; +import React, { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; +import { toLegacyPlugin } from './BackwardsCompatProvider'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { ApiProvider } from '../../../core-app-api/src/apis/system/ApiProvider'; +import { useVersionedContext } from '@backstage/version-bridge'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { type RouteResolver } from '../../../core-plugin-api/src/routing/useRouteRef'; +import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; + +class CompatComponentsApi implements ComponentsApi { + readonly #Progress: ComponentType; + readonly #NotFoundErrorPage: ComponentType; + readonly #ErrorBoundaryFallback: ComponentType; + + constructor(app: AppContext) { + const components = app.getComponents(); + const ErrorBoundaryFallback = (props: CoreErrorBoundaryFallbackProps) => ( + + ); + this.#Progress = components.Progress; + this.#NotFoundErrorPage = components.NotFoundErrorPage; + this.#ErrorBoundaryFallback = ErrorBoundaryFallback; + } + + getComponent(ref: ComponentRef): ComponentType { + switch (ref.id) { + case coreComponentRefs.progress.id: + return this.#Progress as ComponentType; + case coreComponentRefs.notFoundErrorPage.id: + return this.#NotFoundErrorPage as ComponentType; + case coreComponentRefs.errorBoundaryFallback.id: + return this.#ErrorBoundaryFallback as ComponentType; + default: + throw new Error( + `No backwards compatible component is available for ref '${ref.id}'`, + ); + } + } +} + +class CompatIconsApi implements IconsApi { + readonly #app: AppContext; + + constructor(app: AppContext) { + this.#app = app; + } + + getIcon(key: string): IconComponent | undefined { + return this.#app.getSystemIcon(key); + } + + listIconKeys(): string[] { + return Object.keys(this.#app.getSystemIcons()); + } +} + +class CompatRouteResolutionApi implements RouteResolutionApi { + readonly #routeResolver: RouteResolver; + + constructor(routeResolver: RouteResolver) { + this.#routeResolver = routeResolver; + } + + resolve( + anyRouteRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, + options?: RouteResolutionApiResolveOptions | undefined, + ): RouteFunc | undefined { + const legacyRef = convertLegacyRouteRef(anyRouteRef as RouteRef); + return this.#routeResolver.resolve(legacyRef, options?.sourcePath ?? '/'); + } +} + +class ForwardsCompatApis implements ApiHolder { + readonly #componentsApi: ComponentsApi; + readonly #iconsApi: IconsApi; + readonly #routeResolutionApi: RouteResolutionApi; + + constructor(app: AppContext, routeResolver: RouteResolver) { + this.#componentsApi = new CompatComponentsApi(app); + this.#iconsApi = new CompatIconsApi(app); + this.#routeResolutionApi = new CompatRouteResolutionApi(routeResolver); + } + + get(ref: ApiRef): T | undefined { + if (ref.id === componentsApiRef.id) { + return this.#componentsApi as T; + } else if (ref.id === iconsApiRef.id) { + return this.#iconsApi as T; + } else if (ref.id === routeResolutionApiRef.id) { + return this.#routeResolutionApi as T; + } + return undefined; + } +} + +function NewAppApisProvider(props: { children: ReactNode }) { + const app = useApp(); + const versionedRouteResolverContext = useVersionedContext<{ + 1: RouteResolver; + }>('routing-context'); + if (!versionedRouteResolverContext) { + throw new Error('Routing context is not available'); + } + const routeResolver = versionedRouteResolverContext.atVersion(1); + if (!routeResolver) { + throw new Error('RoutingContext v1 not available'); + } + + const appFallbackApis = useMemo( + () => new ForwardsCompatApis(app, routeResolver), + [app, routeResolver], + ); + + return {props.children}; +} export function ForwardsCompatProvider(props: { children: ReactNode }) { - // TODO(Rugvip): Implement - return <>{props.children}; + return {props.children}; } diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index 74b9f209eb..04e2361087 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -16,21 +16,28 @@ import React from 'react'; import { + componentsApiRef, + coreComponentRefs, coreExtensionData, createExtension, + iconsApiRef, + useRouteRef as useNewRouteRef, + createRouteRef as createNewRouteRef, + useApi, } from '@backstage/frontend-plugin-api'; import { createExtensionTester, - renderInTestApp, + renderInTestApp as renderInNewTestApp, } from '@backstage/frontend-test-utils'; import { screen } from '@testing-library/react'; import { compatWrapper } from './compatWrapper'; import { - createRouteRef, useApp, - useRouteRef, + useRouteRef as useOldRouteRef, + createRouteRef as createOldRouteRef, } from '@backstage/core-plugin-api'; import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; +import { renderInTestApp as renderInOldTestApp } from '@backstage/test-utils'; describe('BackwardsCompatProvider', () => { it('should convert the app context', () => { @@ -74,14 +81,58 @@ describe('BackwardsCompatProvider', () => { }); it('should convert the routing context', () => { - const routeRef = createRouteRef({ id: 'test' }); + const routeRef = createOldRouteRef({ id: 'test' }); function Component() { - const link = useRouteRef(routeRef); + const link = useOldRouteRef(routeRef); return
link: {link()}
; } - renderInTestApp(compatWrapper(), { + renderInNewTestApp(compatWrapper(), { + mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, + }); + + expect(screen.getByText('link: /test')).toBeInTheDocument(); + }); +}); + +describe('ForwardsCompatProvider', () => { + it('should convert the app context', async () => { + function Component() { + const components = useApi(componentsApiRef); + const icons = useApi(iconsApiRef); + return ( +
+ components:{' '} + {Object.entries(coreComponentRefs) + .map( + ([name, ref]) => + `${name}=${Boolean(components.getComponent(ref))}`, + ) + .join(', ')} + {'\n'} + icons: {icons.listIconKeys().join(', ')} +
+ ); + } + + await renderInOldTestApp(compatWrapper()); + + expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(` + "components: progress=true, notFoundErrorPage=true, errorBoundaryFallback=true + icons: kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, user, warning" + `); + }); + + it('should convert the routing context', async () => { + const routeRef = createNewRouteRef(); + + function Component() { + const link = useNewRouteRef(routeRef); + return
link: {link?.()}
; + } + + await renderInOldTestApp(compatWrapper(), { mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, }); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts new file mode 100644 index 0000000000..f09060212c --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -0,0 +1,203 @@ +/* + * Copyright 2023 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 { + RouteRef as OldRouteRef, + SubRouteRef as OldSubRouteRef, + ExternalRouteRef as OldExternalRouteRef, + createRouteRef as createOldRouteRef, + createSubRouteRef as createOldSubRouteRef, + createExternalRouteRef as createOldExternalRouteRef, +} from '@backstage/core-plugin-api'; +import { + RouteRef as NewRouteRef, + SubRouteRef as NewSubRouteRef, + ExternalRouteRef as NewExternalRouteRef, + createRouteRef as createNewRouteRef, + createSubRouteRef as createNewSubRouteRef, + createExternalRouteRef as createNewExternalRouteRef, +} from '@backstage/frontend-plugin-api'; +import { convertLegacyRouteRef } from './convertLegacyRouteRef'; + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalRouteRef as toInternalNewRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalSubRouteRef as toInternalNewSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalExternalRouteRef as toInternalNewExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef'; + +describe('convertLegacyRouteRef', () => { + it('converts old to new', () => { + const ref1 = createOldRouteRef({ id: 'ref1' }); + const ref2 = createOldRouteRef({ id: 'ref2', params: ['p1', 'p2'] }); + const ref1sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createOldSubRouteRef({ + id: 'sub2', + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createOldExternalRouteRef({ + id: 'ref3', + }); + const ref4 = createOldExternalRouteRef({ + id: 'ref4', + optional: true, + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: NewRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: NewRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: NewExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: NewExternalRouteRef = convertLegacyRouteRef(ref4); + + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + + const ref1Internal = toInternalNewRouteRef(ref1Converted); + const ref2Internal = toInternalNewRouteRef(ref2Converted); + const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); + const ref1sub2Internal = toInternalNewSubRouteRef(ref1sub2Converted); + const ref2sub1Internal = toInternalNewSubRouteRef(ref2sub1Converted); + const ref3Internal = toInternalNewExternalRouteRef(ref3Converted); + const ref4Internal = toInternalNewExternalRouteRef(ref4Converted); + + expect(ref1Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref1}', + ); + expect(ref1Internal.getParams()).toEqual([]); + expect(ref2Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref2}', + ); + expect(ref2Internal.getParams()).toEqual(['p1', 'p2']); + + expect(ref1sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref1sub1Internal.getParams()).toEqual([]); + expect(ref1sub1Internal.getParent()).toBe(ref1); + expect(ref1sub2Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub2}', + ); + expect(ref1sub2Internal.getParams()).toEqual(['p3']); + expect(ref1sub2Internal.getParent()).toBe(ref1); + expect(ref2sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref2sub1Internal.getParams()).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Internal.getParent()).toBe(ref2); + + expect(ref3Internal.getDefaultTarget()).toBe(undefined); + expect(ref3Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref3}', + ); + expect(ref3Internal.getParams()).toEqual([]); + expect(ref4Internal.getDefaultTarget()).toBe('ref2'); + expect(ref4Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref4}', + ); + expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); + }); + + it('converts new to old', () => { + const ref1 = createNewRouteRef(); + const ref2 = createNewRouteRef({ params: ['p1', 'p2'] }); + const ref1sub1 = createNewSubRouteRef({ + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createNewSubRouteRef({ + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createNewSubRouteRef({ + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createNewExternalRouteRef(); + const ref4 = createNewExternalRouteRef({ + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: OldRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: OldRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: OldExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: OldExternalRouteRef = convertLegacyRouteRef(ref4); + + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + + expect(String(ref1Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref1Converted.params).toEqual([]); + expect(String(ref2Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref2Converted.params).toEqual(['p1', 'p2']); + + expect(String(ref1sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1 with parent created at '.*'\}$/, + ); + expect(ref1sub1Converted.params).toEqual([]); + expect(ref1sub1Converted.parent).toBe(ref1); + expect(String(ref1sub2Converted)).toMatch( + /^SubRouteRef\{at \/sub2\/:p3 with parent created at '.*'\}$/, + ); + expect(ref1sub2Converted.params).toEqual(['p3']); + expect(ref1sub2Converted.parent).toBe(ref1); + expect(String(ref2sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1\/:p3 with parent created at '.*'\}$/, + ); + expect(ref2sub1Converted.params).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Converted.parent).toBe(ref2); + + expect(String(ref3Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref3Converted.params).toEqual([]); + expect(ref3Converted.optional).toBe(true); + expect(String(ref4Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref4Converted.params).toEqual(['p1', 'p2']); + expect(ref4Converted.optional).toBe(true); + }); +}); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index c967f9e9b5..40a95a81ff 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -51,8 +51,8 @@ export type ToNewRouteRef< ? RouteRef : T extends LegacySubRouteRef ? SubRouteRef - : T extends LegacyExternalRouteRef - ? ExternalRouteRef + : T extends LegacyExternalRouteRef + ? ExternalRouteRef : never; /** @@ -109,23 +109,115 @@ export function convertLegacyRouteRef( * * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. */ -export function convertLegacyRouteRef< - TParams extends AnyRouteRefParams, - TOptional extends boolean, ->( - ref: LegacyExternalRouteRef, -): ExternalRouteRef; +export function convertLegacyRouteRef( + ref: LegacyExternalRouteRef, +): ExternalRouteRef; +/** + * A temporary helper to convert a new route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: RouteRef, +): LegacyRouteRef; + +/** + * A temporary helper to convert a new sub route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createSubRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: SubRouteRef, +): LegacySubRouteRef; + +/** + * A temporary helper to convert a new external route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: ExternalRouteRef, +): LegacyExternalRouteRef; export function convertLegacyRouteRef( - ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, -): RouteRef | SubRouteRef | ExternalRouteRef { + ref: + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): + | RouteRef + | SubRouteRef + | ExternalRouteRef + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef { + const isNew = '$$type' in ref; + const oldType = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + // Ref has already been converted - if ('$$type' in ref) { - return ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef; + if (isNew && oldType) { + return ref as any; } - const type = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + if (isNew) { + return convertNewToOld( + ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef, + ); + } + return convertOldToNew(ref, oldType); +} + +function convertNewToOld( + ref: RouteRef | SubRouteRef | ExternalRouteRef, +): LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef { + if (ref.$$type === '@backstage/RouteRef') { + const newRef = toInternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'absolute', + params: newRef.getParams(), + title: newRef.getDescription(), + } as Omit) as unknown as LegacyRouteRef; + } + if (ref.$$type === '@backstage/SubRouteRef') { + const newRef = toInternalSubRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'sub', + parent: convertLegacyRouteRef(newRef.getParent()), + params: newRef.getParams(), + } as Omit) as unknown as LegacySubRouteRef; + } + if (ref.$$type === '@backstage/ExternalRouteRef') { + const newRef = toInternalExternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'external', + optional: true, + params: newRef.getParams(), + defaultTarget: newRef.getDefaultTarget(), + } as Omit) as unknown as LegacyExternalRouteRef; + } + + throw new Error( + `Failed to convert route ref, unknown type '${(ref as any).$$type}'`, + ); +} + +function convertOldToNew( + ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, + type: unknown, +): RouteRef | SubRouteRef | ExternalRouteRef { if (type === 'absolute') { const legacyRef = ref as LegacyRouteRef; const legacyRefStr = String(legacyRef); @@ -185,7 +277,6 @@ export function convertLegacyRouteRef( const newRef = toInternalExternalRouteRef( createExternalRouteRef<{ [key in string]: string }>({ params: legacyRef.params as string[], - optional: legacyRef.optional, defaultTarget: 'getDefaultTarget' in legacyRef ? (legacyRef.getDefaultTarget as () => string | undefined)() @@ -196,7 +287,6 @@ export function convertLegacyRouteRef( $$type: '@backstage/ExternalRouteRef' as const, version: 'v1', T: newRef.T, - optional: newRef.optional, getParams() { return newRef.getParams(); }, diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index af800e77a8..a2463f594e 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/core-components +## 0.14.10-next.0 + +### Patch Changes + +- 678971a: Move the `Link` component to the `RoutedTabs` instead of the `HeaderTabs` component +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/version-bridge@1.0.8 + +## 0.14.9 + +### Patch Changes + +- d4ffdbb: Fixed bug where `` component with empty string as placeholder gave an error +- Updated dependencies + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/version-bridge@1.0.8 + ## 0.14.8 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index 74cb0d7fbb..a2688a0037 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.14.8", + "version": "0.14.10-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/core-components/src/components/Select/Select.test.tsx b/packages/core-components/src/components/Select/Select.test.tsx index 71159150e4..b83ac68d5b 100644 --- a/packages/core-components/src/components/Select/Select.test.tsx +++ b/packages/core-components/src/components/Select/Select.test.tsx @@ -55,6 +55,25 @@ describe(', + ); + + expect(getByTestId('select').textContent).toBe(initialValue); + + rerender(, @@ -66,4 +85,12 @@ describe(', + ); + const input = getByTestId('custom-select'); + expect(input.textContent).toBe('All results'); + }); }); diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index 924b49f640..6b5174820b 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -136,6 +136,7 @@ export type SelectProps = { native?: boolean; disabled?: boolean; margin?: 'dense' | 'none'; + 'data-testid'?: string; }; /** @public */ @@ -151,6 +152,7 @@ export function SelectComponent(props: SelectProps) { native = false, disabled = false, margin, + 'data-testid': dataTestId = 'select', } = props; const classes = useStyles(); const [value, setValue] = useState( @@ -203,7 +205,7 @@ export function SelectComponent(props: SelectProps) { value={value} native={native} disabled={disabled} - data-testid="select" + data-testid={dataTestId} displayEmpty multiple={multiple} margin={margin} @@ -254,7 +256,7 @@ export function SelectComponent(props: SelectProps) { getContentAnchorEl: null, }} > - {placeholder && !multiple && ( + {!!placeholder && !multiple && ( {placeholder} )} {native diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx index ec91208eee..754ac2ec8d 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.test.tsx @@ -160,4 +160,17 @@ describe('RoutedTabs', () => { rendered.queryByText('tabbed-test-content-2'), ).not.toBeInTheDocument(); }); + + it('should render the tabs as links', async () => { + const routes = [testRoute1, testRoute2, testRoute3]; + const expectedHrefs = ['/', '/some-other-path', '/some-other-path-similar']; + const rendered = await renderInTestApp(); + + const tabs = rendered.queryAllByRole('tab'); + + for (const [k, v] of Object.entries(tabs)) { + expect(v.tagName).toBe('A'); + expect(v).toHaveAttribute('href', expectedHrefs[Number(k)]); + } + }); }); diff --git a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx index 2c727efe1b..b57a3782bb 100644 --- a/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx +++ b/packages/core-components/src/components/TabbedLayout/RoutedTabs.tsx @@ -15,15 +15,11 @@ */ import React, { useMemo } from 'react'; import { Helmet } from 'react-helmet'; -import { - matchRoutes, - useNavigate, - useParams, - useRoutes, -} from 'react-router-dom'; +import { matchRoutes, useParams, useRoutes } from 'react-router-dom'; import { Content } from '../../layout/Content'; import { HeaderTabs } from '../../layout/HeaderTabs'; import { SubRoute } from './types'; +import { Link } from '../Link'; export function useSelectedSubRoute(subRoutes: SubRoute[]): { index: number; @@ -68,35 +64,33 @@ export function useSelectedSubRoute(subRoutes: SubRoute[]): { export function RoutedTabs(props: { routes: SubRoute[] }) { const { routes } = props; - const navigate = useNavigate(); + const { index, route, element } = useSelectedSubRoute(routes); const headerTabs = useMemo( () => - routes.map(t => ({ - id: t.path, - label: t.title, - tabProps: t.tabProps, - })), + routes.map(t => { + const { path, title, tabProps } = t; + let to = path; + // Remove trailing /* + to = to.replace(/\/\*$/, ''); + // And remove leading / for relative navigation + to = to.replace(/^\//, ''); + return { + id: path, + label: title, + tabProps: { + component: Link, + to, + ...tabProps, + }, + }; + }), [routes], ); - const onTabChange = (tabIndex: number) => { - let { path } = routes[tabIndex]; - // Remove trailing /* - path = path.replace(/\/\*$/, ''); - // And remove leading / for relative navigation - path = path.replace(/^\//, ''); - // Note! route resolves relative to the position in the React tree, - // not relative to current location - navigate(path); - }; return ( <> - + {element} diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx index dad40c4615..5aec83bc2b 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.test.tsx @@ -95,13 +95,4 @@ describe('', () => { await user.click(rendered.getByText('Docs')); expect(mockOnChange).toHaveBeenCalledTimes(1); }); - - it('should render 2 nav tabs', async () => { - const rendered = await renderInTestApp(); - const tabs = rendered.queryAllByTestId(id => id.startsWith('header-tab')); - expect(tabs).toHaveLength(2); - tabs.forEach(tab => { - expect(tab.tagName.toLocaleLowerCase('en-US')).toBe('a'); - }); - }); }); diff --git a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx index 9ee9c5525f..002c9fe1ea 100644 --- a/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core-components/src/layout/HeaderTabs/HeaderTabs.tsx @@ -18,7 +18,6 @@ import { makeStyles } from '@material-ui/core/styles'; import TabUI, { TabProps } from '@material-ui/core/Tab'; import Tabs from '@material-ui/core/Tabs'; import React, { useCallback, useEffect, useState } from 'react'; -import { Link } from '../../components/Link'; // TODO(blam): Remove this implementation when the Tabs are ready // This is just a temporary solution to implementing tabs for now @@ -96,9 +95,7 @@ export function HeaderTabs(props: HeaderTabsProps) { setSelectedTab(selectedIndex); } }, [selectedIndex]); - function removeLeadingSlash(path: string) { - return path.replace(/^\//, ''); - } + return ( { afterEach(() => jest.useRealTimers()); const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('runs the happy path', async () => { const getBaseUrl = jest.fn(); diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index 05960cd213..e4e65b613f 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -20,7 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { TestApiProvider, - setupRequestMockHandlers, + registerMswTestHooks, wrapInTestApp, } from '@backstage/test-utils'; import { ProxiedSignInPage } from './ProxiedSignInPage'; @@ -28,7 +28,7 @@ import { discoveryApiRef } from '@backstage/core-plugin-api'; describe('ProxiedSignInPage', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const Subject = wrapInTestApp(
authenticated
, { components: { diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 6ab3cb0882..dce729b811 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,70 @@ # @backstage/create-app +## 0.5.18-next.3 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 0.5.18-next.2 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 0.5.18-next.1 + +### Patch Changes + +- 6c1081c: Updated dockerfile and `app-config.production.yaml` to make it easier to get started with example data +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 0.5.18-next.0 + +### Patch Changes + +- c0a705d: Added the Kubernetes plugin to `create-app` +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 0.5.17 + +### Patch Changes + +- 780d994: Added `MyGroupsSidebarItem` to the sidebar in the `create-app` template +- f03d12a: Bumped create-app version. +- e90a2cd: Added the Catalog logs module to the `create-app` template +- 3ac2a6a: Added the Postgres Search Engine to the `create-app` template +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 0.5.17-next.2 + +### Patch Changes + +- e90a2cd: Added the Catalog logs module to the `create-app` template + +## 0.5.17-next.1 + +### Patch Changes + +- 780d994: Added `MyGroupsSidebarItem` to the sidebar in the `create-app` template +- Updated dependencies + - @backstage/cli-common@0.1.14 + +## 0.5.17-next.0 + +### Patch Changes + +- Bumped create-app version. +- Updated dependencies + - @backstage/cli-common@0.1.14 + ## 0.5.16 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 066dad2b57..8886ed9a6d 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.5.16", + "version": "0.5.18-next.3", "publishConfig": { "access": "public" }, @@ -39,7 +39,7 @@ "fs-extra": "^11.2.0", "handlebars": "^4.7.3", "inquirer": "^8.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "ora": "^5.3.0", "recursive-readdir": "^2.2.2" }, diff --git a/packages/create-app/seed-yarn.lock b/packages/create-app/seed-yarn.lock index 2f41ba9ca3..00a86105cc 100644 --- a/packages/create-app/seed-yarn.lock +++ b/packages/create-app/seed-yarn.lock @@ -16,8 +16,3 @@ // package: the name of the package, e.g. @testing-library/react // query: the version query to pin the version for, e.g. ^14.0.0 // version: the version to pin to, must be in range of the query, e.g. 14.11.0 - -ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.12.0, ajv@^8.14.0, ajv@^8.6.0, ajv@^8.6.3, ajv@^8.8.2, ajv@^8.9.0: - version "8.14.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.14.0.tgz#f514ddfd4756abb200e1704414963620a625ebbb" - integrity sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA== diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 38a09f7ac6..443b474bd3 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -31,7 +31,7 @@ import { } from './tasks'; import { createMockDirectory, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -62,6 +62,7 @@ jest.mock('./versions', () => ({ '@backstage/plugin-auth-backend-module-github-provider': '1.0.0', '@backstage/plugin-auth-backend-module-guest-provider': '1.0.0', '@backstage/plugin-catalog-backend': '1.0.0', + '@backstage/plugin-catalog-backend-module-logs': '1.0.0', '@backstage/plugin-catalog-backend-module-scaffolder-entity-model': '1.0.0', '@backstage/plugin-permission-common': '1.0.0', '@backstage/plugin-permission-node': '1.0.0', @@ -87,6 +88,8 @@ jest.mock('./versions', () => ({ '@backstage/plugin-catalog-graph': '1.0.0', '@backstage/plugin-catalog-import': '1.0.0', '@backstage/plugin-catalog-react': '1.0.0', + '@backstage/plugin-kubernetes': '1.0.0', + '@backstage/plugin-kubernetes-backend': '1.0.0', '@backstage/plugin-org': '1.0.0', '@backstage/plugin-scaffolder': '1.0.0', '@backstage/plugin-permission-react': '1.0.0', @@ -422,7 +425,7 @@ describe('tasks', () => { describe('fetchYarnLockSeedTask', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('should fetch the yarn.lock seed file', async () => { worker.use( diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index febc68102c..194ffa21d9 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -58,9 +58,12 @@ import { version as pluginCatalog } from '../../../../plugins/catalog/package.js import { version as pluginCatalogCommon } from '../../../../plugins/catalog-common/package.json'; import { version as pluginCatalogReact } from '../../../../plugins/catalog-react/package.json'; import { version as pluginCatalogBackend } from '../../../../plugins/catalog-backend/package.json'; +import { version as pluginCatalogBackendModuleLogs } from '../../../../plugins/catalog-backend-module-logs/package.json'; import { version as pluginCatalogBackendModuleScaffolderEntityModel } from '../../../../plugins/catalog-backend-module-scaffolder-entity-model/package.json'; import { version as pluginCatalogGraph } from '../../../../plugins/catalog-graph/package.json'; import { version as pluginCatalogImport } from '../../../../plugins/catalog-import/package.json'; +import { version as pluginKubernetes } from '../../../../plugins/kubernetes/package.json'; +import { version as pluginKubernetesBackend } from '../../../../plugins/kubernetes-backend/package.json'; import { version as pluginOrg } from '../../../../plugins/org/package.json'; import { version as pluginPermissionBackend } from '../../../../plugins/permission-backend/package.json'; import { version as pluginPermissionBackendModulePolicyAllowAll } from '../../../../plugins/permission-backend-module-policy-allow-all/package.json'; @@ -111,10 +114,14 @@ export const packageVersions = { '@backstage/plugin-catalog-common': pluginCatalogCommon, '@backstage/plugin-catalog-react': pluginCatalogReact, '@backstage/plugin-catalog-backend': pluginCatalogBackend, + '@backstage/plugin-catalog-backend-module-logs': + pluginCatalogBackendModuleLogs, '@backstage/plugin-catalog-backend-module-scaffolder-entity-model': pluginCatalogBackendModuleScaffolderEntityModel, '@backstage/plugin-catalog-graph': pluginCatalogGraph, '@backstage/plugin-catalog-import': pluginCatalogImport, + '@backstage/plugin-kubernetes': pluginKubernetes, + '@backstage/plugin-kubernetes-backend': pluginKubernetesBackend, '@backstage/plugin-org': pluginOrg, '@backstage/plugin-permission-backend': pluginPermissionBackend, '@backstage/plugin-permission-backend-module-allow-all-policy': diff --git a/packages/create-app/templates/default-app/app-config.production.yaml b/packages/create-app/templates/default-app/app-config.production.yaml index 5532d9067e..5d426f5392 100644 --- a/packages/create-app/templates/default-app/app-config.production.yaml +++ b/packages/create-app/templates/default-app/app-config.production.yaml @@ -30,10 +30,26 @@ backend: auth: providers: - guest: null + guest: {} catalog: # Overrides the default list locations from app-config.yaml as these contain example data. # See https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog for more details # on how to get entities into the catalog. - locations: [] + locations: + # Local example data, replace this with your production config, these are intended for demo use only. + # File locations are relative to the backend process, typically in a deployed context, such as in a Docker container, this will be the root + - type: file + target: ./examples/entities.yaml + + # Local example template + - type: file + target: ./examples/template/template.yaml + rules: + - allow: [Template] + + # Local example organizational data + - type: file + target: ./examples/org.yaml + rules: + - allow: [User, Group] diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index 248cff137a..ca52ec530c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -103,3 +103,11 @@ catalog: # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml # rules: # - allow: [User, Group] + +kubernetes: + # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options + +# see https://backstage.io/docs/permissions/getting-started for more on the permission framework +permission: + # setting this to `false` will disable permissions + enabled: true diff --git a/packages/create-app/templates/default-app/packages/app/package.json.hbs b/packages/create-app/templates/default-app/packages/app/package.json.hbs index d4db9e5885..08a1d58362 100644 --- a/packages/create-app/templates/default-app/packages/app/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/app/package.json.hbs @@ -27,6 +27,7 @@ "@backstage/plugin-catalog-graph": "^{{version '@backstage/plugin-catalog-graph'}}", "@backstage/plugin-catalog-import": "^{{version '@backstage/plugin-catalog-import'}}", "@backstage/plugin-catalog-react": "^{{version '@backstage/plugin-catalog-react'}}", + "@backstage/plugin-kubernetes": "^{{version '@backstage/plugin-kubernetes'}}", "@backstage/plugin-org": "^{{version '@backstage/plugin-org'}}", "@backstage/plugin-permission-react": "^{{version '@backstage/plugin-permission-react'}}", "@backstage/plugin-scaffolder": "^{{version '@backstage/plugin-scaffolder'}}", diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index 71c04122ae..853d175bae 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -25,6 +25,8 @@ import { } from '@backstage/core-components'; import MenuIcon from '@material-ui/icons/Menu'; import SearchIcon from '@material-ui/icons/Search'; +import { MyGroupsSidebarItem } from '@backstage/plugin-org'; +import GroupIcon from '@material-ui/icons/People'; const useSidebarLogoStyles = makeStyles({ root: { @@ -65,6 +67,11 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( }> {/* Global nav, not org-specific */} + diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 53b6685c3f..6164c76004 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -54,6 +54,11 @@ import { import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; +import { + EntityKubernetesContent, + isKubernetesAvailable, +} from '@backstage/plugin-kubernetes'; + const techdocsContent = ( @@ -150,6 +155,14 @@ const serviceEntityPage = ( {cicdContent} + + + + @@ -188,6 +201,14 @@ const websiteEntityPage = ( {cicdContent} + + + + diff --git a/packages/create-app/templates/default-app/packages/backend/Dockerfile b/packages/create-app/templates/default-app/packages/backend/Dockerfile index 18548e9337..bef67b5ce7 100644 --- a/packages/create-app/templates/default-app/packages/backend/Dockerfile +++ b/packages/create-app/templates/default-app/packages/backend/Dockerfile @@ -45,6 +45,9 @@ RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ yarn install --frozen-lockfile --production --network-timeout 300000 +# This will include the examples, if you don't need these simply remove this line +COPY --chown=node:node examples ./examples + # Then copy the rest of the backend bundle, along with any other files we might want. COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ RUN tar xzf bundle.tar.gz && rm bundle.tar.gz diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 8d95ee60a5..757d849fd7 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -26,7 +26,9 @@ "@backstage/plugin-auth-backend-module-guest-provider": "^{{version '@backstage/plugin-auth-backend-module-guest-provider'}}", "@backstage/plugin-auth-node": "^{{version '@backstage/plugin-auth-node'}}", "@backstage/plugin-catalog-backend": "^{{version '@backstage/plugin-catalog-backend'}}", + "@backstage/plugin-catalog-backend-module-logs": "^{{version '@backstage/plugin-catalog-backend-module-logs'}}", "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^{{version '@backstage/plugin-catalog-backend-module-scaffolder-entity-model'}}", + "@backstage/plugin-kubernetes-backend": "^{{version '@backstage/plugin-kubernetes-backend'}}", "@backstage/plugin-permission-backend": "^{{version '@backstage/plugin-permission-backend'}}", "@backstage/plugin-permission-backend-module-allow-all-policy": "^{{version '@backstage/plugin-permission-backend-module-allow-all-policy'}}", "@backstage/plugin-permission-common": "^{{version '@backstage/plugin-permission-common'}}", @@ -35,6 +37,7 @@ "@backstage/plugin-scaffolder-backend": "^{{version '@backstage/plugin-scaffolder-backend'}}", "@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}", "@backstage/plugin-search-backend-module-catalog": "^{{version '@backstage/plugin-search-backend-module-catalog'}}", + "@backstage/plugin-search-backend-module-pg": "^{{version '@backstage/plugin-search-backend-module-pg'}}", "@backstage/plugin-search-backend-module-techdocs": "^{{version '@backstage/plugin-search-backend-module-techdocs'}}", "@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 1c442a8d95..f57d4c5f87 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -27,15 +27,28 @@ backend.add( import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'), ); +// See https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors +backend.add(import('@backstage/plugin-catalog-backend-module-logs')); + // permission plugin backend.add(import('@backstage/plugin-permission-backend/alpha')); +// See https://backstage.io/docs/permissions/getting-started for how to create your own permission policy backend.add( import('@backstage/plugin-permission-backend-module-allow-all-policy'), ); // search plugin backend.add(import('@backstage/plugin-search-backend/alpha')); + +// search engine +// See https://backstage.io/docs/features/search/search-engines +backend.add(import('@backstage/plugin-search-backend-module-pg/alpha')); + +// search collators backend.add(import('@backstage/plugin-search-backend-module-catalog/alpha')); backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha')); +// kubernetes +backend.add(import('@backstage/plugin-kubernetes-backend/alpha')); + backend.start(); diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index b28267c160..5ea08c7762 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,99 @@ # @backstage/dev-utils +## 1.0.37-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## 1.0.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## 1.0.36-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + +## 1.0.35 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 1.0.35-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## 1.0.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + +## 1.0.34-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6 + ## 1.0.33 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index ed887bce11..b2b6eb3292 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/dev-utils", - "version": "1.0.33", + "version": "1.0.37-next.2", "description": "Utilities for developing Backstage plugins.", "backstage": { "role": "web-library" diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md index 4a4edf005e..4609771fb5 100644 --- a/packages/e2e-test/CHANGELOG.md +++ b/packages/e2e-test/CHANGELOG.md @@ -1,5 +1,75 @@ # e2e-test +## 0.2.19-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.3 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.2.19-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.2 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.2.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.2.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.18-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.2.18 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.2 + +## 0.2.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/create-app@0.5.17-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + ## 0.2.17 ### Patch Changes diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 63536bc4a3..481b011d01 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.2.17", + "version": "0.2.19-next.3", "private": true, "backstage": { "role": "cli" diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 111c638146..0a9ac6368c 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -537,7 +537,9 @@ async function testBackendStart(appDir: string, ...args: string[]) { try { await waitFor( - () => stdout.includes('Listening on ') || filterStderr(stderr).length > 0, + () => + stdout.includes('Plugin initialization complete') || + filterStderr(stderr).length > 0, ); const stderrLines = filterStderr(stderr); if (stderrLines.length > 0) { diff --git a/packages/frontend-app-api/CHANGELOG.md b/packages/frontend-app-api/CHANGELOG.md index be9eab1863..dd419c284b 100644 --- a/packages/frontend-app-api/CHANGELOG.md +++ b/packages/frontend-app-api/CHANGELOG.md @@ -1,5 +1,129 @@ # @backstage/frontend-app-api +## 0.7.5-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.7.5-next.1 + +### Patch Changes + +- 3be9aeb: Added support for v2 extensions, which declare their inputs and outputs without using a data map. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.7.4-next.0 + +### Patch Changes + +- 7777b5f: Support icon overriding with the new `IconBundleBlueprint` API. +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.7.3 + +### Patch Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.7.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## 0.7.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.7.2-next.0 + +### Patch Changes + +- d3c39fc: Allow for the disabling of external routes through config, which was rendered impossible after the introduction of default targets. + + ```yaml + app: + routes: + bindings: + # This has the effect of removing the button for registering new + # catalog entities in the scaffolder template list view + scaffolder.registerComponent: false + ``` + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.7.1 ### Patch Changes diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 0375a67482..27d486b6a8 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -44,7 +44,7 @@ export type CreateAppRouteBinder = < externalRoutes: TExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, - KeysWithType> + KeysWithType> >, ) => void; diff --git a/packages/frontend-app-api/config.d.ts b/packages/frontend-app-api/config.d.ts index 28355f5376..fc265e8833 100644 --- a/packages/frontend-app-api/config.d.ts +++ b/packages/frontend-app-api/config.d.ts @@ -26,9 +26,14 @@ export interface Config { routes?: { /** + * Maps external route references to regular route references. Both the + * key and the value is expected to be on the form `.`. + * If the value is `false`, the route will be disabled even if it has a + * default mapping. + * * @deepVisibility frontend */ - bindings?: { [externalRouteRefId: string]: string }; + bindings?: { [externalRouteRefId: string]: string | false }; }; /** diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 401fcfc529..0d462e2721 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-app-api", - "version": "0.7.1", + "version": "0.7.5-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index 82e7600178..7794aceb79 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -24,6 +24,7 @@ import { createExtensionInput, createThemeExtension, createTranslationExtension, + IconBundleBlueprint, } from '@backstage/frontend-plugin-api'; export const App = createExtension({ @@ -42,6 +43,9 @@ export const App = createExtension({ translations: createExtensionInput({ translation: createTranslationExtension.translationDataRef, }), + icons: createExtensionInput({ + icon: IconBundleBlueprint.dataRefs.icons, + }), root: createExtensionInput( { element: coreExtensionData.reactElement, diff --git a/packages/frontend-app-api/src/extensions/AppNav.tsx b/packages/frontend-app-api/src/extensions/AppNav.tsx index 761c6d5a24..ffef1c7c61 100644 --- a/packages/frontend-app-api/src/extensions/AppNav.tsx +++ b/packages/frontend-app-api/src/extensions/AppNav.tsx @@ -73,9 +73,12 @@ const SidebarNavItem = ( props: (typeof createNavItemExtension.targetDataRef)['T'], ) => { const { icon: Icon, title, routeRef } = props; - const to = useRouteRef(routeRef)(); + const link = useRouteRef(routeRef); + if (!link) { + return null; + } // TODO: Support opening modal, for example, the search one - return ; + return ; }; export const AppNav = createExtension({ diff --git a/packages/frontend-app-api/src/routing/RouteResolver.test.ts b/packages/frontend-app-api/src/routing/RouteResolver.test.ts index ba0a66a380..497e7397cb 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.test.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.test.ts @@ -41,9 +41,7 @@ const subRef2 = createSubRouteRef({ parent: ref1, path: '/foo/:a' }); const subRef3 = createSubRouteRef({ parent: ref2, path: '/bar' }); const subRef4 = createSubRouteRef({ parent: ref2, path: '/bar/:a' }); const externalRef1 = createExternalRouteRef(); -const externalRef2 = createExternalRouteRef({ optional: true }); -const externalRef3 = createExternalRouteRef({ params: ['x'] }); -const externalRef4 = createExternalRouteRef({ optional: true, params: ['x'] }); +const externalRef2 = createExternalRouteRef({ params: ['x'] }); function src(sourcePath: string) { return { sourcePath }; @@ -62,9 +60,7 @@ describe('RouteResolver', () => { undefined, ); expect(r.resolve(externalRef1, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe(undefined); + expect(r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toBe(undefined); }); it('should resolve an absolute route', () => { @@ -87,9 +83,7 @@ describe('RouteResolver', () => { undefined, ); expect(r.resolve(externalRef1, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe(undefined); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe(undefined); + expect(r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toBe(undefined); }); it('should resolve an absolute route with a param and with a parent', () => { @@ -112,8 +106,7 @@ describe('RouteResolver', () => { ], new Map([ [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], + [externalRef2, subRef3], ]), '', ); @@ -133,11 +126,7 @@ describe('RouteResolver', () => { '/my-route/my-parent/4x/bar/4a', ); expect(r.resolve(externalRef1, src('/'))?.()).toBe('/my-route'); - expect(r.resolve(externalRef2, src('/'))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toBe( - '/my-route/my-parent/5x', - ); - expect(r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toBe( + expect(r.resolve(externalRef2, src('/'))?.({ x: '6x' })).toBe( '/my-route/my-parent/6x/bar', ); }); @@ -230,8 +219,7 @@ describe('RouteResolver', () => { ], new Map([ [externalRef1, ref1], - [externalRef3, ref2], - [externalRef4, subRef3], + [externalRef2, subRef3], ]), '', ); @@ -282,17 +270,10 @@ describe('RouteResolver', () => { expect(() => r.resolve(externalRef1, src('/'))?.()).toThrow( /^Cannot route.*with parent.*as it has parameters$/, ); - expect(r.resolve(externalRef2, src(l))?.()).toBe(undefined); - expect(r.resolve(externalRef3, src(l))?.({ x: '5x' })).toBe( - '/my-grandparent/my-y/my-parent/5x', + expect(r.resolve(externalRef2, src(l))?.({ x: '5x' })).toBe( + '/my-grandparent/my-y/my-parent/5x/bar', ); - expect(() => r.resolve(externalRef3, src('/'))?.({ x: '5x' })).toThrow( - /^Cannot route.*with parent.*as it has parameters$/, - ); - expect(r.resolve(externalRef4, src(l))?.({ x: '6x' })).toBe( - '/my-grandparent/my-y/my-parent/6x/bar', - ); - expect(() => r.resolve(externalRef4, src('/'))?.({ x: '6x' })).toThrow( + expect(() => r.resolve(externalRef2, src('/'))?.({ x: '5x' })).toThrow( /^Cannot route.*with parent.*as it has parameters$/, ); }); diff --git a/packages/frontend-app-api/src/routing/RouteResolver.ts b/packages/frontend-app-api/src/routing/RouteResolver.ts index 1ec1822fe9..ff7966c4db 100644 --- a/packages/frontend-app-api/src/routing/RouteResolver.ts +++ b/packages/frontend-app-api/src/routing/RouteResolver.ts @@ -195,7 +195,7 @@ export class RouteResolver implements RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined { // First figure out what our target absolute ref is, as well as our target path. diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index 1c332b331f..bdab2781e0 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -70,7 +70,7 @@ function createTestExtension(options: { }); } -function routeInfoFromExtensions(extensions: ExtensionDefinition[]) { +function routeInfoFromExtensions(extensions: ExtensionDefinition[]) { const plugin = createPlugin({ id: 'test', extensions, diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts index 9b150eb127..908a99d3aa 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.test.ts @@ -161,4 +161,31 @@ describe('resolveRouteBindings', () => { expect(result.get(source)).toBe(target2); }); + + it('can disable external routes that have defaults', () => { + const source = createExternalRouteRef({ defaultTarget: 'target1' }); + const target1 = createRouteRef(); + const routesById = { + routes: new Map([['target1', target1]]), + externalRoutes: new Map([['source', source]]), + }; + + // resolves normally with no config + let result = resolveRouteBindings( + () => {}, + new ConfigReader({}), + routesById, + ); + + expect(result.get(source)).toBe(target1); + + // can be disabled + result = resolveRouteBindings( + () => {}, + new ConfigReader({ app: { routes: { bindings: { source: false } } } }), + routesById, + ); + + expect(result.get(source)).toBe(undefined); + }); }); diff --git a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts index 49e3b1a1be..a8f641daff 100644 --- a/packages/frontend-app-api/src/routing/resolveRouteBindings.ts +++ b/packages/frontend-app-api/src/routing/resolveRouteBindings.ts @@ -53,8 +53,7 @@ type TargetRouteMap< ExternalRoutes extends { [name: string]: ExternalRouteRef }, > = { [name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef< - infer Params, - any + infer Params > ? RouteRef | SubRouteRef : never; @@ -72,7 +71,7 @@ export type CreateAppRouteBinder = < externalRoutes: TExternalRoutes, targetRoutes: PartialKeys< TargetRouteMap, - KeysWithType> + KeysWithType> >, ) => void; @@ -95,11 +94,6 @@ export function resolveRouteBindings( if (!externalRoute) { throw new Error(`Key ${key} is not an existing external route`); } - if (!value && !externalRoute.optional) { - throw new Error( - `External route ${key} is required but was undefined`, - ); - } if (value) { result.set(externalRoute, value); } @@ -112,11 +106,12 @@ export function resolveRouteBindings( const bindings = config .getOptionalConfig('app.routes.bindings') ?.get(); + const disabledExternalRefs = new Set(); if (bindings) { for (const [externalRefId, targetRefId] of Object.entries(bindings)) { - if (typeof targetRefId !== 'string' || targetRefId === '') { + if (!isValidTargetRefId(targetRefId)) { throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string`, + `Invalid config at app.routes.bindings['${externalRefId}'], value must be a non-empty string or false`, ); } @@ -126,23 +121,27 @@ export function resolveRouteBindings( `Invalid config at app.routes.bindings, '${externalRefId}' is not a valid external route`, ); } - if (result.has(externalRef)) { - continue; - } - const targetRef = routesById.routes.get(targetRefId); - if (!targetRef) { - throw new Error( - `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, - ); - } - result.set(externalRef, targetRef); + if (targetRefId === false) { + disabledExternalRefs.add(externalRef); + + result.delete(externalRef); + } else if (!result.has(externalRef)) { + const targetRef = routesById.routes.get(targetRefId); + if (!targetRef) { + throw new Error( + `Invalid config at app.routes.bindings['${externalRefId}'], '${targetRefId}' is not a valid route`, + ); + } + + result.set(externalRef, targetRef); + } } } // Finally fall back to attempting to map defaults, at lowest priority for (const externalRef of routesById.externalRoutes.values()) { - if (!result.has(externalRef)) { + if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) { const defaultRefId = toInternalExternalRouteRef(externalRef).getDefaultTarget(); if (defaultRefId) { @@ -156,3 +155,15 @@ export function resolveRouteBindings( return result; } + +function isValidTargetRefId(value: unknown): value is string | false { + if (value === false) { + return true; + } + + if (typeof value === 'string' && value) { + return true; + } + + return false; +} diff --git a/packages/frontend-app-api/src/tree/createAppTree.ts b/packages/frontend-app-api/src/tree/createAppTree.ts index 12c7f7fc3a..ee9704c3c3 100644 --- a/packages/frontend-app-api/src/tree/createAppTree.ts +++ b/packages/frontend-app-api/src/tree/createAppTree.ts @@ -25,7 +25,7 @@ import { instantiateAppNodeTree } from './instantiateAppNodeTree'; /** @internal */ export interface CreateAppTreeOptions { features: FrontendFeature[]; - builtinExtensions: Extension[]; + builtinExtensions: Extension[]; config: Config; } diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts index 3330b9add8..aded5d7826 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.test.ts @@ -17,6 +17,8 @@ import { AppNode, Extension, + ExtensionInput, + ResolvedExtensionInput, createExtension, createExtensionDataRef, createExtensionInput, @@ -32,47 +34,28 @@ import { resolveAppTree } from './resolveAppTree'; import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; import { withLogCollector } from '@backstage/test-utils'; -const testDataRef = createExtensionDataRef('test'); -const otherDataRef = createExtensionDataRef('other'); -const inputMirrorDataRef = createExtensionDataRef('mirror'); +const testDataRef = createExtensionDataRef().with({ id: 'test' }); +const otherDataRef = createExtensionDataRef().with({ id: 'other' }); +const inputMirrorDataRef = createExtensionDataRef().with({ + id: 'mirror', +}); -const simpleExtension = resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - other: otherDataRef.optional(), - }, - configSchema: createSchemaFromZod(z => - z.object({ - output: z.string().default('test'), - other: z.number().optional(), - }), - ), - factory({ config }) { - return { test: config.output, other: config.other }; - }, - }), -); - -function makeSpec( - extension: Extension, +function makeSpec( + extension: Extension, spec?: Partial, ): AppNodeSpec { return { id: extension.id, attachTo: extension.attachTo, disabled: extension.disabled, - extension, + extension: extension as Extension, source: undefined, ...spec, }; } -function makeNode( - extension: Extension, +function makeNode( + extension: Extension, spec?: Partial, ): AppNode { return { @@ -83,9 +66,9 @@ function makeNode( }; } -function makeInstanceWithId( - extension: Extension, - config?: TConfig, +function makeInstanceWithId( + extension: Extension, + config?: TConfigInput, ): AppNode { const node = makeNode(extension, { config }); return { @@ -98,78 +81,54 @@ function makeInstanceWithId( } describe('instantiateAppNodeTree', () => { - it('should instantiate a single node', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node' }), - ]); - expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root); - expect(tree.root.instance).toBeDefined(); - expect(tree.root.instance?.getData(testDataRef)).toBe('test'); - - // Multiple calls should have no effect - instantiateAppNodeTree(tree.root); - expect(tree.root.instance).toBeDefined(); - }); - - it('should not instantiate disabled nodes', () => { - const tree = resolveAppTree('root-node', [ - makeSpec(simpleExtension, { id: 'root-node', disabled: true }), - ]); - expect(tree.root.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root); - expect(tree.root.instance).not.toBeDefined(); - }); - - it('should instantiate a node with attachments', () => { - const tree = resolveAppTree('root-node', [ - makeSpec( - resolveExtensionDefinition( - createExtension({ - namespace: 'root-node', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - test: createExtensionInput({ test: testDataRef }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, + describe('v1', () => { + const simpleExtension = resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + other: otherDataRef.optional(), + }, + configSchema: createSchemaFromZod(z => + z.object({ + output: z.string().default('test'), + other: z.number().optional(), }), ), - ), - makeSpec(simpleExtension, { - id: 'child-node', - attachTo: { id: 'root-node', input: 'test' }, + factory({ config }) { + return { test: config.output, other: config.other }; + }, }), - ]); + ); - const childNode = tree.nodes.get('child-node'); - expect(childNode).toBeDefined(); + it('should instantiate a single node', () => { + const tree = resolveAppTree('root-node', [ + makeSpec(simpleExtension, { id: 'root-node' }), + ]); + expect(tree.root.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(tree.root.instance?.getData(testDataRef)).toBe('test'); - expect(tree.root.instance).not.toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ - test: [ - { node: { spec: { id: 'child-node' } }, output: { test: 'test' } }, - ], + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); }); - // Multiple calls should have no effect - instantiateAppNodeTree(tree.root); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).toBeDefined(); - }); + it('should not instantiate disabled nodes', () => { + const tree = resolveAppTree('root-node', [ + makeSpec(simpleExtension, { id: 'root-node', disabled: true }), + ]); + expect(tree.root.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).not.toBeDefined(); + }); - it('should not instantiate disabled attachments', () => { - const tree = resolveAppTree('root-node', [ - { - ...makeSpec( + it('should instantiate a node with attachments', () => { + const tree = resolveAppTree('root-node', [ + makeSpec( resolveExtensionDefinition( createExtension({ namespace: 'root-node', @@ -186,437 +145,1062 @@ describe('instantiateAppNodeTree', () => { }), ), ), - }, - { - ...makeSpec(simpleExtension), - id: 'child-node', - // Using an invalid input should not be an error when disabled - attachTo: { id: 'root-node', input: 'invalid' }, - disabled: true, - }, - ]); + makeSpec(simpleExtension, { + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }), + ]); - const childNode = tree.nodes.get('child-node'); - expect(childNode).toBeDefined(); + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); - expect(tree.root.instance).not.toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - instantiateAppNodeTree(tree.root); - expect(tree.root.instance).toBeDefined(); - expect(childNode?.instance).not.toBeDefined(); - expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ - test: [], - }); - }); -}); + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: [ + { node: { spec: { id: 'child-node' } }, output: { test: 'test' } }, + ], + }); -describe('createAppNodeInstance', () => { - it('should create a simple extension instance', () => { - const attachments = new Map(); - const instance = createAppNodeInstance({ - node: makeNode(simpleExtension), - attachments, + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); }); - expect(Array.from(instance.getDataRefs())).toEqual([ - testDataRef, - otherDataRef.optional(), - ]); - expect(instance.getData(testDataRef)).toEqual('test'); - }); - - it('should create an extension with different kind of inputs', () => { - const attachments = new Map([ - [ - 'optionalSingletonPresent', - [ - makeInstanceWithId(simpleExtension, { - output: 'optionalSingletonPresent', - }), - ], - ], - [ - 'singleton', - [ - makeInstanceWithId(simpleExtension, { - output: 'singleton', - other: 2, - }), - ], - ], - [ - 'many', - [ - makeInstanceWithId(simpleExtension, { output: 'many1' }), - makeInstanceWithId(simpleExtension, { output: 'many2', other: 3 }), - ], - ], - ]); - const instance = createAppNodeInstance({ - attachments, - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - optionalSingletonPresent: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true, optional: true }, - ), - optionalSingletonMissing: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true, optional: true }, - ), - singleton: createExtensionInput( - { - test: testDataRef, - other: otherDataRef.optional(), - }, - { singleton: true }, - ), - many: createExtensionInput({ - test: testDataRef, - other: otherDataRef.optional(), - }), - }, - output: { - inputMirror: inputMirrorDataRef, - }, - factory({ inputs }) { - return { inputMirror: inputs }; - }, - }), - ), - ), - }); - - expect(Array.from(instance.getDataRefs())).toEqual([inputMirrorDataRef]); - expect(instance.getData(inputMirrorDataRef)).toMatchObject({ - optionalSingletonPresent: { - node: { spec: { id: 'app/test' } }, - output: { test: 'optionalSingletonPresent' }, - }, - singleton: { - node: { spec: { id: 'app/test' } }, - output: { test: 'singleton', other: 2 }, - }, - many: [ - { node: { spec: { id: 'app/test' } }, output: { test: 'many1' } }, + it('should not instantiate disabled attachments', () => { + const tree = resolveAppTree('root-node', [ { - node: { spec: { id: 'app/test' } }, - output: { test: 'many2', other: 3 }, + ...makeSpec( + resolveExtensionDefinition( + createExtension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput({ test: testDataRef }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), + ), + ), }, - ], + { + ...makeSpec(simpleExtension), + id: 'child-node', + // Using an invalid input should not be an error when disabled + attachTo: { id: 'root-node', input: 'invalid' }, + disabled: true, + }, + ]); + + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ + test: [], + }); + }); + + describe('createAppNodeInstance', () => { + it('should create a simple extension instance', () => { + const attachments = new Map(); + const instance = createAppNodeInstance({ + node: makeNode(simpleExtension), + attachments, + }); + + expect(Array.from(instance.getDataRefs())).toEqual([ + testDataRef, + otherDataRef.optional(), + ]); + expect(instance.getData(testDataRef)).toEqual('test'); + }); + + it('should create an extension with different kind of inputs', () => { + const attachments = new Map([ + [ + 'optionalSingletonPresent', + [ + makeInstanceWithId(simpleExtension, { + output: 'optionalSingletonPresent', + }), + ], + ], + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { + output: 'singleton', + other: 2, + }), + ], + ], + [ + 'many', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { + output: 'many2', + other: 3, + }), + ], + ], + ]); + const instance = createAppNodeInstance({ + attachments, + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + optionalSingletonPresent: createExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true, optional: true }, + ), + optionalSingletonMissing: createExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true, optional: true }, + ), + singleton: createExtensionInput( + { + test: testDataRef, + other: otherDataRef.optional(), + }, + { singleton: true }, + ), + many: createExtensionInput({ + test: testDataRef, + other: otherDataRef.optional(), + }), + }, + output: { + inputMirror: inputMirrorDataRef, + }, + factory({ inputs }) { + return { inputMirror: inputs }; + }, + }), + ), + ), + }); + + expect(Array.from(instance.getDataRefs())).toEqual([ + inputMirrorDataRef, + ]); + expect(instance.getData(inputMirrorDataRef)).toMatchObject({ + optionalSingletonPresent: { + node: { spec: { id: 'app/test' } }, + output: { test: 'optionalSingletonPresent' }, + }, + singleton: { + node: { spec: { id: 'app/test' } }, + output: { test: 'singleton', other: 2 }, + }, + many: [ + { node: { spec: { id: 'app/test' } }, output: { test: 'many1' } }, + { + node: { spec: { id: 'app/test' } }, + output: { test: 'many2', other: 3 }, + }, + ], + }); + }); + + it('should refuse to create an extension with invalid config', () => { + expect(() => + createAppNodeInstance({ + node: makeNode(simpleExtension, { + config: { other: 'not-a-number' }, + }), + attachments: new Map(), + }), + ).toThrow( + "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", + ); + }); + + it('should forward extension factory errors', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", + ); + }); + + it('should refuse to create an instance with duplicate output', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test1: testDataRef, + test2: testDataRef, + }, + factory({}) { + return { test1: 'test', test2: 'test2' }; + }, + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", + ); + }); + + it('should refuse to create an instance with disconnected output data', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + test: testDataRef, + }, + factory({}) { + return { nonexistent: 'test' } as any; + }, + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", + ); + }); + + it('should refuse to create an instance with missing required input', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", + ); + }); + + it('should warn when creating an instance with undeclared inputs', () => { + const { warn } = withLogCollector(['warn'], () => + createAppNodeInstance({ + attachments: new Map([ + [ + 'declared', + [ + makeInstanceWithId(simpleExtension, { + output: 'many1', + }), + ], + ], + [ + 'undeclared', + [ + makeInstanceWithId(simpleExtension, { + output: 'many1', + }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'parent', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + declared: createExtensionInput({ + test: testDataRef, + }), + }, + output: {}, + factory: () => ({}), + }), + ), + ), + }), + ); + + expect(warn).toEqual([ + "The extension 'app/test' is attached to the input 'undeclared' of the extension 'app/parent', but it has no such input (candidates are 'declared')", + ]); + }); + + it('should refuse to create an instance with multiple undeclared inputs', () => { + const { warn } = withLogCollector(['warn'], () => + createAppNodeInstance({ + attachments: new Map([ + [ + 'undeclared1', + [makeInstanceWithId(simpleExtension, { output: 'many1' })], + ], + [ + 'undeclared2', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many1' }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'parent', + attachTo: { id: 'ignored', input: 'ignored' }, + output: {}, + factory: () => ({}), + }), + ), + ), + }), + ); + + expect(warn).toEqual([ + "The extension 'app/test' is attached to the input 'undeclared1' of the extension 'app/parent', but it has no inputs", + "The extensions 'app/test', 'app/test' are attached to the input 'undeclared2' of the extension 'app/parent', but it has no inputs", + ]); + }); + + it('should refuse to create an instance with multiple inputs for required singleton', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2' }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + test: testDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ), + ), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", + ); + }); + + it('should refuse to create an instance with multiple inputs for optional singleton', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2' }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + test: testDataRef, + }, + { singleton: true, optional: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ), + ), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", + ); + }); + + it('should refuse to create an instance with multiple inputs that did not provide required data', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput( + { + other: otherDataRef, + }, + { singleton: true }, + ), + }, + output: {}, + factory: () => ({}), + }), + ), + ), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')"`, + ); + }); }); }); - it('should refuse to create an extension with invalid config', () => { - expect(() => - createAppNodeInstance({ - node: makeNode(simpleExtension, { config: { other: 'not-a-number' } }), - attachments: new Map(), + describe('v2', () => { + const simpleExtension = resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef, otherDataRef.optional()], + config: { + schema: { + output: z => z.string().default('test'), + other: z => z.number().optional(), + }, + }, + factory({ config }) { + return [ + testDataRef(config.output), + ...(config.other ? [otherDataRef(config.other)] : []), + ]; + }, }), - ).toThrow( - "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", ); - }); - it('should forward extension factory errors', () => { - expect(() => - createAppNodeInstance({ - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory() { - const error = new Error('NOPE'); - error.name = 'NopeError'; - throw error; - }, - }), - ), - ), - attachments: new Map(), - }), - ).toThrow( - "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", - ); - }); - - it('should refuse to create an instance with duplicate output', () => { - expect(() => - createAppNodeInstance({ - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test1: testDataRef, - test2: testDataRef, - }, - factory({}) { - return { test1: 'test', test2: 'test2' }; - }, - }), - ), - ), - attachments: new Map(), - }), - ).toThrow( - "Failed to instantiate extension 'app/test', duplicate extension data 'test' received via output 'test2'", - ); - }); - - it('should refuse to create an instance with disconnected output data', () => { - expect(() => - createAppNodeInstance({ - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - test: testDataRef, - }, - factory({}) { - return { nonexistent: 'test' } as any; - }, - }), - ), - ), - attachments: new Map(), - }), - ).toThrow( - "Failed to instantiate extension 'app/test', unknown output provided via 'nonexistent'", - ); - }); - - it('should refuse to create an instance with missing required input', () => { - expect(() => - createAppNodeInstance({ - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, + function mirrorInputs(ctx: { + inputs: { + [name in string]: + | undefined + | ResolvedExtensionInput< + ExtensionInput + > + | Array< + ResolvedExtensionInput< + ExtensionInput + > + >; + }; + }) { + return [ + inputMirrorDataRef( + Object.fromEntries( + Object.entries(ctx.inputs).map(([k, v]) => [ + k, + Array.isArray(v) + ? v.map(vi => ({ + node: vi.node, + test: vi.get(testDataRef), + other: vi.get(otherDataRef), + })) + : { + node: v?.node, + test: v?.get(testDataRef), + other: v?.get(otherDataRef), }, - { singleton: true }, - ), + ]), + ), + ), + ]; + } + + it('should instantiate a single node', () => { + const tree = resolveAppTree('root-node', [ + makeSpec(simpleExtension, { id: 'root-node' }), + ]); + expect(tree.root.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(tree.root.instance?.getData(testDataRef)).toBe('test'); + + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + }); + + it('should not instantiate disabled nodes', () => { + const tree = resolveAppTree('root-node', [ + makeSpec(simpleExtension, { id: 'root-node', disabled: true }), + ]); + expect(tree.root.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).not.toBeDefined(); + }); + + it('should instantiate a node with attachments', () => { + const tree = resolveAppTree('root-node', [ + makeSpec( + resolveExtensionDefinition( + createExtension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput([testDataRef]), }, - output: {}, - factory: () => ({}), + output: [inputMirrorDataRef], + factory: mirrorInputs, }), ), ), - attachments: new Map(), - }), - ).toThrow( - "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", - ); - }); + makeSpec(simpleExtension, { + id: 'child-node', + attachTo: { id: 'root-node', input: 'test' }, + }), + ]); - it('should warn when creating an instance with undeclared inputs', () => { - const { warn } = withLogCollector(['warn'], () => - createAppNodeInstance({ - attachments: new Map([ + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({ + test: [{ node: { spec: { id: 'child-node' } }, test: 'test' }], + }); + + // Multiple calls should have no effect + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).toBeDefined(); + }); + + it('should not instantiate disabled attachments', () => { + const tree = resolveAppTree('root-node', [ + { + ...makeSpec( + resolveExtensionDefinition( + createExtension({ + namespace: 'root-node', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + test: createExtensionInput([testDataRef]), + }, + output: [inputMirrorDataRef], + factory: mirrorInputs, + }), + ), + ), + }, + { + ...makeSpec(simpleExtension), + id: 'child-node', + // Using an invalid input should not be an error when disabled + attachTo: { id: 'root-node', input: 'invalid' }, + disabled: true, + }, + ]); + + const childNode = tree.nodes.get('child-node'); + expect(childNode).toBeDefined(); + + expect(tree.root.instance).not.toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + instantiateAppNodeTree(tree.root); + expect(tree.root.instance).toBeDefined(); + expect(childNode?.instance).not.toBeDefined(); + expect(tree.root.instance?.getData(inputMirrorDataRef)).toEqual({ + test: [], + }); + }); + + describe('createAppNodeInstance', () => { + it('should create a simple extension instance', () => { + const attachments = new Map(); + const instance = createAppNodeInstance({ + node: makeNode(simpleExtension), + attachments, + }); + + expect(Array.from(instance.getDataRefs())).toEqual([testDataRef]); + expect(instance.getData(testDataRef)).toEqual('test'); + }); + + it('should create an extension with different kind of inputs', () => { + const attachments = new Map([ [ - 'declared', + 'optionalSingletonPresent', [ makeInstanceWithId(simpleExtension, { - output: 'many1', + output: 'optionalSingletonPresent', }), ], ], [ - 'undeclared', + 'singleton', [ makeInstanceWithId(simpleExtension, { - output: 'many1', + output: 'singleton', + other: 2, }), ], ], - ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'parent', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - declared: createExtensionInput({ - test: testDataRef, + [ + 'many', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { + output: 'many2', + other: 3, + }), + ], + ], + ]); + + const instance = createAppNodeInstance({ + attachments, + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + optionalSingletonPresent: createExtensionInput( + [testDataRef, otherDataRef.optional()], + { singleton: true, optional: true }, + ), + optionalSingletonMissing: createExtensionInput( + [testDataRef, otherDataRef.optional()], + { singleton: true, optional: true }, + ), + singleton: createExtensionInput( + [testDataRef, otherDataRef.optional()], + { singleton: true }, + ), + many: createExtensionInput([ + testDataRef, + otherDataRef.optional(), + ]), + }, + output: [inputMirrorDataRef], + factory: mirrorInputs, + }), + ), + ), + }); + + expect(Array.from(instance.getDataRefs())).toEqual([ + inputMirrorDataRef, + ]); + expect(instance.getData(inputMirrorDataRef)).toMatchObject({ + optionalSingletonPresent: { + node: { spec: { id: 'app/test' } }, + test: 'optionalSingletonPresent', + }, + singleton: { + node: { spec: { id: 'app/test' } }, + test: 'singleton', + other: 2, + }, + many: [ + { node: { spec: { id: 'app/test' } }, test: 'many1' }, + { + node: { spec: { id: 'app/test' } }, + test: 'many2', + other: 3, + }, + ], + }); + }); + + it('should refuse to create an extension with invalid config', () => { + expect(() => + createAppNodeInstance({ + node: makeNode(simpleExtension, { + config: { other: 'not-a-number' }, + }), + attachments: new Map(), + }), + ).toThrow( + "Invalid configuration for extension 'app/test'; caused by Error: Expected number, received string at 'other'", + ); + }); + + it('should forward extension factory errors', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef], + factory() { + const error = new Error('NOPE'); + error.name = 'NopeError'; + throw error; + }, }), - }, - output: {}, - factory: () => ({}), - }), - ), - ), - }), - ); + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test'; caused by NopeError: NOPE", + ); + }); - expect(warn).toEqual([ - "The extension 'app/test' is attached to the input 'undeclared' of the extension 'app/parent', but it has no such input (candidates are 'declared')", - ]); - }); - - it('should refuse to create an instance with multiple undeclared inputs', () => { - const { warn } = withLogCollector(['warn'], () => - createAppNodeInstance({ - attachments: new Map([ - [ - 'undeclared1', - [makeInstanceWithId(simpleExtension, { output: 'many1' })], - ], - [ - 'undeclared2', - [ - makeInstanceWithId(simpleExtension, { output: 'many1' }), - makeInstanceWithId(simpleExtension, { output: 'many1' }), - ], - ], - ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'parent', - attachTo: { id: 'ignored', input: 'ignored' }, - output: {}, - factory: () => ({}), - }), - ), - ), - }), - ); - - expect(warn).toEqual([ - "The extension 'app/test' is attached to the input 'undeclared1' of the extension 'app/parent', but it has no inputs", - "The extensions 'app/test', 'app/test' are attached to the input 'undeclared2' of the extension 'app/parent', but it has no inputs", - ]); - }); - - it('should refuse to create an instance with multiple inputs for required singleton', () => { - expect(() => - createAppNodeInstance({ - attachments: new Map([ - [ - 'singleton', - [ - makeInstanceWithId(simpleExtension, { output: 'many1' }), - makeInstanceWithId(simpleExtension, { output: 'many2' }), - ], - ], - ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, + it('should refuse to create an instance with duplicate output', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef, testDataRef], + factory({}) { + return [testDataRef('test'), testDataRef('test2')]; }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), - ), - }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); - }); + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', duplicate extension data output 'test'", + ); + }); - it('should refuse to create an instance with multiple inputs for optional singleton', () => { - expect(() => - createAppNodeInstance({ - attachments: new Map([ - [ - 'singleton', - [ - makeInstanceWithId(simpleExtension, { output: 'many1' }), - makeInstanceWithId(simpleExtension, { output: 'many2' }), - ], - ], - ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - test: testDataRef, + it('should refuse to create an instance without required', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [testDataRef], + factory({}) { + return [] as any; }, - { singleton: true, optional: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), - ), - }), - ).toThrow( - "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", - ); - }); + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', missing required extension data output 'test'", + ); + }); - it('should refuse to create an instance with multiple inputs that did not provide required data', () => { - expect(() => - createAppNodeInstance({ - attachments: new Map([ - ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], - ]), - node: makeNode( - resolveExtensionDefinition( - createExtension({ - namespace: 'app', - name: 'test', - attachTo: { id: 'ignored', input: 'ignored' }, - inputs: { - singleton: createExtensionInput( - { - other: otherDataRef, + it('should refuse to create an instance with unknown output data', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + // @ts-expect-error + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [], // Output not declared + factory({}) { + return [testDataRef('test')] as any; }, - { singleton: true }, - ), - }, - output: {}, - factory: () => ({}), - }), - ), - ), - }), - ).toThrowErrorMatchingInlineSnapshot( - `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test', 'other') does not match what the input 'singleton' requires ('other')"`, - ); + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', unexpected output 'test'", + ); + }); + + it('should refuse to create an instance with missing required input', () => { + expect(() => + createAppNodeInstance({ + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + ), + ), + attachments: new Map(), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', input 'singleton' is required but was not received", + ); + }); + + it('should warn when creating an instance with undeclared inputs', () => { + const { warn } = withLogCollector(['warn'], () => + createAppNodeInstance({ + attachments: new Map([ + [ + 'declared', + [ + makeInstanceWithId(simpleExtension, { + output: 'many1', + }), + ], + ], + [ + 'undeclared', + [ + makeInstanceWithId(simpleExtension, { + output: 'many1', + }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'parent', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + declared: createExtensionInput([testDataRef]), + }, + output: [], + factory: () => [], + }), + ), + ), + }), + ); + + expect(warn).toEqual([ + "The extension 'app/test' is attached to the input 'undeclared' of the extension 'app/parent', but it has no such input (candidates are 'declared')", + ]); + }); + + it('should refuse to create an instance with multiple undeclared inputs', () => { + const { warn } = withLogCollector(['warn'], () => + createAppNodeInstance({ + attachments: new Map([ + [ + 'undeclared1', + [makeInstanceWithId(simpleExtension, { output: 'many1' })], + ], + [ + 'undeclared2', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many1' }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'parent', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [], + factory: () => [], + }), + ), + ), + }), + ); + + expect(warn).toEqual([ + "The extension 'app/test' is attached to the input 'undeclared1' of the extension 'app/parent', but it has no inputs", + "The extensions 'app/test', 'app/test' are attached to the input 'undeclared2' of the extension 'app/parent', but it has no inputs", + ]); + }); + + it('should refuse to create an instance with multiple inputs for required singleton', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2' }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + ), + ), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', expected exactly one 'singleton' input but received multiple: 'app/test', 'app/test'", + ); + }); + + it('should refuse to create an instance with multiple inputs for optional singleton', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + [ + 'singleton', + [ + makeInstanceWithId(simpleExtension, { output: 'many1' }), + makeInstanceWithId(simpleExtension, { output: 'many2' }), + ], + ], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([testDataRef], { + singleton: true, + optional: true, + }), + }, + output: [], + factory: () => [], + }), + ), + ), + }), + ).toThrow( + "Failed to instantiate extension 'app/test', expected at most one 'singleton' input but received multiple: 'app/test', 'app/test'", + ); + }); + + it('should refuse to create an instance with multiple inputs that did not provide required data', () => { + expect(() => + createAppNodeInstance({ + attachments: new Map([ + ['singleton', [makeInstanceWithId(simpleExtension, undefined)]], + ]), + node: makeNode( + resolveExtensionDefinition( + createExtension({ + namespace: 'app', + name: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + inputs: { + singleton: createExtensionInput([otherDataRef], { + singleton: true, + }), + }, + output: [], + factory: () => [], + }), + ), + ), + }), + ).toThrowErrorMatchingInlineSnapshot( + `"Failed to instantiate extension 'app/test', extension 'app/test' could not be attached because its output data ('test') does not match what the input 'singleton' requires ('other')"`, + ); + }); + }); }); }); diff --git a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts index 63eca749b7..65626e0be1 100644 --- a/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts +++ b/packages/frontend-app-api/src/tree/instantiateAppNodeTree.ts @@ -16,8 +16,11 @@ import { AnyExtensionDataMap, + AnyExtensionDataRef, AnyExtensionInputMap, + ExtensionDataContainer, ExtensionDataRef, + ExtensionInput, ResolvedExtensionInputs, } from '@backstage/frontend-plugin-api'; import mapValues from 'lodash/mapValues'; @@ -29,7 +32,7 @@ type Mutable = { -readonly [P in keyof T]: T[P]; }; -function resolveInputData( +function resolveInputDataMap( dataMap: AnyExtensionDataMap, attachment: AppNode, inputName: string, @@ -54,35 +57,76 @@ function resolveInputData( }); } -function resolveInputs( +function resolveInputDataContainer( + extensionData: Array, + attachment: AppNode, + inputName: string, +): { node: AppNode } & ExtensionDataContainer { + const dataMap = new Map(); + + for (const ref of extensionData) { + if (dataMap.has(ref.id)) { + throw new Error(`Unexpected duplicate input data '${ref.id}'`); + } + const value = attachment.instance?.getData(ref); + if (value === undefined && !ref.config.optional) { + const expected = extensionData + .filter(r => !r.config.optional) + .map(r => `'${r.id}'`) + .join(', '); + + const provided = [...(attachment.instance?.getDataRefs() ?? [])] + .map(r => `'${r.id}'`) + .join(', '); + + throw new Error( + `extension '${attachment.spec.id}' could not be attached because its output data (${provided}) does not match what the input '${inputName}' requires (${expected})`, + ); + } + + dataMap.set(ref.id, value); + } + + return { + node: attachment, + get(ref) { + return dataMap.get(ref.id); + }, + } as { node: AppNode } & ExtensionDataContainer; +} + +function reportUndeclaredAttachments( id: string, - inputMap: AnyExtensionInputMap, + inputMap: { [name in string]: unknown }, attachments: ReadonlyMap, -): ResolvedExtensionInputs { +) { const undeclaredAttachments = Array.from(attachments.entries()).filter( ([inputName]) => inputMap[inputName] === undefined, ); - if (process.env.NODE_ENV !== 'production') { - const inputNames = Object.keys(inputMap); + const inputNames = Object.keys(inputMap); - for (const [name, nodes] of undeclaredAttachments) { - const pl = nodes.length > 1; - // eslint-disable-next-line no-console - console.warn( - [ - `The extension${pl ? 's' : ''} '${nodes - .map(n => n.spec.id) - .join("', '")}' ${pl ? 'are' : 'is'}`, - `attached to the input '${name}' of the extension '${id}', but it`, - inputNames.length === 0 - ? 'has no inputs' - : `has no such input (candidates are '${inputNames.join("', '")}')`, - ].join(' '), - ); - } + for (const [name, nodes] of undeclaredAttachments) { + const pl = nodes.length > 1; + // eslint-disable-next-line no-console + console.warn( + [ + `The extension${pl ? 's' : ''} '${nodes + .map(n => n.spec.id) + .join("', '")}' ${pl ? 'are' : 'is'}`, + `attached to the input '${name}' of the extension '${id}', but it`, + inputNames.length === 0 + ? 'has no inputs' + : `has no such input (candidates are '${inputNames.join("', '")}')`, + ].join(' '), + ); } +} +function resolveV1Inputs( + inputMap: AnyExtensionInputMap, + attachments: ReadonlyMap, +): ResolvedExtensionInputs { return mapValues(inputMap, (input, inputName) => { const attachedNodes = attachments.get(inputName) ?? []; @@ -104,7 +148,7 @@ function resolveInputs( } return { node: attachedNodes[0], - output: resolveInputData( + output: resolveInputDataMap( input.extensionData, attachedNodes[0], inputName, @@ -114,11 +158,62 @@ function resolveInputs( return attachedNodes.map(attachment => ({ node: attachment, - output: resolveInputData(input.extensionData, attachment, inputName), + output: resolveInputDataMap(input.extensionData, attachment, inputName), })); }) as ResolvedExtensionInputs; } +function resolveV2Inputs( + inputMap: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + attachments: ReadonlyMap, +): ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; +}> { + return mapValues(inputMap, (input, inputName) => { + const attachedNodes = attachments.get(inputName) ?? []; + + if (input.config.singleton) { + if (attachedNodes.length > 1) { + const attachedNodeIds = attachedNodes.map(e => e.spec.id); + throw Error( + `expected ${ + input.config.optional ? 'at most' : 'exactly' + } one '${inputName}' input but received multiple: '${attachedNodeIds.join( + "', '", + )}'`, + ); + } else if (attachedNodes.length === 0) { + if (input.config.optional) { + return undefined; + } + throw Error(`input '${inputName}' is required but was not received`); + } + return resolveInputDataContainer( + input.extensionData, + attachedNodes[0], + inputName, + ); + } + + return attachedNodes.map(attachment => + resolveInputDataContainer(input.extensionData, attachment, inputName), + ); + }) as ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }>; +} + /** @internal */ export function createAppNodeInstance(options: { node: AppNode; @@ -141,24 +236,71 @@ export function createAppNodeInstance(options: { try { const internalExtension = toInternalExtension(extension); - const namedOutputs = internalExtension.factory({ - node, - config: parsedConfig, - inputs: resolveInputs(id, internalExtension.inputs, attachments), - }); + if (process.env.NODE_ENV !== 'production') { + reportUndeclaredAttachments(id, internalExtension.inputs, attachments); + } - for (const [name, output] of Object.entries(namedOutputs)) { - const ref = internalExtension.output[name]; - if (!ref) { - throw new Error(`unknown output provided via '${name}'`); + if (internalExtension.version === 'v1') { + const namedOutputs = internalExtension.factory({ + node, + config: parsedConfig, + inputs: resolveV1Inputs(internalExtension.inputs, attachments), + }); + + for (const [name, output] of Object.entries(namedOutputs)) { + const ref = internalExtension.output[name]; + if (!ref) { + throw new Error(`unknown output provided via '${name}'`); + } + if (extensionData.has(ref.id)) { + throw new Error( + `duplicate extension data '${ref.id}' received via output '${name}'`, + ); + } + extensionData.set(ref.id, output); + extensionDataRefs.add(ref); } - if (extensionData.has(ref.id)) { + } else if (internalExtension.version === 'v2') { + const outputDataValues = internalExtension.factory({ + node, + config: parsedConfig, + inputs: resolveV2Inputs(internalExtension.inputs, attachments), + }); + + const outputDataMap = new Map(); + for (const value of outputDataValues) { + if (outputDataMap.has(value.id)) { + throw new Error(`duplicate extension data output '${value.id}'`); + } + outputDataMap.set(value.id, value.value); + } + + for (const ref of internalExtension.output) { + const value = outputDataMap.get(ref.id); + outputDataMap.delete(ref.id); + if (value === undefined) { + if (!ref.config.optional) { + throw new Error( + `missing required extension data output '${ref.id}'`, + ); + } + } else { + extensionData.set(ref.id, value); + extensionDataRefs.add(ref); + } + } + + if (outputDataMap.size > 0) { throw new Error( - `duplicate extension data '${ref.id}' received via output '${name}'`, + `unexpected output '${Array.from(outputDataMap.keys()).join( + "', '", + )}'`, ); } - extensionData.set(ref.id, output); - extensionDataRefs.add(ref); + } else { + throw new Error( + `unexpected extension version '${(internalExtension as any).version}'`, + ); } } catch (e) { throw new Error( diff --git a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts index 8af2aef070..65f3f77640 100644 --- a/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppNodeSpecs.test.ts @@ -48,6 +48,7 @@ function makeExtDef( name, attachTo: { id: attachId, input: 'default' }, disabled: status === 'disabled', + override: () => ({} as ExtensionDefinition), } as ExtensionDefinition; } diff --git a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts index f964fceb50..d9b65555fe 100644 --- a/packages/frontend-app-api/src/tree/resolveAppTree.test.ts +++ b/packages/frontend-app-api/src/tree/resolveAppTree.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createExtension } from '@backstage/frontend-plugin-api'; +import { createExtension, Extension } from '@backstage/frontend-plugin-api'; import { resolveAppTree } from './resolveAppTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; @@ -26,7 +26,7 @@ const extension = resolveExtensionDefinition( output: {}, factory: () => ({}), }), -); +) as Extension; const baseSpec = { extension, diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index cf6ea6dab4..d82d0ca782 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -25,6 +25,7 @@ import { createThemeExtension, createTranslationExtension, FrontendFeature, + IconBundleBlueprint, iconsApiRef, RouteResolutionApi, routeResolutionApiRef, @@ -171,6 +172,7 @@ export interface CreateAppFeatureLoader { /** @public */ export function createApp(options?: { + /** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ icons?: { [key in string]: IconComponent }; features?: (FrontendFeature | CreateAppFeatureLoader)[]; configLoader?: () => Promise<{ config: ConfigApi }>; @@ -246,6 +248,7 @@ export function createApp(options?: { * @public */ export function createSpecializedApp(options?: { + /** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */ icons?: { [key in string]: IconComponent }; features?: FrontendFeature[]; config?: ConfigApi; @@ -373,6 +376,11 @@ function createApiHolder( (x): x is typeof createTranslationExtension.translationDataRef.T => !!x, ) ?? []; + const extensionIcons = tree.root.edges.attachments + .get('icons') + ?.map(e => e.instance?.getData(IconBundleBlueprint.dataRefs.icons)) + .reduce((acc, bundle) => ({ ...acc, ...bundle }), {}); + for (const factory of pluginApis) { factoryRegistry.register('default', factory); } @@ -413,7 +421,8 @@ function createApiHolder( factoryRegistry.register('static', { api: iconsApiRef, deps: {}, - factory: () => new DefaultIconsApi({ ...defaultIcons, ...icons }), + factory: () => + new DefaultIconsApi({ ...defaultIcons, ...extensionIcons, ...icons }), }); factoryRegistry.register('static', { diff --git a/packages/frontend-plugin-api/CHANGELOG.md b/packages/frontend-plugin-api/CHANGELOG.md index 0ac7968290..302d541b18 100644 --- a/packages/frontend-plugin-api/CHANGELOG.md +++ b/packages/frontend-plugin-api/CHANGELOG.md @@ -1,5 +1,186 @@ # @backstage/frontend-plugin-api +## 0.7.0-next.2 + +### Minor Changes + +- 72754db: **BREAKING**: All types of route refs are always considered optional by `useRouteRef`, which means the caller must always handle a potential `undefined` return value. Related to this change, the `optional` option from `createExternalRouteRef` has been removed, since it is no longer necessary. + + This is released as an immediate breaking change as we expect the usage of the new route refs to be extremely low or zero, since plugins that support the new system will still use route refs and `useRouteRef` from `@backstage/core-plugin-api` in combination with `convertLegacyRouteRef` from `@backstage/core-compat-api`. + +### Patch Changes + +- 210d066: Added support for using the `params` in other properties of the `createExtensionBlueprint` options by providing a callback. +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.6.8-next.1 + +### Patch Changes + +- 3be9aeb: Extensions have been changed to be declared with an array of inputs and outputs, rather than a map of named data refs. This change was made to reduce confusion around the role of the input and output names, as well as enable more powerful APIs for overriding extensions. + + An extension that was previously declared like this: + + ```tsx + const exampleExtension = createExtension({ + name: 'example', + inputs: { + items: createExtensionInput({ + element: coreExtensionData.reactElement, + }), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( +
+ Example + {inputs.items.map(item => { + return
{item.output.element}
; + })} +
+ ), + }; + }, + }); + ``` + + Should be migrated to the following: + + ```tsx + const exampleExtension = createExtension({ + name: 'example', + inputs: { + items: createExtensionInput([coreExtensionData.reactElement]), + }, + output: [coreExtensionData.reactElement], + factory({ inputs }) { + return [ + coreExtensionData.reactElement( +
+ Example + {inputs.items.map(item => { + return
{item.get(coreExtensionData.reactElement)}
; + })} +
, + ), + ]; + }, + }); + ``` + +- 3fb421d: Added support to be able to define `zod` config schema in Blueprints, with built in schema merging from the Blueprint and the extension instances. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.6.8-next.0 + +### Patch Changes + +- 4e53ad6: Introduce a new way to encapsulate extension kinds that replaces the extension creator pattern with `createExtensionBlueprint` + + This allows the creation of extension instances with the following pattern: + + ```tsx + // create the extension blueprint which is used to create instances + const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', + attachTo: { id: 'test', input: 'default' }, + output: { + element: coreExtensionData.reactElement, + }, + factory(params: { text: string }) { + return { + element:

{params.text}

, + }; + }, + }); + + // create an instance of the extension blueprint with params + const testExtension = EntityCardBlueprint.make({ + name: 'foo', + params: { + text: 'Hello World', + }, + }); + ``` + +- 9b89b82: The `ExtensionBoundary` now by default infers whether it's routable from whether it outputs a route path. +- 7777b5f: Added a new `IconBundleBlueprint` that lets you create icon bundle extensions that can be installed in an App in order to override or add new app icons. + + ```tsx + import { IconBundleBlueprint } from '@backstage/frontend-plugin-api'; + + const exampleIconBundle = IconBundleBlueprint.make({ + name: 'example-bundle', + params: { + icons: { + user: MyOwnUserIcon, + }, + }, + }); + ``` + +- 31bfc44: Extension data references can now be defined in a way that encapsulates the ID string in the type, in addition to the data type itself. The old way of creating extension data references is deprecated and will be removed in a future release. + + For example, the following code: + + ```ts + export const myExtension = + createExtensionDataRef('my-plugin.my-data'); + ``` + + Should be updated to the following: + + ```ts + export const myExtension = createExtensionDataRef().with({ + id: 'my-plugin.my-data', + }); + ``` + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.6.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 0.6.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.6.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + ## 0.6.6 ### Patch Changes diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 3e19573d95..f39add924a 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -147,19 +147,23 @@ export { AnyApiFactory }; export { AnyApiRef }; -// @public (undocumented) +// @public @deprecated (undocumented) export type AnyExtensionDataMap = { - [name in string]: ExtensionDataRef< - unknown, - { - optional?: true; - } - >; + [name in string]: AnyExtensionDataRef; }; // @public (undocumented) +export type AnyExtensionDataRef = ExtensionDataRef< + unknown, + string, + { + optional?: true; + } +>; + +// @public @deprecated (undocumented) export type AnyExtensionInputMap = { - [inputName in string]: ExtensionInput< + [inputName in string]: LegacyExtensionInput< AnyExtensionDataMap, { optional: boolean; @@ -185,6 +189,27 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef; }; +// @public +export const ApiBlueprint: ExtensionBlueprint< + 'api', + undefined, + undefined, + { + factory: AnyApiFactory; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + factory: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; + } +>; + export { ApiFactory }; export { ApiHolder }; @@ -229,13 +254,57 @@ export interface AppNodeSpec { // (undocumented) readonly disabled: boolean; // (undocumented) - readonly extension: Extension; + readonly extension: Extension; // (undocumented) readonly id: string; // (undocumented) readonly source?: BackstagePlugin; } +// @public +export const AppRootElementBlueprint: ExtensionBlueprint< + 'app-root-element', + undefined, + undefined, + { + element: JSX.Element | (() => JSX.Element); + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + never +>; + +// @public +export const AppRootWrapperBlueprint: ExtensionBlueprint< + 'app-root-wrapper', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType<{ + children?: React_2.ReactNode; + }>, + 'app.root.wrapper', + {} + >; + } +>; + export { AppTheme }; export { AppThemeApi }; @@ -271,17 +340,22 @@ export { BackstageIdentityResponse }; // @public (undocumented) export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { + [id in string]: ExtensionDefinition; + } = {}, > { // (undocumented) readonly $$type: '@backstage/BackstagePlugin'; // (undocumented) - readonly externalRoutes: ExternalRoutes; + readonly externalRoutes: TExternalRoutes; + // (undocumented) + getExtension(id: TId): TExtensionMap[TId]; // (undocumented) readonly id: string; // (undocumented) - readonly routes: Routes; + readonly routes: TRoutes; } export { BackstageUserIdentity }; @@ -318,13 +392,17 @@ export { configApiRef }; // @public (undocumented) export interface ConfigurableExtensionDataRef< TData, + TId extends string, TConfig extends { optional?: true; } = {}, -> extends ExtensionDataRef { +> extends ExtensionDataRef { + // (undocumented) + (t: TData): ExtensionDataValue; // (undocumented) optional(): ConfigurableExtensionDataRef< TData, + TId, TData & { optional: true; } @@ -347,9 +425,17 @@ export type CoreErrorBoundaryFallbackProps = { // @public (undocumented) export const coreExtensionData: { - reactElement: ConfigurableExtensionDataRef; - routePath: ConfigurableExtensionDataRef; - routeRef: ConfigurableExtensionDataRef, {}>; + reactElement: ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + {} + >; + routePath: ConfigurableExtensionDataRef; + routeRef: ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + {} + >; }; // @public (undocumented) @@ -360,7 +446,7 @@ export type CoreNotFoundErrorPageProps = { // @public (undocumented) export type CoreProgressProps = {}; -// @public (undocumented) +// @public @deprecated (undocumented) export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -380,19 +466,31 @@ export function createApiExtension< configSchema?: PortableSchema; inputs?: TInputs; }, -): ExtensionDefinition; +): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createApiExtension { const // (undocumented) - factoryDataRef: ConfigurableExtensionDataRef; + factoryDataRef: ConfigurableExtensionDataRef< + AnyApiFactory, + 'core.api.factory', + {} + >; } export { createApiFactory }; export { createApiRef }; -// @public +// @public @deprecated export function createAppRootElementExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -414,7 +512,7 @@ export function createAppRootElementExtension< }) => JSX_2.Element); }): ExtensionDefinition; -// @public +// @public @deprecated export function createAppRootWrapperExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -443,6 +541,7 @@ export namespace createAppRootWrapperExtension { React_2.ComponentType<{ children?: React_2.ReactNode; }>, + 'app.root.wrapper', {} >; } @@ -471,7 +570,15 @@ export function createComponentExtension< inputs: Expand>; }) => ComponentType; }; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createComponentExtension { @@ -481,6 +588,7 @@ export namespace createComponentExtension { ref: ComponentRef; impl: ComponentType; }, + 'core.component.component', {} >; } @@ -491,31 +599,196 @@ export function createComponentRef(options: { }): ComponentRef; // @public (undocumented) +export function createExtension< + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + TConfigSchema extends { + [key: string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, +>( + options: CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + >, +): ExtensionDefinition< + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName +>; + +// @public @deprecated (undocumented) export function createExtension< TOutput extends AnyExtensionDataMap, TInputs extends AnyExtensionInputMap, - TConfig = never, + TConfig, + TConfigInput, >( - options: CreateExtensionOptions, -): ExtensionDefinition; + options: LegacyCreateExtensionOptions< + TOutput, + TInputs, + TConfig, + TConfigInput + >, +): ExtensionDefinition; + +// @public +export function createExtensionBlueprint< + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + TConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, + TDataRefs extends { + [name in string]: AnyExtensionDataRef; + } = never, +>( + options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, + TParams, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput, + TDataRefs + >, +): ExtensionBlueprint< + TKind, + TNamespace, + TName, + TParams, + UOutput, + string extends keyof TInputs ? {} : TInputs, + string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer>; + }, + string extends keyof TConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + TDataRefs +>; // @public (undocumented) +export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + TConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + TDataRefs extends { + [name in string]: AnyExtensionDataRef; + }, +> = { + kind: TKind; + namespace?: TNamespace; + attachTo: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TInputs; + output: Array; + name?: TName; + config?: { + schema: TConfigSchema; + }; + factory( + params: TParams, + context: { + node: AppNode; + config: { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs: Expand>; + }, + ): Iterable; + dataRefs?: TDataRefs; +} & VerifyExtensionFactoryOutput; + +// @public @deprecated (undocumented) export function createExtensionDataRef( id: string, -): ConfigurableExtensionDataRef; +): ConfigurableExtensionDataRef; // @public (undocumented) +export function createExtensionDataRef(): { + with(options: { + id: TId; + }): ConfigurableExtensionDataRef; +}; + +// @public @deprecated (undocumented) export function createExtensionInput< - TExtensionData extends AnyExtensionDataMap, + TExtensionDataMap extends AnyExtensionDataMap, TConfig extends { singleton?: boolean; optional?: boolean; }, >( - extensionData: TExtensionData, + extensionData: TExtensionDataMap, config?: TConfig, -): ExtensionInput< - TExtensionData, +): LegacyExtensionInput< + TExtensionDataMap, { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; @@ -523,37 +796,70 @@ export function createExtensionInput< >; // @public (undocumented) -export interface CreateExtensionOptions< - TOutput extends AnyExtensionDataMap, - TInputs extends AnyExtensionInputMap, - TConfig, -> { - // (undocumented) +export function createExtensionInput< + UExtensionData extends ExtensionDataRef< + unknown, + string, + { + optional?: true; + } + >, + TConfig extends { + singleton?: boolean; + optional?: boolean; + }, +>( + extensionData: Array, + config?: TConfig, +): ExtensionInput< + UExtensionData, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } +>; + +// @public (undocumented) +export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + TConfigSchema extends { + [key: string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, +> = { + kind?: TKind; + namespace?: TNamespace; + name?: TName; attachTo: { id: string; input: string; }; - // (undocumented) - configSchema?: PortableSchema; - // (undocumented) disabled?: boolean; - // (undocumented) - factory(options: { - node: AppNode; - config: TConfig; - inputs: Expand>; - }): Expand>; - // (undocumented) inputs?: TInputs; - // (undocumented) - kind?: string; - // (undocumented) - name?: string; - // (undocumented) - namespace?: string; - // (undocumented) - output: TOutput; -} + output: Array; + config?: { + schema: TConfigSchema; + }; + factory(context: { + node: AppNode; + config: { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs: Expand>; + }): Iterable; +} & VerifyExtensionFactoryOutput; // @public (undocumented) export function createExtensionOverrides( @@ -567,13 +873,11 @@ export function createExternalRouteRef< [param in TParamKeys]: string; } | undefined = undefined, - TOptional extends boolean = false, TParamKeys extends string = string, >(options?: { readonly params?: string extends TParamKeys ? (keyof TParams)[] : TParamKeys[]; - optional?: TOptional; defaultTarget?: string; }): ExternalRouteRef< keyof TParams extends never @@ -582,20 +886,29 @@ export function createExternalRouteRef< ? TParams : { [param in TParamKeys]: string; - }, - TOptional + } >; -// @public +// @public @deprecated export function createNavItemExtension(options: { namespace?: string; name?: string; routeRef: RouteRef; title: string; icon: IconComponent_2; -}): ExtensionDefinition<{ - title: string; -}>; +}): ExtensionDefinition< + { + title: string; + }, + { + title?: string | undefined; + }, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createNavItemExtension { @@ -606,17 +919,26 @@ export namespace createNavItemExtension { icon: IconComponent_2; routeRef: RouteRef; }, + 'core.nav-item.target', {} >; } -// @public +// @public @deprecated export function createNavLogoExtension(options: { name?: string; namespace?: string; logoIcon: JSX.Element; logoFull: JSX.Element; -}): ExtensionDefinition; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @public (undocumented) export namespace createNavLogoExtension { @@ -626,11 +948,12 @@ export namespace createNavLogoExtension { logoIcon?: JSX.Element | undefined; logoFull?: JSX.Element | undefined; }, + 'core.nav-logo.logo-elements', {} >; } -// @public +// @public @deprecated export function createPageExtension< TConfig extends { path: string; @@ -663,11 +986,22 @@ export function createPageExtension< // @public (undocumented) export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin; + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +>; // @public export function createRouteRef< @@ -689,7 +1023,7 @@ export function createRouteRef< } >; -// @public +// @public @deprecated export function createRouterExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -718,16 +1052,17 @@ export namespace createRouterExtension { React_2.ComponentType<{ children?: React_2.ReactNode; }>, + 'app.router.wrapper', {} >; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, -): PortableSchema; +): PortableSchema; -// @public (undocumented) +// @public @deprecated (undocumented) export function createSignInPageExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -752,6 +1087,7 @@ export namespace createSignInPageExtension { const // (undocumented) componentDataRef: ConfigurableExtensionDataRef< React_2.ComponentType, + 'core.sign-in-page.component', {} >; } @@ -765,24 +1101,44 @@ export function createSubRouteRef< parent: RouteRef; }): MakeSubRouteRef, ParentParams>; -// @public (undocumented) +// @public @deprecated (undocumented) export function createThemeExtension( theme: AppTheme, -): ExtensionDefinition; +): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createThemeExtension { const // (undocumented) - themeDataRef: ConfigurableExtensionDataRef; + themeDataRef: ConfigurableExtensionDataRef< + AppTheme, + 'core.theme.theme', + {} + >; } -// @public (undocumented) +// @public @deprecated (undocumented) export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; -}): ExtensionDefinition; +}): ExtensionDefinition< + unknown, + unknown, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; -// @public (undocumented) +// @public @deprecated (undocumented) export namespace createTranslationExtension { const // (undocumented) translationDataRef: ConfigurableExtensionDataRef< @@ -794,6 +1150,7 @@ export namespace createTranslationExtension { }, boolean >, + 'core.translation.translation', {} >; } @@ -817,7 +1174,7 @@ export { ErrorApiErrorContext }; export { errorApiRef }; // @public (undocumented) -export interface Extension { +export interface Extension { // (undocumented) $$type: '@backstage/Extension'; // (undocumented) @@ -826,13 +1183,143 @@ export interface Extension { input: string; }; // (undocumented) - readonly configSchema?: PortableSchema; + readonly configSchema?: PortableSchema; // (undocumented) readonly disabled: boolean; // (undocumented) readonly id: string; } +// @public (undocumented) +export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + TConfig extends { + [key in string]: unknown; + }, + TConfigInput extends { + [key in string]: unknown; + }, + TDataRefs extends { + [name in string]: AnyExtensionDataRef; + }, +> { + // (undocumented) + dataRefs: TDataRefs; + // (undocumented) + make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + makeWithOverrides< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; +} + // @public (undocumented) export function ExtensionBoundary( props: ExtensionBoundaryProps, @@ -844,24 +1331,59 @@ export interface ExtensionBoundaryProps { children: ReactNode; // (undocumented) node: AppNode; - // (undocumented) routable?: boolean; } +// @public (undocumented) +export type ExtensionDataContainer = + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { + get( + ref: ExtensionDataRef, + ): UExtensionData extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; + }; + // @public (undocumented) export type ExtensionDataRef< TData, + TId extends string = string, TConfig extends { optional?: true; } = {}, > = { - id: string; - T: TData; - config: TConfig; - $$type: '@backstage/ExtensionDataRef'; + readonly $$type: '@backstage/ExtensionDataRef'; + readonly id: TId; + readonly T: TData; + readonly config: TConfig; }; -// @public +// @public (undocumented) +export type ExtensionDataRefToValue = + TDataRef extends ExtensionDataRef + ? ExtensionDataValue + : never; + +// @public (undocumented) +export type ExtensionDataValue = { + readonly $$type: '@backstage/ExtensionDataValue'; + readonly id: TId; + readonly value: TData; +}; + +// @public @deprecated export type ExtensionDataValues = { [DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends { optional: true; @@ -877,7 +1399,23 @@ export type ExtensionDataValues = { }; // @public (undocumented) -export interface ExtensionDefinition { +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> { // (undocumented) $$type: '@backstage/ExtensionDefinition'; // (undocumented) @@ -886,20 +1424,99 @@ export interface ExtensionDefinition { input: string; }; // (undocumented) - readonly configSchema?: PortableSchema; + readonly configSchema?: PortableSchema; // (undocumented) readonly disabled: boolean; // (undocumented) - readonly kind?: string; + readonly kind?: TKind; // (undocumented) - readonly name?: string; + readonly name?: TName; // (undocumented) - readonly namespace?: string; + readonly namespace?: TNamespace; + // (undocumented) + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { + optional: boolean; + singleton: boolean; + } + >; + }, + >( + args: { + attachTo?: { + id: string; + input: string; + }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >, + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + TNamespace, + TName + >; } // @public (undocumented) export interface ExtensionInput< - TExtensionData extends AnyExtensionDataMap, + TExtensionData extends ExtensionDataRef< + unknown, + string, + { + optional?: true; + } + >, TConfig extends { singleton: boolean; optional: boolean; @@ -910,7 +1527,7 @@ export interface ExtensionInput< // (undocumented) config: TConfig; // (undocumented) - extensionData: TExtensionData; + extensionData: Array; } // @public (undocumented) @@ -922,7 +1539,7 @@ export interface ExtensionOverrides { // @public (undocumented) export interface ExtensionOverridesOptions { // (undocumented) - extensions: ExtensionDefinition[]; + extensions: ExtensionDefinition[]; // (undocumented) featureFlags?: FeatureFlagConfig[]; } @@ -930,13 +1547,10 @@ export interface ExtensionOverridesOptions { // @public export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, > { // (undocumented) readonly $$type: '@backstage/ExternalRouteRef'; // (undocumented) - readonly optional: TOptional; - // (undocumented) readonly T: TParams; } @@ -968,6 +1582,43 @@ export { gitlabAuthApiRef }; export { googleAuthApiRef }; +// @public (undocumented) +export const IconBundleBlueprint: ExtensionBlueprint< + 'icon-bundle', + 'app', + undefined, + { + icons: { + [x: string]: IconComponent; + }; + }, + ConfigurableExtensionDataRef< + { + [x: string]: IconComponent; + }, + 'core.icons', + {} + >, + {}, + { + icons: string; + test: string; + }, + { + test: string; + icons?: string | undefined; + }, + { + icons: ConfigurableExtensionDataRef< + { + [x: string]: IconComponent; + }, + 'core.icons', + {} + >; + } +>; + // @public export type IconComponent = ComponentType< | { @@ -993,8 +1644,125 @@ export { IdentityApi }; export { identityApiRef }; +// @public @deprecated (undocumented) +export interface LegacyCreateExtensionOptions< + TOutput extends AnyExtensionDataMap, + TInputs extends AnyExtensionInputMap, + TConfig, + TConfigInput, +> { + // (undocumented) + attachTo: { + id: string; + input: string; + }; + // (undocumented) + configSchema?: PortableSchema; + // (undocumented) + disabled?: boolean; + // (undocumented) + factory(context: { + node: AppNode; + config: TConfig; + inputs: Expand>; + }): Expand>; + // (undocumented) + inputs?: TInputs; + // (undocumented) + kind?: string; + // (undocumented) + name?: string; + // (undocumented) + namespace?: string; + // (undocumented) + output: TOutput; +} + +// @public @deprecated (undocumented) +export interface LegacyExtensionInput< + TExtensionDataMap extends AnyExtensionDataMap, + TConfig extends { + singleton: boolean; + optional: boolean; + }, +> { + // (undocumented) + $$type: '@backstage/ExtensionInput'; + // (undocumented) + config: TConfig; + // (undocumented) + extensionData: TExtensionDataMap; +} + export { microsoftAuthApiRef }; +// @public +export const NavItemBlueprint: ExtensionBlueprint< + 'nav-item', + undefined, + undefined, + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >, + {}, + {}, + {}, + { + target: ConfigurableExtensionDataRef< + { + title: string; + icon: IconComponent_2; + routeRef: RouteRef; + }, + 'core.nav-item.target', + {} + >; + } +>; + +// @public +export const NavLogoBlueprint: ExtensionBlueprint< + 'nav-logo', + undefined, + undefined, + { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }, + ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >, + {}, + {}, + {}, + { + logoElements: ConfigurableExtensionDataRef< + { + logoIcon?: JSX.Element | undefined; + logoFull?: JSX.Element | undefined; + }, + 'core.nav-logo.logo-elements', + {} + >; + } +>; + export { OAuthApi }; export { OAuthRequestApi }; @@ -1013,28 +1781,59 @@ export { oneloginAuthApiRef }; export { OpenIdConnectApi }; +// @public +export const PageBlueprint: ExtensionBlueprint< + 'page', + undefined, + undefined, + { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef | undefined; + }, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef, + 'core.routing.ref', + RouteRef & { + optional: true; + } + >, + {}, + { + path: string | undefined; + }, + { + path?: string | undefined; + }, + never +>; + export { PendingOAuthRequest }; // @public (undocumented) export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { // (undocumented) - extensions?: ExtensionDefinition[]; + extensions?: TExtensions; // (undocumented) - externalRoutes?: ExternalRoutes; + externalRoutes?: TExternalRoutes; // (undocumented) featureFlags?: FeatureFlagConfig[]; // (undocumented) - id: string; + id: TId; // (undocumented) - routes?: Routes; + routes?: TRoutes; } // @public (undocumented) -export type PortableSchema = { - parse: (input: unknown) => TOutput; +export type PortableSchema = { + parse: (input: TInput) => TOutput; schema: JsonObject; }; @@ -1043,25 +1842,30 @@ export { ProfileInfo }; export { ProfileInfoApi }; // @public -export type ResolvedExtensionInput = - { - node: AppNode; - output: ExtensionDataValues; - }; +export type ResolvedExtensionInput< + TExtensionInput extends ExtensionInput, +> = TExtensionInput['extensionData'] extends Array + ? { + node: AppNode; + } & ExtensionDataContainer + : TExtensionInput['extensionData'] extends AnyExtensionDataMap + ? { + node: AppNode; + output: ExtensionDataValues; + } + : never; // @public export type ResolvedExtensionInputs< TInputs extends { - [name in string]: ExtensionInput; + [name in string]: ExtensionInput | LegacyExtensionInput; }, > = { [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] - ? Array>> + ? Array>> : false extends TInputs[InputName]['config']['optional'] - ? Expand> - : Expand< - ResolvedExtensionInput | undefined - >; + ? Expand> + : Expand | undefined>; }; // @public @@ -1071,6 +1875,35 @@ export type RouteFunc = ( : readonly [params: TParams] ) => string; +// @public (undocumented) +export const RouterBlueprint: ExtensionBlueprint< + 'app-router-component', + undefined, + undefined, + { + Component: ComponentType>; + }, + ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + ComponentType<{ + children?: ReactNode; + }>, + 'app.router.wrapper', + {} + >; + } +>; + // @public export interface RouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, @@ -1088,7 +1921,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } @@ -1105,6 +1938,31 @@ export { SessionApi }; export { SessionState }; +// @public +export const SignInPageBlueprint: ExtensionBlueprint< + 'sign-in-page', + undefined, + undefined, + { + loader: () => Promise>; + }, + ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >, + {}, + {}, + {}, + { + component: ConfigurableExtensionDataRef< + React_2.ComponentType, + 'core.sign-in-page.component', + {} + >; + } +>; + export { StorageApi }; export { storageApiRef }; @@ -1123,6 +1981,62 @@ export interface SubRouteRef< readonly T: TParams; } +// @public +export const ThemeBlueprint: ExtensionBlueprint< + 'theme', + 'app', + undefined, + { + theme: AppTheme; + }, + ConfigurableExtensionDataRef, + {}, + {}, + {}, + { + theme: ConfigurableExtensionDataRef; + } +>; + +// @public +export const TranslationBlueprint: ExtensionBlueprint< + 'translation', + undefined, + undefined, + { + resource: TranslationResource | TranslationMessages; + }, + ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >, + {}, + {}, + {}, + { + translation: ConfigurableExtensionDataRef< + | TranslationResource + | TranslationMessages< + string, + { + [x: string]: string; + }, + boolean + >, + 'core.translation.translation', + {} + >; + } +>; + export { TranslationMessages }; export { TranslationMessagesOptions }; @@ -1149,18 +2063,13 @@ export function useComponentRef( ref: ComponentRef, ): ComponentType; -// @public -export function useRouteRef< - TOptional extends boolean, - TParams extends AnyRouteRefParams, ->( - routeRef: ExternalRouteRef, -): TOptional extends true ? RouteFunc | undefined : RouteFunc; - // @public export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; + routeRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): RouteFunc | undefined; // @public export function useRouteRefParams( diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index ad8cb41ca9..8f729a5c4d 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-plugin-api", - "version": "0.6.6", + "version": "0.7.0-next.2", "backstage": { "role": "web-library" }, diff --git a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts index 6c79a16964..4b5672f894 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AppTreeApi.ts @@ -29,7 +29,7 @@ import { BackstagePlugin, Extension, ExtensionDataRef } from '../../wiring'; export interface AppNodeSpec { readonly id: string; readonly attachTo: { id: string; input: string }; - readonly extension: Extension; + readonly extension: Extension; readonly disabled: boolean; readonly config?: unknown; readonly source?: BackstagePlugin; diff --git a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts index 06388cc966..30569bde15 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/RouteResolutionApi.ts @@ -60,7 +60,7 @@ export interface RouteResolutionApi { anyRouteRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, options?: RouteResolutionApiResolveOptions, ): RouteFunc | undefined; } diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts new file mode 100644 index 0000000000..3b865a78bb --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.test.ts @@ -0,0 +1,132 @@ +/* + * Copyright 2024 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 { createExtensionInput } from '../wiring'; +import { ApiBlueprint } from './ApiBlueprint'; +import { createApiFactory, createApiRef } from '@backstage/core-plugin-api'; + +describe('ApiBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = createApiFactory({ + api, + deps: {}, + factory: () => ({ foo: 'bar' }), + }); + + const extension = ApiBlueprint.make({ + params: { + factory, + }, + namespace: 'test', + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should create an extension with custom factory', () => { + const api = createApiRef<{ foo: string }>({ id: 'test' }); + const factory = jest.fn(() => ({ foo: 'bar' })); + + const extension = ApiBlueprint.makeWithOverrides({ + config: { + schema: { + test: z => z.string().default('test'), + }, + }, + inputs: { + test: createExtensionInput([ApiBlueprint.dataRefs.factory]), + }, + namespace: api.id, + factory(originalFactory, { config: _config, inputs: _inputs }) { + return originalFactory({ + factory: createApiFactory({ + api, + deps: {}, + factory, + }), + }); + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "apis", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "test": { + "default": "test", + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": { + "test": { + "$$type": "@backstage/ExtensionInput", + "config": { + "optional": false, + "singleton": false, + }, + "extensionData": [ + [Function], + ], + }, + }, + "kind": "api", + "name": undefined, + "namespace": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts new file mode 100644 index 0000000000..b5ba9e05f6 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ApiBlueprint.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createApiExtension } from '../extensions/createApiExtension'; +import { AnyApiFactory } from '@backstage/core-plugin-api'; + +/** + * Creates utility API extensions. + * + * @public + */ +export const ApiBlueprint = createExtensionBlueprint({ + kind: 'api', + attachTo: { id: 'app', input: 'apis' }, + output: [createApiExtension.factoryDataRef], + dataRefs: { + factory: createApiExtension.factoryDataRef, + }, + *factory(params: { factory: AnyApiFactory }) { + yield createApiExtension.factoryDataRef(params.factory); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx new file mode 100644 index 0000000000..3fb8d7b37a --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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 React from 'react'; +import { AppRootElementBlueprint } from './AppRootElementBlueprint'; + +describe('AppRootElementBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = AppRootElementBlueprint.make({ + params: { + element:
, + }, + }); + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "elements", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-element", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts new file mode 100644 index 0000000000..bc6304923a --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootElementBlueprint.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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 { coreExtensionData, createExtensionBlueprint } from '../wiring'; + +/** + * Creates extensions that render a React element at the app root, outside of + * the app layout. This is useful for example for shared popups and similar. + * + * @public + */ +export const AppRootElementBlueprint = createExtensionBlueprint({ + kind: 'app-root-element', + attachTo: { id: 'app/root', input: 'elements' }, + output: [coreExtensionData.reactElement], + *factory(params: { element: JSX.Element | (() => JSX.Element) }) { + yield coreExtensionData.reactElement( + typeof params.element === 'function' ? params.element() : params.element, + ); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx new file mode 100644 index 0000000000..f7feab1d46 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx @@ -0,0 +1,130 @@ +/* + * Copyright 2024 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 React from 'react'; +import { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { PageBlueprint } from './PageBlueprint'; +import { waitFor } from '@testing-library/react'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '../wiring'; + +describe('AppRootWrapperBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
Hello
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "wrappers", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-root-wrapper", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should render the simple component wrapper', async () => { + const extension = AppRootWrapperBlueprint.make({ + params: { + Component: () =>
Hello
, + }, + }); + + const { getByText } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ) + .add(extension) + .render(); + + await waitFor(() => expect(getByText('Hello')).toBeInTheDocument()); + }); + + it('should render the complex component wrapper', async () => { + const extension = AppRootWrapperBlueprint.makeWithOverrides({ + namespace: 'ns', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( +
+ {children} + {inputs.children.flatMap(c => + c.get(coreExtensionData.reactElement), + )} +
+ ), + }); + }, + }); + + const { getByText, getByTestId } = createExtensionTester( + PageBlueprint.make({ + params: { + defaultPath: '/', + loader: async () =>
Hi
, + }, + }), + ) + .add(extension, { config: { name: 'Robin' } }) + .add( + createExtension({ + attachTo: { id: 'app-root-wrapper:ns/test', input: 'children' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
Its Me
)], + }), + ) + .render(); + + await waitFor(() => { + expect(getByText('Hi')).toBeInTheDocument(); + expect(getByTestId('Robin-1')).toBeInTheDocument(); + expect(getByText('Its Me')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx new file mode 100644 index 0000000000..c54686f54f --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 React from 'react'; +import { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createAppRootWrapperExtension } from '../extensions/createAppRootWrapperExtension'; + +/** + * Creates a extensions that render a React wrapper at the app root, enclosing + * the app layout. This is useful for example for adding global React contexts + * and similar. + * + * @public + */ +export const AppRootWrapperBlueprint = createExtensionBlueprint({ + kind: 'app-root-wrapper', + attachTo: { id: 'app/root', input: 'wrappers' }, + output: [createAppRootWrapperExtension.componentDataRef], + dataRefs: { + component: createAppRootWrapperExtension.componentDataRef, + }, + *factory(params: { Component: ComponentType> }) { + // todo(blam): not sure that this wrapping is even necessary anymore. + const Component = (props: PropsWithChildren<{}>) => { + return {props.children}; + }; + + yield createAppRootWrapperExtension.componentDataRef(Component); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts new file mode 100644 index 0000000000..deeabdb44c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/IconBundleBlueprint.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2024 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 { IconComponent } from '../icons'; +import { createExtensionBlueprint, createExtensionDataRef } from '../wiring'; + +const iconsDataRef = createExtensionDataRef<{ + [key in string]: IconComponent; +}>().with({ id: 'core.icons' }); + +/** @public */ +export const IconBundleBlueprint = createExtensionBlueprint({ + kind: 'icon-bundle', + namespace: 'app', + attachTo: { id: 'app', input: 'icons' }, + output: [iconsDataRef], + config: { + schema: { + icons: z => z.string().default('blob'), + test: z => z.string(), + }, + }, + factory: (params: { icons: { [key in string]: IconComponent } }) => [ + iconsDataRef(params.icons), + ], + dataRefs: { + icons: iconsDataRef, + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx new file mode 100644 index 0000000000..94b9e7b8e6 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx @@ -0,0 +1,106 @@ +/* + * Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createRouteRef } from '../routing'; +import { NavItemBlueprint } from './NavItemBlueprint'; + +describe('NavItemBlueprint', () => { + const mockRouteRef = createRouteRef(); + const MockIcon = () => null; + + it('should return an extension with sensible defaults', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "items", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-item", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the correct extension data', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'TEST', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); + + it('should allow overriding of the title using config', () => { + const extension = NavItemBlueprint.make({ + params: { + icon: MockIcon, + routeRef: mockRouteRef, + title: 'TEST', + }, + }); + + const tester = createExtensionTester(extension, { + config: { title: 'OVERRIDDEN' }, + }); + + expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + title: 'OVERRIDDEN', + icon: MockIcon, + routeRef: mockRouteRef, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts new file mode 100644 index 0000000000..20f57421c7 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2024 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 { IconComponent } from '@backstage/core-plugin-api'; +import { RouteRef } from '../routing'; +import { createExtensionBlueprint } from '../wiring'; +import { createNavItemExtension } from '../extensions/createNavItemExtension'; + +/** + * Creates extensions that make up the items of the nav bar. + * + * @public + */ +export const NavItemBlueprint = createExtensionBlueprint({ + kind: 'nav-item', + attachTo: { id: 'app/nav', input: 'items' }, + output: [createNavItemExtension.targetDataRef], + dataRefs: { + target: createNavItemExtension.targetDataRef, + }, + factory: ( + { + icon, + routeRef, + title, + }: { + title: string; + icon: IconComponent; + routeRef: RouteRef; + }, + { config }, + ) => [ + createNavItemExtension.targetDataRef({ + title: config.title ?? title, + icon, + routeRef, + }), + ], + config: { + schema: { + title: z => z.string().optional(), + }, + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx new file mode 100644 index 0000000000..4a96475cf5 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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 React from 'react'; +import { NavLogoBlueprint } from './NavLogoBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('NavLogoBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const extension = NavLogoBlueprint.make({ + params: { + logoFull:
Logo Full
, + logoIcon:
Logo Icon
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/nav", + "input": "logos", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "nav-logo", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return a valid component ref', () => { + const logoFull =
Logo Full
; + const logoIcon =
Logo Icon
; + + const extension = NavLogoBlueprint.make({ + name: 'test', + params: { + logoFull, + logoIcon, + }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(NavLogoBlueprint.dataRefs.logoElements)).toEqual({ + logoFull, + logoIcon, + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts new file mode 100644 index 0000000000..8066646ca3 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createNavLogoExtension } from '../extensions/createNavLogoExtension'; + +/** + * Creates an extension that replaces the logo in the nav bar with your own. + * + * @public + */ +export const NavLogoBlueprint = createExtensionBlueprint({ + kind: 'nav-logo', + attachTo: { id: 'app/nav', input: 'logos' }, + output: [createNavLogoExtension.logoElementsDataRef], + dataRefs: { + logoElements: createNavLogoExtension.logoElementsDataRef, + }, + *factory({ + logoIcon, + logoFull, + }: { + logoIcon: JSX.Element; + logoFull: JSX.Element; + }) { + yield createNavLogoExtension.logoElementsDataRef({ + logoIcon, + logoFull, + }); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx new file mode 100644 index 0000000000..c4bcb4b467 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2024 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 React from 'react'; +import { createRouteRef } from '../routing'; +import { PageBlueprint } from './PageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtensionBlueprint, + createExtensionInput, +} from '../wiring'; +import { waitFor } from '@testing-library/react'; + +describe('PageBlueprint', () => { + const mockRouteRef = createRouteRef(); + + it('should return an extension when calling make with sensible defaults', () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
Test
), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + expect(myPage).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/routes", + "input": "routes", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "page", + "name": "test-page", + "namespace": undefined, + "output": [ + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [Function], + }, + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a routeRef with the input routeRef', async () => { + const myPage = PageBlueprint.make({ + name: 'test-page', + params: { + loader: () => Promise.resolve(
Test
), + defaultPath: '/test', + routeRef: mockRouteRef, + }, + }); + + const tester = createExtensionTester(myPage); + + // TODO(blam): test for the routePath output doesn't work, due to the the way the test harness works + // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + + const { getByTestId } = tester.render(); + + await waitFor(() => expect(getByTestId('test')).toBeInTheDocument()); + }); + + it('should allow defining additional inputs to the extension', async () => { + const myPage = PageBlueprint.makeWithOverrides({ + name: 'test-page', + inputs: { + cards: createExtensionInput([coreExtensionData.reactElement], { + optional: false, + singleton: false, + }), + }, + factory(originalFactory, { inputs }) { + return originalFactory({ + loader: async () => ( +
+ {inputs.cards.map(c => c.get(coreExtensionData.reactElement))} +
+ ), + defaultPath: '/test', + routeRef: mockRouteRef, + }); + }, + }); + + const CardBlueprint = createExtensionBlueprint({ + kind: 'card', + attachTo: { id: 'page:test-page', input: 'cards' }, + output: [coreExtensionData.reactElement], + factory() { + return [ + coreExtensionData.reactElement( +
I'm a lovely card
, + ), + ]; + }, + }); + + const tester = createExtensionTester(myPage).add( + CardBlueprint.make({ name: 'card', params: {} }), + ); + + const { getByTestId, getByText } = tester.render(); + + await waitFor(() => expect(getByTestId('card')).toBeInTheDocument()); + await waitFor(() => + expect(getByText("I'm a lovely card")).toBeInTheDocument(), + ); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx new file mode 100644 index 0000000000..b224ea491e --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2024 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 React, { lazy } from 'react'; +import { RouteRef } from '../routing'; +import { coreExtensionData, createExtensionBlueprint } from '../wiring'; +import { ExtensionBoundary } from '../components'; + +/** + * Createx extensions that are routable React page components. + * + * @public + */ +export const PageBlueprint = createExtensionBlueprint({ + kind: 'page', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [ + coreExtensionData.routePath, + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + config: { + schema: { + path: z => z.string().optional(), + }, + }, + *factory( + { + defaultPath, + loader, + routeRef, + }: { + defaultPath: string; + loader: () => Promise; + routeRef?: RouteRef; + }, + { config, node }, + ) { + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.routePath(config.path ?? defaultPath); + yield coreExtensionData.reactElement( + + + , + ); + + if (routeRef) { + yield coreExtensionData.routeRef(routeRef); + } + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx new file mode 100644 index 0000000000..fe40aaa8d7 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.test.tsx @@ -0,0 +1,170 @@ +/* + * Copyright 2024 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 React from 'react'; +import { RouterBlueprint } from './RouterBlueprint'; +import { MemoryRouter } from 'react-router-dom'; +import { render, waitFor } from '@testing-library/react'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createExtensionOverrides, +} from '../wiring'; +import { MockConfigApi } from '@backstage/test-utils'; +import { PageBlueprint } from './PageBlueprint'; + +describe('RouterBlueprint', () => { + it('should return an extension when calling make with sensible defaults', () => { + const extension = RouterBlueprint.make({ + params: { + Component: props =>
{props.children}
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "router", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "app-router-component", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should work with simple options', async () => { + const extension = RouterBlueprint.make({ + namespace: 'test', + params: { + Component: ({ children }) => ( + +
{children}
+
+ ), + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ], + }), + ], + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router')).toBeInTheDocument(); + }); + }); + + it('should work with complex options and props', async () => { + const extension = RouterBlueprint.makeWithOverrides({ + namespace: 'test', + name: 'test', + config: { + schema: { + name: z => z.string(), + }, + }, + inputs: { + children: createExtensionInput([coreExtensionData.reactElement]), + }, + *factory(originalFactory, { inputs, config }) { + yield* originalFactory({ + Component: ({ children }) => ( + +
+ {children} +
+
+ ), + }); + }, + }); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions: [ + extension, + createExtension({ + namespace: 'test', + attachTo: { + id: 'app-router-component:test/test', + input: 'children', + }, + output: [coreExtensionData.reactElement], + *factory() { + yield coreExtensionData.reactElement(
); + }, + }), + PageBlueprint.make({ + namespace: 'test', + params: { + defaultPath: '/', + loader: async () =>
, + }, + }), + ], + }), + ], + config: new MockConfigApi({ + app: { + extensions: [ + { + 'app-router-component:test/test': { config: { name: 'Robin' } }, + }, + ], + }, + }), + }); + + const { getByTestId } = render(app.createRoot()); + + await waitFor(() => { + expect(getByTestId('test-contents')).toBeInTheDocument(); + expect(getByTestId('test-router-Robin-1')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx new file mode 100644 index 0000000000..24ee805659 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/RouterBlueprint.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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 { ComponentType, PropsWithChildren } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createRouterExtension } from '../extensions/createRouterExtension'; + +/** @public */ +export const RouterBlueprint = createExtensionBlueprint({ + kind: 'app-router-component', + attachTo: { id: 'app/root', input: 'router' }, + output: [createRouterExtension.componentDataRef], + dataRefs: { + component: createRouterExtension.componentDataRef, + }, + *factory({ Component }: { Component: ComponentType> }) { + yield createRouterExtension.componentDataRef(Component); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx new file mode 100644 index 0000000000..f97798a64c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright 2024 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 React from 'react'; +import { SignInPageBlueprint } from './SignInPageBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { screen, waitFor } from '@testing-library/react'; +import { coreExtensionData, createExtension } from '../wiring'; + +describe('SignInPageBlueprint', () => { + it('should create an extension with sensible defaults', () => { + expect( + SignInPageBlueprint.make({ + params: { loader: async () => () =>
}, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app/root", + "input": "signInPage", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "sign-in-page", + "name": undefined, + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the component as the componentRef', async () => { + const MockSignInPage = () =>
; + + const extension = SignInPageBlueprint.make({ + name: 'test', + params: { loader: async () => () => }, + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); + + createExtensionTester( + createExtension({ + name: 'dummy', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { + element: coreExtensionData.reactElement, + }, + factory: () => ({ element:
}), + }), + ) + .add(extension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx new file mode 100644 index 0000000000..f87ae91452 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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 React, { ComponentType, lazy } from 'react'; +import { createExtensionBlueprint } from '../wiring'; +import { createSignInPageExtension } from '../extensions/createSignInPageExtension'; +import { SignInPageProps } from '@backstage/core-plugin-api'; +import { ExtensionBoundary } from '../components'; + +/** + * Creates an extension that replaces the sign in page. + * + * @public + */ +export const SignInPageBlueprint = createExtensionBlueprint({ + kind: 'sign-in-page', + attachTo: { id: 'app/root', input: 'signInPage' }, + output: [createSignInPageExtension.componentDataRef], + dataRefs: { + component: createSignInPageExtension.componentDataRef, + }, + *factory( + { + loader, + }: { + loader: () => Promise>; + }, + { node }, + ) { + const ExtensionComponent = lazy(() => + loader().then(component => ({ default: component })), + ); + + yield createSignInPageExtension.componentDataRef(props => ( + + + + )); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts new file mode 100644 index 0000000000..40323b6a31 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright 2024 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 { AppTheme } from '@backstage/core-plugin-api'; +import { ThemeBlueprint } from './ThemeBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; + +describe('ThemeBlueprint', () => { + const theme = { + id: 'light', + colors: { primary: 'blue' }, + variant: 'dark', + title: 'lols', + Provider: (_: { children: React.ReactNode }) => null, + } as AppTheme; + + it('should create an extension with sensible defaults', () => { + expect(ThemeBlueprint.make({ name: 'light', params: { theme } })) + .toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "themes", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "theme", + "name": "light", + "namespace": "app", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should return the theme as an themeDataRef', async () => { + const extension = ThemeBlueprint.make({ params: { theme } }); + + expect( + createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), + ).toEqual(theme); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts new file mode 100644 index 0000000000..686c498c06 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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 { AppTheme } from '@backstage/core-plugin-api'; +import { createExtensionBlueprint } from '../wiring'; +import { createThemeExtension } from '../extensions/createThemeExtension'; + +/** + * Creates an extension that adds/replaces an app theme. + * + * @public + */ +export const ThemeBlueprint = createExtensionBlueprint({ + kind: 'theme', + namespace: 'app', + attachTo: { id: 'app', input: 'themes' }, + output: [createThemeExtension.themeDataRef], + dataRefs: { + theme: createThemeExtension.themeDataRef, + }, + factory: ({ theme }: { theme: AppTheme }) => [ + createThemeExtension.themeDataRef(theme), + ], +}); diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts new file mode 100644 index 0000000000..dfb918053a --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2024 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + createTranslationMessages, + createTranslationRef, +} from '../translation'; +import { TranslationBlueprint } from './TranslationBlueprint'; + +describe('TranslationBlueprint', () => { + const translationRef = createTranslationRef({ + id: 'translationRefId', + messages: { + test: 'test', + }, + }); + + const messages = createTranslationMessages({ + ref: translationRef, + messages: { + test: 'test2', + }, + }); + + it('should return an extension instance with sane defaults', () => { + expect( + TranslationBlueprint.make({ + name: 'blob', + params: { + resource: messages, + }, + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "app", + "input": "translations", + }, + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "translation", + "name": "blob", + "namespace": undefined, + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); + + it('should output a translation data ref', () => { + const extension = TranslationBlueprint.make({ + name: 'blob', + params: { + resource: messages, + }, + }); + + expect( + createExtensionTester(extension).data( + TranslationBlueprint.dataRefs.translation, + ), + ).toBe(messages); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts new file mode 100644 index 0000000000..bdb36e2191 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2024 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 { createExtensionBlueprint } from '../wiring'; +import { createTranslationExtension } from '../extensions/createTranslationExtension'; +import { TranslationMessages, TranslationResource } from '../translation'; + +/** + * Creates an extension that adds translations to your app. + * + * @public + */ +export const TranslationBlueprint = createExtensionBlueprint({ + kind: 'translation', + attachTo: { id: 'app', input: 'translations' }, + output: [createTranslationExtension.translationDataRef], + dataRefs: { + translation: createTranslationExtension.translationDataRef, + }, + factory: ({ + resource, + }: { + resource: TranslationResource | TranslationMessages; + }) => [createTranslationExtension.translationDataRef(resource)], +}); diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts new file mode 100644 index 0000000000..85f7a99a2c --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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. + */ + +export { ApiBlueprint } from './ApiBlueprint'; +export { AppRootElementBlueprint } from './AppRootElementBlueprint'; +export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +export { IconBundleBlueprint } from './IconBundleBlueprint'; +export { NavItemBlueprint } from './NavItemBlueprint'; +export { NavLogoBlueprint } from './NavLogoBlueprint'; +export { PageBlueprint } from './PageBlueprint'; +export { RouterBlueprint } from './RouterBlueprint'; +export { SignInPageBlueprint } from './SignInPageBlueprint'; +export { ThemeBlueprint } from './ThemeBlueprint'; +export { TranslationBlueprint } from './TranslationBlueprint'; diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 66fb3901fc..1f12f127e8 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -15,15 +15,24 @@ */ import React, { useEffect } from 'react'; -import { screen, waitFor } from '@testing-library/react'; -import { MockAnalyticsApi, TestApiProvider } from '@backstage/test-utils'; +import { act, screen, waitFor } from '@testing-library/react'; +import { + MockAnalyticsApi, + TestApiProvider, + withLogCollector, +} from '@backstage/test-utils'; import { ExtensionBoundary } from './ExtensionBoundary'; import { coreExtensionData, createExtension } from '../wiring'; -import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; +import { + analyticsApiRef, + createApiFactory, + useAnalytics, +} from '@backstage/core-plugin-api'; import { createRouteRef } from '../routing'; import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createApiExtension } from '../extensions'; -const wrapInBoundaryExtension = (element: JSX.Element) => { +const wrapInBoundaryExtension = (element?: JSX.Element) => { const routeRef = createRouteRef(); return createExtension({ name: 'test', @@ -54,12 +63,25 @@ describe('ExtensionBoundary', () => { }); it('should show app error component when an error is thrown', async () => { - const error = 'Something went wrong'; + const errorMsg = 'Something went wrong'; const ErrorComponent = () => { - throw new Error(error); + throw new Error(errorMsg); }; - createExtensionTester(wrapInBoundaryExtension()).render(); - await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); + const { error } = await withLogCollector(['error'], async () => { + createExtensionTester( + wrapInBoundaryExtension(), + ).render(); + await waitFor(() => + expect(screen.getByText(errorMsg)).toBeInTheDocument(), + ); + }); + expect(error).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + message: expect.stringContaining(errorMsg), + }), + ]), + ); }); it('should wrap children with analytics context', async () => { @@ -97,4 +119,38 @@ describe('ExtensionBoundary', () => { }); }); }); + + // TODO(Rugvip): It's annoying to test the inverse of this currently, because the extension tester overrides the subject to always output a path + it('should emit analytics events if routable', async () => { + const Emitter = () => { + const analytics = useAnalytics(); + useEffect(() => { + analytics.captureEvent('dummy', 'dummy'); + }); + return null; + }; + const analyticsApiMock = new MockAnalyticsApi(); + + await act(async () => { + createExtensionTester(wrapInBoundaryExtension()) + .add( + createApiExtension({ + factory: createApiFactory(analyticsApiRef, analyticsApiMock), + }), + ) + .render(); + }); + + expect(analyticsApiMock.getEvents()).toEqual([ + expect.objectContaining({ + action: 'navigate', + subject: '/', + context: expect.objectContaining({ + pluginId: 'root', + extensionId: 'test', + }), + }), + expect.objectContaining({ action: 'dummy' }), + ]); + }); }); diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 768352b76e..b6269484b0 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -26,6 +26,7 @@ import { ErrorBoundary } from './ErrorBoundary'; import { routableExtensionRenderedEvent } from '../../../core-plugin-api/src/analytics/Tracker'; import { AppNode, useComponentRef } from '../apis'; import { coreComponentRefs } from './coreComponentRefs'; +import { coreExtensionData } from '../wiring'; type RouteTrackerProps = PropsWithChildren<{ disableTracking?: boolean; @@ -50,6 +51,11 @@ const RouteTracker = (props: RouteTrackerProps) => { /** @public */ export interface ExtensionBoundaryProps { node: AppNode; + /** + * This explicitly marks the extension as routable for the purpose of + * capturing analytics events. If not provided, the extension boundary will be + * marked as routable if it outputs a routePath. + */ routable?: boolean; children: ReactNode; } @@ -58,6 +64,10 @@ export interface ExtensionBoundaryProps { export function ExtensionBoundary(props: ExtensionBoundaryProps) { const { node, routable, children } = props; + const doesOutputRoutePath = Boolean( + node.instance?.getData(coreExtensionData.routePath), + ); + const plugin = node.spec.source; const Progress = useComponentRef(coreComponentRefs.progress); const fallback = useComponentRef(coreComponentRefs.errorBoundaryFallback); @@ -72,7 +82,9 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) { }> - {children} + + {children} + diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index c4d0239094..8f8a0cd9ac 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -48,6 +48,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); @@ -85,6 +86,7 @@ describe('createApiExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index 1680332960..b1db2a0106 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -24,7 +24,10 @@ import { import { AnyExtensionInputMap } from '../wiring/createExtension'; import { Expand } from '../types'; -/** @public */ +/** + * @public + * @deprecated Use {@link ApiBlueprint} instead. + */ export function createApiExtension< TConfig extends {}, TInputs extends AnyExtensionInputMap, @@ -72,6 +75,7 @@ export function createApiExtension< /** @public */ export namespace createApiExtension { - export const factoryDataRef = - createExtensionDataRef('core.api.factory'); + export const factoryDataRef = createExtensionDataRef().with({ + id: 'core.api.factory', + }); } diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx index c8bf6ee355..6e80f6fe13 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.test.tsx @@ -41,6 +41,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension).render(); @@ -88,6 +89,7 @@ describe('createAppRootElementExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester(extension, { config: { name: 'Robin' } }) diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts index 8be82d5540..c8f7bdc366 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createAppRootElementExtension.ts @@ -30,6 +30,7 @@ import { * the app layout. This is useful for example for shared popups and similar. * * @public + * @deprecated Use {@link AppRootElementBlueprint} instead. */ export function createAppRootElementExtension< TConfig extends {}, diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx index d21cfa7583..993c4fdee3 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.test.tsx @@ -42,6 +42,7 @@ describe('createAppRootWrapperExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); createExtensionTester( @@ -94,6 +95,7 @@ describe('createAppRootWrapperExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx index bbc51806af..4f70f29470 100644 --- a/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createAppRootWrapperExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * and similar. * * @public + * @deprecated Use {@link AppRootWrapperBlueprint} instead. */ export function createAppRootWrapperExtension< TConfig extends {}, @@ -77,8 +78,7 @@ export function createAppRootWrapperExtension< /** @public */ export namespace createAppRootWrapperExtension { - export const componentDataRef = - createExtensionDataRef>>( - 'app.root.wrapper', - ); + export const componentDataRef = createExtensionDataRef< + ComponentType> + >().with({ id: 'app.root.wrapper' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx index a1231d8353..a40ee3b069 100644 --- a/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createComponentExtension.tsx @@ -92,5 +92,5 @@ export namespace createComponentExtension { export const componentDataRef = createExtensionDataRef<{ ref: ComponentRef; impl: ComponentType; - }>('core.component.component'); + }>().with({ id: 'core.component.component' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 4b92d4dace..d694440427 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -21,7 +21,9 @@ import { RouteRef } from '../routing'; /** * Helper for creating extensions for a nav item. + * * @public + * @deprecated Use {@link NavItemBlueprint} instead. */ export function createNavItemExtension(options: { namespace?: string; @@ -61,5 +63,5 @@ export namespace createNavItemExtension { title: string; icon: IconComponent; routeRef: RouteRef; - }>('core.nav-item.target'); + }>().with({ id: 'core.nav-item.target' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx index 8c54743f1d..17187a339d 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.test.tsx @@ -41,6 +41,7 @@ describe('createNavLogoExtension', () => { logos: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx index 3d66b26d6b..02c32e7f34 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavLogoExtension.tsx @@ -18,7 +18,9 @@ import { createExtension, createExtensionDataRef } from '../wiring'; /** * Helper for creating extensions for a nav logos. + * * @public + * @deprecated Use {@link NavLogoBlueprint} instead. */ export function createNavLogoExtension(options: { name?: string; @@ -51,5 +53,5 @@ export namespace createNavLogoExtension { export const logoElementsDataRef = createExtensionDataRef<{ logoIcon?: JSX.Element; logoFull?: JSX.Element; - }>('core.nav-logo.logo-elements'); + }>().with({ id: 'core.nav-logo.logo-elements' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 374400fce8..e5dd4027c0 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -56,6 +56,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); expect( @@ -78,6 +79,7 @@ describe('createPageExtension', () => { kind: 'page', attachTo: { id: 'other', input: 'place' }, configSchema: expect.anything(), + override: expect.any(Function), disabled: true, inputs: { first: createExtensionInput({ @@ -115,6 +117,7 @@ describe('createPageExtension', () => { }, factory: expect.any(Function), toString: expect.any(Function), + override: expect.any(Function), }); }); diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index dfe549f32b..bef642e465 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -31,6 +31,7 @@ import { ExtensionDefinition } from '../wiring/createExtension'; * Helper for creating extensions for a routable React page component. * * @public + * @deprecated Use {@link PageBlueprint} instead. */ export function createPageExtension< TConfig extends { path: string }, @@ -87,7 +88,7 @@ export function createPageExtension< path: config.path, routeRef: options.routeRef, element: ( - + ), diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx index 7109e1a8aa..2c16076fcf 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.test.tsx @@ -50,6 +50,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); @@ -119,6 +120,7 @@ describe('createRouterExtension', () => { component: expect.anything(), }, factory: expect.any(Function), + override: expect.any(Function), toString: expect.any(Function), }); diff --git a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx index 32d61f7e60..943711f5e0 100644 --- a/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createRouterExtension.tsx @@ -31,6 +31,7 @@ import { Expand } from '../types'; * MemoryRouter in tests, or to add additional props to a BrowserRouter. * * @public + * @deprecated Use {@link RouterBlueprint} instead. */ export function createRouterExtension< TConfig extends {}, @@ -77,8 +78,7 @@ export function createRouterExtension< /** @public */ export namespace createRouterExtension { - export const componentDataRef = - createExtensionDataRef>>( - 'app.router.wrapper', - ); + export const componentDataRef = createExtensionDataRef< + ComponentType> + >().with({ id: 'app.router.wrapper' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx index b525cc3a7b..3f41202f11 100644 --- a/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createSignInPageExtension.tsx @@ -30,6 +30,7 @@ import { SignInPageProps } from '@backstage/core-plugin-api'; /** * * @public + * @deprecated Use {@link SignInPageBlueprint} instead. */ export function createSignInPageExtension< TConfig extends {}, @@ -79,5 +80,5 @@ export function createSignInPageExtension< export namespace createSignInPageExtension { export const componentDataRef = createExtensionDataRef< ComponentType - >('core.sign-in-page.component'); + >().with({ id: 'core.sign-in-page.component' }); } diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index cc6dbdabe9..e523fd9c05 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -17,7 +17,10 @@ import { createExtension, createExtensionDataRef } from '../wiring'; import { AppTheme } from '@backstage/core-plugin-api'; -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export function createThemeExtension(theme: AppTheme) { return createExtension({ kind: 'theme', @@ -31,8 +34,12 @@ export function createThemeExtension(theme: AppTheme) { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link ThemeBlueprint} instead. + */ export namespace createThemeExtension { - export const themeDataRef = - createExtensionDataRef('core.theme.theme'); + export const themeDataRef = createExtensionDataRef().with({ + id: 'core.theme.theme', + }); } diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts index 975901b8e5..019c565cbd 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.test.ts @@ -46,6 +46,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -84,6 +85,7 @@ describe('createTranslationExtension', () => { namespace: 'test', attachTo: { id: 'app', input: 'translations' }, disabled: false, + override: expect.any(Function), inputs: {}, output: { resource: createTranslationExtension.translationDataRef, @@ -120,6 +122,7 @@ describe('createTranslationExtension', () => { version: 'v1', kind: 'translation', namespace: 'test', + override: expect.any(Function), name: 'sv', attachTo: { id: 'app', input: 'translations' }, disabled: false, diff --git a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts index 8497a64b88..c4d2f6037d 100644 --- a/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createTranslationExtension.ts @@ -17,7 +17,10 @@ import { TranslationMessages, TranslationResource } from '../translation'; import { createExtension, createExtensionDataRef } from '../wiring'; -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export function createTranslationExtension(options: { name?: string; resource: TranslationResource | TranslationMessages; @@ -34,9 +37,12 @@ export function createTranslationExtension(options: { }); } -/** @public */ +/** + * @public + * @deprecated Use {@link TranslationBlueprint} instead. + */ export namespace createTranslationExtension { export const translationDataRef = createExtensionDataRef< TranslationResource | TranslationMessages - >('core.translation.translation'); + >().with({ id: 'core.translation.translation' }); } diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 263380b1f8..957ca50bba 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -22,6 +22,7 @@ export * from './analytics'; export * from './apis'; +export * from './blueprints'; export * from './components'; export * from './extensions'; export * from './icons'; diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts index 727dc1f53e..97d3dd2264 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.test.ts @@ -26,7 +26,6 @@ describe('ExternalRouteRef', () => { const routeRef: ExternalRouteRef = createExternalRouteRef(); const internal = toInternalExternalRouteRef(routeRef); expect(internal.getParams()).toEqual([]); - expect(internal.optional).toBe(false); expect(String(internal)).toMatch( /^ExternalRouteRef\{created at '.*ExternalRouteRef\.test\.ts.*'\}$/, @@ -35,16 +34,6 @@ describe('ExternalRouteRef', () => { expect(String(internal)).toBe('ExternalRouteRef{some-id}'); }); - it('should be created as optional', () => { - const routeRef: ExternalRouteRef = createExternalRouteRef({ - params: [], - optional: true, - }); - const internal = toInternalExternalRouteRef(routeRef); - expect(internal.getParams()).toEqual([]); - expect(internal.optional).toEqual(true); - }); - it('should be created with params', () => { const routeRef: ExternalRouteRef<{ x: string; @@ -52,63 +41,39 @@ describe('ExternalRouteRef', () => { }> = createExternalRouteRef({ params: ['x', 'y'] }); const internal = toInternalExternalRouteRef(routeRef); expect(internal.getParams()).toEqual(['x', 'y']); - expect(internal.optional).toEqual(false); - }); - - it('should be created as optional with params', () => { - const routeRef: ExternalRouteRef<{ - x: string; - y: string; - }> = createExternalRouteRef({ params: ['x', 'y'], optional: true }); - const internal = toInternalExternalRouteRef(routeRef); - expect(internal.getParams()).toEqual(['x', 'y']); - expect(internal.optional).toEqual(true); }); it('should properly infer and validate parameter types and assignments', () => { - function checkRouteRef< - T extends AnyRouteRefParams, - TOptional extends boolean, - TCheck extends TOptional, - >( - _ref: ExternalRouteRef, + function checkRouteRef( + _ref: ExternalRouteRef, _params: T extends undefined ? undefined : T, - _optional: TCheck, ) {} - const _1 = createExternalRouteRef({ params: ['notX'] }); - checkRouteRef(_1, { notX: '' }, false); + const _1 = createExternalRouteRef(); + checkRouteRef(_1, undefined); // @ts-expect-error - checkRouteRef(_1, { x: '' }, false); + checkRouteRef(_1, { x: '' }); - const _2 = createExternalRouteRef({ params: ['x'], optional: true }); - checkRouteRef(_2, { x: '' }, true); + const _2 = createExternalRouteRef({ params: ['x'] }); + checkRouteRef(_2, { x: '' }); // @ts-expect-error - checkRouteRef(_2, undefined, false); + checkRouteRef(_2, { notX: '' }); + // @ts-expect-error + checkRouteRef(_2, undefined); const _3 = createExternalRouteRef({ params: ['x', 'y'] }); - checkRouteRef(_3, { x: '', y: '' }, false); + checkRouteRef(_3, { x: '', y: '' }); // @ts-expect-error - checkRouteRef(_3, { x: '' }, false); + checkRouteRef(_3, { x: '' }); // @ts-expect-error - checkRouteRef(_3, { x: '', y: '', z: '' }, false); + checkRouteRef(_3, { x: '', y: '', z: '' }); const _4 = createExternalRouteRef({ params: [] }); - checkRouteRef(_4, undefined, false); + checkRouteRef(_4, undefined); // @ts-expect-error - checkRouteRef(_4, { x: '' }); - - const _5 = createExternalRouteRef(); - checkRouteRef(_5, undefined, false); - // @ts-expect-error - checkRouteRef(_5, { x: '' }); - - const _6 = createExternalRouteRef({ optional: true }); - checkRouteRef(_6, undefined, true); - // @ts-expect-error - checkRouteRef(_6, undefined, false); + checkRouteRef(_4, { x: '' }); // To avoid complains about missing expectations and unused vars - expect([_1, _2, _3, _4, _5, _6].join('')).toEqual(expect.any(String)); + expect([_1, _2, _3, _4].join('')).toEqual(expect.any(String)); }); }); diff --git a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts index 7a754ffa7b..6ce36707c0 100644 --- a/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts +++ b/packages/frontend-plugin-api/src/routing/ExternalRouteRef.ts @@ -29,18 +29,15 @@ import { AnyRouteRefParams } from './types'; */ export interface ExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, > { readonly $$type: '@backstage/ExternalRouteRef'; readonly T: TParams; - readonly optional: TOptional; } /** @internal */ export interface InternalExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, -> extends ExternalRouteRef { +> extends ExternalRouteRef { readonly version: 'v1'; getParams(): string[]; getDescription(): string; @@ -52,11 +49,8 @@ export interface InternalExternalRouteRef< /** @internal */ export function toInternalExternalRouteRef< TParams extends AnyRouteRefParams = AnyRouteRefParams, - TOptional extends boolean = boolean, ->( - resource: ExternalRouteRef, -): InternalExternalRouteRef { - const r = resource as InternalExternalRouteRef; +>(resource: ExternalRouteRef): InternalExternalRouteRef { + const r = resource as InternalExternalRouteRef; if (r.$$type !== '@backstage/ExternalRouteRef') { throw new Error(`Invalid ExternalRouteRef, bad type '${r.$$type}'`); } @@ -79,7 +73,6 @@ class ExternalRouteRefImpl readonly $$type = '@backstage/ExternalRouteRef' as any; constructor( - readonly optional: boolean, readonly params: string[] = [], readonly defaultTarget: string | undefined, creationSite: string, @@ -104,7 +97,6 @@ class ExternalRouteRefImpl */ export function createExternalRouteRef< TParams extends { [param in TParamKeys]: string } | undefined = undefined, - TOptional extends boolean = false, TParamKeys extends string = string, >(options?: { /** @@ -114,14 +106,6 @@ export function createExternalRouteRef< ? (keyof TParams)[] : TParamKeys[]; - /** - * Whether or not this route is optional, defaults to false. - * - * Optional external routes are not required to be bound in the app, and - * if they aren't, `useExternalRouteRef` will return `undefined`. - */ - optional?: TOptional; - /** * The route (typically in another plugin) that this should map to by default. * @@ -134,13 +118,11 @@ export function createExternalRouteRef< ? undefined : string extends TParamKeys ? TParams - : { [param in TParamKeys]: string }, - TOptional + : { [param in TParamKeys]: string } > { return new ExternalRouteRefImpl( - Boolean(options?.optional), options?.params as string[] | undefined, options?.defaultTarget, describeParentCallSite(), - ) as ExternalRouteRef; + ); } diff --git a/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts b/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts index 72997e07db..ab5fc2f034 100644 --- a/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts +++ b/packages/frontend-plugin-api/src/routing/describeParentCallSite.ts @@ -16,6 +16,8 @@ const MESSAGE_MARKER = 'eHgtF5hmbrXyiEvo'; +// NOTE: This function is also imported and used in backend code + /** * Internal helper that describes the location of the parent caller. * @internal diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx b/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx index 4f43cf8d01..bdac95a5a0 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.test.tsx @@ -45,7 +45,7 @@ describe('v1 consumer', () => { }); const routeFunc = renderedHook.result.current; - expect(routeFunc()).toBe('/hello'); + expect(routeFunc?.()).toBe('/hello'); expect(resolve).toHaveBeenCalledWith( routeRef, expect.objectContaining({ @@ -54,6 +54,23 @@ describe('v1 consumer', () => { ); }); + it('should ignore missing routes', () => { + const routeRef = createRouteRef(); + + const renderedHook = renderHook(() => useRouteRef(routeRef), { + wrapper: ({ children }: React.PropsWithChildren<{}>) => ( + undefined }]]} + > + + + ), + }); + + const routeFunc = renderedHook.result.current; + expect(routeFunc).toBeUndefined(); + }); + it('re-resolves the routeFunc when the search parameters change', () => { const resolve = jest.fn(() => () => '/hello'); diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.tsx b/packages/frontend-plugin-api/src/routing/useRouteRef.tsx index fdf831953d..2d2b3a5458 100644 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.tsx +++ b/packages/frontend-plugin-api/src/routing/useRouteRef.tsx @@ -30,47 +30,14 @@ import { RouteFunc, routeResolutionApiRef, useApi } from '../apis'; * See {@link https://backstage.io/docs/plugins/composability#routing-system} * * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. - * @public - */ -export function useRouteRef< - TOptional extends boolean, - TParams extends AnyRouteRefParams, ->( - routeRef: ExternalRouteRef, -): TOptional extends true ? RouteFunc | undefined : RouteFunc; - -/** - * React hook for constructing URLs to routes. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system} - * - * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. - * @public - */ -export function useRouteRef( - routeRef: RouteRef | SubRouteRef, -): RouteFunc; - -/** - * React hook for constructing URLs to routes. - * - * @remarks - * - * See {@link https://backstage.io/docs/plugins/composability#routing-system} - * - * @param routeRef - The ref to route that should be converted to URL. - * @returns A function that will in turn return the concrete URL of the `routeRef`. + * @returns A function that will in turn return the concrete URL of the `routeRef`, or `undefined` if the route is not available. * @public */ export function useRouteRef( routeRef: | RouteRef | SubRouteRef - | ExternalRouteRef, + | ExternalRouteRef, ): RouteFunc | undefined { const { pathname } = useLocation(); const routeResolutionApi = useApi(routeResolutionApiRef); @@ -80,10 +47,5 @@ export function useRouteRef( [routeResolutionApi, routeRef, pathname], ); - const isOptional = 'optional' in routeRef && routeRef.optional; - if (!routeFunc && !isOptional) { - throw new Error(`No path for ${routeRef}`); - } - return routeFunc; } diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts index 90eba44249..477a1a58c6 100644 --- a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.test.ts @@ -25,10 +25,19 @@ describe('createSchemaFromZod', () => { }), ); - expect(() => parse({ derp: { bar: 'derp' } })).toThrow( + expect(() => { + // @ts-expect-error + return parse({ derp: { bar: 'derp' } }); + }).toThrow( `Missing required value at 'foo'; Expected number, received string at 'derp.bar'`, ); - expect(() => parse(undefined)).toThrow(`Missing required value`); - expect(() => parse('derp')).toThrow(`Expected object, received string`); + expect(() => { + // @ts-expect-error + return parse(undefined); + }).toThrow(`Missing required value`); + expect(() => { + // @ts-expect-error + return parse('derp'); + }).toThrow(`Expected object, received string`); }); }); diff --git a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts index bfaf57f522..9e63515ae4 100644 --- a/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts +++ b/packages/frontend-plugin-api/src/schema/createSchemaFromZod.ts @@ -19,10 +19,13 @@ import { z, ZodSchema, ZodTypeDef } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; import { PortableSchema } from './types'; -/** @public */ +/** + * @public + * @deprecated Use the `config.schema` option of `createExtension` instead, or use `createExtensionBlueprint`. + */ export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, -): PortableSchema { +): PortableSchema { const schema = schemaCreator(z); return { // TODO: Types allow z.array etc here but it will break stuff diff --git a/packages/frontend-plugin-api/src/schema/types.ts b/packages/frontend-plugin-api/src/schema/types.ts index 9636d3b896..5644068e90 100644 --- a/packages/frontend-plugin-api/src/schema/types.ts +++ b/packages/frontend-plugin-api/src/schema/types.ts @@ -17,7 +17,7 @@ import { JsonObject } from '@backstage/types'; /** @public */ -export type PortableSchema = { - parse: (input: unknown) => TOutput; +export type PortableSchema = { + parse: (input: TInput) => TOutput; schema: JsonObject; }; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index a6ff41f6b0..cb48cb8df3 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -20,7 +20,9 @@ import { createExtensionDataRef } from './createExtensionDataRef'; /** @public */ export const coreExtensionData = { - reactElement: createExtensionDataRef('core.reactElement'), - routePath: createExtensionDataRef('core.routing.path'), - routeRef: createExtensionDataRef('core.routing.ref'), + reactElement: createExtensionDataRef().with({ + id: 'core.reactElement', + }), + routePath: createExtensionDataRef().with({ id: 'core.routing.path' }), + routeRef: createExtensionDataRef().with({ id: 'core.routing.ref' }), }; diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 42ab21db80..f9e39e98de 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { createExtensionTester } from '@backstage/frontend-test-utils'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { createExtensionInput } from './createExtensionInput'; -const stringData = createExtensionDataRef('string'); +const stringDataRef = createExtensionDataRef().with({ id: 'string' }); +const numberDataRef = createExtensionDataRef().with({ id: 'number' }); function unused(..._any: any[]) {} @@ -28,7 +30,7 @@ describe('createExtension', () => { namespace: 'test', attachTo: { id: 'root', input: 'default' }, output: { - foo: stringData, + foo: stringDataRef, }, }; const extension = createExtension({ @@ -39,14 +41,14 @@ describe('createExtension', () => { }; }, }); - expect(extension.namespace).toBe('test'); + expect(extension).toMatchObject({ version: 'v1', namespace: 'test' }); // When declared as an error function without a block the TypeScript errors - // are a more specific and will point at the property that is problematic. + // are a more specific and will often point at the property that is problematic. + // @ts-expect-error createExtension({ ...baseConfig, factory: () => ({ - // @ts-expect-error foo: 3, }), }); @@ -166,8 +168,8 @@ describe('createExtension', () => { namespace: 'test', attachTo: { id: 'root', input: 'default' }, output: { - foo: stringData, - bar: stringData.optional(), + foo: stringDataRef, + bar: stringDataRef.optional(), }, }; const extension = createExtension({ @@ -176,7 +178,7 @@ describe('createExtension', () => { foo: 'bar', }), }); - expect(extension.namespace).toBe('test'); + expect(extension).toMatchObject({ version: 'v1', namespace: 'test' }); createExtension({ ...baseConfig, @@ -185,18 +187,18 @@ describe('createExtension', () => { bar: 'baz', }), }); + // @ts-expect-error createExtension({ ...baseConfig, factory: () => ({ - // @ts-expect-error foo: 3, }), }); + // @ts-expect-error createExtension({ ...baseConfig, factory: () => ({ foo: 'bar', - // @ts-expect-error bar: 3, }), }); @@ -237,18 +239,18 @@ describe('createExtension', () => { attachTo: { id: 'root', input: 'default' }, inputs: { mixed: createExtensionInput({ - required: stringData, - optional: stringData.optional(), + required: stringDataRef, + optional: stringDataRef.optional(), }), onlyRequired: createExtensionInput({ - required: stringData, + required: stringDataRef, }), onlyOptional: createExtensionInput({ - optional: stringData.optional(), + optional: stringDataRef.optional(), }), }, output: { - foo: stringData, + foo: stringDataRef, }, factory({ inputs }) { const a1: string = inputs.mixed?.[0].output.required; @@ -286,9 +288,449 @@ describe('createExtension', () => { }; }, }); - expect(extension.namespace).toBe('test'); + expect(extension).toMatchObject({ version: 'v1', namespace: 'test' }); expect(String(extension)).toBe( 'ExtensionDefinition{namespace=test,attachTo=root@default}', ); }); + + it('should create an extension with config', () => { + const extension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + config: { + schema: { + foo: z => z.string(), + bar: z => z.string().default('bar'), + baz: z => z.string().optional(), + }, + }, + output: [stringDataRef], + factory({ config }) { + const a1: string = config.foo; + const a2: string = config.bar; + // @ts-expect-error + const a3: string = config.baz; + // @ts-expect-error + const c1: number = config.foo; + // @ts-expect-error + const c2: number = config.bar; + // @ts-expect-error + const c3: number = config.baz; + unused(a1, a2, a3, c1, c2, c3); + + return [stringDataRef('bar')]; + }, + }); + expect(extension).toMatchObject({ version: 'v2', namespace: 'test' }); + expect(String(extension)).toBe( + 'ExtensionDefinition{namespace=test,attachTo=root@default}', + ); + + expect( + extension.configSchema?.parse({ + foo: 'x', + bar: 'y', + baz: 'z', + // @ts-expect-error + qux: 'w', + }), + ).toEqual({ + foo: 'x', + bar: 'y', + baz: 'z', + }); + expect( + extension.configSchema?.parse({ + foo: 'x', + }), + ).toEqual({ + foo: 'x', + bar: 'bar', + }); + expect(() => { + // @ts-expect-error + return extension.configSchema?.parse({}); + }).toThrow("Missing required value at 'foo'"); + }); + + it('should support new form of outputs', () => { + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + factory() { + return []; // Missing all outputs + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + factory() { + return [stringDataRef('hello')]; // Missing number output + }, + }), + ).toMatchObject({ version: 'v2' }); + + // Duplicate output, we won't attempt to handle this a compile time and instead error out at runtime + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + factory() { + return [stringDataRef('hello'), stringDataRef('hello')]; + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + factory() { + return [stringDataRef('hello'), numberDataRef(4)]; + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + factory() { + return [stringDataRef('hello'), numberDataRef(4)]; + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + factory() { + return [stringDataRef('hello'), numberDataRef(4)]; + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + factory() { + return [stringDataRef('hello')]; // Missing number output, but it's optional so that's allowed + }, + }), + ).toMatchObject({ version: 'v2' }); + }); + + it('should support new form of outputs with a generator', () => { + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + // Missing all outputs + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + yield stringDataRef('hello'); // Missing number output + }, + }), + ).toMatchObject({ version: 'v2' }); + + // Duplicate output, we won't attempt to handle this a compile time and instead error out at runtime + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + *factory() { + yield stringDataRef('hello'); + yield stringDataRef('hello'); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + // @ts-expect-error + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); // No declared output + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + *factory() { + yield stringDataRef('hello'); + yield numberDataRef(4); + }, + }), + ).toMatchObject({ version: 'v2' }); + + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef.optional()], + *factory() { + yield stringDataRef('hello'); // Missing number output, but it's optional so that's allowed + }, + }), + ).toMatchObject({ version: 'v2' }); + }); + + it('should support new form of inputs', () => { + expect( + createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + inputs: { + header: createExtensionInput([stringDataRef.optional()], { + optional: true, + singleton: true, + }), + content: createExtensionInput([stringDataRef, numberDataRef], { + optional: false, + singleton: true, + }), + }, + output: [stringDataRef], + factory({ inputs }) { + const headerStr = inputs.header?.get(stringDataRef); + const contentStr = inputs.content.get(stringDataRef); + const contentNum = inputs.content.get(numberDataRef); + + // @ts-expect-error + inputs.header?.get(numberDataRef); + + // @ts-expect-error + const x1: string = headerStr; // string | undefined + + unused(x1); + + return [stringDataRef(contentStr.repeat(contentNum))]; + }, + }), + ).toMatchObject({ version: 'v2' }); + }); + + describe('overrides', () => { + it('should allow overriding of config and merging', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + config: { + schema: { + bar: z => z.string().optional(), + }, + }, + factory(_, { config }) { + return [stringDataRef(config.foo ?? config.bar ?? 'default')]; + }, + }); + + expect(true).toBe(true); + }); + + it('should allow overriding of outputs', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + inputs: { + test: createExtensionInput([stringDataRef], { + singleton: true, + }), + }, + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory({ inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + const override1 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [numberDataRef(inputs.test.get(stringDataRef).length)]; + }, + }); + + // @ts-expect-error - this should fail because string output should be merged? + const override2 = testExtension.override({ + output: [numberDataRef], + factory(_, { inputs }) { + return [stringDataRef(inputs.test.get(stringDataRef))]; + }, + }); + + unused(override1, override2); + + expect(true).toBe(true); + }); + + it('should allow overriding the factory function and calling the original factory', () => { + const testExtension = createExtension({ + namespace: 'test', + attachTo: { id: 'root', input: 'blob' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().optional(), + }, + }, + factory() { + return [stringDataRef('default')]; + }, + }); + + testExtension.override({ + factory(originalFactory) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + // @ts-expect-error - fails because original factory does not return number + const number: boolean = response.get(numberDataRef); + + return [stringDataRef(`foo-${foo}-override`)]; + }, + }); + + expect(true).toBe(true); + }); + + it('should allow overriding the returned values from the parent factory', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef, numberDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo), numberDataRef(42)]; + }, + }); + + const overridden = testExtension.override({ + output: [numberDataRef, stringDataRef], + *factory(originalFactory) { + const output = originalFactory(); + yield* output; + + yield numberDataRef(output.get(numberDataRef) + 1); + }, + }); + + const tester = createExtensionTester(overridden); + + expect(tester.data(numberDataRef)).toBe(43); + }); + + it('should work functionally with overrides', () => { + const testExtension = createExtension({ + kind: 'thing', + namespace: 'test', + attachTo: { id: 'root', input: 'default' }, + output: [stringDataRef], + config: { + schema: { + foo: z => z.string().default('boom'), + }, + }, + factory({ config }) { + return [stringDataRef(config.foo)]; + }, + }); + + const overriden = testExtension.override({ + config: { + schema: { + bar: z => z.string().default('hello'), + }, + }, + factory(originalFactory, { config }) { + const response = originalFactory(); + + const foo: string = response.get(stringDataRef); + + return [stringDataRef(`foo-${foo}-override-${config.bar}`)]; + }, + }); + + expect(createExtensionTester(overriden).data(stringDataRef)).toBe( + 'foo-boom-override-hello', + ); + + expect( + createExtensionTester(overriden, { + config: { foo: 'hello', bar: 'world' }, + }).data(stringDataRef), + ).toBe('foo-hello-override-world'); + }); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index be76fbc069..f2d3d906ab 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -15,19 +15,31 @@ */ import { AppNode } from '../apis'; -import { PortableSchema } from '../schema'; +import { PortableSchema, createSchemaFromZod } from '../schema'; import { Expand } from '../types'; -import { ExtensionDataRef } from './createExtensionDataRef'; -import { ExtensionInput } from './createExtensionInput'; +import { createDataContainer } from './createExtensionBlueprint'; +import { + AnyExtensionDataRef, + ExtensionDataRef, + ExtensionDataValue, +} from './createExtensionDataRef'; +import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput'; +import { z } from 'zod'; -/** @public */ +/** + * @public + * @deprecated Extension data maps will be removed. + */ export type AnyExtensionDataMap = { - [name in string]: ExtensionDataRef; + [name in string]: AnyExtensionDataRef; }; -/** @public */ +/** + * @public + * @deprecated This type will be removed. + */ export type AnyExtensionInputMap = { - [inputName in string]: ExtensionInput< + [inputName in string]: LegacyExtensionInput< AnyExtensionDataMap, { optional: boolean; singleton: boolean } >; @@ -36,6 +48,7 @@ export type AnyExtensionInputMap = { /** * Converts an extension data map into the matching concrete data values type. * @public + * @deprecated Extension data maps will be removed. */ export type ExtensionDataValues = { [DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends { @@ -51,37 +64,70 @@ export type ExtensionDataValues = { : never]?: TExtensionData[DataName]['T']; }; +/** @public */ +export type ExtensionDataContainer = + Iterable< + UExtensionData extends ExtensionDataRef< + infer IData, + infer IId, + infer IConfig + > + ? IConfig['optional'] extends true + ? never + : ExtensionDataValue + : never + > & { + get( + ref: ExtensionDataRef, + ): UExtensionData extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; + }; + /** * Convert a single extension input into a matching resolved input. * @public */ -export type ResolvedExtensionInput = - { - node: AppNode; - output: ExtensionDataValues; - }; +export type ResolvedExtensionInput< + TExtensionInput extends ExtensionInput, +> = TExtensionInput['extensionData'] extends Array + ? { + node: AppNode; + } & ExtensionDataContainer + : TExtensionInput['extensionData'] extends AnyExtensionDataMap + ? { + node: AppNode; + output: ExtensionDataValues; + } + : never; /** * Converts an extension input map into a matching collection of resolved inputs. * @public */ export type ResolvedExtensionInputs< - TInputs extends { [name in string]: ExtensionInput }, + TInputs extends { + [name in string]: ExtensionInput | LegacyExtensionInput; + }, > = { [InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton'] - ? Array>> + ? Array>> : false extends TInputs[InputName]['config']['optional'] - ? Expand> - : Expand< - ResolvedExtensionInput | undefined - >; + ? Expand> + : Expand | undefined>; }; -/** @public */ -export interface CreateExtensionOptions< +/** + * @public + * @deprecated This way of structuring the options is deprecated, this type will be removed in the future + */ +export interface LegacyCreateExtensionOptions< TOutput extends AnyExtensionDataMap, TInputs extends AnyExtensionInputMap, TConfig, + TConfigInput, > { kind?: string; namespace?: string; @@ -90,67 +136,378 @@ export interface CreateExtensionOptions< disabled?: boolean; inputs?: TInputs; output: TOutput; - configSchema?: PortableSchema; - factory(options: { + configSchema?: PortableSchema; + factory(context: { node: AppNode; config: TConfig; inputs: Expand>; }): Expand>; } +/** @ignore */ +export type VerifyExtensionFactoryOutput< + UDeclaredOutput extends AnyExtensionDataRef, + UFactoryOutput extends ExtensionDataValue, +> = ( + UDeclaredOutput extends any + ? UDeclaredOutput['config']['optional'] extends true + ? never + : UDeclaredOutput['id'] + : never +) extends infer IRequiredOutputIds + ? [IRequiredOutputIds] extends [UFactoryOutput['id']] + ? [UFactoryOutput['id']] extends [UDeclaredOutput['id']] + ? {} + : { + 'Error: The extension factory has undeclared output(s)': Exclude< + UFactoryOutput['id'], + UDeclaredOutput['id'] + >; + } + : { + 'Error: The extension factory is missing the following output(s)': Exclude< + IRequiredOutputIds, + UFactoryOutput['id'] + >; + } + : never; + /** @public */ -export interface ExtensionDefinition { +export type CreateExtensionOptions< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, +> = { + kind?: TKind; + namespace?: TNamespace; + name?: TName; + attachTo: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output: Array; + config?: { + schema: TConfigSchema; + }; + factory(context: { + node: AppNode; + config: { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs: Expand>; + }): Iterable; +} & VerifyExtensionFactoryOutput; + +/** @public */ +export interface ExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> { $$type: '@backstage/ExtensionDefinition'; - readonly kind?: string; - readonly namespace?: string; - readonly name?: string; + readonly kind?: TKind; + readonly namespace?: TNamespace; + readonly name?: TName; readonly attachTo: { id: string; input: string }; readonly disabled: boolean; - readonly configSchema?: PortableSchema; + readonly configSchema?: PortableSchema; + + override< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >( + args: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: (context?: { + config?: TConfig; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + } & VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >, + ): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + TNamespace, + TName + >; } /** @internal */ -export interface InternalExtensionDefinition - extends ExtensionDefinition { - readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; - readonly output: AnyExtensionDataMap; - factory(options: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs; - }): ExtensionDataValues; -} +export type InternalExtensionDefinition< + TConfig, + TConfigInput = TConfig, + UOutput extends AnyExtensionDataRef = AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + } = {}, + TKind extends string | undefined = string | undefined, + TNamespace extends string | undefined = string | undefined, + TName extends string | undefined = string | undefined, +> = ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + TNamespace, + TName +> & + ( + | { + readonly version: 'v1'; + readonly inputs: AnyExtensionInputMap; + readonly output: AnyExtensionDataMap; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs; + }): ExtensionDataValues; + } + | { + readonly version: 'v2'; + readonly inputs: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }; + readonly output: Array; + factory(context: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }>; + }): Iterable>; + } + ); /** @internal */ -export function toInternalExtensionDefinition( - overrides: ExtensionDefinition, -): InternalExtensionDefinition { - const internal = overrides as InternalExtensionDefinition; +export function toInternalExtensionDefinition( + overrides: ExtensionDefinition, +): InternalExtensionDefinition { + const internal = overrides as InternalExtensionDefinition< + TConfig, + TConfigInput + >; if (internal.$$type !== '@backstage/ExtensionDefinition') { throw new Error( `Invalid extension definition instance, bad type '${internal.$$type}'`, ); } - if (internal.version !== 'v1') { + const version = internal.version; + if (version !== 'v1' && version !== 'v2') { throw new Error( - `Invalid extension definition instance, bad version '${internal.version}'`, + `Invalid extension definition instance, bad version '${version}'`, ); } return internal; } /** @public */ +export function createExtension< + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, + const TKind extends string | undefined = undefined, + const TNamespace extends string | undefined = undefined, + const TName extends string | undefined = undefined, +>( + options: CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + >, +): ExtensionDefinition< + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + UOutput, + TInputs, + string | undefined extends TKind ? undefined : TKind, + string | undefined extends TNamespace ? undefined : TNamespace, + string | undefined extends TName ? undefined : TName +>; +/** + * @public + * @deprecated - use the array format of `output` instead, see TODO-doc-link + */ export function createExtension< TOutput extends AnyExtensionDataMap, TInputs extends AnyExtensionInputMap, - TConfig = never, + TConfig, + TConfigInput, >( - options: CreateExtensionOptions, -): ExtensionDefinition { + options: LegacyCreateExtensionOptions< + TOutput, + TInputs, + TConfig, + TConfigInput + >, +): ExtensionDefinition; +export function createExtension< + const TKind extends string | undefined, + const TNamespace extends string | undefined, + const TName extends string | undefined, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TLegacyInputs extends AnyExtensionInputMap, + TConfig, + TConfigInput, + TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, +>( + options: + | CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + > + | LegacyCreateExtensionOptions< + AnyExtensionDataMap, + TLegacyInputs, + TConfig, + TConfigInput + >, +): ExtensionDefinition< + TConfig & + (string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer>; + }), + TConfigInput & + (string extends keyof TConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >), + UOutput, + TInputs, + TKind, + TNamespace, + TName +> { + if ('configSchema' in options && 'config' in options) { + throw new Error(`Cannot provide both configSchema and config.schema`); + } + let configSchema: PortableSchema | undefined; + if ('configSchema' in options) { + configSchema = options.configSchema; + } + if ('config' in options) { + const newConfigSchema = options.config?.schema; + configSchema = + newConfigSchema && + createSchemaFromZod(innerZ => + innerZ.object( + Object.fromEntries( + Object.entries(newConfigSchema).map(([k, v]) => [k, v(innerZ)]), + ), + ), + ); + } + return { $$type: '@backstage/ExtensionDefinition', - version: 'v1', + version: Symbol.iterator in options.output ? 'v2' : 'v1', kind: options.kind, namespace: options.namespace, name: options.name, @@ -158,14 +515,8 @@ export function createExtension< disabled: options.disabled ?? false, inputs: options.inputs ?? {}, output: options.output, - configSchema: options.configSchema, - factory({ inputs, ...rest }) { - // TODO: Simplify this, but TS wouldn't infer the input type for some reason - return options.factory({ - inputs: inputs as Expand>, - ...rest, - }); - }, + configSchema, + factory: options.factory, toString() { const parts: string[] = []; if (options.kind) { @@ -180,5 +531,168 @@ export function createExtension< parts.push(`attachTo=${options.attachTo.id}@${options.attachTo.input}`); return `ExtensionDefinition{${parts.join(',')}}`; }, - } as InternalExtensionDefinition; + override: < + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UOverrideFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >(overrideOptions: { + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs; + output?: Array; + config?: { + schema: TExtensionConfigSchema; + }; + factory( + originalFactory: (context?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }) => ExtensionDataContainer, + context: { + node: AppNode; + config: { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable; + }): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject< + { + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + } & { + [key in keyof TConfigSchema]: ReturnType; + } + > + >, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + TNamespace, + TName + > => { + if (!Array.isArray(options.output)) { + throw new Error( + 'Cannot override an extension that is not declared using the new format with outputs as an array', + ); + } + const newOptions = options as CreateExtensionOptions< + TKind, + TNamespace, + TName, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput + >; + const overrideNewConfigSchema = overrideOptions.config?.schema; + + const schema = { + ...newOptions.config?.schema, + ...overrideNewConfigSchema, + } as TConfigSchema & TExtensionConfigSchema; + + return createExtension({ + kind: newOptions.kind, + namespace: newOptions.namespace, + name: newOptions.name, + attachTo: overrideOptions.attachTo ?? newOptions.attachTo, + disabled: overrideOptions.disabled ?? newOptions.disabled, + inputs: { ...overrideOptions.inputs, ...newOptions.inputs }, + output: overrideOptions.output ?? newOptions.output, + config: Object.keys(schema).length === 0 ? undefined : { schema }, + factory: ({ node, config, inputs }) => { + if (!overrideOptions.factory) { + return newOptions.factory({ + node, + config, + inputs: inputs as unknown as Expand< + ResolvedExtensionInputs + >, + }); + } + const parentResult = overrideOptions.factory( + (innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }): ExtensionDataContainer => { + return createDataContainer( + newOptions.factory({ + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }) as Iterable, + ); + }, + { + node, + config, + inputs, + }, + ); + + const deduplicatedResult = new Map(); + for (const item of parentResult) { + deduplicatedResult.set(item.id, item); + } + + return deduplicatedResult.values() as Iterable; + }, + } as CreateExtensionOptions); + }, + } as InternalExtensionDefinition< + TConfig & + (string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }), + TConfigInput & + (string extends keyof TConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >), + UOutput, + TInputs, + TKind, + TNamespace, + TName + >; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx new file mode 100644 index 0000000000..b9d1475074 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.test.tsx @@ -0,0 +1,536 @@ +/* + * Copyright 2024 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 React from 'react'; +import { coreExtensionData } from './coreExtensionData'; +import { createExtensionBlueprint } from './createExtensionBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { createExtensionDataRef } from './createExtensionDataRef'; +import { createExtensionInput } from './createExtensionInput'; +import { RouteRef } from '../routing'; +import { + ExtensionDefinition, + toInternalExtensionDefinition, +} from './createExtension'; + +function unused(..._any: any[]) {} + +function factoryOutput(ext: ExtensionDefinition) { + const int = toInternalExtensionDefinition(ext); + if (int.version !== 'v2') { + throw new Error('Expected v2 extension'); + } + return Array.from(int.factory({} as any)); +} + +describe('createExtensionBlueprint', () => { + it('should allow creation of extension blueprints', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + factory(params: { text: string }) { + return [coreExtensionData.reactElement(

{params.text}

)]; + }, + }); + + const extension = TestExtensionBlueprint.make({ + name: 'my-extension', + params: { + text: 'Hello, world!', + }, + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + attachTo: { + id: 'test', + input: 'default', + }, + configSchema: undefined, + disabled: false, + inputs: {}, + kind: 'test-extension', + name: 'my-extension', + namespace: undefined, + output: [coreExtensionData.reactElement], + factory: expect.any(Function), + toString: expect.any(Function), + override: expect.any(Function), + version: 'v2', + }); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + }); + + it('should allow creation of extension blueprints with a generator', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + *factory(params: { text: string }) { + yield coreExtensionData.reactElement(

{params.text}

); + }, + }); + + const extension = TestExtensionBlueprint.make({ + name: 'my-extension', + params: { + text: 'Hello, world!', + }, + }); + + expect(extension).toEqual({ + $$type: '@backstage/ExtensionDefinition', + attachTo: { + id: 'test', + input: 'default', + }, + configSchema: undefined, + disabled: false, + inputs: {}, + kind: 'test-extension', + name: 'my-extension', + namespace: undefined, + output: [coreExtensionData.reactElement], + factory: expect.any(Function), + toString: expect.any(Function), + override: expect.any(Function), + version: 'v2', + }); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + }); + + it('should allow overriding of the default factory', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + factory(params: { text: string }) { + return [coreExtensionData.reactElement(

{params.text}

)]; + }, + }); + + const extension = TestExtensionBlueprint.makeWithOverrides({ + name: 'my-extension', + factory(origFactory) { + return origFactory({ + text: 'Hello, world!', + }); + }, + }); + + expect(extension).toBeDefined(); + + const { container } = createExtensionTester(extension).render(); + expect(container.querySelector('h1')).toHaveTextContent('Hello, world!'); + }); + + it('should allow exporting the dataRefs from the extension blueprint', () => { + const dataRef = createExtensionDataRef().with({ id: 'test.data' }); + + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + dataRefs: { + data: dataRef, + }, + factory(params: { text: string }) { + return [coreExtensionData.reactElement(

{params.text}

)]; + }, + }); + + expect(TestExtensionBlueprint.dataRefs).toEqual({ + data: dataRef, + }); + }); + + it('should allow defining a config schema with additional properties in the instance', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + config: { + schema: { + text: z => z.string(), + }, + }, + factory(_, { config }) { + // @ts-expect-error + const b = config.something; + + const a: string = config.text; + unused(a); + + expect(config.text).toBe('Hello, world!'); + + return [coreExtensionData.reactElement(

{config.text}

)]; + }, + }); + + const extension = TestExtensionBlueprint.makeWithOverrides({ + name: 'my-extension', + config: { + schema: { + something: z => z.string(), + defaulted: z => z.string().optional().default('default'), + }, + }, + factory(origFactory, { config }) { + const b: string = config.something; + const c: string = config.text; + const d: string = config.defaulted; + + unused(b, c, d); + + expect(config.text).toBe('Hello, world!'); + expect(config.something).toBe('something new!'); + expect(config.defaulted).toBe('lolz'); + return origFactory({}); + }, + }); + + expect.assertions(4); + + createExtensionTester(extension, { + config: { + something: 'something new!', + text: 'Hello, world!', + defaulted: 'lolz', + }, + }).render(); + }); + + it('should not allow overlapping config keys', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + config: { + schema: { + text: z => z.string(), + }, + }, + factory(params: { text: string }) { + return [coreExtensionData.reactElement(
{params.text}
)]; + }, + }); + + TestExtensionBlueprint.makeWithOverrides({ + name: 'my-extension', + params: { + text: 'Hello, world!', + }, + config: { + schema: { + // @ts-expect-error + text: z => z.number(), + something: z => z.string(), + }, + }, + }); + + expect('test').toBe('test'); + }); + + it('should allow setting config when one was not already defined in the blueprint', () => { + const TestExtensionBlueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [coreExtensionData.reactElement], + factory(_, { config }) { + // @ts-expect-error + const b = config.something; + + return [coreExtensionData.reactElement(
)]; + }, + }); + + const extension = TestExtensionBlueprint.makeWithOverrides({ + name: 'my-extension', + config: { + schema: { + something: z => z.string(), + defaulted: z => z.string().optional().default('default'), + }, + }, + factory(origFactory, { config }) { + const b: string = config.something; + + unused(b); + + expect(config.something).toBe('something new!'); + expect(config.defaulted).toBe('lolz'); + return origFactory({}); + }, + }); + + expect.assertions(2); + + createExtensionTester(extension, { + config: { + something: 'something new!', + defaulted: 'lolz', + }, + }).render(); + }); + + it('should allow getting inputs properly', () => { + createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput( + [ + coreExtensionData.routeRef, + coreExtensionData.reactElement.optional(), + ], + { singleton: true }, + ), + }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routeRef.optional(), + ], + factory(_, { inputs }) { + const route = inputs.test.get(coreExtensionData.routeRef); + const optional = inputs.test.get(coreExtensionData.reactElement); + + // @ts-expect-error + const optional2: JSX.Element = optional; + const optional3: JSX.Element | undefined = optional; + + const route2: RouteRef = route; + + unused(optional2, optional3, route2); + + if (!route) { + return [coreExtensionData.reactElement(
)]; + } + + return [ + coreExtensionData.reactElement(
), + coreExtensionData.routeRef(route), + ]; + }, + }); + + expect(true).toBe(true); + }); + + it('should allow merging of inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef], { + singleton: true, + }), + }, + output: [coreExtensionData.reactElement.optional()], + factory(_params: { x?: string }, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + unused(ref); + return []; + }, + }); + + blueprint.makeWithOverrides({ + inputs: { + test2: createExtensionInput([coreExtensionData.reactElement], { + singleton: true, + }), + }, + factory(origFactory, { inputs }) { + const ref: RouteRef = inputs.test.get(coreExtensionData.routeRef); + + const el: JSX.Element = inputs.test2.get( + coreExtensionData.reactElement, + ); + + unused(ref, el); + + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); + + it('should not allow overriding inputs', () => { + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + inputs: { + test: createExtensionInput([coreExtensionData.routeRef]), + }, + output: [coreExtensionData.reactElement.optional()], + factory() { + return []; + }, + }); + + blueprint.makeWithOverrides({ + inputs: { + // @ts-expect-error + test: createExtensionInput([]), // Overrides are not allowed + }, + factory(origFactory) { + return origFactory({}); + }, + }); + + expect(true).toBe(true); + }); + + it('should replace the outputs when provided through make', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + const ext = toInternalExtensionDefinition( + blueprint.makeWithOverrides({ + output: [testDataRef2], + factory(origFactory) { + const parent = origFactory({}); + return [testDataRef2(`${parent.get(testDataRef1)}bar`)]; + }, + }), + ); + + expect(ext.output).toEqual([testDataRef2]); + + expect(factoryOutput(ext)).toEqual([testDataRef2('foobar')]); + }); + + it('should allow returning of the parent data container', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1], + factory() { + return [testDataRef1('foo')]; + }, + }); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + factory(origFactory) { + return [...origFactory({}), testDataRef2('bar')]; + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); + }); + + it('should not allow returning parent output if outputs are overridden', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.makeWithOverrides({ + output: [testDataRef2.optional()], + // @ts-expect-error + *factory() { + yield testDataRef1('foo'); + yield testDataRef2('bar'); + }, + }); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef2.optional()], + *factory() { + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef2('bar')]); + }); + + it('should not rely on optional outputs when forwarding from parent', () => { + const testDataRef1 = createExtensionDataRef().with({ id: 'test1' }); + const testDataRef2 = createExtensionDataRef().with({ id: 'test2' }); + + const blueprint = createExtensionBlueprint({ + kind: 'test-extension', + attachTo: { id: 'test', input: 'default' }, + output: [testDataRef1, testDataRef2.optional()], + factory() { + return [testDataRef1('foo')]; + }, + }); + + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + // @ts-expect-error + *factory(origFactory) { + yield* origFactory({}); + }, + }); + + expect( + factoryOutput( + blueprint.makeWithOverrides({ + output: [testDataRef1, testDataRef2], + *factory(origFactory) { + yield* origFactory({}); + yield testDataRef2('bar'); + }, + }), + ), + ).toEqual([testDataRef1('foo'), testDataRef2('bar')]); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts new file mode 100644 index 0000000000..656e681a2b --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionBlueprint.ts @@ -0,0 +1,477 @@ +/* + * Copyright 2024 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 { AppNode } from '../apis'; +import { Expand } from '../types'; +import { + CreateExtensionOptions, + ExtensionDataContainer, + ExtensionDefinition, + ResolvedExtensionInputs, + VerifyExtensionFactoryOutput, + createExtension, +} from './createExtension'; +import { z } from 'zod'; +import { ExtensionInput } from './createExtensionInput'; +import { + AnyExtensionDataRef, + ExtensionDataRef, + ExtensionDataValue, +} from './createExtensionDataRef'; + +/** + * @public + */ +export type CreateExtensionBlueprintOptions< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, + TDataRefs extends { [name in string]: AnyExtensionDataRef }, +> = { + kind: TKind; + namespace?: TNamespace; + attachTo: { id: string; input: string }; + disabled?: boolean; + inputs?: TInputs; + output: Array; + name?: TName; + config?: { + schema: TConfigSchema; + }; + factory( + params: TParams, + context: { + node: AppNode; + config: { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs: Expand>; + }, + ): Iterable; + + dataRefs?: TDataRefs; +} & VerifyExtensionFactoryOutput; + +/** + * @public + */ +export interface ExtensionBlueprint< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfig extends { [key in string]: unknown }, + TConfigInput extends { [key in string]: unknown }, + TDataRefs extends { [name in string]: AnyExtensionDataRef }, +> { + dataRefs: TDataRefs; + + make< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + TConfig, + TConfigInput, + UOutput, + TInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; + + /** + * Creates a new extension from the blueprint. + * + * You must either pass `params` directly, or define a `factory` that can + * optionally call the original factory with the same params. + */ + makeWithOverrides< + TNewNamespace extends string | undefined, + TNewName extends string | undefined, + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs & { + [KName in keyof TInputs]?: `Error: Input '${KName & + string}' is already defined in parent definition`; + }; + output?: Array; + config?: { + schema: TExtensionConfigSchema & { + [KName in keyof TConfig]?: `Error: Config key '${KName & + string}' is already defined in parent schema`; + }; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: TConfig; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: TConfig & { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + }; + inputs: Expand>; + }, + ): Iterable & + VerifyExtensionFactoryOutput< + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + UFactoryOutput + >; + }): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & TConfig, + z.input< + z.ZodObject<{ + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + }> + > & + TConfigInput, + AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, + TInputs & TExtraInputs, + TKind, + string | undefined extends TNewNamespace ? TNamespace : TNewNamespace, + string | undefined extends TNewName ? TName : TNewName + >; +} + +/** @internal */ +export function createDataContainer( + values: Iterable< + UData extends ExtensionDataRef + ? ExtensionDataValue + : never + >, +): ExtensionDataContainer { + const container = new Map>(); + + for (const output of values) { + container.set(output.id, output); + } + + return { + get(ref) { + return container.get(ref.id)?.value; + }, + [Symbol.iterator]() { + return container.values(); + }, + } as ExtensionDataContainer; +} + +/** + * @internal + */ +class ExtensionBlueprintImpl< + TKind extends string, + TNamespace extends string | undefined, + TName extends string | undefined, + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, + TDataRefs extends { [name in string]: AnyExtensionDataRef }, +> { + constructor( + private readonly options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, + TParams, + UOutput, + TInputs, + TConfigSchema, + any, + TDataRefs + >, + ) { + this.dataRefs = options.dataRefs!; + } + + dataRefs: TDataRefs; + + public makeWithOverrides< + TExtensionConfigSchema extends { + [key in string]: (zImpl: typeof z) => z.ZodType; + }, + UFactoryOutput extends ExtensionDataValue, + UNewOutput extends AnyExtensionDataRef, + TExtraInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + inputs?: TExtraInputs; + output?: Array; + config?: { + schema: TExtensionConfigSchema; + }; + factory( + originalFactory: ( + params: TParams, + context?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }, + ) => ExtensionDataContainer, + context: { + node: AppNode; + config: { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer>; + }; + inputs: Expand>; + }, + ): Iterable; + }): ExtensionDefinition< + { + [key in keyof TExtensionConfigSchema]: z.infer< + ReturnType + >; + } & { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject< + { + [key in keyof TExtensionConfigSchema]: ReturnType< + TExtensionConfigSchema[key] + >; + } & { + [key in keyof TConfigSchema]: ReturnType; + } + > + > + > { + const schema = { + ...this.options.config?.schema, + ...args.config?.schema, + } as TConfigSchema & TExtensionConfigSchema; + + return createExtension({ + kind: this.options.kind, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: { ...args.inputs, ...this.options.inputs }, + output: args.output ?? this.options.output, + config: Object.keys(schema).length === 0 ? undefined : { schema }, + factory: ({ node, config, inputs }) => { + return args.factory( + ( + innerParams: TParams, + innerContext?: { + config?: { + [key in keyof TConfigSchema]: z.infer< + ReturnType + >; + }; + inputs?: Expand>; + }, + ): ExtensionDataContainer => { + return createDataContainer( + this.options.factory(innerParams, { + node, + config: innerContext?.config ?? config, + inputs: (innerContext?.inputs ?? inputs) as any, // TODO: Fix the way input values are overridden + }), + ); + }, + { + node, + config, + inputs, + }, + ); + }, + } as CreateExtensionOptions); + } + + public make< + TNewNamespace extends string | undefined = undefined, + TNewName extends string | undefined = undefined, + >(args: { + namespace?: TNewNamespace; + name?: TNewName; + attachTo?: { id: string; input: string }; + disabled?: boolean; + params: TParams; + }): ExtensionDefinition< + { + [key in keyof TConfigSchema]: z.infer>; + }, + z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + > + > { + return createExtension({ + kind: this.options.kind, + namespace: args.namespace ?? this.options.namespace, + name: args.name ?? this.options.name, + attachTo: args.attachTo ?? this.options.attachTo, + disabled: args.disabled ?? this.options.disabled, + inputs: this.options.inputs, + output: this.options.output, + config: this.options.config, + factory: ctx => this.options.factory(args.params, ctx), + } as CreateExtensionOptions); + } +} + +/** + * A simpler replacement for wrapping up `createExtension` inside a kind or type. This allows for a cleaner API for creating + * types and instances of those types. + * + * @public + */ +export function createExtensionBlueprint< + TParams, + UOutput extends AnyExtensionDataRef, + TInputs extends { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }, + TConfigSchema extends { [key in string]: (zImpl: typeof z) => z.ZodType }, + UFactoryOutput extends ExtensionDataValue, + TKind extends string, + TNamespace extends string | undefined = undefined, + TName extends string | undefined = undefined, + TDataRefs extends { [name in string]: AnyExtensionDataRef } = never, +>( + options: CreateExtensionBlueprintOptions< + TKind, + TNamespace, + TName, + TParams, + UOutput, + TInputs, + TConfigSchema, + UFactoryOutput, + TDataRefs + >, +): ExtensionBlueprint< + TKind, + TNamespace, + TName, + TParams, + UOutput, + string extends keyof TInputs ? {} : TInputs, + string extends keyof TConfigSchema + ? {} + : { [key in keyof TConfigSchema]: z.infer> }, + string extends keyof TConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + TDataRefs +> { + return new ExtensionBlueprintImpl(options) as ExtensionBlueprint< + TKind, + TNamespace, + TName, + TParams, + UOutput, + string extends keyof TInputs ? {} : TInputs, + string extends keyof TConfigSchema + ? {} + : { + [key in keyof TConfigSchema]: z.infer>; + }, + string extends keyof TConfigSchema + ? {} + : z.input< + z.ZodObject<{ + [key in keyof TConfigSchema]: ReturnType; + }> + >, + TDataRefs + >; +} diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts index f7a0dc82d0..4b652298f1 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.test.ts @@ -14,10 +14,22 @@ * limitations under the License. */ -import { createExtensionDataRef } from './createExtensionDataRef'; +import { + ExtensionDataValue, + createExtensionDataRef, +} from './createExtensionDataRef'; describe('createExtensionDataRef', () => { it('can be created and read', () => { + const ref = createExtensionDataRef().with({ id: 'foo' }); + expect(ref.id).toBe('foo'); + expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); + const refOptional = ref.optional(); + expect(refOptional.id).toBe('foo'); + expect(String(refOptional)).toBe('ExtensionDataRef{id=foo,optional=true}'); + }); + + it('can be created and read in the deprecated way', () => { const ref = createExtensionDataRef('foo'); expect(ref.id).toBe('foo'); expect(String(ref)).toBe('ExtensionDataRef{id=foo,optional=false}'); @@ -25,4 +37,18 @@ describe('createExtensionDataRef', () => { expect(refOptional.id).toBe('foo'); expect(String(refOptional)).toBe('ExtensionDataRef{id=foo,optional=true}'); }); + + it('can be used to encapsulate a value', () => { + const ref = createExtensionDataRef().with({ id: 'foo' }); + const val: ExtensionDataValue = ref('hello'); + expect(val).toEqual({ + $$type: '@backstage/ExtensionDataValue', + id: 'foo', + value: 'hello', + }); + // @ts-expect-error + ref(3); + // @ts-expect-error + ref(); + }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts index a7f4e4374e..c77aa8c394 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionDataRef.ts @@ -14,43 +14,101 @@ * limitations under the License. */ +/** @public */ +export type ExtensionDataValue = { + readonly $$type: '@backstage/ExtensionDataValue'; + readonly id: TId; + readonly value: TData; +}; + /** @public */ export type ExtensionDataRef< TData, + TId extends string = string, TConfig extends { optional?: true } = {}, > = { - id: string; - T: TData; - config: TConfig; - $$type: '@backstage/ExtensionDataRef'; + readonly $$type: '@backstage/ExtensionDataRef'; + readonly id: TId; + readonly T: TData; + readonly config: TConfig; }; +/** @public */ +export type ExtensionDataRefToValue = + TDataRef extends ExtensionDataRef + ? ExtensionDataValue + : never; + +/** @public */ +export type AnyExtensionDataRef = ExtensionDataRef< + unknown, + string, + { optional?: true } +>; + /** @public */ export interface ConfigurableExtensionDataRef< TData, + TId extends string, TConfig extends { optional?: true } = {}, -> extends ExtensionDataRef { - optional(): ConfigurableExtensionDataRef; +> extends ExtensionDataRef { + optional(): ConfigurableExtensionDataRef< + TData, + TId, + TData & { optional: true } + >; + (t: TData): ExtensionDataValue; } -// TODO: change to options object with ID. -/** @public */ +/** + * @public + * @deprecated Use the following form instead: `createExtensionDataRef().with({ id: 'core.foo' })` + */ export function createExtensionDataRef( id: string, -): ConfigurableExtensionDataRef { +): ConfigurableExtensionDataRef; +/** @public */ +export function createExtensionDataRef(): { + with(options: { + id: TId; + }): ConfigurableExtensionDataRef; +}; +export function createExtensionDataRef(id?: string): + | ConfigurableExtensionDataRef + | { + with(options: { + id: TId; + }): ConfigurableExtensionDataRef; + } { + const createRef = (refId: TId) => + Object.assign( + (value: TData): ExtensionDataValue => ({ + $$type: '@backstage/ExtensionDataValue', + id: refId, + value, + }), + { + id: refId, + $$type: '@backstage/ExtensionDataRef', + config: {}, + optional() { + return { + ...this, + config: { ...this.config, optional: true }, + }; + }, + toString() { + const optional = Boolean(this.config.optional); + return `ExtensionDataRef{id=${refId},optional=${optional}}`; + }, + } as ConfigurableExtensionDataRef, + ); + if (id) { + return createRef(id); + } return { - id, - $$type: '@backstage/ExtensionDataRef', - config: {}, - optional() { - return { - ...this, - config: { ...this.config, optional: true }, - }; + with(options: { id: TId }) { + return createRef(options.id); }, - toString() { - const optional = Boolean(this.config.optional); - return `ExtensionDataRef{id=${id},optional=${optional}}`; - }, - } as ConfigurableExtensionDataRef; + }; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts new file mode 100644 index 0000000000..e401adc675 --- /dev/null +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright 2023 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 { createExtensionDataRef } from './createExtensionDataRef'; +import { + ExtensionInput, + LegacyExtensionInput, + createExtensionInput, +} from './createExtensionInput'; + +const stringDataRef = createExtensionDataRef().with({ id: 'str' }); +const numberDataRef = createExtensionDataRef().with({ id: 'num' }); + +function unused(..._any: any[]) {} + +describe('createExtensionInput', () => { + it('should create a regular input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef]); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: false, optional: false }, + }); + + const x1: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + typeof stringDataRef, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + + it('should create a singleton input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef], { + singleton: true, + }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: true, optional: false }, + }); + + const x1: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + typeof stringDataRef, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + + it('should create an optional singleton input', () => { + const input = createExtensionInput([stringDataRef, numberDataRef], { + singleton: true, + optional: true, + }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: [stringDataRef, numberDataRef], + config: { singleton: true, optional: true }, + }); + + const x1: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: true; optional: true } + > = input; + // @ts-expect-error + const x2: ExtensionInput< + typeof stringDataRef, + { singleton: true; optional: true } + > = input; + // @ts-expect-error + const x3: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x4: ExtensionInput< + typeof stringDataRef | typeof numberDataRef, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + + it('should not allow duplicate data refs', () => { + expect(() => + createExtensionInput([stringDataRef, stringDataRef], { singleton: true }), + ).toThrow("ExtensionInput may not have duplicate data refs: 'str'"); + }); + + describe('old api', () => { + it('should create a regular input', () => { + const input = createExtensionInput({ + str: stringDataRef, + num: numberDataRef, + }); + expect(input).toEqual({ + $$type: '@backstage/ExtensionInput', + extensionData: { str: stringDataRef, num: numberDataRef }, + config: { singleton: false, optional: false }, + }); + + const x1: LegacyExtensionInput< + { str: typeof stringDataRef; num: typeof numberDataRef }, + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x2: LegacyExtensionInput< + { str: typeof numberDataRef; num: typeof stringDataRef }, // switched + { singleton: false; optional: false } + > = input; + // @ts-expect-error + const x3: LegacyExtensionInput< + { str: typeof stringDataRef; num: typeof numberDataRef }, + { singleton: true; optional: false } + > = input; + // @ts-expect-error + const x4: LegacyExtensionInput< + { str: typeof stringDataRef; num: typeof numberDataRef }, + { singleton: false; optional: true } + > = input; + + unused(x1, x2, x3, x4); + }); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts index 1fd8fea972..b7efb13e6e 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionInput.ts @@ -15,31 +15,104 @@ */ import { AnyExtensionDataMap } from './createExtension'; +import { ExtensionDataRef } from './createExtensionDataRef'; /** @public */ export interface ExtensionInput< - TExtensionData extends AnyExtensionDataMap, + TExtensionData extends ExtensionDataRef, TConfig extends { singleton: boolean; optional: boolean }, > { $$type: '@backstage/ExtensionInput'; - extensionData: TExtensionData; + extensionData: Array; config: TConfig; } -/** @public */ +/** + * @public + * @deprecated This type will be removed. Use `ExtensionInput` instead. + */ +export interface LegacyExtensionInput< + TExtensionDataMap extends AnyExtensionDataMap, + TConfig extends { singleton: boolean; optional: boolean }, +> { + $$type: '@backstage/ExtensionInput'; + extensionData: TExtensionDataMap; + config: TConfig; +} + +/** + * @public + * @deprecated Use the following form instead: `createExtensionInput([dataRef1, dataRef2])` + */ export function createExtensionInput< - TExtensionData extends AnyExtensionDataMap, + TExtensionDataMap extends AnyExtensionDataMap, TConfig extends { singleton?: boolean; optional?: boolean }, >( - extensionData: TExtensionData, + extensionData: TExtensionDataMap, config?: TConfig, -): ExtensionInput< - TExtensionData, +): LegacyExtensionInput< + TExtensionDataMap, { singleton: TConfig['singleton'] extends true ? true : false; optional: TConfig['optional'] extends true ? true : false; } -> { +>; +/** @public */ +export function createExtensionInput< + UExtensionData extends ExtensionDataRef, + TConfig extends { singleton?: boolean; optional?: boolean }, +>( + extensionData: Array, + config?: TConfig, +): ExtensionInput< + UExtensionData, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } +>; +export function createExtensionInput< + TExtensionData extends ExtensionDataRef, + TExtensionDataMap extends AnyExtensionDataMap, + TConfig extends { singleton?: boolean; optional?: boolean }, +>( + extensionData: Array | TExtensionDataMap, + config?: TConfig, +): + | LegacyExtensionInput< + TExtensionDataMap, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } + > + | ExtensionInput< + TExtensionData, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } + > { + if (process.env.NODE_ENV !== 'production') { + if (Array.isArray(extensionData)) { + const seen = new Set(); + const duplicates = []; + for (const dataRef of extensionData) { + if (seen.has(dataRef.id)) { + duplicates.push(dataRef.id); + } else { + seen.add(dataRef.id); + } + } + if (duplicates.length > 0) { + throw new Error( + `ExtensionInput may not have duplicate data refs: '${duplicates.join( + "', '", + )}'`, + ); + } + } + } return { $$type: '@backstage/ExtensionInput', extensionData, @@ -51,5 +124,19 @@ export function createExtensionInput< ? true : false, }, - }; + } as + | LegacyExtensionInput< + TExtensionDataMap, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } + > + | ExtensionInput< + TExtensionData, + { + singleton: TConfig['singleton'] extends true ? true : false; + optional: TConfig['optional'] extends true ? true : false; + } + >; } diff --git a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts index 1f88038944..75c43a36d2 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtensionOverrides.ts @@ -23,7 +23,7 @@ import { ExtensionOverrides, FeatureFlagConfig } from './types'; /** @public */ export interface ExtensionOverridesOptions { - extensions: ExtensionDefinition[]; + extensions: ExtensionDefinition[]; featureFlags?: FeatureFlagConfig[]; } diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index dab8a9778a..76cc030be3 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -27,16 +27,16 @@ import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import { createExtensionInput } from './createExtensionInput'; import { BackstagePlugin } from './types'; -const nameExtensionDataRef = createExtensionDataRef('name'); +const nameExtensionDataRef = createExtensionDataRef().with({ + id: 'name', +}); const Extension1 = createExtension({ name: '1', attachTo: { id: 'test/output', input: 'names' }, - output: { - name: nameExtensionDataRef, - }, + output: [nameExtensionDataRef], factory() { - return { name: 'extension-1' }; + return [nameExtensionDataRef('extension-1')]; }, }); @@ -148,6 +148,10 @@ describe('createPlugin', () => { }); expect(plugin).toBeDefined(); + expect(plugin.getExtension('test/1')).toBe(Extension1); + // @ts-expect-error + expect(plugin.getExtension('nonexistent')).toBeUndefined(); + await renderWithEffects( createTestAppRoot({ features: [plugin], diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.ts index 797c46e58e..1d2a12ffe2 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.ts @@ -17,6 +17,7 @@ import { ExtensionDefinition } from './createExtension'; import { Extension, + ResolveExtensionId, resolveExtensionDefinition, } from './resolveExtensionDefinition'; import { @@ -28,21 +29,23 @@ import { /** @public */ export interface PluginOptions< - Routes extends AnyRoutes, - ExternalRoutes extends AnyExternalRoutes, + TId extends string, + TRoutes extends AnyRoutes, + TExternalRoutes extends AnyExternalRoutes, + TExtensions extends readonly ExtensionDefinition[], > { - id: string; - routes?: Routes; - externalRoutes?: ExternalRoutes; - extensions?: ExtensionDefinition[]; + id: TId; + routes?: TRoutes; + externalRoutes?: TExternalRoutes; + extensions?: TExtensions; featureFlags?: FeatureFlagConfig[]; } /** @public */ export interface InternalBackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, -> extends BackstagePlugin { + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, +> extends BackstagePlugin { readonly version: 'v1'; readonly extensions: Extension[]; readonly featureFlags: FeatureFlagConfig[]; @@ -50,17 +53,36 @@ export interface InternalBackstagePlugin< /** @public */ export function createPlugin< - Routes extends AnyRoutes = {}, - ExternalRoutes extends AnyExternalRoutes = {}, + TId extends string, + TRoutes extends AnyRoutes = {}, + TExternalRoutes extends AnyExternalRoutes = {}, + TExtensions extends readonly ExtensionDefinition[] = [], >( - options: PluginOptions, -): BackstagePlugin { - const extensions = (options.extensions ?? []).map(def => - resolveExtensionDefinition(def, { namespace: options.id }), - ); + options: PluginOptions, +): BackstagePlugin< + TRoutes, + TExternalRoutes, + { + [KExtension in TExtensions[number] as ResolveExtensionId< + KExtension, + TId + >]: KExtension; + } +> { + const extensions = new Array>(); + const extensionDefinitionsById = new Map< + string, + ExtensionDefinition + >(); - const extensionIds = extensions.map(e => e.id); - if (extensionIds.length !== new Set(extensionIds).size) { + for (const def of options.extensions ?? []) { + const ext = resolveExtensionDefinition(def, { namespace: options.id }); + extensions.push(ext); + extensionDefinitionsById.set(ext.id, def); + } + + if (extensions.length !== extensionDefinitionsById.size) { + const extensionIds = extensions.map(e => e.id); const duplicates = Array.from( new Set( extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index), @@ -78,14 +100,17 @@ export function createPlugin< $$type: '@backstage/BackstagePlugin', version: 'v1', id: options.id, - routes: options.routes ?? ({} as Routes), - externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes), + routes: options.routes ?? ({} as TRoutes), + externalRoutes: options.externalRoutes ?? ({} as TExternalRoutes), featureFlags: options.featureFlags ?? [], extensions, + getExtension(id) { + return extensionDefinitionsById.get(id); + }, toString() { return `Plugin{id=${options.id}}`; }, - } as InternalBackstagePlugin; + } as InternalBackstagePlugin; } /** @internal */ diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 61f821c7ce..a17dfedc16 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -17,21 +17,27 @@ export { coreExtensionData } from './coreExtensionData'; export { createExtension, + type ExtensionDataContainer, type ExtensionDefinition, type CreateExtensionOptions, type ExtensionDataValues, type ResolvedExtensionInput, type ResolvedExtensionInputs, + type LegacyCreateExtensionOptions, type AnyExtensionInputMap, type AnyExtensionDataMap, } from './createExtension'; export { createExtensionInput, type ExtensionInput, + type LegacyExtensionInput, } from './createExtensionInput'; export { createExtensionDataRef, + type AnyExtensionDataRef, type ExtensionDataRef, + type ExtensionDataRefToValue, + type ExtensionDataValue, type ConfigurableExtensionDataRef, } from './createExtensionDataRef'; export { createPlugin, type PluginOptions } from './createPlugin'; @@ -48,3 +54,8 @@ export { type FeatureFlagConfig, type FrontendFeature, } from './types'; +export { + type CreateExtensionBlueprintOptions, + type ExtensionBlueprint, + createExtensionBlueprint, +} from './createExtensionBlueprint'; diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts index 8970cfe168..5a42e9100e 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.test.ts @@ -15,16 +15,20 @@ */ import { ExtensionDefinition } from './createExtension'; -import { resolveExtensionDefinition } from './resolveExtensionDefinition'; - -const baseDef = { - $$type: '@backstage/ExtensionDefinition', - version: 'v1', - attachTo: { id: '', input: '' }, - disabled: false, -}; +import { + ResolveExtensionId, + resolveExtensionDefinition, +} from './resolveExtensionDefinition'; describe('resolveExtensionDefinition', () => { + const baseDef = { + $$type: '@backstage/ExtensionDefinition', + version: 'v2', + attachTo: { id: '', input: '' }, + disabled: false, + override: () => ({} as ExtensionDefinition), + }; + it.each([ [{ namespace: 'ns' }, 'ns'], [{ namespace: 'n' }, 'n'], @@ -56,3 +60,144 @@ describe('resolveExtensionDefinition', () => { ); }); }); + +describe('old resolveExtensionDefinition', () => { + const baseDef = { + $$type: '@backstage/ExtensionDefinition', + version: 'v1', + attachTo: { id: '', input: '' }, + disabled: false, + override: () => ({} as ExtensionDefinition), + }; + + it.each([ + [{ namespace: 'ns' }, 'ns'], + [{ namespace: 'n' }, 'n'], + [{ namespace: 'ns', name: 'n' }, 'ns/n'], + [{ kind: 'k', namespace: 'ns' }, 'k:ns'], + [{ kind: 'k', namespace: 'ns', name: 'n' }, 'k:ns/n'], + ])(`should resolve extension IDs %s`, (definition, expected) => { + const resolved = resolveExtensionDefinition({ + ...baseDef, + ...definition, + } as ExtensionDefinition); + expect(resolved.id).toBe(expected); + expect(String(resolved)).toBe(`Extension{id=${expected}}`); + }); + + it('should fail to resolve extension ID without namespace', () => { + expect(() => + resolveExtensionDefinition({ + ...baseDef, + kind: 'k', + } as ExtensionDefinition), + ).toThrow( + 'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=k namespace=undefined name=undefined', + ); + expect(() => + resolveExtensionDefinition(baseDef as ExtensionDefinition), + ).toThrow( + 'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=undefined namespace=undefined name=undefined', + ); + }); +}); + +describe('ResolveExtensionId', () => { + it('should resolve extension IDs correctly', () => { + type NamedExtension< + TKind extends string | undefined, + TNamespace extends string | undefined, + TName extends string | undefined, + > = ExtensionDefinition; + + const id1: 'k:ns' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + undefined + >; + const id2: 'k:n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + undefined + >; + const id3: 'ns/n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id4: never = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + undefined + >; + const id5: 'ns' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id6: 'n' = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const id7: 'k:ns/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + undefined + >; + const id8: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', undefined>, + 'ns2' + >; + const id9: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, 'n'>, + 'ns2' + >; + const ida: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idb: 'k:ns2' = {} as ResolveExtensionId< + NamedExtension<'k', undefined, undefined>, + 'ns2' + >; + const idc: 'ns2' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const idd: 'ns2/n' = {} as ResolveExtensionId< + NamedExtension, + 'ns2' + >; + const ide: 'k:ns2/n' = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', 'n'>, + 'ns2' + >; + + const invalid1: never = {} as ResolveExtensionId< + NamedExtension, + undefined + >; + const invalid2: never = {} as ResolveExtensionId< + NamedExtension<'k', string | undefined, 'n'>, + undefined + >; + const invalid3: never = {} as ResolveExtensionId< + NamedExtension<'k', 'ns', string | undefined>, + undefined + >; + + expect([ + id1, + id2, + id3, + id4, + id5, + id6, + id7, + id8, + id9, + ida, + idb, + idc, + idd, + ide, + invalid1, + invalid2, + invalid3, + ]).toBeDefined(); + }); +}); diff --git a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts index 0a44de4ab4..35bf9c6b05 100644 --- a/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts +++ b/packages/frontend-plugin-api/src/wiring/resolveExtensionDefinition.ts @@ -24,53 +24,119 @@ import { toInternalExtensionDefinition, } from './createExtension'; import { PortableSchema } from '../schema'; +import { ExtensionInput } from './createExtensionInput'; +import { + AnyExtensionDataRef, + ExtensionDataValue, +} from './createExtensionDataRef'; /** @public */ -export interface Extension { +export interface Extension { $$type: '@backstage/Extension'; readonly id: string; readonly attachTo: { id: string; input: string }; readonly disabled: boolean; - readonly configSchema?: PortableSchema; + readonly configSchema?: PortableSchema; } /** @internal */ -export interface InternalExtension extends Extension { - readonly version: 'v1'; - readonly inputs: AnyExtensionInputMap; - readonly output: AnyExtensionDataMap; - factory(options: { - node: AppNode; - config: TConfig; - inputs: ResolvedExtensionInputs; - }): ExtensionDataValues; -} +export type InternalExtension = Extension< + TConfig, + TConfigInput +> & + ( + | { + readonly version: 'v1'; + readonly inputs: AnyExtensionInputMap; + readonly output: AnyExtensionDataMap; + factory(options: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs; + }): ExtensionDataValues; + } + | { + readonly version: 'v2'; + readonly inputs: { + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }; + readonly output: Array; + factory(options: { + node: AppNode; + config: TConfig; + inputs: ResolvedExtensionInputs<{ + [inputName in string]: ExtensionInput< + AnyExtensionDataRef, + { optional: boolean; singleton: boolean } + >; + }>; + }): Iterable>; + } + ); /** @internal */ -export function toInternalExtension( - overrides: Extension, -): InternalExtension { - const internal = overrides as InternalExtension; +export function toInternalExtension( + overrides: Extension, +): InternalExtension { + const internal = overrides as InternalExtension; if (internal.$$type !== '@backstage/Extension') { throw new Error( `Invalid extension instance, bad type '${internal.$$type}'`, ); } - if (internal.version !== 'v1') { - throw new Error( - `Invalid extension instance, bad version '${internal.version}'`, - ); + const version = internal.version; + if (version !== 'v1' && version !== 'v2') { + throw new Error(`Invalid extension instance, bad version '${version}'`); } return internal; } +/** @ignore */ +export type ResolveExtensionId< + TExtension extends ExtensionDefinition, + TDefaultNamespace extends string | undefined, +> = TExtension extends ExtensionDefinition< + any, + any, + any, + any, + infer IKind, + infer INamespace, + infer IName +> + ? [string | undefined] extends [IKind | INamespace | IName] + ? never + : ( + ( + undefined extends TDefaultNamespace ? INamespace : TDefaultNamespace + ) extends infer ISelectedNamespace extends string + ? undefined extends IName + ? ISelectedNamespace + : `${ISelectedNamespace}/${IName}` + : IName + ) extends infer INamePart extends string + ? IKind extends string + ? `${IKind}:${INamePart}` + : INamePart + : never + : never; + /** @internal */ -export function resolveExtensionDefinition( - definition: ExtensionDefinition, +export function resolveExtensionDefinition( + definition: ExtensionDefinition, context?: { namespace?: string }, -): Extension { +): Extension { const internalDefinition = toInternalExtensionDefinition(definition); - const { name, kind, namespace: _, ...rest } = internalDefinition; + const { + name, + kind, + namespace: _skip1, + override: _skip2, + ...rest + } = internalDefinition; const namespace = internalDefinition.namespace ?? context?.namespace; const namePart = @@ -86,10 +152,10 @@ export function resolveExtensionDefinition( return { ...rest, $$type: '@backstage/Extension', - version: 'v1', + version: internalDefinition.version, id, toString() { return `Extension{id=${id}}`; }, - } as Extension; + } as InternalExtension; } diff --git a/packages/frontend-plugin-api/src/wiring/types.ts b/packages/frontend-plugin-api/src/wiring/types.ts index 7e0650e06c..b1afd4e7ea 100644 --- a/packages/frontend-plugin-api/src/wiring/types.ts +++ b/packages/frontend-plugin-api/src/wiring/types.ts @@ -15,6 +15,7 @@ */ import { ExternalRouteRef, RouteRef, SubRouteRef } from '../routing'; +import { ExtensionDefinition } from './createExtension'; /** * Feature flag configuration. @@ -32,15 +33,24 @@ export type AnyRoutes = { [name in string]: RouteRef | SubRouteRef }; /** @public */ export type AnyExternalRoutes = { [name in string]: ExternalRouteRef }; +/** @public */ +export type ExtensionMap< + TExtensionMap extends { [id in string]: ExtensionDefinition }, +> = { + get(id: TId): TExtensionMap[TId]; +}; + /** @public */ export interface BackstagePlugin< - Routes extends AnyRoutes = AnyRoutes, - ExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TRoutes extends AnyRoutes = AnyRoutes, + TExternalRoutes extends AnyExternalRoutes = AnyExternalRoutes, + TExtensionMap extends { [id in string]: ExtensionDefinition } = {}, > { readonly $$type: '@backstage/BackstagePlugin'; readonly id: string; - readonly routes: Routes; - readonly externalRoutes: ExternalRoutes; + readonly routes: TRoutes; + readonly externalRoutes: TExternalRoutes; + getExtension(id: TId): TExtensionMap[TId]; } /** @public */ diff --git a/packages/frontend-test-utils/CHANGELOG.md b/packages/frontend-test-utils/CHANGELOG.md index db6adef523..4ba38f5065 100644 --- a/packages/frontend-test-utils/CHANGELOG.md +++ b/packages/frontend-test-utils/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/frontend-test-utils +## 0.1.12-next.2 + +### Patch Changes + +- 8209449: Added new APIs for testing extensions +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/frontend-app-api@0.7.5-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.1.12-next.1 + +### Patch Changes + +- 3be9aeb: Added support for v2 extensions, which declare their inputs and outputs without using a data map. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/frontend-app-api@0.7.5-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/types@1.1.1 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/frontend-app-api@0.7.4-next.0 + - @backstage/test-utils@1.5.9-next.0 + - @backstage/types@1.1.1 + +## 0.1.10 + +### Patch Changes + +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- Updated dependencies + - @backstage/frontend-app-api@0.7.3 + - @backstage/test-utils@1.5.8 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/types@1.1.1 + +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.3-next.2 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.3-next.1 + - @backstage/test-utils@1.5.8-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/types@1.1.1 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-app-api@0.7.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/test-utils@1.5.7-next.0 + - @backstage/types@1.1.1 + ## 0.1.8 ### Patch Changes diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 2605499a96..3aa9069219 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -7,7 +7,10 @@ import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; +import { AppNode } from '@backstage/frontend-plugin-api'; +import { AppNodeInstance } from '@backstage/frontend-plugin-api'; import { ErrorWithContext } from '@backstage/test-utils'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JsonObject } from '@backstage/types'; import { MockConfigApi } from '@backstage/test-utils'; @@ -18,9 +21,9 @@ import { MockFetchApiOptions } from '@backstage/test-utils'; import { MockPermissionApi } from '@backstage/test-utils'; import { MockStorageApi } from '@backstage/test-utils'; import { MockStorageBucket } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { RenderResult } from '@testing-library/react'; import { RouteRef } from '@backstage/frontend-plugin-api'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; import { TestApiProvider } from '@backstage/test-utils'; import { TestApiProviderProps } from '@backstage/test-utils'; import { TestApiRegistry } from '@backstage/test-utils'; @@ -36,16 +39,31 @@ export function createExtensionTester( export { ErrorWithContext }; +// @public (undocumented) +export class ExtensionQuery { + constructor(node: AppNode); + // (undocumented) + data(ref: ExtensionDataRef): T | undefined; + // (undocumented) + get instance(): AppNodeInstance; + // (undocumented) + get node(): AppNode; +} + // @public (undocumented) export class ExtensionTester { // (undocumented) - add( - extension: ExtensionDefinition, + add( + extension: ExtensionDefinition, options?: { - config?: TConfig; + config?: TConfigInput; }, ): ExtensionTester; // (undocumented) + data(ref: ExtensionDataRef): T | undefined; + // (undocumented) + query(id: string | ExtensionDefinition): ExtensionQuery; + // (undocumented) render(options?: { config?: JsonObject }): RenderResult; } @@ -73,13 +91,20 @@ export { MockStorageApi }; export { MockStorageBucket }; +export { registerMswTestHooks }; + // @public export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, ): RenderResult; -export { setupRequestMockHandlers }; +// @public @deprecated (undocumented) +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; export { TestApiProvider }; diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index f51df52828..e8152cfdda 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/frontend-test-utils", - "version": "0.1.8", + "version": "0.1.12-next.2", "backstage": { "role": "web-library" }, @@ -31,6 +31,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/config": "workspace:^", "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index f59d44f6d7..889c815fc7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -24,6 +24,8 @@ import { createApiExtension, createApiFactory, createExtension, + createExtensionDataRef, + createExtensionInput, createSchemaFromZod, useAnalytics, useApi, @@ -31,6 +33,10 @@ import { import { MockAnalyticsApi } from '../apis'; import { createExtensionTester } from './createExtensionTester'; +const stringDataRef = createExtensionDataRef().with({ + id: 'test.string', +}); + describe('createExtensionTester', () => { const defaultDefinition = { namespace: 'test', @@ -229,4 +235,133 @@ describe('createExtensionTester', () => { ), ); }); + + it('should return the correct dataRef when called', () => { + const extension = createExtension({ + namespace: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension); + + expect(tester.data(stringDataRef)).toBe('test-text'); + }); + + it('should throw an error if trying to access an instance not provided to the tester', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension); + + expect(() => tester.query(extension2)).toThrow( + "Extension with ID 'test/e2' not found, please make sure it's added to the tester", + ); + }); + + it('should throw an error if trying to access an instance which is not part of the tree', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension).add(extension2); + + expect(() => tester.query(extension2)).toThrow( + "Extension with ID 'test/e2' has not been instantiated, because it is not part of the test subject's extension tree", + ); + }); + + // TODO: this should be implemented + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should allow querying an extension and getting outputs', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + inputs: { + input: createExtensionInput( + { + output: stringDataRef, + }, + { singleton: true }, + ), + }, + factory: ({ inputs }) => ({ + text: `nest-${inputs.input.output.output}`, + }), + }); + + const extension2 = createExtension({ + namespace: 'test', + name: 'e2', + attachTo: { id: 'test/e1', input: 'blob' }, + output: { text: stringDataRef }, + factory: () => ({ text: 'test-text' }), + }); + + const tester = createExtensionTester(extension).add(extension2); + + expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension2).data(stringDataRef)).toBe('test-text'); + // @ts-expect-error + expect(tester.query(extension).input('input').data(stringDataRef)).toBe( + 'nest-test-text', + ); + }); + + // TODO: this should be implemented + // eslint-disable-next-line jest/no-disabled-tests + it.skip('should allow defining inputs to extensions without a corresponding extension definition', () => { + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { text: stringDataRef }, + inputs: { + input: createExtensionInput( + { + output: stringDataRef, + }, + { singleton: true }, + ), + }, + factory: ({ inputs }) => ({ + text: `nest-${inputs.input.output.output}`, + }), + }); + + const tester = createExtensionTester(extension, { + // @ts-expect-error + inputs: { input: 'test-text' }, + }); + + expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index a98879fbba..4bfc8392cb 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -19,6 +19,10 @@ import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { + AppNode, + AppTree, + Extension, + ExtensionDataRef, ExtensionDefinition, IconComponent, RouteRef, @@ -30,12 +34,20 @@ import { createRouterExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; -import { MockConfigApi } from '@backstage/test-utils'; +import { Config, ConfigReader } from '@backstage/config'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { toInternalExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/createExtension'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { resolveAppTree } from '../../../frontend-app-api/src/tree/resolveAppTree'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveAppNodeSpecs'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; const NavItem = (props: { routeRef: RouteRef; @@ -43,10 +55,13 @@ const NavItem = (props: { icon: IconComponent; }) => { const { routeRef, title, icon: Icon } = props; - const to = useRouteRef(routeRef)(); + const link = useRouteRef(routeRef); + if (!link) { + return null; + } return (
  • - + {title}
  • @@ -85,42 +100,105 @@ const TestAppNavExtension = createExtension({ }, }); +/** @public */ +export class ExtensionQuery { + #node: AppNode; + + constructor(node: AppNode) { + this.#node = node; + } + + get node() { + return this.#node; + } + + get instance() { + const instance = this.#node.instance; + if (!instance) { + throw new Error( + `Unable to access the instance of extension with ID '${ + this.#node.spec.id + }'`, + ); + } + return instance; + } + + data(ref: ExtensionDataRef): T | undefined { + return this.instance.getData(ref); + } +} + /** @public */ export class ExtensionTester { /** @internal */ - static forSubject( - subject: ExtensionDefinition, - options?: { config?: TConfig }, + static forSubject( + subject: ExtensionDefinition, + options?: { config?: TConfigInput }, ): ExtensionTester { const tester = new ExtensionTester(); - const { output, factory, ...rest } = toInternalExtensionDefinition(subject); + const internal = toInternalExtensionDefinition(subject); + // attaching to app/routes to render as index route - const extension = createExtension({ - ...rest, - attachTo: { id: 'app/routes', input: 'routes' }, - output: { - ...output, - path: coreExtensionData.routePath, - }, - factory: params => ({ - ...factory(params), - path: '/', - }), - }); - tester.add(extension, options); + if (internal.version === 'v1') { + tester.add( + createExtension({ + ...internal, + attachTo: { id: 'app/routes', input: 'routes' }, + output: { + ...internal.output, + path: coreExtensionData.routePath, + }, + factory: params => ({ + ...internal.factory(params as any), + path: '/', + }), + }), + options as TConfigInput & {}, + ); + } else if (internal.version === 'v2') { + tester.add( + createExtension({ + ...internal, + attachTo: { id: 'app/routes', input: 'routes' }, + output: internal.output.find( + ref => ref.id === coreExtensionData.routePath.id, + ) + ? internal.output + : [...internal.output, coreExtensionData.routePath], + factory: params => { + const parentOutput = Array.from( + internal.factory(params as any), + ).filter(val => val.id !== coreExtensionData.routePath.id); + + return [...parentOutput, coreExtensionData.routePath('/')]; + }, + }), + options as TConfigInput & {}, + ); + } return tester; } + #tree?: AppTree; + readonly #extensions = new Array<{ id: string; + extension: Extension; definition: ExtensionDefinition; config?: JsonValue; }>(); - add( - extension: ExtensionDefinition, - options?: { config?: TConfig }, + add( + extension: ExtensionDefinition, + options?: { config?: TConfigInput }, ): ExtensionTester { + if (this.#tree) { + throw new Error( + 'Cannot add more extensions accessing the extension tree', + ); + } + const { name, namespace } = extension; const definition = { @@ -129,10 +207,11 @@ export class ExtensionTester { name: !namespace && !name ? 'test' : name, }; - const { id } = resolveExtensionDefinition(definition); + const resolvedExtension = resolveExtensionDefinition(definition); this.#extensions.push({ - id, + id: resolvedExtension.id, + extension: resolvedExtension, definition, config: options?.config as JsonValue, }); @@ -140,38 +219,42 @@ export class ExtensionTester { return this; } + data(ref: ExtensionDataRef): T | undefined { + const tree = this.#resolveTree(); + + return new ExtensionQuery(tree.root).data(ref); + } + + query(id: string | ExtensionDefinition): ExtensionQuery { + const tree = this.#resolveTree(); + + const actualId = + typeof id === 'string' ? id : resolveExtensionDefinition(id).id; + + const node = tree.nodes.get(actualId); + + if (!node) { + throw new Error( + `Extension with ID '${actualId}' not found, please make sure it's added to the tester.`, + ); + } else if (!node.instance) { + throw new Error( + `Extension with ID '${actualId}' has not been instantiated, because it is not part of the test subject's extension tree.`, + ); + } + return new ExtensionQuery(node); + } + render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; - const [subject, ...rest] = this.#extensions; + const [subject] = this.#extensions; if (!subject) { throw new Error( 'No subject found. At least one extension should be added to the tester.', ); } - const extensionsConfig: JsonArray = [ - ...rest.map(extension => ({ - [extension.id]: { - config: extension.config, - }, - })), - { - [subject.id]: { - config: subject.config, - disabled: false, - }, - }, - ]; - - const finalConfig = { - ...config, - app: { - ...(typeof config.app === 'object' ? config.app : undefined), - extensions: extensionsConfig, - }, - }; - const app = createSpecializedApp({ features: [ createExtensionOverrides({ @@ -187,11 +270,69 @@ export class ExtensionTester { ], }), ], - config: new MockConfigApi(finalConfig), + config: this.#getConfig(config), }); return render(app.createRoot()); } + + #resolveTree() { + if (this.#tree) { + return this.#tree; + } + + const [subject] = this.#extensions; + if (!subject) { + throw new Error( + 'No subject found. At least one extension should be added to the tester.', + ); + } + + const tree = resolveAppTree( + subject.id, + resolveAppNodeSpecs({ + features: [], + builtinExtensions: this.#extensions.map(_ => _.extension), + parameters: readAppExtensionsConfig(this.#getConfig()), + }), + ); + + instantiateAppNodeTree(tree.root); + + this.#tree = tree; + + return tree; + } + + #getConfig(additionalConfig?: JsonObject): Config { + const [subject, ...rest] = this.#extensions; + + const extensionsConfig: JsonArray = [ + ...rest.map(extension => ({ + [extension.id]: { + config: extension.config, + }, + })), + { + [subject.id]: { + config: subject.config, + disabled: false, + }, + }, + ]; + + return ConfigReader.fromConfigs([ + { context: 'render-config', data: additionalConfig ?? {} }, + { + context: 'test', + data: { + app: { + extensions: extensionsConfig, + }, + }, + }, + ]); + } } /** @public */ diff --git a/packages/frontend-test-utils/src/app/index.ts b/packages/frontend-test-utils/src/app/index.ts index 2351561022..6c6ef26e4c 100644 --- a/packages/frontend-test-utils/src/app/index.ts +++ b/packages/frontend-test-utils/src/app/index.ts @@ -17,6 +17,7 @@ export { createExtensionTester, type ExtensionTester, + type ExtensionQuery, } from './createExtensionTester'; export { renderInTestApp, type TestAppOptions } from './renderInTestApp'; diff --git a/packages/frontend-test-utils/src/deprecated.ts b/packages/frontend-test-utils/src/deprecated.ts new file mode 100644 index 0000000000..1b67f73e6b --- /dev/null +++ b/packages/frontend-test-utils/src/deprecated.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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 { registerMswTestHooks } from '@backstage/test-utils'; + +/** + * @public + * @deprecated Use `registerMswTestHooks` from `@backstage/frontend-test-utils` instead. + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void { + registerMswTestHooks(worker); +} diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts index 0dab8e75b7..20f393706e 100644 --- a/packages/frontend-test-utils/src/index.ts +++ b/packages/frontend-test-utils/src/index.ts @@ -20,6 +20,7 @@ * Contains utilities that can be used when testing frontend features such as extensions. */ +export * from './deprecated'; export * from './apis'; export * from './app'; @@ -28,4 +29,4 @@ export type { TestApiProviderProps } from '@backstage/test-utils'; export { withLogCollector } from '@backstage/test-utils'; -export { setupRequestMockHandlers } from '@backstage/test-utils'; +export { registerMswTestHooks } from '@backstage/test-utils'; diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 5952b43cbd..36ecf6f718 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/integration-react +## 1.1.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## 1.1.29 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.13.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## 1.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.13.0-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + ## 1.1.28 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 34e91e0110..de42eb2798 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration-react", - "version": "1.1.28", + "version": "1.1.30-next.0", "description": "Frontend package for managing integrations towards external systems", "backstage": { "role": "web-library" diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index f50b992f3c..b49c3410ec 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/integration +## 1.14.0-next.0 + +### Minor Changes + +- 78c1329: Updated `GitlabUrlReader.readUrl` and `GitlabUrlReader.readTree` to accept a user-provided token, supporting both bearer and private tokens. + +### Patch Changes + +- c591670: Updated functions for `getHarnessEditContentsUrl`, `getHarnessFileContentsUrl`, `getHarnessArchiveUrl`, `getHarnessLatestCommitUrl` and `parseHarnessUrl` to handle account and org level urls +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 1.13.0 + +### Minor Changes + +- b5deed0: Add support for `token` for `bitbucketCloud` integration + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 1.13.0-next.0 + +### Minor Changes + +- b5deed0: Add support for `token` for `bitbucketCloud` integration + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 1.12.0 ### Minor Changes diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index f9a8426cf5..cf4b380ad9 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -186,6 +186,7 @@ export type BitbucketCloudIntegrationConfig = { apiBaseUrl: string; username?: string; appPassword?: string; + token?: string; }; // @public @deprecated @@ -507,7 +508,10 @@ export function getGitLabIntegrationRelativePath( ): string; // @public -export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { +export function getGitLabRequestOptions( + config: GitLabIntegrationConfig, + token?: string, +): { headers: Record; }; diff --git a/packages/integration/package.json b/packages/integration/package.json index 56b7b1bd82..0d1b733bbf 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "1.12.0", + "version": "1.14.0-next.0", "description": "Helpers for managing integrations towards external systems", "backstage": { "role": "common-library" diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 6af974723f..f1b4e04f15 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { BitbucketIntegrationConfig } from './config'; import { getBitbucketDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucket core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getBitbucketRequestOptions', () => { it('inserts a token when needed', () => { diff --git a/packages/integration/src/bitbucketCloud/config.ts b/packages/integration/src/bitbucketCloud/config.ts index 5ecabfedc4..32a2af9313 100644 --- a/packages/integration/src/bitbucketCloud/config.ts +++ b/packages/integration/src/bitbucketCloud/config.ts @@ -46,6 +46,11 @@ export type BitbucketCloudIntegrationConfig = { * See https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/ */ appPassword?: string; + + /** + * The access token to use for requests to Bitbucket Cloud (bitbucket.org). + */ + token?: string; }; /** diff --git a/packages/integration/src/bitbucketCloud/core.test.ts b/packages/integration/src/bitbucketCloud/core.test.ts index 3a342bae8b..56a53b924d 100644 --- a/packages/integration/src/bitbucketCloud/core.test.ts +++ b/packages/integration/src/bitbucketCloud/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { BitbucketCloudIntegrationConfig } from './config'; import { getBitbucketCloudDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucketCloud core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getBitbucketCloudRequestOptions', () => { it('insert basic auth when needed', () => { diff --git a/packages/integration/src/bitbucketServer/core.test.ts b/packages/integration/src/bitbucketServer/core.test.ts index b03a8dda83..c1c72b38dd 100644 --- a/packages/integration/src/bitbucketServer/core.test.ts +++ b/packages/integration/src/bitbucketServer/core.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { BitbucketServerIntegrationConfig } from './config'; import { getBitbucketServerDefaultBranch, @@ -27,7 +27,7 @@ import { describe('bitbucketServer core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getBitbucketServerRequestOptions', () => { it('inserts a token when needed', () => { diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index c2bf5d3ed2..35632c654e 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -17,7 +17,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import fetch from 'cross-fetch'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { GerritIntegrationConfig } from './config'; import { buildGerritGitilesArchiveUrl, @@ -32,7 +32,7 @@ import { describe('gerrit core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('buildGerritGitilesArchiveUrl', () => { const config: GerritIntegrationConfig = { diff --git a/packages/integration/src/gitea/core.test.ts b/packages/integration/src/gitea/core.test.ts index ac29c469b6..e9d7c592f0 100644 --- a/packages/integration/src/gitea/core.test.ts +++ b/packages/integration/src/gitea/core.test.ts @@ -15,7 +15,7 @@ */ import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { GiteaIntegrationConfig } from './config'; import { getGiteaArchiveUrl, @@ -28,7 +28,7 @@ import { describe('gitea core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getGiteaFileCatalogInfoCntentsUrl', () => { it('can create an url from arguments', () => { diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 98fd9b7c13..81adc29701 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -17,7 +17,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { GitLabIntegrationConfig } from './config'; -import { getGitLabFileFetchUrl } from './core'; +import { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; const worker = setupServer(); @@ -186,4 +186,47 @@ describe('gitlab core', () => { }); }); }); + + describe('getGitLabRequestOptions', () => { + it('should return Authorization header when oauthToken is provided', () => { + const token = '1234567890'; + const result = getGitLabRequestOptions( + configSelfHosteWithRelativePath, + token, + ); + + expect(result).toEqual({ + headers: { + Authorization: `Bearer ${token}`, + }, + }); + }); + + it('should return private-token header when gl-token is provided', () => { + const token = 'glpat-1234566'; + const result = getGitLabRequestOptions( + configSelfHosteWithRelativePath, + token, + ); + + expect(result).toEqual({ + headers: { + 'PRIVATE-TOKEN': token, + }, + }); + }); + + it('should return private-token header when oauthToken is undefined', () => { + const oauthToken = undefined; + const result = getGitLabRequestOptions( + configSelfHosteWithRelativePath, + oauthToken, + ); + expect(result).toEqual({ + headers: { + 'PRIVATE-TOKEN': configSelfHosteWithRelativePath.token, + }, + }); + }); + }); }); diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index 040ca202d6..94f4b65d9f 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -14,11 +14,11 @@ * limitations under the License. */ +import fetch from 'cross-fetch'; import { getGitLabIntegrationRelativePath, GitLabIntegrationConfig, } from './config'; -import fetch from 'cross-fetch'; /** * Given a URL pointing to a file on a provider, returns a URL that is suitable @@ -51,14 +51,23 @@ export async function getGitLabFileFetchUrl( * @param config - The relevant provider config * @public */ -export function getGitLabRequestOptions(config: GitLabIntegrationConfig): { - headers: Record; -} { - const { token = '' } = config; +export function getGitLabRequestOptions( + config: GitLabIntegrationConfig, + token?: string, +): { headers: Record } { + if (token) { + // If token comes from the user and starts with "gl", it's a private token (see https://docs.gitlab.com/ee/security/token_overview.html#token-prefixes) + return { + headers: token.startsWith('gl') + ? { 'PRIVATE-TOKEN': token } + : { Authorization: `Bearer ${token}` }, // Otherwise, it's a bearer token + }; + } + + // If token not provided, fetch the integration token + const { token: configToken = '' } = config; return { - headers: { - 'PRIVATE-TOKEN': token, - }, + headers: { 'PRIVATE-TOKEN': configToken }, }; } diff --git a/packages/integration/src/harness/core.test.ts b/packages/integration/src/harness/core.test.ts index 78ca68a98a..90f37aaa08 100644 --- a/packages/integration/src/harness/core.test.ts +++ b/packages/integration/src/harness/core.test.ts @@ -15,7 +15,7 @@ */ import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../helpers'; +import { registerMswTestHooks } from '../helpers'; import { HarnessIntegrationConfig } from './config'; import { getHarnessArchiveUrl, @@ -28,7 +28,7 @@ import { describe('Harness code core', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getHarnessFileContentsUrl', () => { it('can create an url from arguments', () => { diff --git a/packages/integration/src/harness/core.ts b/packages/integration/src/harness/core.ts index 81281eaa6e..1887a5c0d8 100644 --- a/packages/integration/src/harness/core.ts +++ b/packages/integration/src/harness/core.ts @@ -34,7 +34,12 @@ export function getHarnessEditContentsUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code/orgs/${parsedUrl.orgName}/projects/${parsedUrl.projectName}/repos/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; + + return `${parsedUrl.baseUrl}/ng/account/${parsedUrl.accountId}/module/code${ + parsedUrl.orgName !== '' ? `/orgs/${parsedUrl.orgName}` : '' + }${ + parsedUrl.projectName !== '' ? `/projects/${parsedUrl.projectName}` : '' + }/repos/${parsedUrl.repoName}/files/${parsedUrl.branch}/~/${parsedUrl.path}`; } /** @@ -44,6 +49,7 @@ export function getHarnessEditContentsUrl( * * Converts * from: https://app.harness.io/ng/account/accountId/module/code/orgs/orgName/projects/projName/repos/repoName/files/refMain/~/all-apis.yaml + * https://qa.harness.io/ng/account/bDCAuAjFSJCLFj_0ug3lCg/module/code/orgs/HiteshTest/repos/impoorter/files/main/~/catalog.yaml * to: https://app.harness.io/gateway/code/api/v1/repos/accountId/orgName/projName/repoName/+/content/all-apis.yaml?routingId=accountId&include_commit=false&ref=refMain * * @param url - A URL pointing to a file @@ -55,7 +61,14 @@ export function getHarnessFileContentsUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${parsedUrl.accountId}&git_ref=refs/heads/${parsedUrl.refString}`; + + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/raw/${parsedUrl.path}?routingId=${ + parsedUrl.accountId + }&git_ref=refs/heads/${parsedUrl.refString}`; } /** @@ -77,7 +90,13 @@ export function getHarnessArchiveUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${parsedUrl.accountId}`; + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/archive/${parsedUrl.branch}.zip?routingId=${ + parsedUrl.accountId + }`; } /** @@ -99,7 +118,13 @@ export function getHarnessLatestCommitUrl( url: string, ) { const parsedUrl = parseHarnessUrl(config, url); - return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${parsedUrl.accountId}/${parsedUrl.orgName}/${parsedUrl.projectName}/${parsedUrl.repoName}/+/content?routingId=${parsedUrl.accountId}&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; + return `${parsedUrl.baseUrl}/gateway/code/api/v1/repos/${ + parsedUrl.accountId + }/${parsedUrl.orgName}${ + parsedUrl.projectName !== '' ? `/${parsedUrl.projectName}/` : '/' + }${parsedUrl.repoName}/+/content?routingId=${ + parsedUrl.accountId + }&include_commit=true&git_ref=refs/heads/${parsedUrl.branch}`; } /** @@ -153,27 +178,30 @@ export function parseHarnessUrl( .split('/') .filter(segment => segment !== ''); const urlParts = pathUrl.pathname.split('/'); - const [ - _ng, - _account, - accountId, - _module, - _moduleName, - _org, - orgName, - _projects, - projectName, - _repos, - repoName, - _files, - _ref, - _branch, - ..._path - ] = pathSegments; + + const accountIdIndex = + pathSegments.findIndex(segment => segment === 'account') + 1; + const accountId = pathSegments[accountIdIndex]; + + const orgNameIndex = pathSegments.findIndex(segment => segment === 'orgs'); + const orgName = orgNameIndex !== -1 ? pathSegments[orgNameIndex + 1] : ''; + + const projectNameIndex = pathSegments.findIndex( + segment => segment === 'projects', + ); + + const projectName = + projectNameIndex !== -1 ? pathSegments[projectNameIndex + 1] : ''; + + const repoNameIndex = + pathSegments.findIndex(segment => segment === 'repos') + 1; + const repoName = pathSegments[repoNameIndex]; + const refAndPath = urlParts.slice( urlParts.findIndex(i => i === 'files' || i === 'edit') + 1, ); const refIndex = refAndPath.findIndex(item => item === '~'); + const refString = refAndPath.slice(0, refIndex).join('/'); const pathWithoutSlash = refIndex !== -1 @@ -182,6 +210,7 @@ export function parseHarnessUrl( .join('/') .replace(/^\//, '') : ''; + return { baseUrl: baseUrl, accountId: accountId, diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index b403155cc5..f56d360325 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -114,7 +114,7 @@ export function defaultScmResolveUrl(options: { * * @param worker - service worker */ -export function setupRequestMockHandlers(worker: { +export function registerMswTestHooks(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/packages/release-manifests/src/manifest.test.ts b/packages/release-manifests/src/manifest.test.ts index 6a53f993c8..3c7657a83f 100644 --- a/packages/release-manifests/src/manifest.test.ts +++ b/packages/release-manifests/src/manifest.test.ts @@ -16,7 +16,7 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { getManifestByReleaseLine, getManifestByVersion, @@ -25,7 +25,7 @@ import { describe('Release Manifests', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); describe('getManifestByVersion', () => { it('should return a list of packages in a release', async () => { diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index aa65b502a0..f74dd57ea0 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/repo-tools +## 0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/cli-node@0.2.7 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.9.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/errors@1.2.4 + +## 0.9.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.7 + - @backstage/errors@1.2.4 + +## 0.9.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/cli-node@0.2.7 + - @backstage/config-loader@1.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/errors@1.2.4 + +## 0.9.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + +## 0.9.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/cli-node@0.2.6 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + ## 0.9.1 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index 30f003aea5..63be4d526e 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.9.1", + "version": "0.9.5-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index 6a523af6a9..122f04cc7e 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -145,7 +145,7 @@ ApiReportGenerator.generateReviewFileContent = if (!localName) { // Sometimes the symbol name is suffixed with a number to disambiguate, // e.g. "Props_14" instead of "Props" if there are multiple Props interfaces - // so we tyry to strip that suffix and look up the symbol again. + // so we try to strip that suffix and look up the symbol again. const [, trimmedSymbolName] = symbolName.match(/(.*)_\d+/) || []; localName = (sourceFile as any).identifiers?.get(trimmedSymbolName); } diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index da77dd57ad..57fb204390 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,151 @@ # techdocs-cli-embedded-app +## 0.2.99-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/cli@0.27.0-next.3 + - @backstage/plugin-techdocs@1.10.8-next.2 + - @backstage/test-utils@1.5.10-next.2 + - @backstage/app-defaults@1.5.10-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.2.99-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.2 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.2.99-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.8-next.1 + - @backstage/cli@0.27.0-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/test-utils@1.5.10-next.1 + - @backstage/app-defaults@1.5.10-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.2.99-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.27.0-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/app-defaults@1.5.9-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/test-utils@1.5.9-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-techdocs@1.10.8-next.0 + - @backstage/plugin-techdocs-react@1.2.7-next.0 + +## 0.2.98 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7 + - @backstage/plugin-techdocs-react@1.2.6 + - @backstage/core-components@0.14.9 + - @backstage/cli@0.26.11 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog@1.21.1 + - @backstage/test-utils@1.5.8 + - @backstage/app-defaults@1.5.8 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 0.2.98-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.10.7-next.2 + - @backstage/core-components@0.14.9-next.1 + - @backstage/app-defaults@1.5.8-next.2 + - @backstage/cli@0.26.11-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-techdocs-react@1.2.6-next.1 + +## 0.2.98-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/plugin-techdocs@1.10.7-next.1 + - @backstage/cli@0.26.11-next.1 + - @backstage/app-defaults@1.5.8-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/test-utils@1.5.8-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/theme@0.5.6 + - @backstage/plugin-techdocs-react@1.2.6-next.0 + +## 0.2.98-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-react@1.2.6-next.0 + - @backstage/plugin-techdocs@1.10.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/cli@0.26.10-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/app-defaults@1.5.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/test-utils@1.5.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + ## 0.2.97 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index ad2da35606..472936193a 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.97", + "version": "0.2.99-next.3", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 5ff165c4a9..76f9891bf0 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,73 @@ # @techdocs/cli +## 1.8.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.2 + - @backstage/plugin-techdocs-node@1.12.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.9-next.1 + - @backstage/backend-defaults@0.4.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-defaults@0.4.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/plugin-techdocs-node@1.12.9-next.0 + +## 1.8.16 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.4.0 + - @backstage/plugin-techdocs-node@1.12.8 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs-node@1.12.8-next.1 + - @backstage/backend-defaults@0.3.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + +## 1.8.15-next.0 + +### Patch Changes + +- c964a3d: Import discovery from backend-defaults instead of backend-common +- Updated dependencies + - @backstage/backend-defaults@0.3.3-next.0 + - @backstage/plugin-techdocs-node@1.12.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + ## 1.8.12 ### Patch Changes diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 9c87235af7..c655649ceb 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.8.12", + "version": "1.8.17-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md index 893316d288..a6c013a34a 100644 --- a/packages/test-utils/CHANGELOG.md +++ b/packages/test-utils/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/test-utils +## 1.5.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## 1.5.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## 1.5.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-react@0.4.24 + +## 1.5.8 + +### Patch Changes + +- 95a3a0b: Rename frontend and backend `setupRequestMockHandlers` methods to `registerMswTestHooks`. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + +## 1.5.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + +## 1.5.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + ## 1.5.6 ### Patch Changes diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index e31a820a1c..cc1c645387 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -197,6 +197,13 @@ export type MockStorageBucket = { [key: string]: any; }; +// @public +export function registerMswTestHooks(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void; + // @public export function renderInTestApp( Component: ComponentType> | ReactNode, @@ -209,7 +216,7 @@ export function renderWithEffects( options?: Pick & LegacyRootOption, ): Promise; -// @public +// @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { listen: (t: any) => void; close: () => void; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index d2294c27b6..3007c734bd 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/test-utils", - "version": "1.5.6", + "version": "1.5.10-next.2", "description": "Utilities to test Backstage plugins and apps.", "backstage": { "role": "web-library" diff --git a/packages/test-utils/src/deprecated.ts b/packages/test-utils/src/deprecated.ts new file mode 100644 index 0000000000..2033bbea2e --- /dev/null +++ b/packages/test-utils/src/deprecated.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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 { registerMswTestHooks } from './testUtils/msw'; + +/** + * @public + * @deprecated Use `registerMswTestHooks` from `@backstage/test-utils` instead. + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}): void { + registerMswTestHooks(worker); +} diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 83a955baa7..b971825f66 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export * from './deprecated'; export * from './testUtils'; diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts index 37b9d874f9..d33036ca8c 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts @@ -16,12 +16,12 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '../../msw'; +import { registerMswTestHooks } from '../../msw'; import { MockFetchApi } from './MockFetchApi'; describe('MockFetchApi', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('works with default constructor', async () => { worker.use( diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index 29cbdf854a..fa6a37445f 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { setupRequestMockHandlers } from './setupRequestMockHandlers'; +export { registerMswTestHooks } from './registerMswTestHooks'; diff --git a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts b/packages/test-utils/src/testUtils/msw/registerMswTestHooks.ts similarity index 94% rename from packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts rename to packages/test-utils/src/testUtils/msw/registerMswTestHooks.ts index 625ef0e0fc..4e26c38321 100644 --- a/packages/backend-test-utils/src/msw/setupRequestMockHandlers.ts +++ b/packages/test-utils/src/testUtils/msw/registerMswTestHooks.ts @@ -19,7 +19,7 @@ * @public * @param worker - service worker */ -export function setupRequestMockHandlers(worker: { +export function registerMswTestHooks(worker: { listen: (t: any) => void; close: () => void; resetHandlers: () => void; diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json index ad42bdba23..206adeddbe 100644 --- a/packages/yarn-plugin/package.json +++ b/packages/yarn-plugin/package.json @@ -34,6 +34,8 @@ "@backstage/release-manifests": "workspace:^", "@yarnpkg/core": "^4.0.3", "@yarnpkg/fslib": "^3.0.2", + "chalk": "^4.0.0", + "lodash": "^4.17.21", "semver": "^7.6.0" }, "devDependencies": { diff --git a/packages/yarn-plugin/src/index.ts b/packages/yarn-plugin/src/index.ts index 6c1c805fd8..f548463829 100644 --- a/packages/yarn-plugin/src/index.ts +++ b/packages/yarn-plugin/src/index.ts @@ -21,10 +21,21 @@ * @packageDocumentation */ -import { Plugin } from '@yarnpkg/core'; +import { Plugin, semverUtils, YarnVersion } from '@yarnpkg/core'; +import chalk from 'chalk'; import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking'; import { BackstageResolver } from './resolver/BackstageResolver'; +if (!semverUtils.satisfiesWithPrereleases(YarnVersion, '^4.1.1')) { + console.error(); + console.error( + `${chalk.bold.red( + 'Unsupported yarn version.', + )}: The Backstage yarn plugin only works with yarn ^4.1.1. Please upgrade yarn, or remove this plugin with "yarn plugin remove @yarnpkg/plugin-backstage".`, + ); + console.error(); +} + /** * @public */ diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts index 4cc0c1c9f2..9ac3401041 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts @@ -16,6 +16,7 @@ import { structUtils } from '@yarnpkg/core'; import { npath, ppath } from '@yarnpkg/fslib'; +import { getManifestByVersion } from '@backstage/release-manifests'; import { BackstageResolver } from './BackstageResolver'; import { createMockDirectory } from '@backstage/backend-test-utils'; @@ -31,6 +32,10 @@ jest.mock('@backstage/release-manifests', () => ({ }), })); +const getManifestByVersionMock = getManifestByVersion as jest.MockedFunction< + typeof getManifestByVersion +>; + describe('BackstageResolver', () => { const mockDir = createMockDirectory(); let backstageResolver: BackstageResolver; @@ -177,5 +182,112 @@ describe('BackstageResolver', () => { ), ).rejects.toThrow(/invalid backstage version/i); }); + + it('memoizes manifest retrieval', async () => { + const descriptor1 = structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ); + + for (let i = 0; i < 5; i++) { + await backstageResolver.getCandidates(descriptor1); + } + + expect(getManifestByVersionMock).toHaveBeenCalledTimes(1); + + const descriptor2 = structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:6.78.90', + ); + + for (let i = 0; i < 5; i++) { + await backstageResolver.getCandidates(descriptor2); + } + + expect(getManifestByVersionMock).toHaveBeenCalledTimes(2); + }); + }); + + describe('getSatisfying', () => { + it('filters out locators for other packages', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + {}, + [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'foo'), + 'npm:1.2.3', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'bar'), + 'npm:1.2.3', + ), + ], + ), + ).resolves.toEqual({ + locators: [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + ], + sorted: true, + }); + }); + + it('filters out locators for other package versions', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + {}, + [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:5.6.7', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'bar'), + 'npm:7.8.9', + ), + ], + ), + ).resolves.toEqual({ + locators: [ + structUtils.makeLocator( + structUtils.makeIdent('backstage', 'core'), + 'npm:6.7.8', + ), + ], + sorted: true, + }); + }); + + it('throws for non `backstage:` descriptors', async () => { + await expect( + backstageResolver.getSatisfying( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'npm:1.2.3', + ), + {}, + [], + ), + ).rejects.toThrow(/unexpected npm: range/i); + }); }); }); diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts index 33bba5df53..82e6c013a3 100644 --- a/packages/yarn-plugin/src/resolver/BackstageResolver.ts +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.ts @@ -92,6 +92,29 @@ export class BackstageResolver implements Resolver { ]; } + /** + * Given a descriptor and a list of possible locators, return a filtered list + * containing only locators that satisfy the descriptor. Since each Backstage + * release version corresponds to a single version for each package, we just + * need to filter that array for locators with that exact version. + */ + async getSatisfying( + descriptor: Descriptor, + _dependencies: Record, + locators: Array, + ): Promise<{ locators: Locator[]; sorted: boolean }> { + const packageVersion = await getPackageVersion(descriptor); + + return { + locators: locators.filter( + locator => + structUtils.areIdentsEqual(descriptor, locator) && + locator.reference === `npm:${packageVersion}`, + ), + sorted: true, + }; + } + /** * This plugin does not need to support any locators itself, since the * `getCandidates` method will always convert `backstage:` versions into @@ -106,14 +129,6 @@ export class BackstageResolver implements Resolver { */ getResolutionDependencies = () => ({}); - /** - * Candidate versions produced by this resolver always use the `npm:` - * protocol, so this function will never be called. - */ - async getSatisfying(): Promise<{ locators: Locator[]; sorted: boolean }> { - throw new Error('Unreachable'); - } - /** * Once transformed into locators (through getCandidates), the versions are * resolved by the NpmSemverResolver diff --git a/packages/yarn-plugin/src/util.ts b/packages/yarn-plugin/src/util.ts index 8431e23cfa..0b613a477c 100644 --- a/packages/yarn-plugin/src/util.ts +++ b/packages/yarn-plugin/src/util.ts @@ -16,11 +16,17 @@ import { ppath, xfs } from '@yarnpkg/fslib'; import { valid as semverValid } from 'semver'; -import { getManifestByVersion } from '@backstage/release-manifests'; +import { memoize } from 'lodash'; +import { getManifestByVersion as getManifestByVersionBase } from '@backstage/release-manifests'; import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common'; import { Descriptor, structUtils } from '@yarnpkg/core'; import { PROTOCOL } from './constants'; +const getManifestByVersion = memoize( + getManifestByVersionBase, + ({ version }) => version, +); + export const getCurrentBackstageVersion = () => { const workspaceRoot = ppath.resolve(findPaths(ppath.cwd()).targetRoot); diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 5b8e039c4e..c87bfcaafa 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,107 @@ # @backstage/plugin-api-docs +## 0.11.8-next.2 + +### Patch Changes + +- 4b6d2cb: Updated dependency `@graphiql/react` to `^0.23.0`. +- Updated dependencies + - @backstage/plugin-catalog@1.22.0-next.2 + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.11.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-catalog@1.21.2-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.11.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog@1.21.2-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-react@0.4.24 + +## 0.11.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog@1.21.1 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + +## 0.11.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog@1.21.1-next.2 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.11.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-catalog@1.21.1-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + +## 0.11.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-catalog@1.21.1-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + ## 0.11.6 ### Patch Changes diff --git a/plugins/api-docs/api-report-alpha.md b/plugins/api-docs/api-report-alpha.md index 1d22283884..6003056cb3 100644 --- a/plugins/api-docs/api-report-alpha.md +++ b/plugins/api-docs/api-report-alpha.md @@ -13,8 +13,9 @@ const _default: BackstagePlugin< root: RouteRef; }, { - registerApi: ExternalRouteRef; - } + registerApi: ExternalRouteRef; + }, + {} >; export default _default; diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index b8aed9f360..96b52002d8 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.11.6", + "version": "0.11.8-next.2", "description": "A Backstage plugin that helps represent API entities in the frontend", "backstage": { "role": "frontend-plugin", @@ -63,7 +63,7 @@ "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-permission-react": "workspace:^", - "@graphiql/react": "^0.20.0", + "@graphiql/react": "^0.23.0", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 90e9a080dd..305d90e2ec 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,90 @@ # @backstage/plugin-app-backend +## 0.3.72-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-app-node@0.1.23-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.3.72-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-app-node@0.1.23-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.3.72-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.23-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.3.71 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-app-node@0.1.22 + - @backstage/config-loader@1.8.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.3.71-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-app-node@0.1.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.3.70-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-app-node@0.1.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.3.68 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 5071bdaf86..d6ac29953e 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.68", + "version": "0.3.72-next.2", "description": "A Backstage backend plugin that serves the Backstage frontend app", "backstage": { "role": "backend-plugin", @@ -82,7 +82,7 @@ "@backstage/cli": "workspace:^", "@backstage/types": "workspace:^", "@types/supertest": "^2.0.8", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "supertest": "^6.1.3" }, "configSchema": "config.d.ts" diff --git a/plugins/app-node/CHANGELOG.md b/plugins/app-node/CHANGELOG.md index d16d233b2c..25861bc8a1 100644 --- a/plugins/app-node/CHANGELOG.md +++ b/plugins/app-node/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-app-node +## 0.1.23-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/config-loader@1.9.0-next.2 + +## 0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + +## 0.1.23-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + +## 0.1.22 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/config-loader@1.8.1 + +## 0.1.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config-loader@1.8.1 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/config-loader@1.8.1 + ## 0.1.19 ### Patch Changes diff --git a/plugins/app-node/package.json b/plugins/app-node/package.json index d008a82284..e5e4d19a47 100644 --- a/plugins/app-node/package.json +++ b/plugins/app-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-node", - "version": "0.1.19", + "version": "0.1.23-next.2", "description": "Node.js library for the app plugin", "backstage": { "role": "node-library", diff --git a/plugins/app-visualizer/CHANGELOG.md b/plugins/app-visualizer/CHANGELOG.md index d30dbfc0ca..a7f8d8a012 100644 --- a/plugins/app-visualizer/CHANGELOG.md +++ b/plugins/app-visualizer/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-app-visualizer +## 0.1.9-next.2 + +### Patch Changes + +- 72754db: Updated usage of `useRouteRef`, which can now always return `undefined`. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.7 ### Patch Changes diff --git a/plugins/app-visualizer/api-report.md b/plugins/app-visualizer/api-report.md index 764caa7190..fd3749b159 100644 --- a/plugins/app-visualizer/api-report.md +++ b/plugins/app-visualizer/api-report.md @@ -6,7 +6,7 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; // @public (undocumented) -const visualizerPlugin: BackstagePlugin<{}, {}>; +const visualizerPlugin: BackstagePlugin<{}, {}, {}>; export default visualizerPlugin; // (No @packageDocumentation comment for this package) diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index 5d8f9b2649..0d825be137 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-visualizer", - "version": "0.1.7", + "version": "0.1.9-next.2", "description": "Visualizes the Backstage app structure", "backstage": { "role": "frontend-plugin", diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 2356f38cfa..b2c9685bab 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -189,18 +189,12 @@ function OutputLink(props: { }) { const routeRef = props.node?.instance?.getData(coreExtensionData.routeRef); - let link: string | undefined = undefined; - try { - // eslint-disable-next-line react-hooks/rules-of-hooks - link = useRouteRef(routeRef as RouteRef)(); - } catch { - /* ignore */ - } + const link = useRouteRef(routeRef as RouteRef); return ( {props.dataRef.id}}> - {link ? link : null} + {link ? link : null} ); diff --git a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md index ee68bd7df5..e2014d6749 100644 --- a/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-atlassian-provider/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-auth-backend-module-atlassian-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.2.3 + +### Patch Changes + +- b9832ae: Fix several issues with the Atlassian auth provider (type definition, profile url, profile transformation, scopes) +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/auth-backend-module-atlassian-provider/config.d.ts b/plugins/auth-backend-module-atlassian-provider/config.d.ts index d2c8a249c1..b6ca96d62f 100644 --- a/plugins/auth-backend-module-atlassian-provider/config.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/config.d.ts @@ -28,6 +28,13 @@ export interface Config { audience?: string; callbackUrl?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'usernameMatchingUserEntityName' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-atlassian-provider/package.json b/plugins/auth-backend-module-atlassian-provider/package.json index c961892674..995b980124 100644 --- a/plugins/auth-backend-module-atlassian-provider/package.json +++ b/plugins/auth-backend-module-atlassian-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-atlassian-provider", - "version": "0.2.0", + "version": "0.2.4-next.2", "description": "The atlassian-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts new file mode 100644 index 0000000000..ab0a4840f9 --- /dev/null +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.test.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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 Strategy from 'passport-atlassian-oauth2'; + +describe('Strategy', () => { + it('should be an instance of', () => { + const strategy = new Strategy( + { + authorizationURL: 'https://auth.atlassian.com/authorize', + tokenURL: 'https://auth.atlassian.com/oauth/token', + clientID: 'my-client-id', + clientSecret: 'my-client-secret', + scope: ['offline_access', 'read:jira-work', 'read:jira-user'], + }, + () => {}, + ); + expect((strategy as any).name).toBe('atlassian'); + }); +}); diff --git a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts index 0ced9cf831..d4f46b5a8c 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/authenticator.ts @@ -20,14 +20,14 @@ import { PassportOAuthDoneCallback, PassportProfile, } from '@backstage/plugin-auth-node'; -import { Strategy as AtlassianStrategy } from 'passport-atlassian-oauth2'; +import AtlassianStrategy from 'passport-atlassian-oauth2'; /** @public */ export const atlassianAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, scopes: { - required: ['offline_access', 'read:jira-work', 'read:jira-user'], + required: ['offline_access', 'read:me', 'read:jira-work', 'read:jira-user'], }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); @@ -49,7 +49,8 @@ export const atlassianAuthenticator = createOAuthAuthenticator({ baseURL: baseUrl, authorizationURL: `${baseUrl}/authorize`, tokenURL: `${baseUrl}/oauth/token`, - profileURL: `${baseUrl}/api/v4/user`, + profileURL: 'https://api.atlassian.com/me', + scope: [], // the Atlassian strategy requires a scope, but Backstage passes the right set of scopes when calling OAuth2Strategy.prototype.authenticate }, ( accessToken: string, diff --git a/plugins/auth-backend-module-atlassian-provider/src/index.ts b/plugins/auth-backend-module-atlassian-provider/src/index.ts index cc71c9b7e3..8f090b76d4 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/index.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/index.ts @@ -13,13 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /** * The atlassian-provider backend module for the auth plugin. * * @packageDocumentation */ - export { atlassianAuthenticator } from './authenticator'; export { authModuleAtlassianProvider as default } from './module'; export { atlassianSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts index 7b92218d56..7e83aed179 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/module.test.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/module.test.ts @@ -64,10 +64,12 @@ describe('authModuleAtlassianProvider', () => { expect(startUrl.pathname).toBe('/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', + audience: 'api.atlassian.com', client_id: 'my-client-id', + prompt: 'consent', redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`, state: expect.any(String), - scope: 'offline_access read:jira-work read:jira-user', + scope: 'offline_access read:me read:jira-work read:jira-user', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ @@ -92,10 +94,7 @@ describe('authModuleAtlassianProvider', () => { development: { clientId: 'my-client-id', clientSecret: 'my-client-secret', - additionalScopes: [ - 'read:filter:jira', - 'read:jira-work', // already required - ], + additionalScopes: 'read:filter:jira read:jira-work', // 2nd is already required }, }, }, @@ -123,11 +122,14 @@ describe('authModuleAtlassianProvider', () => { expect(startUrl.origin).toBe('https://auth.atlassian.com'); expect(startUrl.pathname).toBe('/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + audience: 'api.atlassian.com', response_type: 'code', client_id: 'my-client-id', + prompt: 'consent', redirect_uri: `http://localhost:${server.port()}/api/auth/atlassian/handler/frame`, state: expect.any(String), - scope: 'offline_access read:jira-work read:jira-user read:filter:jira', + scope: + 'offline_access read:me read:jira-work read:jira-user read:filter:jira', }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ diff --git a/plugins/auth-backend-module-atlassian-provider/src/types.d.ts b/plugins/auth-backend-module-atlassian-provider/src/types.d.ts index 5fd47bf885..aa945f3705 100644 --- a/plugins/auth-backend-module-atlassian-provider/src/types.d.ts +++ b/plugins/auth-backend-module-atlassian-provider/src/types.d.ts @@ -18,8 +18,9 @@ declare module 'passport-atlassian-oauth2' { import { Request } from 'express'; import { StrategyCreated } from 'passport'; - export class Strategy { + export default class Strategy { constructor(options: any, verify: any); + name?: string; authenticate(this: StrategyCreated, req: Request, options?: any): any; } } diff --git a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md index f96de94fa6..ce99594759 100644 --- a/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-aws-alb-provider/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-auth-backend-module-aws-alb-provider +## 0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## 0.1.15-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend@0.22.9 + - @backstage/errors@1.2.4 + +## 0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + ## 0.1.11 ### Patch Changes diff --git a/plugins/auth-backend-module-aws-alb-provider/config.d.ts b/plugins/auth-backend-module-aws-alb-provider/config.d.ts index 1978e5a4df..5a3e90cf6b 100644 --- a/plugins/auth-backend-module-aws-alb-provider/config.d.ts +++ b/plugins/auth-backend-module-aws-alb-provider/config.d.ts @@ -21,6 +21,12 @@ export interface Config { awsalb?: { issuer?: string; region: string; + signIn?: { + resolvers: Array< + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-aws-alb-provider/package.json b/plugins/auth-backend-module-aws-alb-provider/package.json index c65df9e16e..2ddace0546 100644 --- a/plugins/auth-backend-module-aws-alb-provider/package.json +++ b/plugins/auth-backend-module-aws-alb-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-aws-alb-provider", - "version": "0.1.11", + "version": "0.1.15-next.2", "description": "The aws-alb provider module for the Backstage auth backend.", "backstage": { "role": "backend-plugin-module", @@ -44,7 +44,7 @@ "@backstage/plugin-auth-node": "workspace:^", "jose": "^5.0.0", "node-cache": "^5.1.2", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts index 0bbcf9b5c1..81251584e9 100644 --- a/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-aws-alb-provider/src/helpers.test.ts @@ -19,7 +19,7 @@ import { JWTHeaderParameters, UnsecuredJWT } from 'jose'; import NodeCache from 'node-cache'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { PassportProfile } from '@backstage/plugin-auth-node'; import { makeProfileInfo, provisionKeyCache } from './helpers'; @@ -34,7 +34,7 @@ describe.each([ ], ])('helpers', (region, url) => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const nodeCache = jest.fn() as unknown as NodeCache; nodeCache.set = jest.fn(); diff --git a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md index 2771262546..471f86842e 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-azure-easyauth-provider/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-auth-backend-module-azure-easyauth-provider +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## 0.1.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.2 ### Patch Changes diff --git a/plugins/auth-backend-module-azure-easyauth-provider/package.json b/plugins/auth-backend-module-azure-easyauth-provider/package.json index 8303ada7a9..f48e7ce40b 100644 --- a/plugins/auth-backend-module-azure-easyauth-provider/package.json +++ b/plugins/auth-backend-module-azure-easyauth-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-azure-easyauth-provider", - "version": "0.1.2", + "version": "0.1.6-next.2", "description": "The azure-easyauth-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md index 3c21cc0498..e70bd7f8ee 100644 --- a/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-bitbucket-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-bitbucket-provider +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.1.6-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.1.2 ### Patch Changes diff --git a/plugins/auth-backend-module-bitbucket-provider/config.d.ts b/plugins/auth-backend-module-bitbucket-provider/config.d.ts index 17cac1c194..df68209469 100644 --- a/plugins/auth-backend-module-bitbucket-provider/config.d.ts +++ b/plugins/auth-backend-module-bitbucket-provider/config.d.ts @@ -26,6 +26,13 @@ export interface Config { */ clientSecret: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'userIdMatchingUserEntityAnnotation' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-bitbucket-provider/package.json b/plugins/auth-backend-module-bitbucket-provider/package.json index 21653b691a..ae9692bfe8 100644 --- a/plugins/auth-backend-module-bitbucket-provider/package.json +++ b/plugins/auth-backend-module-bitbucket-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-bitbucket-provider", - "version": "0.1.2", + "version": "0.1.6-next.2", "description": "The bitbucket-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md index 781c1e2a7d..493b57d26a 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-cloudflare-access-provider/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-auth-backend-module-cloudflare-access-provider +## 0.1.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.6-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.2 ### Patch Changes diff --git a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts index ff7d722d48..1300eb8c57 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/config.d.ts @@ -27,6 +27,12 @@ export interface Config { token: string; subject: string; }>; + signIn?: { + resolvers: Array< + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; /** * The backstage token expiration. diff --git a/plugins/auth-backend-module-cloudflare-access-provider/package.json b/plugins/auth-backend-module-cloudflare-access-provider/package.json index 1d99725963..c388ead3cb 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/package.json +++ b/plugins/auth-backend-module-cloudflare-access-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-cloudflare-access-provider", - "version": "0.1.2", + "version": "0.1.6-next.2", "description": "The cloudflare-access-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", @@ -40,7 +40,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "devDependencies": { "@backstage/backend-defaults": "workspace:^", diff --git a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts index 8043f4e214..9efab07ba5 100644 --- a/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts +++ b/plugins/auth-backend-module-cloudflare-access-provider/src/helpers.test.ts @@ -16,7 +16,7 @@ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { SignJWT, exportJWK, generateKeyPair } from 'jose'; @@ -89,7 +89,7 @@ describe('helpers', () => { const tokenFactory = new MockTokenFactory(); const cache = mockServices.cache.mock(); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md index f42462e619..3b4f24a64a 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gcp-iap-provider/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-auth-backend-module-gcp-iap-provider +## 0.2.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.18-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.2.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.2.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.2.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.14 ### Patch Changes diff --git a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts index 945378ada6..452f9819c3 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/config.d.ts +++ b/plugins/auth-backend-module-gcp-iap-provider/config.d.ts @@ -32,6 +32,15 @@ export interface Config { * The name of the header to read the JWT token from, defaults to `'x-goog-iap-jwt-assertion'`. */ jwtHeader?: string; + + signIn?: { + resolvers: Array< + | { resolver: 'emailMatchingUserEntityAnnotation' } + | { resolver: 'idMatchingUserEntityAnnotation' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-gcp-iap-provider/package.json b/plugins/auth-backend-module-gcp-iap-provider/package.json index e8932d908c..20b3f88606 100644 --- a/plugins/auth-backend-module-gcp-iap-provider/package.json +++ b/plugins/auth-backend-module-gcp-iap-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gcp-iap-provider", - "version": "0.2.14", + "version": "0.2.18-next.2", "description": "A GCP IAP auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-github-provider/CHANGELOG.md b/plugins/auth-backend-module-github-provider/CHANGELOG.md index 8a050e26c3..83db20aaa5 100644 --- a/plugins/auth-backend-module-github-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-github-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-github-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.1.20-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/auth-backend-module-github-provider/config.d.ts b/plugins/auth-backend-module-github-provider/config.d.ts index f60d4c239d..2ca17998f0 100644 --- a/plugins/auth-backend-module-github-provider/config.d.ts +++ b/plugins/auth-backend-module-github-provider/config.d.ts @@ -28,6 +28,13 @@ export interface Config { callbackUrl?: string; enterpriseInstanceUrl?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'usernameMatchingUserEntityName' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-github-provider/package.json b/plugins/auth-backend-module-github-provider/package.json index d8d115454e..e773518782 100644 --- a/plugins/auth-backend-module-github-provider/package.json +++ b/plugins/auth-backend-module-github-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-github-provider", - "version": "0.1.16", + "version": "0.1.20-next.2", "description": "The github-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md index e448079c05..8f502140b7 100644 --- a/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-gitlab-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-gitlab-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.1.20-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/auth-backend-module-gitlab-provider/config.d.ts b/plugins/auth-backend-module-gitlab-provider/config.d.ts index 18f7cb5005..d21c2bd62a 100644 --- a/plugins/auth-backend-module-gitlab-provider/config.d.ts +++ b/plugins/auth-backend-module-gitlab-provider/config.d.ts @@ -28,6 +28,13 @@ export interface Config { audience?: string; callbackUrl?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'usernameMatchingUserEntityName' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-gitlab-provider/package.json b/plugins/auth-backend-module-gitlab-provider/package.json index 4b7117e863..88fd39a53b 100644 --- a/plugins/auth-backend-module-gitlab-provider/package.json +++ b/plugins/auth-backend-module-gitlab-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-gitlab-provider", - "version": "0.1.16", + "version": "0.1.20-next.2", "description": "The gitlab-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-google-provider/CHANGELOG.md b/plugins/auth-backend-module-google-provider/CHANGELOG.md index 412e50d6aa..64d59dc5aa 100644 --- a/plugins/auth-backend-module-google-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-google-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-google-provider +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.1.20-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.19 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.1.16 ### Patch Changes diff --git a/plugins/auth-backend-module-google-provider/config.d.ts b/plugins/auth-backend-module-google-provider/config.d.ts index 08a0e08f2b..3abab05cfe 100644 --- a/plugins/auth-backend-module-google-provider/config.d.ts +++ b/plugins/auth-backend-module-google-provider/config.d.ts @@ -27,6 +27,13 @@ export interface Config { clientSecret: string; callbackUrl?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'emailMatchingUserEntityAnnotation' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-google-provider/package.json b/plugins/auth-backend-module-google-provider/package.json index 48271c34b9..ed2e536396 100644 --- a/plugins/auth-backend-module-google-provider/package.json +++ b/plugins/auth-backend-module-google-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-google-provider", - "version": "0.1.16", + "version": "0.1.20-next.2", "description": "A Google auth provider module for the Backstage auth backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-guest-provider/CHANGELOG.md b/plugins/auth-backend-module-guest-provider/CHANGELOG.md index 79f9767571..4c5df385c7 100644 --- a/plugins/auth-backend-module-guest-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-guest-provider/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-auth-backend-module-guest-provider +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## 0.1.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.1.5 ### Patch Changes diff --git a/plugins/auth-backend-module-guest-provider/package.json b/plugins/auth-backend-module-guest-provider/package.json index cf7491adf4..d41612d32a 100644 --- a/plugins/auth-backend-module-guest-provider/package.json +++ b/plugins/auth-backend-module-guest-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-guest-provider", - "version": "0.1.5", + "version": "0.1.9-next.2", "description": "The guest-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md index 2cb76d0866..9bbf02ba3c 100644 --- a/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-microsoft-provider/CHANGELOG.md @@ -1,5 +1,56 @@ # @backstage/plugin-auth-backend-module-microsoft-provider +## 0.1.18-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.1.18-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.18-next.0 + +### Patch Changes + +- 39f36a9: Updated the Microsoft authenticator to accurately define required scopes, but to also omit the required and additional scopes when requesting resource scopes. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/auth-backend-module-microsoft-provider/config.d.ts b/plugins/auth-backend-module-microsoft-provider/config.d.ts index a7ec027083..b1ed2d5766 100644 --- a/plugins/auth-backend-module-microsoft-provider/config.d.ts +++ b/plugins/auth-backend-module-microsoft-provider/config.d.ts @@ -29,6 +29,13 @@ export interface Config { domainHint?: string; callbackUrl?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'emailMatchingUserEntityAnnotation' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-microsoft-provider/package.json b/plugins/auth-backend-module-microsoft-provider/package.json index 598c69118c..f18104ba15 100644 --- a/plugins/auth-backend-module-microsoft-provider/package.json +++ b/plugins/auth-backend-module-microsoft-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-microsoft-provider", - "version": "0.1.14", + "version": "0.1.18-next.2", "description": "The microsoft-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", @@ -38,9 +38,7 @@ "@backstage/plugin-auth-node": "workspace:^", "express": "^4.18.2", "jose": "^5.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.6.7", - "passport": "^0.7.0", + "node-fetch": "^2.7.0", "passport-microsoft": "^1.0.0" }, "devDependencies": { diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts index 21a60a229b..74a7ab9974 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { encodeOAuthState, @@ -43,7 +43,7 @@ describe('microsoftAuthenticator', () => { const microsoftApi = new FakeMicrosoftAPI(); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); let implementation: { domainHint: string | undefined; diff --git a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts index 1b6eb87fae..fe7dc8f7e1 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/authenticator.ts @@ -21,21 +21,30 @@ import { PassportProfile, } from '@backstage/plugin-auth-node'; import { ExtendedMicrosoftStrategy } from './strategy'; -import { union } from 'lodash'; /** @public */ export const microsoftAuthenticator = createOAuthAuthenticator({ defaultProfileTransform: PassportOAuthAuthenticatorHelper.defaultProfileTransform, + scopes: { + required: ['email', 'openid', 'offline_access', 'user.read'], + transform({ requested, granted, required, additional }) { + // Resources scopes are of the form `/`, and are handled + // separately from the normal scopes in the client. When request a + // resource scope we should only include forward the request scope along + // with offline_access. + const hasResourceScope = Array.from(requested).some(s => s.includes('/')); + if (hasResourceScope) { + return [...requested, 'offline_access']; + } + return [...requested, ...granted, ...required, ...additional]; + }, + }, initialize({ callbackUrl, config }) { const clientId = config.getString('clientId'); const clientSecret = config.getString('clientSecret'); const tenantId = config.getString('tenantId'); const domainHint = config.getOptionalString('domainHint'); - const scope = union( - ['user.read'], - config.getOptionalStringArray('additionalScopes'), - ); const helper = PassportOAuthAuthenticatorHelper.from( new ExtendedMicrosoftStrategy( @@ -44,7 +53,6 @@ export const microsoftAuthenticator = createOAuthAuthenticator({ clientSecret: clientSecret, callbackURL: callbackUrl, tenant: tenantId, - scope: scope, }, ( accessToken: string, diff --git a/plugins/auth-backend-module-microsoft-provider/src/module.test.ts b/plugins/auth-backend-module-microsoft-provider/src/module.test.ts index b50956b9eb..a072b99bde 100644 --- a/plugins/auth-backend-module-microsoft-provider/src/module.test.ts +++ b/plugins/auth-backend-module-microsoft-provider/src/module.test.ts @@ -67,7 +67,7 @@ describe('authModuleMicrosoftProvider', () => { expect(startUrl.pathname).toBe('/my-tenant-id/oauth2/v2.0/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: 'User.Read.All', + scope: 'email openid offline_access user.read User.Read.All', client_id: 'my-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, state: expect.any(String), @@ -97,6 +97,7 @@ describe('authModuleMicrosoftProvider', () => { clientSecret: 'another-client-secret', tenantId: 'another-tenant-id', domainHint: 'somedomain', + additionalScopes: ['some-extra-scope'], }, }, }, @@ -125,7 +126,7 @@ describe('authModuleMicrosoftProvider', () => { expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize'); expect(Object.fromEntries(startUrl.searchParams)).toEqual({ response_type: 'code', - scope: 'user.read', + scope: 'email openid offline_access user.read some-extra-scope', client_id: 'another-client-id', redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, state: expect.any(String), @@ -137,4 +138,64 @@ describe('authModuleMicrosoftProvider', () => { nonce: decodeURIComponent(nonceCookie.value), }); }); + + it('should not include required scopes for resources', async () => { + const { server } = await startTestBackend({ + features: [ + authPlugin, + authModuleMicrosoftProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + microsoft: { + development: { + clientId: 'another-client-id', + clientSecret: 'another-client-secret', + tenantId: 'another-tenant-id', + additionalScopes: ['some-extra-scope'], + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get( + '/api/auth/microsoft/start?env=development&scope=some-resource/some-scope', + ); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('microsoft-nonce', { + domain: 'localhost', + path: '/api/auth/microsoft/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://login.microsoftonline.com'); + expect(startUrl.pathname).toBe('/another-tenant-id/oauth2/v2.0/authorize'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + scope: 'some-resource/some-scope offline_access', + client_id: 'another-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/microsoft/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); }); diff --git a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md index acec3a3065..63c6662332 100644 --- a/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-oauth2-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/auth-backend-module-oauth2-provider/config.d.ts b/plugins/auth-backend-module-oauth2-provider/config.d.ts index 9b1ee18f1b..fa9dec7ce1 100644 --- a/plugins/auth-backend-module-oauth2-provider/config.d.ts +++ b/plugins/auth-backend-module-oauth2-provider/config.d.ts @@ -32,6 +32,13 @@ export interface Config { additionalScopes?: string | string[]; disableRefresh?: boolean; includeBasicAuth?: boolean; + signIn?: { + resolvers: Array< + | { resolver: 'usernameMatchingUserEntityName' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-oauth2-provider/package.json b/plugins/auth-backend-module-oauth2-provider/package.json index 99a8e04c65..77a00a52d6 100644 --- a/plugins/auth-backend-module-oauth2-provider/package.json +++ b/plugins/auth-backend-module-oauth2-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-provider", - "version": "0.2.0", + "version": "0.2.4-next.2", "description": "The oauth2-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md index 0223e64d35..a9d6523201 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oauth2-proxy-provider/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-auth-backend-module-oauth2-proxy-provider +## 0.1.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + ## 0.1.12 ### Patch Changes diff --git a/plugins/auth-backend-module-oauth2-proxy-provider/package.json b/plugins/auth-backend-module-oauth2-proxy-provider/package.json index c865a15f47..4df4e14dfe 100644 --- a/plugins/auth-backend-module-oauth2-proxy-provider/package.json +++ b/plugins/auth-backend-module-oauth2-proxy-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oauth2-proxy-provider", - "version": "0.1.12", + "version": "0.1.16-next.2", "description": "The oauth2-proxy-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md index 0a10e2e98b..6c32155ebe 100644 --- a/plugins/auth-backend-module-oidc-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-oidc-provider/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-auth-backend-module-oidc-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-backend@0.22.10-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-backend@0.22.10-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-backend@0.22.10-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend@0.22.9 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-backend@0.22.9-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-backend@0.22.8-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/auth-backend-module-oidc-provider/config.d.ts b/plugins/auth-backend-module-oidc-provider/config.d.ts index 398e63c466..dbc06e51bb 100644 --- a/plugins/auth-backend-module-oidc-provider/config.d.ts +++ b/plugins/auth-backend-module-oidc-provider/config.d.ts @@ -31,6 +31,12 @@ export interface Config { tokenSignedResponseAlg?: string; additionalScopes?: string | string[]; prompt?: string; + signIn?: { + resolvers: Array< + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-oidc-provider/package.json b/plugins/auth-backend-module-oidc-provider/package.json index c42d85652c..5f59a4e0d1 100644 --- a/plugins/auth-backend-module-oidc-provider/package.json +++ b/plugins/auth-backend-module-oidc-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-oidc-provider", - "version": "0.2.0", + "version": "0.2.4-next.2", "description": "The oidc-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index 4203a70153..38e134bcd0 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-auth-node'; import { oidcAuthenticator } from './authenticator'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -38,7 +38,7 @@ describe('oidcAuthenticator', () => { const revokedTokenMap: Record = {}; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://oidc.test', diff --git a/plugins/auth-backend-module-oidc-provider/src/module.test.ts b/plugins/auth-backend-module-oidc-provider/src/module.test.ts index 1359634950..1bbb5c1cff 100644 --- a/plugins/auth-backend-module-oidc-provider/src/module.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/module.test.ts @@ -20,7 +20,7 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { Server } from 'http'; @@ -35,7 +35,7 @@ describe('authModuleOidcProvider', () => { let publicKey: JWK; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://oidc.test', diff --git a/plugins/auth-backend-module-okta-provider/CHANGELOG.md b/plugins/auth-backend-module-okta-provider/CHANGELOG.md index d1da6db5fa..38fe3bed5d 100644 --- a/plugins/auth-backend-module-okta-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-okta-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-okta-provider +## 0.0.16-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.0.16-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.0.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.0.15 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.0.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.0.12 ### Patch Changes diff --git a/plugins/auth-backend-module-okta-provider/config.d.ts b/plugins/auth-backend-module-okta-provider/config.d.ts index aaf6ec1f91..0689171153 100644 --- a/plugins/auth-backend-module-okta-provider/config.d.ts +++ b/plugins/auth-backend-module-okta-provider/config.d.ts @@ -30,6 +30,13 @@ export interface Config { idp?: string; callbackUrl?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'emailMatchingUserEntityAnnotation' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-okta-provider/package.json b/plugins/auth-backend-module-okta-provider/package.json index fa9dcf1ee5..ad0c5c5ecb 100644 --- a/plugins/auth-backend-module-okta-provider/package.json +++ b/plugins/auth-backend-module-okta-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-okta-provider", - "version": "0.0.12", + "version": "0.0.16-next.2", "description": "The okta-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md index 685ce0f1ff..476df484d2 100644 --- a/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-onelogin-provider/CHANGELOG.md @@ -1,5 +1,54 @@ # @backstage/plugin-auth-backend-module-onelogin-provider +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + +## 0.1.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + ## 0.1.0 ### Minor Changes diff --git a/plugins/auth-backend-module-onelogin-provider/config.d.ts b/plugins/auth-backend-module-onelogin-provider/config.d.ts index d4f1457461..8257e2ac71 100644 --- a/plugins/auth-backend-module-onelogin-provider/config.d.ts +++ b/plugins/auth-backend-module-onelogin-provider/config.d.ts @@ -27,6 +27,13 @@ export interface Config { clientSecret: string; issuer: string; callbackUrl?: string; + signIn?: { + resolvers: Array< + | { resolver: 'usernameMatchingUserEntityName' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json index 6724e51cb7..1d1feee009 100644 --- a/plugins/auth-backend-module-onelogin-provider/package.json +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-onelogin-provider", - "version": "0.1.0", + "version": "0.1.4-next.2", "description": "The onelogin-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md index ed5fdd4819..8b23915d26 100644 --- a/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-pinniped-provider/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-auth-backend-module-pinniped-provider +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + +## 0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/auth-backend-module-pinniped-provider/package.json b/plugins/auth-backend-module-pinniped-provider/package.json index f99d1baabd..c720d9b501 100644 --- a/plugins/auth-backend-module-pinniped-provider/package.json +++ b/plugins/auth-backend-module-pinniped-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-pinniped-provider", - "version": "0.1.13", + "version": "0.1.17-next.2", "description": "The pinniped-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts index e2dedf2129..359e497e5f 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/authenticator.test.ts @@ -23,7 +23,7 @@ import { } from '@backstage/plugin-auth-node'; import { pinnipedAuthenticator } from './authenticator'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { JWK, SignJWT, exportJWK, generateKeyPair } from 'jose'; import { rest } from 'msw'; @@ -37,7 +37,7 @@ describe('pinnipedAuthenticator', () => { let publicKey: JWK; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://pinniped.test', diff --git a/plugins/auth-backend-module-pinniped-provider/src/module.test.ts b/plugins/auth-backend-module-pinniped-provider/src/module.test.ts index a5c8fb4599..e170c8d4ed 100644 --- a/plugins/auth-backend-module-pinniped-provider/src/module.test.ts +++ b/plugins/auth-backend-module-pinniped-provider/src/module.test.ts @@ -15,7 +15,7 @@ */ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { Server } from 'http'; @@ -32,7 +32,7 @@ describe('authModulePinnipedProvider', () => { let publicKey: JWK; const mswServer = setupServer(); - setupRequestMockHandlers(mswServer); + registerMswTestHooks(mswServer); const issuerMetadata = { issuer: 'https://pinniped.test', diff --git a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md index 5d323e0319..9f81a53961 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md +++ b/plugins/auth-backend-module-vmware-cloud-provider/CHANGELOG.md @@ -1,5 +1,60 @@ # @backstage/plugin-auth-backend-module-vmware-cloud-provider +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/catalog-model@1.5.0 + +## 0.2.4-next.1 + +### Patch Changes + +- c8f1cae: Add `signIn` to authentication provider configuration schema +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/catalog-model@1.5.0 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/catalog-model@1.5.0 + ## 0.2.0 ### Minor Changes diff --git a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts index 6ea07f1936..a08451cff3 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/config.d.ts @@ -25,6 +25,13 @@ export interface Config { scope?: string; consoleEndpoint?: string; additionalScopes?: string | string[]; + signIn?: { + resolvers: Array< + | { resolver: 'profileEmailMatchingUserEntityEmail' } + | { resolver: 'emailLocalPartMatchingUserEntityName' } + | { resolver: 'emailMatchingUserEntityProfileEmail' } + >; + }; }; }; }; diff --git a/plugins/auth-backend-module-vmware-cloud-provider/package.json b/plugins/auth-backend-module-vmware-cloud-provider/package.json index ff29719d22..166b7e8e82 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/package.json +++ b/plugins/auth-backend-module-vmware-cloud-provider/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend-module-vmware-cloud-provider", - "version": "0.2.0", + "version": "0.2.4-next.2", "description": "The vmware-cloud-provider backend module for the auth plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts index c69d4e9ab1..0e363d4700 100644 --- a/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-vmware-cloud-provider/src/authenticator.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { AuthResolverContext, @@ -37,7 +37,7 @@ jest.mock('uid2', () => jest.fn().mockReturnValue('sessionid')); describe('vmwareCloudAuthenticator', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); let oAuthState: OAuthState = { nonce: 'nonce', diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index 6900d56b24..f99c8d0453 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,186 @@ # @backstage/plugin-auth-backend +## 0.22.10-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.2 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.2 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.2 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.2 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.2 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.2 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.2 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.22.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.1 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.1 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.1 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.1 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.1 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.1 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.1 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.1 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.1 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.1 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.22.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.18-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.4-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.15-next.0 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.6-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.18-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.20-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.4-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.4-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.16-next.0 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.4-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 0.22.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19 + - @backstage/plugin-auth-backend-module-google-provider@0.1.19 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.3 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.15 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.22.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.3-next.1 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.14-next.1 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.5-next.1 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.5-next.1 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.5-next.1 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.17-next.1 + - @backstage/plugin-auth-backend-module-github-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-google-provider@0.1.19-next.1 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.17-next.1 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.3-next.1 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.15-next.1 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.3-next.1 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.15-next.1 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.3-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.22.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-backend-module-atlassian-provider@0.2.2-next.0 + - @backstage/plugin-auth-backend-module-aws-alb-provider@0.1.13-next.0 + - @backstage/plugin-auth-backend-module-azure-easyauth-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-bitbucket-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-cloudflare-access-provider@0.1.4-next.0 + - @backstage/plugin-auth-backend-module-gcp-iap-provider@0.2.16-next.0 + - @backstage/plugin-auth-backend-module-github-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-gitlab-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-google-provider@0.1.18-next.0 + - @backstage/plugin-auth-backend-module-microsoft-provider@0.1.16-next.0 + - @backstage/plugin-auth-backend-module-oauth2-provider@0.2.2-next.0 + - @backstage/plugin-auth-backend-module-oauth2-proxy-provider@0.1.14-next.0 + - @backstage/plugin-auth-backend-module-oidc-provider@0.2.2-next.0 + - @backstage/plugin-auth-backend-module-okta-provider@0.0.14-next.0 + - @backstage/plugin-auth-backend-module-onelogin-provider@0.1.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.22.6 ### Patch Changes diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 85dd17d311..8dec959418 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.22.6", + "version": "0.22.10-next.2", "description": "A Backstage backend plugin that handles authentication", "backstage": { "role": "backend-plugin", @@ -87,7 +87,7 @@ "minimatch": "^9.0.0", "morgan": "^1.10.0", "node-cache": "^5.1.2", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "openid-client": "^5.2.1", "passport": "^0.7.0", "passport-auth0": "^1.4.3", diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts index 187c3b09a5..3756e00b38 100644 --- a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -22,7 +22,7 @@ import { BitbucketServerOAuthResult, } from './provider'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { AuthResolverContext } from '@backstage/plugin-auth-node'; @@ -131,7 +131,7 @@ describe('BitbucketServerAuthProvider', () => { describe('when transforming to type OAuthResponse', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); it('should map to a valid response', async () => { server.use(whoAmIHandler(), getUserHandler()); @@ -311,7 +311,7 @@ describe('BitbucketServerAuthProvider', () => { describe('when authenticating', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); it('should forward the refresh token', async () => { server.use(whoAmIHandler(), getUserHandler()); diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index c7e9c68580..89cf9c3b2c 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -15,7 +15,7 @@ */ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { LoggerService } from '@backstage/backend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; @@ -38,7 +38,7 @@ describe('oidc.create', () => { nonce: 'foo', }; const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); let publicKey: JWK; let tokenset: object; diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index b91ce2141c..1905329e0a 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,89 @@ # @backstage/plugin-auth-node +## 0.5.0-next.2 + +### Minor Changes + +- 579afd0: **BREAKING**: Sign-in resolvers configured via `.signIn.resolvers` now take precedence over sign-in resolvers passed to `signInResolver` option of `createOAuthProviderFactory`. This effectively makes sign-in resolvers passed via the `signInResolver` the default one, which you can then override through configuration. + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.17 + +### Patch Changes + +- 55c1a72: Fix issues with Atlassian OAuth provider: retrieve the email and photo that were not in arrays but rather in single props. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.14 ### Patch Changes diff --git a/plugins/auth-node/api-report.md b/plugins/auth-node/api-report.md index dc1d86308a..a3750819f9 100644 --- a/plugins/auth-node/api-report.md +++ b/plugins/auth-node/api-report.md @@ -552,6 +552,8 @@ export type PassportOAuthResult = { // @public (undocumented) export type PassportProfile = Profile & { avatarUrl?: string; + email?: string; + photo?: string; }; // @public diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 4f42e80c92..348db40dd7 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.4.14", + "version": "0.5.0-next.2", "backstage": { "role": "node-library", "pluginId": "auth", @@ -49,7 +49,7 @@ "express": "^4.17.1", "jose": "^5.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "passport": "^0.7.0", "winston": "^3.2.1", "zod": "^3.22.4", diff --git a/plugins/auth-node/src/identity/IdentityClient.test.ts b/plugins/auth-node/src/identity/IdentityClient.test.ts index bdc7dd5cd7..418eb544ad 100644 --- a/plugins/auth-node/src/identity/IdentityClient.test.ts +++ b/plugins/auth-node/src/identity/IdentityClient.test.ts @@ -15,7 +15,7 @@ */ import { PluginEndpointDiscovery } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { decodeProtectedHeader, exportJWK, @@ -102,7 +102,7 @@ describe('IdentityClient', () => { let factory: FakeTokenFactory; const keyDurationSeconds = 5; - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { client = IdentityClient.create({ discovery, issuer: mockBaseUrl }); diff --git a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts index a3387b7038..fccfcb94de 100644 --- a/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts +++ b/plugins/auth-node/src/oauth/createOAuthProviderFactory.ts @@ -43,11 +43,10 @@ export function createOAuthProviderFactory(options: { return ctx => { return OAuthEnvironmentHandler.mapConfig(ctx.config, envConfig => { const signInResolver = - options.signInResolver ?? readDeclarativeSignInResolver({ config: envConfig, signInResolverFactories: options.signInResolverFactories ?? {}, - }); + }) ?? options.signInResolver; return createOAuthRouteHandlers({ authenticator: options.authenticator, diff --git a/plugins/auth-node/src/passport/PassportHelpers.ts b/plugins/auth-node/src/passport/PassportHelpers.ts index d7b554d56a..d1ddee20a7 100644 --- a/plugins/auth-node/src/passport/PassportHelpers.ts +++ b/plugins/auth-node/src/passport/PassportHelpers.ts @@ -40,6 +40,9 @@ export class PassportHelpers { if (profile.emails && profile.emails.length > 0) { const [firstEmail] = profile.emails; email = firstEmail.value; + } else if (profile.email) { + // This is the case for Atlassian + email = profile.email; } let picture: string | undefined = undefined; @@ -48,6 +51,9 @@ export class PassportHelpers { } else if (profile.photos && profile.photos.length > 0) { const [firstPhoto] = profile.photos; picture = firstPhoto.value; + } else if (profile.photo) { + // This is the case for Atlassian + picture = profile.photo; } let displayName: string | undefined = diff --git a/plugins/auth-node/src/passport/types.ts b/plugins/auth-node/src/passport/types.ts index 4a3f41b6b2..d5681fa9e3 100644 --- a/plugins/auth-node/src/passport/types.ts +++ b/plugins/auth-node/src/passport/types.ts @@ -19,6 +19,8 @@ import { Profile } from 'passport'; /** @public */ export type PassportProfile = Profile & { avatarUrl?: string; + email?: string; + photo?: string; }; /** @public */ diff --git a/plugins/auth-react/CHANGELOG.md b/plugins/auth-react/CHANGELOG.md index c36109cb91..f7d621164c 100644 --- a/plugins/auth-react/CHANGELOG.md +++ b/plugins/auth-react/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-auth-react +## 0.1.5-next.0 + +### Patch Changes + +- aeac3e9: feat: Hide visibility of CookieAuthRedirect +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.1.4 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.1.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + ## 0.1.3 ### Patch Changes diff --git a/plugins/auth-react/package.json b/plugins/auth-react/package.json index c48438d465..b68e581c74 100644 --- a/plugins/auth-react/package.json +++ b/plugins/auth-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-react", - "version": "0.1.3", + "version": "0.1.5-next.0", "description": "Web library for the auth plugin", "backstage": { "role": "web-library", diff --git a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.tsx b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.tsx index f719aebdae..c89aa6a54d 100644 --- a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.tsx +++ b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.tsx @@ -45,6 +45,7 @@ export function CookieAuthRedirect() { ref={form => form?.submit()} action={window.location.href} method="POST" + style={{ visibility: 'hidden' }} > diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md index ccd47aa5e4..30e2e77a6e 100644 --- a/plugins/bitbucket-cloud-common/CHANGELOG.md +++ b/plugins/bitbucket-cloud-common/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-bitbucket-cloud-common +## 0.2.22-next.1 + +### Patch Changes + +- 3fca643: Added method `listBranchesByRepository` to `BitbucketCloudClient` +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + +## 0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + +## 0.2.21 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/integration@1.13.0 + +## 0.2.21-next.0 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/integration@1.13.0-next.0 + ## 0.2.20 ### Patch Changes diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 0f4e82d5d1..833a367c03 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -12,11 +12,26 @@ export class BitbucketCloudClient { config: BitbucketCloudIntegrationConfig, ): BitbucketCloudClient; // (undocumented) + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination; + // (undocumented) + listProjectsByWorkspace( + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination; + // (undocumented) listRepositoriesByWorkspace( workspace: string, options?: FilterAndSortOptions & PartialResponseOptions, ): WithPagination; // (undocumented) + listWorkspaces( + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination; + // (undocumented) searchCode( workspace: string, query: string, @@ -200,9 +215,18 @@ export namespace Models { size?: number; values?: Array | Set; } + export interface PaginatedBranches extends Paginated { + values?: Set; + } + export interface PaginatedProjects extends Paginated { + values?: Set; + } export interface PaginatedRepositories extends Paginated { values?: Set; } + export interface PaginatedWorkspaces extends Paginated { + values?: Set; + } export interface Participant extends ModelObject { // (undocumented) approved?: boolean; diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json index a08b0f31bf..d2f22cc966 100644 --- a/plugins/bitbucket-cloud-common/package.json +++ b/plugins/bitbucket-cloud-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bitbucket-cloud-common", - "version": "0.2.20", + "version": "0.2.22-next.1", "description": "Common functionalities for bitbucket-cloud plugins", "backstage": { "role": "common-library", diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts index c5c0d604be..7558c92138 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.test.ts @@ -107,4 +107,88 @@ describe('BitbucketCloudClient', () => { expect(results).toHaveLength(1); expect(results[0].slug).toEqual('repo1'); }); + + it('listProjectsByWorkspace', async () => { + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/workspaces/ws/projects', + (_, res, ctx) => { + const response = { + values: [ + { + type: 'project', + slug: 'project1', + } as Models.Project, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + const pagination = client.listProjectsByWorkspace('ws'); + + const results = []; + for await (const result of pagination.iterateResults()) { + results.push(result); + } + + expect(results).toHaveLength(1); + expect(results[0].slug).toEqual('project1'); + }); + + it('listWorkspaces', async () => { + server.use( + rest.get('https://api.bitbucket.org/2.0/workspaces', (_, res, ctx) => { + const response = { + values: [ + { + type: 'workspace', + slug: 'workspace1', + } as Models.Workspace, + ], + }; + return res(ctx.json(response)); + }), + ); + + const pagination = client.listWorkspaces(); + + const results = []; + for await (const result of pagination.iterateResults()) { + results.push(result); + } + + expect(results).toHaveLength(1); + expect(results[0].slug).toEqual('workspace1'); + }); + + it('listBranchesByRepository', async () => { + server.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/workspace1/repo1/refs/branches', + (_, res, ctx) => { + const response = { + values: [ + { + type: 'branch', + name: 'branch1', + } as Models.Branch, + ], + }; + return res(ctx.json(response)); + }, + ), + ); + + const pagination = client.listBranchesByRepository('repo1', 'workspace1'); + + const results = []; + for await (const result of pagination.iterateResults()) { + results.push(result); + } + + expect(results).toHaveLength(1); + expect(results[0].name).toEqual('branch1'); + }); }); diff --git a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts index 3ac005fe2b..86f56dd70d 100644 --- a/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts +++ b/plugins/bitbucket-cloud-common/src/BitbucketCloudClient.ts @@ -69,6 +69,52 @@ export class BitbucketCloudClient { ); } + listProjectsByWorkspace( + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + const workspaceEnc = encodeURIComponent(workspace); + + return new WithPagination( + paginationOptions => + this.createUrl(`/workspaces/${workspaceEnc}/projects`, { + ...paginationOptions, + ...options, + }), + url => this.getTypeMapped(url), + ); + } + + listWorkspaces( + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + return new WithPagination( + paginationOptions => + this.createUrl('/workspaces', { ...paginationOptions, ...options }), + url => this.getTypeMapped(url), + ); + } + + listBranchesByRepository( + repository: string, + workspace: string, + options?: FilterAndSortOptions & PartialResponseOptions, + ): WithPagination { + const workspaceEnc = encodeURIComponent(workspace); + + return new WithPagination( + paginationOptions => + this.createUrl( + `/repositories/${workspaceEnc}/${repository}/refs/branches`, + { + ...paginationOptions, + ...options, + }, + ), + url => this.getTypeMapped(url), + ); + } + private createUrl(endpoint: string, options?: RequestOptions): URL { const request = new URL(this.config.apiBaseUrl + endpoint); for (const key in options) { @@ -113,6 +159,8 @@ export class BitbucketCloudClient { 'utf8', ); headers.Authorization = `Basic ${buffer.toString('base64')}`; + } else if (this.config.token) { + headers.Authorization = `Bearer ${this.config.token}`; } return headers; diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 8fce389a32..28b1b45e1b 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -253,6 +253,39 @@ export namespace Models { values?: Set; } + /** + * A paginated list of projects. + * @public + */ + export interface PaginatedProjects extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + + /** + * A paginated list of workspaces. + * @public + */ + export interface PaginatedWorkspaces extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + + /** + * A paginated list of branches. + * @public + */ + export interface PaginatedBranches extends Paginated { + /** + * The values of the current page. + */ + values?: Set; + } + /** * Object describing a user's role on resources like commits or pull requests. * @public diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index e925fa7de3..69c16e9875 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,110 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.3.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.3.18-next.0 + +### Patch Changes + +- ba8571e: Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. +- 90a7340: `AwsOrganizationCloudAccountProcessor` configuration field `roleArn` is deprecated in favor of new field `accountId` +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-kubernetes-common@0.8.1 + +## 0.3.17 + +### Patch Changes + +- 4afa050: Export `defaultEksClusterEntityTransformer` to allow library consumers to layer additional changes on top of the default transformer. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + +## 0.3.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## 0.3.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-kubernetes-common@0.8.0 + ## 0.3.14 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 5098ab5c32..428a39d934 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -106,6 +106,9 @@ export class AwsS3EntityProvider implements EntityProvider { refresh(logger: LoggerService): Promise; } +// @public +export const defaultEksClusterEntityTransformer: EksClusterEntityTransformer; + // @public export type EksClusterEntityTransformer = ( cluster: Cluster, diff --git a/plugins/catalog-backend-module-aws/config.d.ts b/plugins/catalog-backend-module-aws/config.d.ts index 4c9c98512e..0a9d9c97db 100644 --- a/plugins/catalog-backend-module-aws/config.d.ts +++ b/plugins/catalog-backend-module-aws/config.d.ts @@ -26,8 +26,14 @@ export interface Config { provider: { /** * The role to be assumed by this processor + * @deprecated Use `accountId` instead. */ roleArn?: string; + + /** + * The AWS account ID to query for organizational data + */ + accountId?: string; }; }; }; diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 299da7d302..443dfebb87 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", - "version": "0.3.14", + "version": "0.3.18-next.2", "description": "A Backstage catalog backend module that helps integrate towards AWS", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts index 7e0dc58559..a84c65abda 100644 --- a/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts +++ b/plugins/catalog-backend-module-aws/src/awsOrganization/config.test.ts @@ -22,11 +22,13 @@ describe('readAwsOrganizationConfig', () => { const config = { provider: { roleArn: 'aws::arn::foo', + accountId: '111111111111', }, }; const actual = readAwsOrganizationConfig(new ConfigReader(config)); const expected = { roleArn: 'aws::arn::foo', + accountId: '111111111111', }; expect(actual).toEqual(expected); }); diff --git a/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts index e6096b932b..ea16394e0a 100644 --- a/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts +++ b/plugins/catalog-backend-module-aws/src/awsOrganization/config.ts @@ -22,8 +22,14 @@ import { Config } from '@backstage/config'; export type AwsOrganizationProviderConfig = { /** * The role to assume for the processor. + * @deprecated Use `accountId` instead. */ roleArn?: string; + + /** + * The AWS accountId + */ + accountId?: string; }; export function readAwsOrganizationConfig( @@ -32,7 +38,9 @@ export function readAwsOrganizationConfig( const providerConfig = config.getOptionalConfig('provider'); const roleArn = providerConfig?.getOptionalString('roleArn'); + const accountId = providerConfig?.getOptionalString('accountId'); return { roleArn, + accountId, }; } diff --git a/plugins/catalog-backend-module-aws/src/index.ts b/plugins/catalog-backend-module-aws/src/index.ts index 72ddc6386a..de8f9fd7b2 100644 --- a/plugins/catalog-backend-module-aws/src/index.ts +++ b/plugins/catalog-backend-module-aws/src/index.ts @@ -24,3 +24,4 @@ export * from './processors'; export * from './providers'; export * from './types'; export * from './constants'; +export { defaultEksClusterEntityTransformer } from './lib'; diff --git a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts index 4addd1cb45..9165eb4322 100644 --- a/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts +++ b/plugins/catalog-backend-module-aws/src/module/catalogModuleAwsS3EntityProvider.test.ts @@ -17,7 +17,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { Duration } from 'luxon'; import { catalogModuleAwsS3EntityProvider } from './catalogModuleAwsS3EntityProvider'; import { AwsS3EntityProvider } from '../providers'; @@ -62,8 +61,8 @@ describe('catalogModuleAwsS3EntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders?.pop()?.getProviderName()).toEqual( 'awsS3-provider:default', diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts index a491dbab7d..a5f5ce2f6c 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts @@ -106,6 +106,7 @@ export class AwsEKSClusterProcessor implements CatalogProcessor { } const eksClient = new EKS({ + customUserAgent: 'backstage-aws-catalog-eks-cluster-processor', credentials, credentialDefaultProvider: providerFunction, region, diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index 4a454fce6f..73d7a63e2b 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -59,10 +59,18 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor { static async fromConfig(config: Config, options: { logger: LoggerService }) { const c = config.getOptionalConfig('catalog.processors.awsOrganization'); const orgConfig = c ? readAwsOrganizationConfig(c) : undefined; + + if (orgConfig?.roleArn) { + options.logger.warn( + 'The roleArn configuration for AwsOrganizationCloudAccountProcessor ignores the role name, use accountId configuration instead', + ); + } + const awsCredentialsManager = DefaultAwsCredentialsManager.fromConfig(config); const credProvider = await awsCredentialsManager.getCredentialProvider({ arn: orgConfig?.roleArn, + accountId: orgConfig?.accountId, }); return new AwsOrganizationCloudAccountProcessor( credProvider, diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index 91ba76b695..0c58fe3f3a 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -154,6 +154,7 @@ export class AwsS3EntityProvider implements EntityProvider { accountId ? { accountId } : undefined, ); this.s3 = new S3({ + customUserAgent: 'backstage-aws-catalog-s3-entity-provider', apiVersion: '2006-03-01', credentialDefaultProvider: () => credProvider.sdkCredentialProvider, endpoint: this.integration.config.endpoint, diff --git a/plugins/catalog-backend-module-aws/src/providers/config.test.ts b/plugins/catalog-backend-module-aws/src/providers/config.test.ts index fb2f3c0790..4c7ec9a964 100644 --- a/plugins/catalog-backend-module-aws/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-aws/src/providers/config.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readAwsS3Configs } from './config'; describe('readAwsS3Configs', () => { @@ -92,7 +91,7 @@ describe('readAwsS3Configs', () => { id: 'provider4', schedule: { ...provider4.schedule, - frequency: Duration.fromISO(provider4.schedule.frequency), + frequency: { minutes: 30 }, }, }); }); diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index dce335f353..89d6847511 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.43-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + +## 0.1.43-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + +## 0.1.43-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + +## 0.1.42 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/config@1.2.0 + +## 0.1.42-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.1.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.1.39 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index aa66f869a6..de18ae5895 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", - "version": "0.1.39", + "version": "0.1.43-next.2", "description": "A Backstage catalog backend module that helps integrate towards Azure", "backstage": { "role": "backend-plugin-module", @@ -58,7 +58,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts index ef00610dc0..4839b625d2 100644 --- a/plugins/catalog-backend-module-azure/src/lib/azure.test.ts +++ b/plugins/catalog-backend-module-azure/src/lib/azure.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { codeSearch, CodeSearchResponse } from './azure'; @@ -26,7 +26,7 @@ import { ConfigReader } from '@backstage/config'; describe('azure', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const createFixture = (host: string, token: string) => { const azureConfig = { diff --git a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts index 2c674a8061..e406c61e07 100644 --- a/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts +++ b/plugins/catalog-backend-module-azure/src/module/catalogModuleAzureDevOpsEntityProvider.test.ts @@ -17,7 +17,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { Duration } from 'luxon'; import { catalogModuleAzureDevOpsEntityProvider } from './catalogModuleAzureDevOpsEntityProvider'; import { AzureDevOpsEntityProvider } from '../providers'; @@ -66,8 +65,8 @@ describe('catalogModuleAzureDevOpsEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders?.pop()?.getProviderName()).toEqual( 'AzureDevOpsEntityProvider:test', diff --git a/plugins/catalog-backend-module-azure/src/providers/config.test.ts b/plugins/catalog-backend-module-azure/src/providers/config.test.ts index 02dbaab90e..5d5ae930c1 100644 --- a/plugins/catalog-backend-module-azure/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-azure/src/providers/config.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readAzureDevOpsConfigs } from './config'; describe('readAzureDevOpsConfigs', () => { @@ -95,7 +94,7 @@ describe('readAzureDevOpsConfigs', () => { id: 'provider4', schedule: { ...provider4.schedule, - frequency: Duration.fromISO(provider4.schedule.frequency), + frequency: { minutes: 30 }, }, }); expect(actual[4]).toEqual({ diff --git a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md index 7081965cc9..771702d86d 100644 --- a/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-backstage-openapi/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-catalog-backend-module-backstage-openapi +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-openapi-utils@0.1.15-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/backend-openapi-utils@0.1.14-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-backstage-openapi/package.json b/plugins/catalog-backend-module-backstage-openapi/package.json index b85332a6f0..d9ba70c806 100644 --- a/plugins/catalog-backend-module-backstage-openapi/package.json +++ b/plugins/catalog-backend-module-backstage-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-backstage-openapi", - "version": "0.2.2", + "version": "0.2.6-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 89f45366fa..ec27de045c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,107 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.2.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.2.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.2.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.2.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.2.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.2.6 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 41285c1e49..3f4389788c 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", - "version": "0.2.6", + "version": "0.2.10-next.2", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts index db53b0b41b..bcf13e786d 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/module/catalogModuleBitbucketCloudEntityProvider.test.ts @@ -21,7 +21,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsServiceRef } from '@backstage/plugin-events-node'; -import { Duration } from 'luxon'; import { catalogModuleBitbucketCloudEntityProvider } from './catalogModuleBitbucketCloudEntityProvider'; import { BitbucketCloudEntityProvider } from '../providers/BitbucketCloudEntityProvider'; @@ -78,8 +77,8 @@ describe('catalogModuleBitbucketCloudEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(runner).not.toHaveBeenCalled(); const provider = addedProviders!.pop()!; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts index 55b701e016..80d09f3e2f 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProvider.test.ts @@ -22,7 +22,7 @@ import { } from '@backstage/backend-tasks'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity, LocationEntity } from '@backstage/catalog-model'; @@ -62,7 +62,7 @@ const logger = mockServices.logger.mock(); const server = setupServer(); describe('BitbucketCloudEntityProvider', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); const simpleConfig = new ConfigReader({ catalog: { diff --git a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts index 5a2cc99437..7f13616586 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-cloud/src/providers/BitbucketCloudEntityProviderConfig.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readProviderConfigs } from './BitbucketCloudEntityProviderConfig'; describe('readProviderConfigs', () => { @@ -130,7 +129,7 @@ describe('readProviderConfigs', () => { repoSlug: undefined, }, schedule: { - frequency: Duration.fromISO('PT30M'), + frequency: { minutes: 30 }, timeout: { minutes: 3, }, diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index 0724dcffab..46469b747a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,90 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.37-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.37-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.37-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.36 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.36-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.1.35-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.33 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index bff2adee0a..eeafae91d5 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.33", + "version": "0.1.37-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", @@ -59,7 +59,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@types/node-fetch": "^2.5.12", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts index a3d4155121..db2a05d0b1 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/lib/BitbucketServerClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { BitbucketServerIntegrationConfig } from '@backstage/integration'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -50,7 +50,7 @@ describe('BitbucketServerClient', () => { config: config, }); - setupRequestMockHandlers(server); + registerMswTestHooks(server); it('listProjects', async () => { server.use( diff --git a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts index 56b44b29f5..5335b04e2d 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/module/catalogModuleBitbucketServerEntityProvider.test.ts @@ -18,7 +18,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { catalogModuleBitbucketServerEntityProvider } from './catalogModuleBitbucketServerEntityProvider'; -import { Duration } from 'luxon'; import { BitbucketServerEntityProvider } from '../providers'; describe('catalogModuleBitbucketServerEntityProvider', () => { @@ -70,8 +69,8 @@ describe('catalogModuleBitbucketServerEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders?.pop()?.getProviderName()).toEqual( 'bitbucketServer-provider:default', diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts index 9624e396cd..49b69f0fa6 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProvider.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-tasks'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -103,7 +103,7 @@ function setupStubs(projects: Project[], baseUrl: string) { } describe('BitbucketServerEntityProvider', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.clearAllMocks(); }); diff --git a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts index 9e8aca5dd9..755fa4b58a 100644 --- a/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-bitbucket-server/src/providers/BitbucketServerEntityProviderConfig.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readProviderConfigs } from './BitbucketServerEntityProviderConfig'; describe('readProviderConfigs', () => { @@ -111,7 +110,7 @@ describe('readProviderConfigs', () => { skipArchivedRepos: undefined, }, schedule: { - frequency: Duration.fromISO('PT30M'), + frequency: { minutes: 30 }, timeout: { minutes: 3, }, diff --git a/plugins/catalog-backend-module-gcp/CHANGELOG.md b/plugins/catalog-backend-module-gcp/CHANGELOG.md index 756c77cd24..f5f805edd9 100644 --- a/plugins/catalog-backend-module-gcp/CHANGELOG.md +++ b/plugins/catalog-backend-module-gcp/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-catalog-backend-module-gcp +## 0.1.24-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.1.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.1.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-kubernetes-common@0.8.1 + +## 0.1.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.1.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## 0.1.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-kubernetes-common@0.8.0 + ## 0.1.20 ### Patch Changes diff --git a/plugins/catalog-backend-module-gcp/package.json b/plugins/catalog-backend-module-gcp/package.json index 3f144c7a9c..0673dd1805 100644 --- a/plugins/catalog-backend-module-gcp/package.json +++ b/plugins/catalog-backend-module-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gcp", - "version": "0.1.20", + "version": "0.1.24-next.2", "description": "A Backstage catalog backend module that helps integrate towards GCP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index b241cbe499..d4b173cc3f 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.40-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.40-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.39 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.1.38-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.36 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index 207eefd381..b10e48d34b 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.36", + "version": "0.1.40-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "catalog", @@ -55,7 +55,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts index 66f0cbab2c..dc40e0525d 100644 --- a/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/module/catalogModuleGerritEntityProvider.test.ts @@ -17,7 +17,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { Duration } from 'luxon'; import { catalogModuleGerritEntityProvider } from './catalogModuleGerritEntityProvider'; import { GerritEntityProvider } from '../providers/GerritEntityProvider'; @@ -76,8 +75,8 @@ describe('catalogModuleGerritEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders?.pop()?.getProviderName()).toEqual( 'gerrit-provider:test', diff --git a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts index 8b1acbdbaa..ee3555c7f9 100644 --- a/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/providers/GerritEntityProvider.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-tasks'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -57,7 +57,7 @@ class PersistingTaskRunner implements TaskRunner { const logger = mockServices.logger.mock(); describe('GerritEntityProvider', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); afterEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts index ecf68d9f3c..85ab3383f0 100644 --- a/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gerrit/src/providers/config.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readGerritConfigs } from './config'; describe('readGerritConfigs', () => { @@ -63,7 +62,7 @@ describe('readGerritConfigs', () => { id: 'active-g3', schedule: { ...provider3.schedule, - frequency: Duration.fromISO(provider3.schedule.frequency), + frequency: { minutes: 30 }, }, }); }); diff --git a/plugins/catalog-backend-module-github-org/CHANGELOG.md b/plugins/catalog-backend-module-github-org/CHANGELOG.md index bac2b053aa..ebaa50af30 100644 --- a/plugins/catalog-backend-module-github-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-github-org/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-catalog-backend-module-github-org +## 0.1.18-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## 0.1.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-backend-module-github@0.6.6-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.1.17 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend-module-github@0.6.5 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/config@1.2.0 + +## 0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-github@0.6.5-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-backend-module-github@0.6.4-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + ## 0.1.14 ### Patch Changes diff --git a/plugins/catalog-backend-module-github-org/package.json b/plugins/catalog-backend-module-github-org/package.json index 3946f1878c..e1f60aec67 100644 --- a/plugins/catalog-backend-module-github-org/package.json +++ b/plugins/catalog-backend-module-github-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github-org", - "version": "0.1.14", + "version": "0.1.18-next.2", "description": "The github-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-github-org/src/module.test.ts b/plugins/catalog-backend-module-github-org/src/module.test.ts index 6e03d8d9cc..76ab4d1fbc 100644 --- a/plugins/catalog-backend-module-github-org/src/module.test.ts +++ b/plugins/catalog-backend-module-github-org/src/module.test.ts @@ -18,7 +18,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { EntityProvider } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { Duration } from 'luxon'; import { catalogModuleGithubOrgEntityProvider } from './module'; describe('catalogModuleGithubOrgEntityProvider', () => { @@ -66,8 +65,8 @@ describe('catalogModuleGithubOrgEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders![0].getProviderName()).toEqual( 'GithubMultiOrgEntityProvider:default', diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index fb15cb0de4..c7a032c43e 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,162 @@ # @backstage/plugin-catalog-backend-module-github +## 0.6.6-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.6.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.6.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.6.5 + +### Patch Changes + +- 9112efc: Adds support for `repository` events. + + The provider adds a subscription to the topic `github.repository`. + + Hereby, it supports events of type `repository` with actions + + - `archived` + - `deleted` + - `edited` + - `renamed` + - `transferred` + - `unarchived` + + Actions skipped as they don't require entity changes: + + - `created` + - `privatized` + - `publicized` + + If the config option `validateLocationsExist` is enabled, an API request + is necessary and will be executed. + This affects the actions `renamed`, `transferred`, and `unarchive` + of event type `repository`. + + Catalog entities related to the `GithubEntityProvider` instance will be adjusted + according to action and its meaning for them. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.6.5-next.1 + +### Patch Changes + +- 9112efc: Adds support for `repository` events. + + The provider adds a subscription to the topic `github.repository`. + + Hereby, it supports events of type `repository` with actions + + - `archived` + - `deleted` + - `edited` + - `renamed` + - `transferred` + - `unarchived` + + Actions skipped as they don't require entity changes: + + - `created` + - `privatized` + - `publicized` + + If the config option `validateLocationsExist` is enabled, an API request + is necessary and will be executed. + This affects the actions `renamed`, `transferred`, and `unarchive` + of event type `repository`. + + Catalog entities related to the `GithubEntityProvider` instance will be adjusted + according to action and its meaning for them. + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.6.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.6.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index 7d3bd62281..dfde8e9718 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-github", - "version": "0.6.2", + "version": "0.6.6-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitHub", "backstage": { "role": "backend-plugin-module", @@ -67,7 +67,7 @@ "git-url-parse": "^14.0.0", "lodash": "^4.17.21", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts index e5c7eb5896..29c8cbc66d 100644 --- a/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts +++ b/plugins/catalog-backend-module-github/src/analyzers/GithubLocationAnalyzer.test.ts @@ -35,7 +35,7 @@ jest.mock('@octokit/rest', () => { import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { GithubLocationAnalyzer } from './GithubLocationAnalyzer'; import { - setupRequestMockHandlers, + registerMswTestHooks, mockServices, } from '@backstage/backend-test-utils'; import { setupServer } from 'msw/node'; @@ -63,7 +63,7 @@ describe('GithubLocationAnalyzer', () => { }, }); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { server.use( diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts index d83e8031cc..9eae9e1161 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.test.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql as graphqlOctokit } from '@octokit/graphql'; import { graphql as graphqlMsw } from 'msw'; @@ -39,7 +39,7 @@ describe('github', () => { const graphql = graphqlOctokit.defaults({ request: { fetch } }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); describe('getOrganizationUsers using defaultUserMapper', () => { it('reads members', async () => { diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts index eff52ea98d..cba65e522e 100644 --- a/plugins/catalog-backend-module-github/src/lib/github.ts +++ b/plugins/catalog-backend-module-github/src/lib/github.ts @@ -38,6 +38,7 @@ export type QueryResponse = { type RepositoryOwnerResponse = { repositories?: Connection; + repository?: RepositoryResponse; }; export type OrganizationResponse = { @@ -509,6 +510,61 @@ export async function getOrganizationRepositories( return { repositories }; } +export async function getOrganizationRepository( + client: typeof graphql, + org: string, + repoName: string, + catalogPath: string, +): Promise { + let relativeCatalogPathRef: string; + // We must strip the leading slash or the query for objects does not work + if (catalogPath.startsWith('/')) { + relativeCatalogPathRef = catalogPath.substring(1); + } else { + relativeCatalogPathRef = catalogPath; + } + const catalogPathRef = `HEAD:${relativeCatalogPathRef}`; + const query = ` + query repository($org: String!, $repoName: String!, $catalogPathRef: String!) { + repositoryOwner(login: $org) { + repository(name: $repoName) { + name + catalogInfoFile: object(expression: $catalogPathRef) { + __typename + ... on Blob { + id + text + } + } + url + isArchived + isFork + visibility + repositoryTopics(first: 100) { + nodes { + ... on RepositoryTopic { + topic { + name + } + } + } + } + defaultBranchRef { + name + } + } + } + }`; + + const response: QueryResponse = await client(query, { + org, + repoName, + catalogPathRef, + }); + + return response.repositoryOwner?.repository || null; +} + /** * Gets all the users out of a Github organization. * diff --git a/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts b/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts index 59f8b41a6d..018d339de8 100644 --- a/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts +++ b/plugins/catalog-backend-module-github/src/module/githubCatalogModule.test.ts @@ -21,7 +21,6 @@ import { catalogAnalysisExtensionPoint, catalogProcessingExtensionPoint, } from '@backstage/plugin-catalog-node/alpha'; -import { Duration } from 'luxon'; import { githubCatalogModule } from './githubCatalogModule'; import { GithubLocationAnalyzer } from '../analyzers/GithubLocationAnalyzer'; @@ -75,8 +74,8 @@ describe('githubCatalogModule', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders?.pop()?.getProviderName()).toEqual( 'github-provider:default', diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts index 42e49c2f72..002b2bbfd1 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.test.ts @@ -19,12 +19,22 @@ import { TaskInvocationDefinition, TaskRunner, } from '@backstage/backend-tasks'; -import { ConfigReader } from '@backstage/config'; -import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; +import { Config, ConfigReader } from '@backstage/config'; +import { + DeferredEntity, + EntityProviderConnection, + locationSpecToMetadataName, +} from '@backstage/plugin-catalog-node'; import { GithubEntityProvider } from './GithubEntityProvider'; import * as helpers from '../lib/github'; import { EventParams } from '@backstage/plugin-events-node'; import { mockServices } from '@backstage/backend-test-utils'; +import { + Commit, + PushEvent, + RepositoryEvent, + RepositoryRenamedEvent, +} from '@octokit/webhooks-types'; jest.mock('../lib/github', () => { return { @@ -47,62 +57,88 @@ class PersistingTaskRunner implements TaskRunner { const logger = mockServices.logger.mock(); describe('GithubEntityProvider', () => { - afterEach(() => jest.clearAllMocks()); + const createSingleProviderConfig = (options?: { + providerConfig?: object; + unwrapped?: boolean; + }): Config => { + const providerConfig = { + organization: 'test-org', + ...options?.providerConfig, + }; - it('no provider config', () => { + return new ConfigReader({ + catalog: { + providers: { + github: options?.unwrapped + ? providerConfig + : { myProvider: providerConfig }, + }, + }, + }); + }; + + const createProviders = (config: Config) => { const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({}); - const providers = GithubEntityProvider.fromConfig(config, { + return GithubEntityProvider.fromConfig(config, { logger, schedule, }); + }; + + const createExpectedEntitiesForUrl = (url: string): DeferredEntity[] => { + return [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + annotations: { + 'backstage.io/managed-by-location': `url:${url}`, + 'backstage.io/managed-by-origin-location': `url:${url}`, + }, + name: locationSpecToMetadataName({ type: 'url', target: url }), + }, + spec: { + presence: 'optional', + target: url, + type: 'url', + }, + }, + locationKey: 'github-provider:myProvider', + }, + ]; + }; + + afterEach(() => jest.clearAllMocks()); + + it('no provider config', () => { + const config = new ConfigReader({}); + const providers = createProviders(config); expect(providers).toHaveLength(0); }); it('single simple provider config', () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - }, - }, - }, - }); - const providers = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - }); + const config = createSingleProviderConfig({ unwrapped: true }); + const providers = createProviders(config); expect(providers).toHaveLength(1); expect(providers[0].getProviderName()).toEqual('github-provider:default'); }); it('throws when the integration config does not exist', () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - host: 'ghe.internal.com', - }, - }, + const config = createSingleProviderConfig({ + providerConfig: { + host: 'ghe.internal.com', }, }); - expect(() => - GithubEntityProvider.fromConfig(config, { - logger, - schedule, - }), - ).toThrow(/There is no GitHub config that matches host ghe.internal.com/); + expect(() => createProviders(config)).toThrow( + /There is no GitHub config that matches host ghe.internal.com/, + ); }); it('multiple provider configs', () => { - const schedule = new PersistingTaskRunner(); const config = new ConfigReader({ catalog: { providers: { @@ -117,10 +153,7 @@ describe('GithubEntityProvider', () => { }, }, }); - const providers = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - }); + const providers = createProviders(config); expect(providers).toHaveLength(2); expect(providers[0].getProviderName()).toEqual( @@ -132,19 +165,12 @@ describe('GithubEntityProvider', () => { }); it('apply full update on scheduled execution with basic filters', async () => { - const config = new ConfigReader({ - catalog: { - providers: { - github: { - myProvider: { - organization: 'test-org', - catalogPath: 'custom/path/catalog-custom.yaml', - filters: { - branch: 'main', - repository: 'test-.*', - }, - }, - }, + const config = createSingleProviderConfig({ + providerConfig: { + catalogPath: 'custom/path/catalog-custom.yaml', + filters: { + branch: 'main', + repository: 'test-.*', }, }, }); @@ -194,27 +220,7 @@ describe('GithubEntityProvider', () => { await (taskDef.fn as () => Promise)(); const url = `https://github.com/test-org/test-repo/blob/main/custom/path/catalog-custom.yaml`; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - }, - name: 'generated-5e4b9498097f15434e88c477cfba6c079aa8ca7f', - }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:myProvider', - }, - ]; + const expectedEntities = createExpectedEntitiesForUrl(url); expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ @@ -379,27 +385,7 @@ describe('GithubEntityProvider', () => { await (taskDef.fn as () => Promise)(); const url = `https://github.com/test-org/test-repo/blob/main/custom/path/catalog-custom.yaml`; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - }, - name: 'generated-5e4b9498097f15434e88c477cfba6c079aa8ca7f', - }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:myProvider', - }, - ]; + const expectedEntities = createExpectedEntitiesForUrl(url); expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ @@ -487,27 +473,7 @@ describe('GithubEntityProvider', () => { await (taskDef.fn as () => Promise)(); const url = `https://github.com/test-org/another-repo/blob/main/catalog-custom.yaml`; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - }, - name: 'generated-934f500db2ba2e8ea3524567926f45a73bb0b532', - }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:myProvider', - }, - ]; + const expectedEntities = createExpectedEntitiesForUrl(url); expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ @@ -636,27 +602,7 @@ describe('GithubEntityProvider', () => { await (taskDef.fn as () => Promise)(); const url = `https://github.com/test-org/test-repo/blob/main/custom/path/catalog-custom.yaml`; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - }, - name: 'generated-5e4b9498097f15434e88c477cfba6c079aa8ca7f', - }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:myProvider', - }, - ]; + const expectedEntities = createExpectedEntitiesForUrl(url); expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ @@ -666,15 +612,7 @@ describe('GithubEntityProvider', () => { }); it('fail without schedule and scheduler', () => { - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - }, - }, - }, - }); + const config = createSingleProviderConfig(); expect(() => GithubEntityProvider.fromConfig(config, { @@ -687,15 +625,7 @@ describe('GithubEntityProvider', () => { const scheduler = { createScheduledTaskRunner: (_: any) => jest.fn(), } as unknown as PluginTaskScheduler; - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - }, - }, - }, - }); + const config = createSingleProviderConfig(); expect(() => GithubEntityProvider.fromConfig(config, { @@ -703,7 +633,7 @@ describe('GithubEntityProvider', () => { scheduler, }), ).toThrow( - 'No schedule provided neither via code nor config for github-provider:default', + 'No schedule provided neither via code nor config for github-provider:myProvider', ); }); @@ -712,18 +642,14 @@ describe('GithubEntityProvider', () => { const scheduler = { createScheduledTaskRunner: (_: any) => schedule, } as unknown as PluginTaskScheduler; - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - schedule: { - frequency: 'P1M', - timeout: 'PT3M', - }, - }, + const config = createSingleProviderConfig({ + providerConfig: { + schedule: { + frequency: 'P1M', + timeout: 'PT3M', }, }, + unwrapped: true, }); const providers = GithubEntityProvider.fromConfig(config, { logger, @@ -734,622 +660,836 @@ describe('GithubEntityProvider', () => { expect(providers[0].getProviderName()).toEqual('github-provider:default'); }); - it('apply delta update on added files from push event with glob catalog path', async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', + describe('on event', () => { + const createExpectedEntitiesForEvent = ( + event: EventParams, + options?: { branch?: string; catalogFilePath?: string }, + ): DeferredEntity[] => { + const url = `${event.eventPayload.repository.url}/blob/${ + options?.branch ?? 'main' + }/${options?.catalogFilePath ?? 'catalog-info.yaml'}`; + return createExpectedEntitiesForUrl(url); + }; + + describe('on push event', () => { + const createPushEvent = (options?: { + catalogFile?: { + action?: 'added' | 'removed' | 'modified'; + path?: string; + }; + org?: string; + ref?: string; + }): EventParams => { + const organization = options?.org ?? 'test-org'; + const repo = { + default_branch: 'main', + master_branch: 'main', + name: 'test-repo', + organization, + topics: [], + url: `https://github.com/${organization}/test-repo`, + } as Partial; + + const catalogCommit = { + added: [] as string[], + modified: [] as string[], + removed: [] as string[], + }; + catalogCommit[options?.catalogFile?.action ?? 'modified'] = [ + options?.catalogFile?.path ?? 'catalog-info.yaml', + ]; + + const event = { + ref: options?.ref ?? 'refs/heads/main', + repository: repo as PushEvent['repository'], + created: true, + deleted: false, + forced: false, + commits: [ + { + added: ['new-file.yaml'], + removed: [], + modified: [], + }, + catalogCommit, + ] as Partial[], + } as PushEvent; + + return { + topic: 'github.push', + metadata: { + 'x-github-event': 'push', + }, + eventPayload: event, + }; + }; + + it('apply delta update on added files with glob catalog path', async () => { + const config = createSingleProviderConfig({ + providerConfig: { catalogPath: '**/catalog-info.yaml', }, - }, - }, - }); + }); + const provider = createProviders(config)[0]; - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); - - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/main', - repository: { - name: 'teste-1', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], + const event = createPushEvent({ + catalogFile: { + action: 'added', + path: 'folder1/folder2/folder3/catalog-info.yaml', }, - { - added: ['folder1/folder2/folder3/catalog-info.yaml'], - removed: [], - modified: [], + }); + const expectedEntities = createExpectedEntitiesForEvent(event, { + catalogFilePath: 'folder1/folder2/folder3/catalog-info.yaml', + }); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: expectedEntities, + removed: [], + }); + }); + + it('apply delta update on added files', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createPushEvent({ + catalogFile: { + action: 'added', }, - ], - }, - }; - const url = - 'https://github.com/test-org/test-repo/blob/main/folder1/folder2/folder3/catalog-info.yaml'; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, + }); + const expectedEntities = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: expectedEntities, + removed: [], + }); + }); + + it('apply delta update on removed files', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createPushEvent({ + catalogFile: { + action: 'removed', + }, + }); + const expectedEntities = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expectedEntities, + }); + }); + + it('apply refresh call on modified files', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createPushEvent(); + + await provider.onEvent(event); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [ + 'url:https://github.com/test-org/test-repo/tree/main/catalog-info.yaml', + 'url:https://github.com/test-org/test-repo/blob/main/catalog-info.yaml', + ], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); + + it('apply refresh call on modified files with glob catalog path', async () => { + const config = new ConfigReader({ + catalog: { + providers: { + github: { + organization: 'test-org', + catalogPath: '**/catalog-info.yaml', + }, }, - name: 'generated-c499bfb5e3f159d2bfefe26cac86a8a0b95b47f0', }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:default', - }, - ]; + }); + const provider = createProviders(config)[0]; - await provider.onEvent(event); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'delta', - added: expectedEntities, - removed: [], - }); - }); + const event = createPushEvent(); - it('apply delta update on added files from push event', async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - }, - }, - }, - }); + await provider.onEvent(event); - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [ + 'url:https://github.com/test-org/test-repo/tree/main/catalog-info.yaml', + 'url:https://github.com/test-org/test-repo/blob/main/catalog-info.yaml', + 'url:https://github.com/test-org/test-repo/tree/main/**/catalog-info.yaml', + ], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); - - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/main', - repository: { - name: 'teste-1', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], - }, - { - added: ['catalog-info.yaml'], - removed: [], - modified: [], - }, - ], - }, - }; - const url = - 'https://github.com/test-org/test-repo/blob/main/catalog-info.yaml'; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - }, - name: 'generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3', - }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:default', - }, - ]; - - await provider.onEvent(event); - - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'delta', - added: expectedEntities, - removed: [], - }); - }); - - it('apply delta update on removed files from push event', async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - }, - }, - }, - }); - - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; - - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); - - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/main', - repository: { - name: 'teste-1', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], - }, - { - added: [], - removed: ['catalog-info.yaml'], - modified: [], - }, - ], - }, - }; - const url = - 'https://github.com/test-org/test-repo/blob/main/catalog-info.yaml'; - const expectedEntities = [ - { - entity: { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Location', - metadata: { - annotations: { - 'backstage.io/managed-by-location': `url:${url}`, - 'backstage.io/managed-by-origin-location': `url:${url}`, - }, - name: 'generated-8688630f57e421bc85f12b9828ed7dad6aff3bb3', - }, - spec: { - presence: 'optional', - target: `${url}`, - type: 'url', - }, - }, - locationKey: 'github-provider:default', - }, - ]; - - await provider.onEvent(event); - - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ - type: 'delta', - added: [], - removed: expectedEntities, - }); - }); - - it('apply refresh call on modified files from push event', async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - }, - }, - }, - }); - - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; - - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); - - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/main', - repository: { - name: 'teste-1', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], - }, - { - added: [], - removed: [], - modified: ['catalog-info.yaml'], - }, - ], - }, - }; - - await provider.onEvent(event); - - expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ - keys: [ - 'url:https://github.com/test-org/test-repo/tree/main/catalog-info.yaml', - 'url:https://github.com/test-org/test-repo/blob/main/catalog-info.yaml', - ], - }); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); - }); - - it('apply refresh call on modified files from push event when catalogPath contains a glob pattern', async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', - catalogPath: '**/catalog-info.yaml', - }, - }, - }, - }); - - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; - - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); - - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/main', - repository: { - name: 'teste-1', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], - }, - { - added: [], - removed: [], - modified: ['catalog-info.yaml'], - }, - ], - }, - }; - - await provider.onEvent(event); - - expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ - keys: [ - 'url:https://github.com/test-org/test-repo/tree/main/catalog-info.yaml', - 'url:https://github.com/test-org/test-repo/blob/main/catalog-info.yaml', - 'url:https://github.com/test-org/test-repo/tree/main/**/catalog-info.yaml', - ], - }); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); - }); - - it('should process repository when match filters from push event', async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', + it('should process repository when match filters', async () => { + const config = createSingleProviderConfig({ + providerConfig: { filters: { branch: 'my-special-branch', repository: 'test-repo', }, }, - }, - }, - }); + }); + const provider = createProviders(config)[0]; - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); + const event = createPushEvent({ ref: 'refs/heads/my-special-branch' }); - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/my-special-branch', - repository: { - name: 'test-repo', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], - }, - { - added: [], - removed: [], - modified: ['catalog-info.yaml'], - }, - ], - }, - }; + await provider.onEvent(event); - await provider.onEvent(event); + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ + keys: [ + 'url:https://github.com/test-org/test-repo/tree/my-special-branch/catalog-info.yaml', + 'url:https://github.com/test-org/test-repo/blob/my-special-branch/catalog-info.yaml', + ], + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); - expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(1); - expect(entityProviderConnection.refresh).toHaveBeenCalledWith({ - keys: [ - 'url:https://github.com/test-org/test-repo/tree/my-special-branch/catalog-info.yaml', - 'url:https://github.com/test-org/test-repo/blob/my-special-branch/catalog-info.yaml', - ], - }); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); - }); - - it("should skip process when didn't match filters from push event", async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', + it("should skip process when didn't match filters", async () => { + const config = createSingleProviderConfig({ + providerConfig: { filters: { repository: 'only-special-repository', }, }, - }, - }, - }); + }); + const provider = createProviders(config)[0]; - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); + const event = createPushEvent(); - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/main', - repository: { - name: 'teste-1', - url: 'https://github.com/test-org/test-repo', - default_branch: 'main', - stargazers: 0, - master_branch: 'main', - organization: 'test-org', - topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], - }, - { - added: [], - removed: [], - modified: ['catalog-info.yaml'], - }, - ], - }, - }; + await provider.onEvent(event); - await provider.onEvent(event); + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); - expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); - }); - - it("should skip process when didn't match org from push event", async () => { - const schedule = new PersistingTaskRunner(); - const config = new ConfigReader({ - catalog: { - providers: { - github: { - organization: 'test-org', + it("should skip process when didn't match org", async () => { + const config = createSingleProviderConfig({ + providerConfig: { filters: { branch: 'my-special-branch', repository: 'test-repo', }, }, - }, - }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createPushEvent({ + ref: 'refs/heads/my-special-branch', + org: 'other-org', + }); + + await provider.onEvent(event); + + expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + }); }); - const provider = GithubEntityProvider.fromConfig(config, { - logger, - schedule, - })[0]; - - const entityProviderConnection: EntityProviderConnection = { - applyMutation: jest.fn(), - refresh: jest.fn(), - }; - await provider.connect(entityProviderConnection); - - const event: EventParams = { - topic: 'github.push', - metadata: { - 'x-github-event': 'push', - }, - eventPayload: { - ref: 'refs/heads/my-special-branch', - repository: { + describe('on repository event', () => { + const createRepoEvent = ( + action: RepositoryEvent['action'], + ): EventParams => { + const repo = { name: 'test-repo', url: 'https://github.com/test-org/test-repo', default_branch: 'main', - stargazers: 0, master_branch: 'main', - organization: 'other-org', + organization: 'test-org', topics: [], - }, - created: true, - deleted: false, - forced: false, - commits: [ - { - added: ['new-file.yaml'], - removed: [], - modified: [], + archived: action === 'archived', + private: action !== 'publicized', + } as Partial; + + const event = { + action, + repository: repo as RepositoryEvent['repository'], + } as RepositoryEvent; + + if (action === 'renamed') { + (event as RepositoryRenamedEvent).changes = { + repository: { + name: { + from: `old-${event.repository.name}`, + }, + }, + }; + } + + return { + topic: 'github.repository', + metadata: { + 'x-github-event': 'repository', }, - { + eventPayload: event, + }; + }; + + describe('on repository archived event', () => { + it('skip on non-matching org', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + organization: 'other-org', + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('archived'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply delta update removing entities', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('archived'); + const expectedEntities = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 1, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', added: [], + removed: expectedEntities, + }); + }); + }); + + describe('on repository created event', () => { + it('skip', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('created'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + }); + + describe('on repository deleted event', () => { + it('skip on non-matching org', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + organization: 'other-org', + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('deleted'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply delta update removing entities', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('deleted'); + const expectedEntities = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 1, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expectedEntities, + }); + }); + }); + + describe('on repository edited event', () => { + it('skip on non-matching org', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + organization: 'other-org', + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('edited'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply delta update removing entities with non-matching filters', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + filters: { + topic: { + include: ['backstage-include'], + }, + }, + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('edited'); + const expectedEntities = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 1, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expectedEntities, + }); + }); + + it('apply no delta update with matching filters', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + filters: { + topic: { + include: ['backstage-include'], + }, + }, + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('edited'); + event.eventPayload.repository.topics = ['backstage-include']; + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + }); + + describe('on repository privatized event', () => { + it('skip', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('privatized'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + }); + + describe('on repository publicized event', () => { + it('skip', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('publicized'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + }); + + describe('on repository renamed event', () => { + it('skip on non-matching org', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + organization: 'other-org', + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('renamed'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply delta update removing entities with non-matching filters', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + filters: { + repository: 'other-.*', + }, + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent( + 'renamed', + ) as EventParams; + const urlOldRepo = `https://github.com/${event.eventPayload.repository.organization}/${event.eventPayload.changes.repository.name.from}/blob/main/catalog-info.yaml`; + const expectedEntitiesRemoved = + createExpectedEntitiesForUrl(urlOldRepo); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 1, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expectedEntitiesRemoved, + }); + }); + + it('apply delta update removing and adding entities with matching filters', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent( + 'renamed', + ) as EventParams; + const urlOldRepo = `https://github.com/${event.eventPayload.repository.organization}/${event.eventPayload.changes.repository.name.from}/blob/main/catalog-info.yaml`; + const expectedEntitiesRemoved = + createExpectedEntitiesForUrl(urlOldRepo); + const expectedEntitiesAdded = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 2, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: [], + removed: expectedEntitiesRemoved, + }); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: expectedEntitiesAdded, removed: [], - modified: ['catalog-info.yaml'], - }, - ], - }, - }; + }); + }); + }); - await provider.onEvent(event); + describe('on repository transferred event', () => { + it('skip on non-matching org', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + organization: 'other-org', + }, + }); + const provider = createProviders(config)[0]; - await provider.onEvent(event); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); - expect(entityProviderConnection.refresh).toHaveBeenCalledTimes(0); - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(0); + const event = createRepoEvent('transferred'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply no delta update with non-matching filters', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + filters: { + topic: { + include: ['backstage-include'], + }, + }, + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('transferred'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply delta update adding entities with matching filters', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('transferred'); + const expectedEntitiesAdded = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 1, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: expectedEntitiesAdded, + removed: [], + }); + }); + }); + + describe('on repository unarchived event', () => { + it('skip on non-matching org', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + organization: 'other-org', + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('unarchived'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply no delta update with non-matching filters', async () => { + const config = createSingleProviderConfig({ + providerConfig: { + filters: { + topic: { + include: ['backstage-include'], + }, + }, + }, + }); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('unarchived'); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 0, + ); + }); + + it('apply delta update adding entities with matching filters', async () => { + const config = createSingleProviderConfig(); + const provider = createProviders(config)[0]; + + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + await provider.connect(entityProviderConnection); + + const event = createRepoEvent('unarchived'); + const expectedEntitiesAdded = createExpectedEntitiesForEvent(event); + + await provider.onEvent(event); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes( + 1, + ); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'delta', + added: expectedEntitiesAdded, + removed: [], + }); + }); + }); + }); }); }); diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts index 978d9593c1..daade9660a 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProvider.ts @@ -38,7 +38,11 @@ import { GithubEntityProviderConfig, readProviderConfigs, } from './GithubEntityProviderConfig'; -import { getOrganizationRepositories } from '../lib/github'; +import { + getOrganizationRepositories, + getOrganizationRepository, + RepositoryResponse, +} from '../lib/github'; import { satisfiesForkFilter, satisfiesTopicFilter, @@ -50,11 +54,21 @@ import { EventsService, EventSubscriber, } from '@backstage/plugin-events-node'; -import { Commit, PushEvent } from '@octokit/webhooks-types'; +import { + Commit, + PushEvent, + RepositoryArchivedEvent, + RepositoryDeletedEvent, + RepositoryEditedEvent, + RepositoryEvent, + RepositoryRenamedEvent, + RepositoryTransferredEvent, + RepositoryUnarchivedEvent, +} from '@octokit/webhooks-types'; import { Minimatch } from 'minimatch'; import { LoggerService } from '@backstage/backend-plugin-api'; -const TOPIC_REPO_PUSH = 'github.push'; +const EVENT_TOPICS = ['github.push', 'github.repository']; type Repository = { name: string; @@ -157,7 +171,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { this.connection = connection; await this.events?.subscribe({ id: this.getProviderName(), - topics: [TOPIC_REPO_PUSH], + topics: EVENT_TOPICS, onEvent: params => this.onEvent(params), }); return await this.scheduleFn(); @@ -194,15 +208,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { const targets = await this.findCatalogFiles(); const matchingTargets = this.matchesFilters(targets); - const entities = matchingTargets - .map(repository => this.createLocationUrl(repository)) - .map(GithubEntityProvider.toLocationSpec) - .map(location => { - return { - locationKey: this.getProviderName(), - entity: locationSpecToLocationEntity({ location }), - }; - }); + const entities = this.toDeferredEntitiesFromRepos(matchingTargets); await this.connection.applyMutation({ type: 'full', @@ -214,38 +220,32 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { ); } - // go to the server and get all of the repositories - private async findCatalogFiles(): Promise { + private async createGraphqlClient() { const organization = this.config.organization; const host = this.integration.host; - const catalogPath = this.config.catalogPath; const orgUrl = `https://${host}/${organization}`; const { headers } = await this.githubCredentialsProvider.getCredentials({ url: orgUrl, }); - const client = graphql.defaults({ + return graphql.defaults({ baseUrl: this.integration.apiBaseUrl, headers, }); + } + + // go to the server and get all repositories + private async findCatalogFiles(): Promise { + const organization = this.config.organization; + const catalogPath = this.config.catalogPath; + const client = await this.createGraphqlClient(); const { repositories: repositoriesFromGithub } = await getOrganizationRepositories(client, organization, catalogPath); - const repositories = repositoriesFromGithub.map(r => { - return { - url: r.url, - name: r.name, - defaultBranchRef: r.defaultBranchRef?.name, - repositoryTopics: r.repositoryTopics.nodes.map(t => t.topic.name), - isArchived: r.isArchived, - isFork: r.isFork, - isCatalogInfoFilePresent: - r.catalogInfoFile?.__typename === 'Blob' && - r.catalogInfoFile.text !== '', - visibility: r.visibility, - }; - }); + const repositories = repositoriesFromGithub.map( + this.createRepoFromGithubResponse, + ); if (this.config.validateLocationsExist) { return repositories.filter( @@ -256,13 +256,13 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { return repositories; } - private matchesFilters(repositories: Repository[]) { + private matchesFilters(repositories: Repository[]): Repository[] { const repositoryFilter = this.config.filters?.repository; const topicFilters = this.config.filters?.topic; const allowForks = this.config.filters?.allowForks ?? true; const visibilities = this.config.filters?.visibility ?? []; - const matchingRepositories = repositories.filter(r => { + return repositories.filter(r => { const repoTopics: string[] = r.repositoryTopics; return ( !r.isArchived && @@ -273,7 +273,6 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { r.defaultBranchRef ); }); - return matchingRepositories; } private createLocationUrl(repository: Repository): string { @@ -296,23 +295,34 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.onEvent} */ async onEvent(params: EventParams): Promise { this.logger.debug(`Received event from ${params.topic}`); - if (params.topic !== TOPIC_REPO_PUSH) { - return; - } + if (EVENT_TOPICS.some(topic => topic === params.topic)) { + if (!this.connection) { + throw new Error('Not initialized'); + } - await this.onRepoPush(params.eventPayload as PushEvent); + switch (params.topic) { + case 'github.push': + await this.onPush(params.eventPayload as PushEvent); + return; + + case 'github.repository': + await this.onRepoChange(params.eventPayload as RepositoryEvent); + return; + + default: // should never be reached + this.logger.warn( + `Missing implementation for event of topic ${params.topic}`, + ); + } + } } /** {@inheritdoc @backstage/plugin-events-node#EventSubscriber.supportsEventTopics} */ supportsEventTopics(): string[] { - return [TOPIC_REPO_PUSH]; + return EVENT_TOPICS; } - private async onRepoPush(event: PushEvent) { - if (!this.connection) { - throw new Error('Not initialized'); - } - + private async onPush(event: PushEvent) { if (this.config.organization !== event.repository.organization) { this.logger.debug( `skipping push event from organization ${event.repository.organization}`, @@ -332,18 +342,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { return; } - const repository: Repository = { - url: event.repository.url, - name: event.repository.name, - defaultBranchRef: event.repository.default_branch, - repositoryTopics: event.repository.topics, - isArchived: event.repository.archived, - isFork: event.repository.fork, - // we can consider this file present because - // only the catalog file will be recovered from the commits - isCatalogInfoFilePresent: true, - visibility: event.repository.visibility, - }; + const repository = this.createRepoFromEvent(event); const matchingTargets = this.matchesFilters([repository]); if (matchingTargets.length === 0) { @@ -378,7 +377,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { ? this.config.catalogPath.substring(1) : this.config.catalogPath; - await this.connection.refresh({ + await this.connection!.refresh({ keys: [ ...new Set([ ...modified.map( @@ -396,7 +395,7 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { } if (added.length > 0 || removed.length > 0) { - await this.connection.applyMutation({ + await this.connection!.applyMutation({ type: 'delta', added: added, removed: removed, @@ -408,6 +407,263 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { ); } + private async onRepoChange(event: RepositoryEvent) { + if (this.config.organization !== event.repository.organization) { + this.logger.debug( + `skipping repository event from organization ${event.repository.organization}`, + ); + return; + } + + const action = event.action; + switch (action) { + case 'archived': + await this.onRepoArchived(event as RepositoryArchivedEvent); + return; + + // A repository was created. + case 'created': + // skip these events + return; + + case 'deleted': + await this.onRepoDeleted(event as RepositoryDeletedEvent); + return; + + case 'edited': + await this.onRepoEdited(event as RepositoryEditedEvent); + return; + + // The visibility of a repository was changed to `private`. + case 'privatized': + // skip these events + return; + + // The visibility of a repository was changed to `public`. + case 'publicized': + // skip these events + return; + + case 'renamed': + await this.onRepoRenamed(event as RepositoryRenamedEvent); + return; + + case 'transferred': + await this.onRepoTransferred(event as RepositoryTransferredEvent); + return; + + case 'unarchived': + await this.onRepoUnarchived(event as RepositoryUnarchivedEvent); + return; + + default: // should never be reached + this.logger.warn( + `Missing implementation for event of topic repository with action ${action}`, + ); + } + } + + /** + * A repository was archived. + * + * Removes all entities associated with the repository. + * + * @param event - The repository archived event. + * @private + */ + private async onRepoArchived(event: RepositoryArchivedEvent) { + const repository = this.createRepoFromEvent(event); + await this.removeEntitiesForRepo(repository); + this.logger.debug( + `Removed entities for archived repository ${repository.name}`, + ); + } + + /** + * A repository was deleted. + * + * Removes all entities associated with the repository. + * + * @param event - The repository deleted event. + * @private + */ + private async onRepoDeleted(event: RepositoryDeletedEvent) { + const repository = this.createRepoFromEvent(event); + await this.removeEntitiesForRepo(repository); + this.logger.debug( + `Removed entities for deleted repository ${repository.name}`, + ); + } + + /** + * The topics, default branch, description, or homepage of a repository was changed. + * + * We are interested in potential topic changes as these can be used as part of the filters. + * + * Removes all entities associated with the repository if the repository no longer matches the filters. + * + * @param event - The repository edited event. + * @private + */ + private async onRepoEdited(event: RepositoryEditedEvent) { + const repository = this.createRepoFromEvent(event); + + const matchingTargets = this.matchesFilters([repository]); + if (matchingTargets.length === 0) { + await this.removeEntitiesForRepo(repository); + } + // else: repository is (still) matching the filters, so we don't need to do anything + } + + /** + * The name of a repository was changed. + * + * Removes all entities associated with the repository's old name. + * Creates new entities for the repository's new name if it still matches the filters. + * + * @param event - The repository renamed event. + * @private + */ + private async onRepoRenamed(event: RepositoryRenamedEvent) { + const repository = this.createRepoFromEvent(event); + const oldRepoName = event.changes.repository.name.from; + const urlParts = event.repository.url.split('/'); + urlParts[urlParts.length - 1] = oldRepoName; + const oldRepoUrl = urlParts.join('/'); + const oldRepository: Repository = { + ...repository, + name: oldRepoName, + url: oldRepoUrl, + }; + await this.removeEntitiesForRepo(oldRepository); + + const matchingTargets = this.matchesFilters([repository]); + if (matchingTargets.length === 0) { + this.logger.debug( + `skipping repository transferred event for repository ${repository.name} because it didn't match provider filters`, + ); + return; + } + + await this.addEntitiesForRepo(repository); + } + + /** + * Ownership of the repository was transferred to a user or organization account. + * This event is only sent to the account where the ownership is transferred. + * To receive the `repository.transferred` event, the new owner account must have the GitHub App installed, + * and the App must be subscribed to "Repository" events. + * + * Creates new entities for the repository if it matches the filters. + * + * @param event - The repository unarchived event. + * @private + */ + private async onRepoTransferred(event: RepositoryTransferredEvent) { + const repository = this.createRepoFromEvent(event); + + const matchingTargets = this.matchesFilters([repository]); + if (matchingTargets.length === 0) { + this.logger.debug( + `skipping repository transferred event for repository ${repository.name} because it didn't match provider filters`, + ); + return; + } + + await this.addEntitiesForRepo(repository); + } + + /** + * A previously archived repository was unarchived. + * + * Creates new entities for the repository if it matches the filters. + * + * @param event - The repository unarchived event. + * @private + */ + private async onRepoUnarchived(event: RepositoryUnarchivedEvent) { + const repository = this.createRepoFromEvent(event); + + const matchingTargets = this.matchesFilters([repository]); + if (matchingTargets.length === 0) { + this.logger.debug( + `skipping repository transferred event for repository ${repository.name} because it didn't match provider filters`, + ); + return; + } + + await this.addEntitiesForRepo(repository); + } + + private async removeEntitiesForRepo(repository: Repository) { + const removed = this.toDeferredEntitiesFromRepos([repository]); + await this.connection!.applyMutation({ + type: 'delta', + added: [], + removed: removed, + }); + } + + private async addEntitiesForRepo(repository: Repository) { + if (this.config.validateLocationsExist) { + const organization = this.config.organization; + const catalogPath = this.config.catalogPath; + const client = await this.createGraphqlClient(); + + const repositoryFromGithub = await getOrganizationRepository( + client, + organization, + repository.name, + catalogPath, + ).then(r => (r ? this.createRepoFromGithubResponse(r) : null)); + + if (!repositoryFromGithub?.isCatalogInfoFilePresent) { + return; + } + } + + const added = this.toDeferredEntitiesFromRepos([repository]); + await this.connection!.applyMutation({ + type: 'delta', + added: added, + removed: [], + }); + } + + private createRepoFromEvent(event: RepositoryEvent | PushEvent): Repository { + return { + url: event.repository.url, + name: event.repository.name, + defaultBranchRef: event.repository.default_branch, + repositoryTopics: event.repository.topics, + isArchived: event.repository.archived, + isFork: event.repository.fork, + // we can consider this file present because + // only the catalog file will be recovered from the commits + isCatalogInfoFilePresent: true, + visibility: event.repository.visibility, + }; + } + + private createRepoFromGithubResponse( + repositoryResponse: RepositoryResponse, + ): Repository { + return { + url: repositoryResponse.url, + name: repositoryResponse.name, + defaultBranchRef: repositoryResponse.defaultBranchRef?.name, + repositoryTopics: repositoryResponse.repositoryTopics.nodes.map( + t => t.topic.name, + ), + isArchived: repositoryResponse.isArchived, + isFork: repositoryResponse.isFork, + isCatalogInfoFilePresent: + repositoryResponse.catalogInfoFile?.__typename === 'Blob' && + repositoryResponse.catalogInfoFile.text !== '', + visibility: repositoryResponse.visibility, + }; + } + private collectDeferredEntitiesFromCommit( repositoryUrl: string, branch: string, @@ -454,6 +710,20 @@ export class GithubEntityProvider implements EntityProvider, EventSubscriber { }; }); } + + private toDeferredEntitiesFromRepos( + repositories: Repository[], + ): DeferredEntity[] { + return repositories + .map(repository => this.createLocationUrl(repository)) + .map(GithubEntityProvider.toLocationSpec) + .map(location => { + return { + locationKey: this.getProviderName(), + entity: locationSpecToLocationEntity({ location }), + }; + }); + } } /* diff --git a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts index c83bfe5db1..296cbc12df 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubEntityProviderConfig.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readProviderConfigs } from './GithubEntityProviderConfig'; describe('readProviderConfigs', () => { @@ -270,7 +269,7 @@ describe('readProviderConfigs', () => { visibility: undefined, }, schedule: { - frequency: Duration.fromISO('PT30M'), + frequency: { minutes: 30 }, timeout: { minutes: 3, }, diff --git a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md index 92dc47ec69..5611260f87 100644 --- a/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab-org/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-catalog-backend-module-gitlab-org +## 0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## 0.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.0.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.22-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.21 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-node@1.12.4 + +## 0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.21-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-catalog-backend-module-gitlab@0.3.20-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + ## 0.0.2 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab-org/package.json b/plugins/catalog-backend-module-gitlab-org/package.json index 781dc54706..68e1c8f252 100644 --- a/plugins/catalog-backend-module-gitlab-org/package.json +++ b/plugins/catalog-backend-module-gitlab-org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab-org", - "version": "0.0.2", + "version": "0.0.6-next.2", "description": "The gitlab-org backend module for the catalog plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts index 1078936950..5fbc321cc2 100644 --- a/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab-org/src/catalogModuleGitlabOrgDiscoveryEntityProvider.test.ts @@ -22,7 +22,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsServiceRef } from '@backstage/plugin-events-node'; -import { Duration } from 'luxon'; import { catalogModuleGitlabOrgDiscoveryEntityProvider } from './catalogModuleGitlabOrgDiscoveryEntityProvider'; describe('catalogModuleGitlabOrgDiscoveryEntityProvider', () => { @@ -90,8 +89,8 @@ describe('catalogModuleGitlabOrgDiscoveryEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(runner).not.toHaveBeenCalled(); diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index ec7bbc1c27..737f79de59 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,99 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.3.22-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.3.22-next.1 + +### Patch Changes + +- c7b14ed: Adds new optional `excludeRepos` configuration option to the Gitlab catalog provider. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.3.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.3.21 + +### Patch Changes + +- 8db30ad: The Gitlab configuration supports an additional optional boolean key `catalog.providers.gitlab..restrictUsersToGroup`. Setting this to `true` will make Backstage only import users from the group defined in the `group` key, instead of all users in the organisation (self-hosted) or of the root group (SaaS). It will default to false, keeping the original implementation intact, when not explicitly set. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.3.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.3.20-next.0 + +### Patch Changes + +- 8db30ad: The Gitlab configuration supports an additional optional boolean key `catalog.providers.gitlab..restrictUsersToGroup`. Setting this to `true` will make Backstage only import users from the group defined in the `group` key, instead of all users in the organisation (self-hosted) or of the root group (SaaS). It will default to false, keeping the original implementation intact, when not explicitly set. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.3.18 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/api-report.md b/plugins/catalog-backend-module-gitlab/api-report.md index 47113af957..4bdca5c5ef 100644 --- a/plugins/catalog-backend-module-gitlab/api-report.md +++ b/plugins/catalog-backend-module-gitlab/api-report.md @@ -98,6 +98,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { export type GitlabProviderConfig = { host: string; group: string; + restrictUsersToGroup?: boolean; id: string; branch?: string; fallbackBranch: string; @@ -110,6 +111,7 @@ export type GitlabProviderConfig = { orgEnabled?: boolean; schedule?: TaskScheduleDefinition; skipForkedRepos?: boolean; + excludeRepos?: string[]; }; // @public @@ -123,6 +125,7 @@ export type GitLabUser = { avatar_url: string; groups?: GitLabGroup[]; group_saml_identity?: GitLabGroupSamlIdentity; + is_using_seat?: boolean; }; // @public diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 7d183b6e58..9f009ee478 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -63,6 +63,11 @@ export interface Config { * (Optional) Skip forked repository */ skipForkedRepos?: boolean; + /** + * (Optional) A list of strings containing the paths of the repositories to skip + * Should be in the format group/subgroup/repo, with no leading or trailing slashes. + */ + excludeRepos?: string[]; }; }; }; diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index d52b452122..d15deafd08 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", - "version": "0.3.18", + "version": "0.3.22-next.2", "description": "A Backstage catalog backend module that helps integrate towards GitLab", "backstage": { "role": "backend-plugin-module", @@ -62,7 +62,7 @@ "@backstage/plugin-events-node": "workspace:^", "@gitbeaker/rest": "^40.0.3", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts index c1608cd386..d7ce338d6d 100644 --- a/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/GitLabDiscoveryProcessor.test.ts @@ -16,7 +16,7 @@ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { LocationSpec } from '@backstage/plugin-catalog-node'; @@ -157,7 +157,7 @@ function getProcessor({ } describe('GitlabDiscoveryProcessor', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeAll(() => { jest.useFakeTimers(); diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts index 80aa0370d3..e49fb54465 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/handlers.ts @@ -19,6 +19,8 @@ import { all_groups_response, all_projects_response, all_saas_users_response, + all_self_hosted_group1_members, + subgroup_saas_users_response, all_users_response, apiBaseUrl, apiBaseUrlSaas, @@ -63,11 +65,21 @@ const httpHandlers = [ rest.get(`${apiBaseUrl}/groups/42`, (_, res, ctx) => { return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' })); }), + rest.get(`${apiBaseUrl}/groups/group1/members/all`, (_req, res, ctx) => { + return res(ctx.json(all_self_hosted_group1_members)); + }), rest.get(`${apiBaseUrlSaas}/groups/group1/members/all`, (_req, res, ctx) => { return res(ctx.json(all_saas_users_response)); }), + rest.get( + `${apiBaseUrlSaas}/groups/subgroup1/members/all`, + (_req, res, ctx) => { + return res(ctx.json(subgroup_saas_users_response)); // To-DO change + }, + ), + /** * Users REST endpoint mocks */ @@ -186,7 +198,34 @@ const graphqlHandlers = [ .link(graphQlBaseUrl) .query('getGroupMembers', async (req, res, ctx) => { const { group, relations } = req.variables; - + // group is actually full_path + if (group === 'group1/subgroup1' && relations.includes('DIRECT')) { + return res( + ctx.data({ + group: { + groupMembers: { + nodes: [ + { + user: { + id: 'gid://gitlab/User/1', + username: 'user1', + publicEmail: 'user1@example.com', + name: 'user1', + state: 'active', + webUrl: 'user1.com', + avatarUrl: 'user1', + }, + }, + ], + pageInfo: { + endCursor: 'end', + hasNextPage: false, + }, + }, + }, + }), + ); + } if (group === 'group1' && relations.includes('DIRECT')) { return res( ctx.data({ @@ -367,7 +406,31 @@ const graphqlHandlers = [ ]), ); } - + if (group === 'group1') { + return res( + ctx.data({ + group: { + descendantGroups: { + nodes: [ + { + id: 'gid://gitlab/Group/6', + name: 'subgroup1', + description: 'description1', + fullPath: 'group1/subgroup1', + parent: { + id: '123', + }, + }, + ], + pageInfo: { + endCursor: 'end', + hasNextPage: false, + }, + }, + }, + }), + ); + } if (group === 'group-with-parent') { return res( ctx.data({ diff --git a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts index e3154eb1ce..4a173c6b50 100644 --- a/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts +++ b/plugins/catalog-backend-module-gitlab/src/__testUtils__/mocks.ts @@ -299,6 +299,33 @@ export const config_single_integration_skip_forks: MockObject = { }, }; +export const config_single_integration_exclude_repos: MockObject = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'group1', + excludeRepos: ['group1/test-repo1'], + schedule: { + frequency: 'PT30M', + timeout: 'PT3M', + }, + }, + }, + }, + }, +}; + export const config_no_schedule: MockObject = { integrations: { gitlab: [ @@ -596,6 +623,127 @@ export const config_org_double_integration: MockObject = { }, }; +export const config_org_group_saas = { + integrations: { + gitlab: [ + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'gitlab.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, +}; + +export const config_org_group_restrictUsers_false_saas = { + integrations: { + gitlab: [ + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'gitlab.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, +}; + +export const config_org_group_restrictUsers_true_saas = { + integrations: { + gitlab: [ + { + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'gitlab.com', + group: 'group1/subgroup1', + restrictUsersToGroup: true, + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, +}; + +export const config_org_group_selfHosted = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + }, + }, + }, + }, +}; + +export const config_org_group_restrictUsers_true_selfHosted = { + integrations: { + gitlab: [ + { + host: 'example.com', + apiBaseUrl: 'https://example.com/api/v4', + token: '1234', + }, + ], + }, + catalog: { + providers: { + gitlab: { + 'test-id': { + host: 'example.com', + group: 'group1', + orgEnabled: true, + skipForkedRepos: true, + restrictUsersToGroup: true, + }, + }, + }, + }, +}; /** * GitLab API responses */ @@ -822,6 +970,12 @@ export const all_groups_response: GitLabGroup[] = [ description: '', full_path: 'parent1/nonMatchingGroup', }, + { + id: 6, + name: 'subgroup1', + description: '', + full_path: 'group1/subgroup1', + }, ]; export const group_with_parent: MockObject[] = [ @@ -1708,7 +1862,7 @@ export const expected_full_org_scan_entities: MockObject[] = [ name: 'JohnDoe', }, spec: { - memberOf: ['group1'], + memberOf: ['group1', 'group1-subgroup1'], profile: { displayName: 'John Doe', email: 'john.doe@company.com', @@ -1815,6 +1969,30 @@ export const expected_full_org_scan_entities: MockObject[] = [ }, locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/group1/subgroup1', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/group1/subgroup1', + 'example.com/team-path': 'group1/subgroup1', + }, + name: 'group1-subgroup1', + }, + spec: { + children: [], + profile: { + displayName: 'subgroup1', + }, + type: 'team', + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, ]; export const expected_full_org_scan_entities_saas: MockObject[] = [ @@ -1871,3 +2049,269 @@ export const expected_full_org_scan_entities_saas: MockObject[] = [ locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', }, ]; + +export const subgroup_saas_users_response: MockObject[] = [ + { + access_level: 30, + created_at: '2023-07-17T08:58:34.984Z', + expires_at: null, + id: 12, + username: 'testuser1', + name: 'Test User 1', + state: 'active', + avatar_url: 'https://secure.gravatar.com/', + web_url: 'https://gitlab.com/testuser1', + email: 'testuser1@example.com', + group_saml_identity: { + provider: 'group_saml', + extern_uid: '51', + saml_provider_id: 1, + }, + is_using_seat: true, + membership_state: 'active', + }, + { + access_level: 50, + created_at: '2023-07-15T08:58:34.984Z', + expires_at: '2023-10-26', + id: 54, + username: 'group_100_bot_23dc8057bef66e05181f39be4652577c', + name: 'Token Bot', + state: 'active', + avatar_url: 'https://secure.gravatar.com/', + web_url: + 'https://gitlab.com/group_100_bot_23dc8057bef66e05181f39be4652577c', + group_saml_identity: null, + is_using_seat: false, + membership_state: 'active', + }, +]; + +export const expected_subgroup_org_scan_entities_saas: MockObject[] = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://gitlab.com/testuser1', + 'backstage.io/managed-by-origin-location': + 'url:https://gitlab.com/testuser1', + 'gitlab.com/user-login': 'https://gitlab.com/testuser1', + 'gitlab.com/saml-external-uid': '51', + }, + name: 'testuser1', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Test User 1', + email: 'testuser1@example.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, +]; + +// Simulate return of all users but only with membership of the descendants of config.group +export const expected_full_members_group_org_scan_entities: MockObject[] = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:https://example.com/JohnDoe', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/JohnDoe', + 'example.com/user-login': 'https://gitlab.example/john_doe', + }, + name: 'JohnDoe', + }, + spec: { + memberOf: ['subgroup1'], + profile: { + displayName: 'John Doe', + email: 'john.doe@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:https://example.com/JaneDoe', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/JaneDoe', + 'example.com/user-login': 'https://gitlab.example/jane_doe', + }, + name: 'JaneDoe', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Jane Doe', + email: 'jane.doe@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/MarySmith', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/MarySmith', + 'example.com/user-login': 'https://gitlab.example/mary_smith', + }, + name: 'MarySmith', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Mary Smith', + email: 'mary.smith@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/MarioMario', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/MarioMario', + 'example.com/user-login': 'https://gitlab.example/mario_mario', + }, + name: 'MarioMario', + }, + spec: { + memberOf: [], + profile: { + displayName: 'Mario Mario', + email: 'mario.mario-company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/group1/subgroup1', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/group1/subgroup1', + 'example.com/team-path': 'group1/subgroup1', + }, + name: 'subgroup1', + }, + spec: { + children: [], + profile: { + displayName: 'subgroup1', + }, + type: 'team', + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, +]; + +export const expected_group_members_group_org_scan_entities: MockObject[] = [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + annotations: { + 'backstage.io/managed-by-location': 'url:https://example.com/JohnDoe', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/JohnDoe', + 'example.com/user-login': 'https://gitlab.example/john_doe', + }, + name: 'JohnDoe', + }, + spec: { + memberOf: ['subgroup1'], + profile: { + displayName: 'John Doe', + email: 'john.doe@company.com', + picture: 'https://secure.gravatar.com/', + }, + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + annotations: { + 'backstage.io/managed-by-location': + 'url:https://example.com/group1/subgroup1', + 'backstage.io/managed-by-origin-location': + 'url:https://example.com/group1/subgroup1', + 'example.com/team-path': 'group1/subgroup1', + }, + name: 'subgroup1', + description: 'description1', + }, + spec: { + children: [], + profile: { + displayName: 'subgroup1', + }, + type: 'team', + }, + }, + locationKey: 'GitlabOrgDiscoveryEntityProvider:test-id', + }, +]; + +export const all_self_hosted_group1_members: MockObject[] = [ + { + id: 1, + username: 'JohnDoe', + name: 'John Doe', + state: 'active', + email: 'john.doe@company.com', + avatar_url: 'https://secure.gravatar.com/', + web_url: 'https://gitlab.example/john_doe', + }, + // inactive + { + id: 5, + username: 'MarioMario', + name: 'Mario Mario', + state: 'inactive', + email: 'mario.mario-company.com', + avatar_url: 'https://secure.gravatar.com/', + web_url: 'https://gitlab.example/mario_mario', + }, +]; diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts index 7d40eb1204..1eebe7d6e0 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts @@ -16,7 +16,7 @@ import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { readGitLabIntegrationConfig } from '@backstage/integration'; @@ -27,7 +27,7 @@ import { GitLabClient, paginated } from './client'; import { GitLabGroup, GitLabUser } from './types'; const server = setupServer(...handlers); -setupRequestMockHandlers(server); +registerMswTestHooks(server); describe('GitLabClient', () => { describe('isSelfManaged', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts index 8f56657f68..b5a2b69131 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts @@ -114,6 +114,16 @@ export class GitLabClient { return response; } + async listGroupMembers( + groupPath: string, + options?: CommonListOptions, + ): Promise> { + return this.pagedRequest( + `/groups/${encodeURIComponent(groupPath)}/members/all`, + options, + ); + } + async listUsers( options?: UserListOptions, ): Promise> { @@ -128,13 +138,10 @@ export class GitLabClient { groupPath: string, options?: CommonListOptions, ): Promise> { - return this.pagedRequest( - `/groups/${encodeURIComponent(groupPath)}/members/all`, - { - ...options, - show_seat_info: true, - }, - ).then(resp => { + return this.listGroupMembers(groupPath, { + ...options, + show_seat_info: true, + }).then(resp => { resp.items = resp.items.filter(user => user.is_using_seat); return resp; }); diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts index 829cb27f6d..3e4aa54e4c 100644 --- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts +++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts @@ -60,6 +60,7 @@ export type GitLabUser = { avatar_url: string; groups?: GitLabGroup[]; group_saml_identity?: GitLabGroupSamlIdentity; + is_using_seat?: boolean; // Only available in responses from the group members endpoint }; /** @@ -149,6 +150,12 @@ export type GitlabProviderConfig = { * Accepts only groups under the provided path (which will be stripped) */ group: string; + + /** + * If true, the provider will only ingest users that are part of the configured group. + */ + restrictUsersToGroup?: boolean; + /** * ??? */ @@ -225,6 +232,11 @@ export type GitlabProviderConfig = { * If the project is a fork, skip repository */ skipForkedRepos?: boolean; + /** + * List of repositories to exclude from discovery, should be the full path to the repository, e.g. `group/project` + * Paths should not start or end with a slash. + */ + excludeRepos?: string[]; }; /** diff --git a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts index e4cff56bbd..636405220b 100644 --- a/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/module/catalogModuleGitlabDiscoveryEntityProvider.test.ts @@ -21,7 +21,6 @@ import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; import { eventsServiceRef } from '@backstage/plugin-events-node'; -import { Duration } from 'luxon'; import { GitlabDiscoveryEntityProvider } from '../providers'; import { catalogModuleGitlabDiscoveryEntityProvider } from './catalogModuleGitlabDiscoveryEntityProvider'; @@ -89,8 +88,8 @@ describe('catalogModuleGitlabDiscoveryEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('P1M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ months: 1 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(runner).not.toHaveBeenCalled(); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts index e89b378cfe..f5e167e564 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-tasks'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -32,7 +32,7 @@ import * as mock from '../__testUtils__/mocks'; import { GitlabDiscoveryEntityProvider } from './GitlabDiscoveryEntityProvider'; const server = setupServer(...handlers); -setupRequestMockHandlers(server); +registerMswTestHooks(server); afterEach(() => jest.clearAllMocks()); class PersistingTaskRunner implements TaskRunner { @@ -130,6 +130,7 @@ describe('GitlabDiscoveryEntityProvider - configuration', () => { ); }); }); + describe('GitlabDiscoveryEntityProvider - refresh', () => { it('should apply full update on scheduled execution', async () => { const config = new ConfigReader(mock.config_no_org_integration); @@ -231,6 +232,38 @@ describe('GitlabDiscoveryEntityProvider - refresh', () => { }); }); + it('should filter repositories that are excluded', async () => { + const config = new ConfigReader( + mock.config_single_integration_exclude_repos, + ); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + + await provider.connect(entityProviderConnection); + + await provider.refresh(logger); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_location_entities_default_branch.filter( + entity => + !entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('test-repo1') && + !entity.entity.metadata.annotations[ + 'backstage.io/managed-by-location' + ].includes('awesome'), + ), + }); + }); + // branch and fallback branch are undefined in the config it('should ingest catalog from project default branch only', async () => { const config = new ConfigReader(mock.config_single_integration); diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts index b2ef475d9d..bbd90795be 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts @@ -470,6 +470,13 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider { return false; } + if (this.config.excludeRepos?.includes(project.path_with_namespace ?? '')) { + this.logger.debug( + `Skipping project ${project.path_with_namespace} as it is excluded.`, + ); + return false; + } + const project_branch = this.config.branch ?? project.default_branch ?? diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts index 5a60f1b4b3..9706384ff0 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-tasks'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { EntityProviderConnection } from '@backstage/plugin-catalog-node'; @@ -33,7 +33,7 @@ import { GroupNameTransformerOptions } from '../lib/types'; import { GitlabOrgDiscoveryEntityProvider } from './GitlabOrgDiscoveryEntityProvider'; const server = setupServer(...handlers); -setupRequestMockHandlers(server); +registerMswTestHooks(server); afterEach(() => jest.clearAllMocks()); class PersistingTaskRunner implements TaskRunner { @@ -260,15 +260,16 @@ describe('GitlabOrgDiscoveryEntityProvider - refresh', () => { const taskDef = schedule.getTasks()[0]; await (taskDef.fn as () => Promise)(); - const userEntities = mock.expected_full_org_scan_entities.filter( + const entities = mock.expected_full_org_scan_entities.filter( element => element.entity.metadata.name !== 'MarioMario', ); // filter out user with non matched e-mail - expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); - expect(userEntities).not.toHaveLength(mock.all_users_response.length); + expect(entities).not.toHaveLength( + mock.expected_full_org_scan_entities.length, + ); expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ type: 'full', - entities: userEntities, + entities: entities, }); }); @@ -301,6 +302,168 @@ describe('GitlabOrgDiscoveryEntityProvider - refresh', () => { entities: mock.expected_full_org_scan_entities_saas, }); }); + + // This should return all members of the SaaS Root group (group1) -> expected_full_org_scan_entities_saas + it('SaaS: should get all saas root group users when restrictUsersToGroup is not set', async () => { + const config = new ConfigReader(mock.config_org_group_saas); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_full_org_scan_entities_saas, // + }); + }); + + // This should return all members of the SaaS Root group (group1) -> expected_full_org_scan_entities_saas + it('SaaS: should get all saas root group users when restrictUsersToGroup is false', async () => { + const config = new ConfigReader( + mock.config_org_group_restrictUsers_false_saas, + ); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_full_org_scan_entities_saas, // + }); + }); + + // This should return only members of the SaaS subgroup (group1/subgroup1) -> expected_subgroup_org_scan_entities_saas + it('SaaS: should get only subgroup users when restrictUsersToGroup is true', async () => { + const config = new ConfigReader( + mock.config_org_group_restrictUsers_true_saas, + ); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_subgroup_org_scan_entities_saas, + }); + }); + + // This should return all members of the self-hosted instance regardless of the group set -> expected_full_members_group_org_scan_entities + // All instance members, but only the group entities below the config.group + it('Self-hosted: should get all instance users when restrictUsersToGroup is not set', async () => { + const config = new ConfigReader(mock.config_org_group_selfHosted); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_full_members_group_org_scan_entities, // This should deliver all users but only their membership in subgroups of config.group + }); + }); + + // This should return all members of the self-hosted config.group and all group entities of config.group -> expected_full_members_group_org_scan_entities + it('Self-hosted: should get only groups users when restrictUsersToGroup is set', async () => { + const config = new ConfigReader( + mock.config_org_group_restrictUsers_true_selfHosted, + ); + const schedule = new PersistingTaskRunner(); + const entityProviderConnection: EntityProviderConnection = { + applyMutation: jest.fn(), + refresh: jest.fn(), + }; + const provider = GitlabOrgDiscoveryEntityProvider.fromConfig(config, { + logger, + schedule, + })[0]; + expect(provider.getProviderName()).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id', + ); + + await provider.connect(entityProviderConnection); + + const taskDef = schedule.getTasks()[0]; + expect(taskDef.id).toEqual( + 'GitlabOrgDiscoveryEntityProvider:test-id:refresh', + ); + await (taskDef.fn as () => Promise)(); + + expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1); + expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: mock.expected_group_members_group_org_scan_entities, // This should deliver all users but only their membership in subgroups of config.group + }); + }); }); describe('GitlabOrgDiscoveryEntityProvider with events support', () => { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts index 3415e01974..5d803745d4 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts @@ -356,7 +356,23 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { let groups; let users; - if (this.gitLabClient.isSelfManaged()) { + // Self-hosted: Fetch the users either from the defined group (restrictUsersToGroup) or fetch all users from the GitLab instance + // SaaS: Fetch the users from the defined group (restrictUsersToGroup) or fetch all users from the root group. + if (this.gitLabClient.isSelfManaged() && this.config.restrictUsersToGroup) { + groups = (await this.gitLabClient.listDescendantGroups(this.config.group)) + .items; + users = paginated( + options => + this.gitLabClient.listGroupMembers(this.config.group, options), // calls /groups//members + { + page: 1, + per_page: 100, + }, + ); + } else if ( + this.gitLabClient.isSelfManaged() && + !this.config.restrictUsersToGroup + ) { groups = paginated( options => this.gitLabClient.listGroups(options), { @@ -365,19 +381,19 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider { all_available: true, }, ); - users = paginated( - options => this.gitLabClient.listUsers(options), - { - page: 1, - per_page: 100, - active: true, - }, + options => this.gitLabClient.listUsers(options), // calls /users? + { page: 1, per_page: 100, active: true }, ); - } else { + } + // SaaS: Fetch the users from the defined group (restrictUsersToGroup) or fetch all users from the root group. + else { groups = (await this.gitLabClient.listDescendantGroups(this.config.group)) .items; - const rootGroup = this.config.group.split('/')[0]; + const rootGroupSplit = this.config.group.split('/'); + const rootGroup = this.config.restrictUsersToGroup + ? rootGroupSplit[rootGroupSplit.length - 1] + : rootGroupSplit[0]; users = paginated( options => this.gitLabClient.listSaaSUsers(rootGroup, options), { diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts index 1db5e24f01..7c044367b9 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readGitlabConfigs } from './config'; describe('config', () => { @@ -62,6 +61,8 @@ describe('config', () => { relations: [], schedule: undefined, skipForkedRepos: false, + excludeRepos: [], + restrictUsersToGroup: false, }), ); }); @@ -101,6 +102,8 @@ describe('config', () => { relations: [], schedule: undefined, skipForkedRepos: false, + excludeRepos: [], + restrictUsersToGroup: false, }), ); }); @@ -140,11 +143,55 @@ describe('config', () => { allowInherited: false, relations: [], schedule: undefined, + restrictUsersToGroup: false, + excludeRepos: [], skipForkedRepos: true, }), ); }); + it('valid config with excludeRepos', () => { + const config = new ConfigReader({ + catalog: { + providers: { + gitlab: { + test: { + group: 'group', + host: 'host', + branch: 'not-master', + fallbackBranch: 'main', + entityFilename: 'custom-file.yaml', + skipForkedRepos: false, + excludeRepos: ['foo/bar', 'quz/qux'], + }, + }, + }, + }, + }); + + const result = readGitlabConfigs(config); + expect(result).toHaveLength(1); + result.forEach(r => + expect(r).toStrictEqual({ + id: 'test', + group: 'group', + branch: 'not-master', + fallbackBranch: 'main', + host: 'host', + catalogFile: 'custom-file.yaml', + projectPattern: /[\s\S]*/, + groupPattern: /[\s\S]*/, + userPattern: /[\s\S]*/, + orgEnabled: false, + allowInherited: false, + schedule: undefined, + restrictUsersToGroup: false, + skipForkedRepos: false, + excludeRepos: ['foo/bar', 'quz/qux'], + }), + ); + }); + it('valid config with schedule', () => { const config = new ConfigReader({ catalog: { @@ -182,8 +229,10 @@ describe('config', () => { allowInherited: false, relations: [], skipForkedRepos: false, + restrictUsersToGroup: false, + excludeRepos: [], schedule: { - frequency: Duration.fromISO('PT30M'), + frequency: { minutes: 30 }, timeout: { minutes: 3, }, diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts index 32d3ad0cf2..fdf5f600e9 100644 --- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts +++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts @@ -49,10 +49,14 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { const skipForkedRepos: boolean = config.getOptionalBoolean('skipForkedRepos') ?? false; + const excludeRepos: string[] = + config.getOptionalStringArray('excludeRepos') ?? []; const schedule = config.has('schedule') ? readTaskScheduleDefinitionFromConfig(config.getConfig('schedule')) : undefined; + const restrictUsersToGroup = + config.getOptionalBoolean('restrictUsersToGroup') ?? false; return { id, @@ -69,6 +73,8 @@ function readGitlabConfig(id: string, config: Config): GitlabProviderConfig { allowInherited, relations, skipForkedRepos, + excludeRepos, + restrictUsersToGroup, }; } diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index 424baa2321..e56e14b95f 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,101 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.4.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.4.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.4.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-common@0.8.0 + +## 0.4.27 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.4.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-common@0.7.14 + +## 0.4.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + ## 0.4.24 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index eba5fc86c1..f3744d9b07 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", - "version": "0.4.24", + "version": "0.4.28-next.2", "description": "An entity provider for streaming large asset sources into the catalog", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index 62b8849ced..8b8907bd7b 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,103 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.7.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.7.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.7.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + +## 0.7.0 + +### Minor Changes + +- cb32ca7: **BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. + + Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.7.0-next.1 + +### Minor Changes + +- cb32ca7: **BREAKING**: `readLdapOrg` and the `LdapProviderConfig` type now always accept arrays of user and group configs, not just single items. + + Added support for single ldap catalog provider to provide list and undefined user and group bindings next to standard single one. + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.6.2-next.0 + +### Patch Changes + +- 083eaf9: Fix bug where ISO durations could no longer be used for schedules +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.6.0 ### Minor Changes diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 84de581387..2ee3c1ec79 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -16,11 +16,11 @@ import { GroupTransformer as GroupTransformer_2 } from '@backstage/plugin-catalo import { JsonValue } from '@backstage/types'; import { LocationSpec } from '@backstage/plugin-catalog-common'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { SchedulerService } from '@backstage/backend-plugin-api'; +import { SchedulerServiceTaskRunner } from '@backstage/backend-plugin-api'; +import { SchedulerServiceTaskScheduleDefinition } from '@backstage/backend-plugin-api'; import { SearchEntry } from 'ldapjs'; import { SearchOptions } from 'ldapjs'; -import { TaskRunner } from '@backstage/backend-tasks'; -import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { UserEntity } from '@backstage/catalog-model'; import { UserTransformer as UserTransformer_2 } from '@backstage/plugin-catalog-backend-module-ldap'; @@ -135,7 +135,7 @@ export interface LdapOrgEntityProviderLegacyOptions { groupTransformer?: GroupTransformer; id: string; logger: LoggerService; - schedule: 'manual' | TaskRunner; + schedule: 'manual' | SchedulerServiceTaskRunner; target: string; userTransformer?: UserTransformer; } @@ -145,8 +145,8 @@ export type LdapOrgEntityProviderOptions = | LdapOrgEntityProviderLegacyOptions | { logger: LoggerService; - schedule?: 'manual' | TaskRunner; - scheduler?: PluginTaskScheduler; + schedule?: 'manual' | SchedulerServiceTaskRunner; + scheduler?: SchedulerService; userTransformer?: UserTransformer | Record; groupTransformer?: GroupTransformer | Record; }; @@ -197,9 +197,9 @@ export type LdapProviderConfig = { target: string; tls?: TLSConfig; bind?: BindConfig; - users: UserConfig; - groups: GroupConfig; - schedule?: TaskScheduleDefinition; + users: UserConfig[]; + groups: GroupConfig[]; + schedule?: SchedulerServiceTaskScheduleDefinition; }; // @public @@ -223,8 +223,8 @@ export function readLdapLegacyConfig(config: Config): LdapProviderConfig[]; // @public export function readLdapOrg( client: LdapClient, - userConfig: UserConfig, - groupConfig: GroupConfig, + userConfig: UserConfig[], + groupConfig: GroupConfig[], options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-ldap/config.d.ts b/plugins/catalog-backend-module-ldap/config.d.ts index a585471033..65914fdf74 100644 --- a/plugins/catalog-backend-module-ldap/config.d.ts +++ b/plugins/catalog-backend-module-ldap/config.d.ts @@ -63,180 +63,352 @@ export interface Config { /** * The settings that govern the reading and interpretation of users. */ - users: { - /** - * The DN under which users are stored. - * - * E.g. "ou=people,ou=example,dc=example,dc=net" - */ - dn: string; - /** - * The search options to use. The default is scope "one" and - * attributes "*" and "+". - * - * It is common to want to specify a filter, to narrow down the set - * of matching items. - */ - options: { - scope?: 'base' | 'one' | 'sub'; - filter?: string; - attributes?: string | string[]; - sizeLimit?: number; - timeLimit?: number; - derefAliases?: number; - typesOnly?: boolean; - paged?: - | boolean - | { - pageSize?: number; - pagePause?: boolean; - }; - }; - /** - * JSON paths (on a.b.c form) and hard coded values to set on those - * paths. - * - * This can be useful for example if you want to hard code a - * namespace or similar on the generated entities. - */ - set?: { [key: string]: JsonValue }; - /** - * Mappings from well known entity fields, to LDAP attribute names - */ - map?: { - /** - * The name of the attribute that holds the relative - * distinguished name of each entry. Defaults to "uid". - */ - rdn?: string; - /** - * The name of the attribute that shall be used for the value of - * the metadata.name field of the entity. Defaults to "uid". - */ - name?: string; - /** - * The name of the attribute that shall be used for the value of - * the metadata.description field of the entity. - */ - description?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.profile.displayName field of the entity. Defaults to - * "cn". - */ - displayName?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.profile.email field of the entity. Defaults to - * "mail". - */ - email?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.profile.picture field of the entity. - */ - picture?: string; - /** - * The name of the attribute that shall be used for the values of - * the spec.memberOf field of the entity. Defaults to "memberOf". - */ - memberOf?: string; - }; - }; + users?: + | { + /** + * The DN under which users are stored. + * + * E.g. "ou=people,ou=example,dc=example,dc=net" + */ + dn: string; + /** + * The search options to use. The default is scope "one" and + * attributes "*" and "+". + * + * It is common to want to specify a filter, to narrow down the set + * of matching items. + */ + options: { + scope?: 'base' | 'one' | 'sub'; + filter?: string; + attributes?: string | string[]; + sizeLimit?: number; + timeLimit?: number; + derefAliases?: number; + typesOnly?: boolean; + paged?: + | boolean + | { + pageSize?: number; + pagePause?: boolean; + }; + }; + /** + * JSON paths (on a.b.c form) and hard coded values to set on those + * paths. + * + * This can be useful for example if you want to hard code a + * namespace or similar on the generated entities. + */ + set?: { [key: string]: JsonValue }; + /** + * Mappings from well known entity fields, to LDAP attribute names + */ + map?: { + /** + * The name of the attribute that holds the relative + * distinguished name of each entry. Defaults to "uid". + */ + rdn?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.name field of the entity. Defaults to "uid". + */ + name?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.description field of the entity. + */ + description?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.displayName field of the entity. Defaults to + * "cn". + */ + displayName?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.email field of the entity. Defaults to + * "mail". + */ + email?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.picture field of the entity. + */ + picture?: string; + /** + * The name of the attribute that shall be used for the values of + * the spec.memberOf field of the entity. Defaults to "memberOf". + */ + memberOf?: string; + }; + } + | Array<{ + /** + * The DN under which users are stored. + * + * E.g. "ou=people,ou=example,dc=example,dc=net" + */ + dn: string; + /** + * The search options to use. The default is scope "one" and + * attributes "*" and "+". + * + * It is common to want to specify a filter, to narrow down the set + * of matching items. + */ + options: { + scope?: 'base' | 'one' | 'sub'; + filter?: string; + attributes?: string | string[]; + sizeLimit?: number; + timeLimit?: number; + derefAliases?: number; + typesOnly?: boolean; + paged?: + | boolean + | { + pageSize?: number; + pagePause?: boolean; + }; + }; + /** + * JSON paths (on a.b.c form) and hard coded values to set on those + * paths. + * + * This can be useful for example if you want to hard code a + * namespace or similar on the generated entities. + */ + set?: { [key: string]: JsonValue }; + /** + * Mappings from well known entity fields, to LDAP attribute names + */ + map?: { + /** + * The name of the attribute that holds the relative + * distinguished name of each entry. Defaults to "uid". + */ + rdn?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.name field of the entity. Defaults to "uid". + */ + name?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.description field of the entity. + */ + description?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.displayName field of the entity. Defaults to + * "cn". + */ + displayName?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.email field of the entity. Defaults to + * "mail". + */ + email?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.picture field of the entity. + */ + picture?: string; + /** + * The name of the attribute that shall be used for the values of + * the spec.memberOf field of the entity. Defaults to "memberOf". + */ + memberOf?: string; + }; + }>; /** * The settings that govern the reading and interpretation of groups. */ - groups: { - /** - * The DN under which groups are stored. - * - * E.g. "ou=people,ou=example,dc=example,dc=net" - */ - dn: string; - /** - * The search options to use. The default is scope "one" and - * attributes "*" and "+". - * - * It is common to want to specify a filter, to narrow down the set - * of matching items. - */ - options: { - scope?: 'base' | 'one' | 'sub'; - filter?: string; - attributes?: string | string[]; - sizeLimit?: number; - timeLimit?: number; - derefAliases?: number; - typesOnly?: boolean; - paged?: - | boolean - | { - pageSize?: number; - pagePause?: boolean; - }; - }; - /** - * JSON paths (on a.b.c form) and hard coded values to set on those - * paths. - * - * This can be useful for example if you want to hard code a - * namespace or similar on the generated entities. - */ - set?: { [key: string]: JsonValue }; - /** - * Mappings from well known entity fields, to LDAP attribute names - */ - map?: { - /** - * The name of the attribute that holds the relative - * distinguished name of each entry. Defaults to "cn". - */ - rdn?: string; - /** - * The name of the attribute that shall be used for the value of - * the metadata.name field of the entity. Defaults to "cn". - */ - name?: string; - /** - * The name of the attribute that shall be used for the value of - * the metadata.description field of the entity. Defaults to - * "description". - */ - description?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.type field of the entity. Defaults to "groupType". - */ - type?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.profile.displayName field of the entity. Defaults to - * "cn". - */ - displayName?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.profile.email field of the entity. - */ - email?: string; - /** - * The name of the attribute that shall be used for the value of - * the spec.profile.picture field of the entity. - */ - picture?: string; - /** - * The name of the attribute that shall be used for the values of - * the spec.parent field of the entity. Defaults to "memberOf". - */ - memberOf?: string; - /** - * The name of the attribute that shall be used for the values of - * the spec.children field of the entity. Defaults to "member". - */ - members?: string; - }; - }; + groups?: + | { + /** + * The DN under which groups are stored. + * + * E.g. "ou=people,ou=example,dc=example,dc=net" + */ + dn: string; + /** + * The search options to use. The default is scope "one" and + * attributes "*" and "+". + * + * It is common to want to specify a filter, to narrow down the set + * of matching items. + */ + options: { + scope?: 'base' | 'one' | 'sub'; + filter?: string; + attributes?: string | string[]; + sizeLimit?: number; + timeLimit?: number; + derefAliases?: number; + typesOnly?: boolean; + paged?: + | boolean + | { + pageSize?: number; + pagePause?: boolean; + }; + }; + /** + * JSON paths (on a.b.c form) and hard coded values to set on those + * paths. + * + * This can be useful for example if you want to hard code a + * namespace or similar on the generated entities. + */ + set?: { [key: string]: JsonValue }; + /** + * Mappings from well known entity fields, to LDAP attribute names + */ + map?: { + /** + * The name of the attribute that holds the relative + * distinguished name of each entry. Defaults to "cn". + */ + rdn?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.name field of the entity. Defaults to "cn". + */ + name?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.description field of the entity. Defaults to + * "description". + */ + description?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.type field of the entity. Defaults to "groupType". + */ + type?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.displayName field of the entity. Defaults to + * "cn". + */ + displayName?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.email field of the entity. + */ + email?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.picture field of the entity. + */ + picture?: string; + /** + * The name of the attribute that shall be used for the values of + * the spec.parent field of the entity. Defaults to "memberOf". + */ + memberOf?: string; + /** + * The name of the attribute that shall be used for the values of + * the spec.children field of the entity. Defaults to "member". + */ + members?: string; + }; + } + | Array<{ + /** + * The DN under which groups are stored. + * + * E.g. "ou=people,ou=example,dc=example,dc=net" + */ + dn: string; + /** + * The search options to use. The default is scope "one" and + * attributes "*" and "+". + * + * It is common to want to specify a filter, to narrow down the set + * of matching items. + */ + options: { + scope?: 'base' | 'one' | 'sub'; + filter?: string; + attributes?: string | string[]; + sizeLimit?: number; + timeLimit?: number; + derefAliases?: number; + typesOnly?: boolean; + paged?: + | boolean + | { + pageSize?: number; + pagePause?: boolean; + }; + }; + /** + * JSON paths (on a.b.c form) and hard coded values to set on those + * paths. + * + * This can be useful for example if you want to hard code a + * namespace or similar on the generated entities. + */ + set?: { [key: string]: JsonValue }; + /** + * Mappings from well known entity fields, to LDAP attribute names + */ + map?: { + /** + * The name of the attribute that holds the relative + * distinguished name of each entry. Defaults to "cn". + */ + rdn?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.name field of the entity. Defaults to "cn". + */ + name?: string; + /** + * The name of the attribute that shall be used for the value of + * the metadata.description field of the entity. Defaults to + * "description". + */ + description?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.type field of the entity. Defaults to "groupType". + */ + type?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.displayName field of the entity. Defaults to + * "cn". + */ + displayName?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.email field of the entity. + */ + email?: string; + /** + * The name of the attribute that shall be used for the value of + * the spec.profile.picture field of the entity. + */ + picture?: string; + /** + * The name of the attribute that shall be used for the values of + * the spec.parent field of the entity. Defaults to "memberOf". + */ + memberOf?: string; + /** + * The name of the attribute that shall be used for the values of + * the spec.children field of the entity. Defaults to "member". + */ + members?: string; + }; + }>; }>; }; diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index beacc5e37f..fa627cb55f 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", - "version": "0.6.0", + "version": "0.7.1-next.2", "description": "A Backstage catalog backend module that helps integrate towards LDAP", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts index ddea9de03f..598bcc8839 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.test.ts @@ -42,38 +42,115 @@ describe('readLdapConfig', () => { id: 'default', target: 'target', bind: undefined, - users: { - dn: 'udn', - options: { - scope: 'one', - attributes: ['*', '+'], + users: [ + { + dn: 'udn', + options: { + scope: 'one', + attributes: ['*', '+'], + }, + set: undefined, + map: { + rdn: 'uid', + name: 'uid', + displayName: 'cn', + email: 'mail', + memberOf: 'memberOf', + }, }, - set: undefined, - map: { - rdn: 'uid', - name: 'uid', - displayName: 'cn', - email: 'mail', - memberOf: 'memberOf', + ], + groups: [ + { + dn: 'gdn', + options: { + scope: 'one', + attributes: ['*', '+'], + }, + set: undefined, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + type: 'groupType', + displayName: 'cn', + memberOf: 'memberOf', + members: 'member', + }, + }, + ], + }, + ]; + expect(actual).toEqual(expected); + }); + + it('reads schedules well', () => { + const config = { + catalog: { + providers: { + ldapOrg: { + default: { + schedule: { + frequency: 'PT3M', // should work for ISO durations + timeout: { minutes: 1 }, + }, + target: 'target', + users: { + dn: 'udn', + }, + groups: { + dn: 'gdn', + }, + }, }, }, - groups: { - dn: 'gdn', - options: { - scope: 'one', - attributes: ['*', '+'], - }, - set: undefined, - map: { - rdn: 'cn', - name: 'cn', - description: 'description', - type: 'groupType', - displayName: 'cn', - memberOf: 'memberOf', - members: 'member', - }, + }, + }; + const actual = readProviderConfigs(new ConfigReader(config)); + const expected = [ + { + id: 'default', + target: 'target', + bind: undefined, + schedule: { + frequency: { minutes: 3 }, + timeout: { minutes: 1 }, }, + users: [ + { + dn: 'udn', + options: { + scope: 'one', + attributes: ['*', '+'], + }, + set: undefined, + map: { + rdn: 'uid', + name: 'uid', + displayName: 'cn', + email: 'mail', + memberOf: 'memberOf', + }, + }, + ], + groups: [ + { + dn: 'gdn', + options: { + scope: 'one', + attributes: ['*', '+'], + }, + set: undefined, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + type: 'groupType', + displayName: 'cn', + memberOf: 'memberOf', + members: 'member', + }, + }, + ], }, ]; expect(actual).toEqual(expected); @@ -159,57 +236,61 @@ describe('readLdapConfig', () => { keys: '/tmp/keys.pem', certs: '/tmp/certs.pem', }, - users: { - dn: 'udn', - options: { - scope: 'base', - attributes: ['*'], - filter: 'f', - paged: true, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 0, - typesOnly: false, - }, - set: { p: 'v' }, - map: { - rdn: 'u', - name: 'v', - description: 'd', - displayName: 'c', - email: 'm', - picture: 'p', - memberOf: 'm', - }, - }, - groups: { - dn: 'gdn', - options: { - scope: 'base', - attributes: ['*'], - filter: 'f', - paged: { - pageSize: 7, - pagePause: true, + users: [ + { + dn: 'udn', + options: { + scope: 'base', + attributes: ['*'], + filter: 'f', + paged: true, + timeLimit: 42, + sizeLimit: 100, + derefAliases: 0, + typesOnly: false, + }, + set: { p: 'v' }, + map: { + rdn: 'u', + name: 'v', + description: 'd', + displayName: 'c', + email: 'm', + picture: 'p', + memberOf: 'm', }, - timeLimit: 42, - sizeLimit: 100, - derefAliases: 1, - typesOnly: true, }, - set: { p: 'v' }, - map: { - rdn: 'u', - name: 'v', - description: 'd', - type: 't', - displayName: 'c', - email: 'm', - picture: 'p', - memberOf: 'm', - members: 'n', + ], + groups: [ + { + dn: 'gdn', + options: { + scope: 'base', + attributes: ['*'], + filter: 'f', + paged: { + pageSize: 7, + pagePause: true, + }, + timeLimit: 42, + sizeLimit: 100, + derefAliases: 1, + typesOnly: true, + }, + set: { p: 'v' }, + map: { + rdn: 'u', + name: 'v', + description: 'd', + type: 't', + displayName: 'c', + email: 'm', + picture: 'p', + memberOf: 'm', + members: 'n', + }, }, - }, + ], }, ]; expect(actual).toEqual(expected); @@ -247,7 +328,7 @@ describe('readLdapConfig', () => { const actual = readProviderConfigs(new ConfigReader(config)); const expected = '(|(cn=foo bar)(cn=bar))'; - expect(actual[0].users.options.filter).toEqual(expected); + expect(actual[0].users[0].options.filter).toEqual(expected); }); it('supports a dot nested set structure', () => { @@ -284,7 +365,9 @@ describe('readLdapConfig', () => { }; const actual = readProviderConfigs(new ConfigReader(config)); - expect(actual[0].users.set).toEqual({ 'metadata.annotations': { a: 'b' } }); + expect(actual[0].users[0].set).toEqual({ + 'metadata.annotations': { a: 'b' }, + }); }); it('throws on attempts to modify the set structure', () => { @@ -320,25 +403,77 @@ describe('readLdapConfig', () => { const actual = readProviderConfigs(new ConfigReader(config)); expect(() => { - (actual[0].users.set as any).y = 2; + (actual[0].users[0].set as any).y = 2; }).toThrowErrorMatchingInlineSnapshot( `"Cannot add property y, object is not extensible"`, ); expect(() => { - (actual[0].users.set as any).x.b = 2; + (actual[0].users[0].set as any).x.b = 2; }).toThrowErrorMatchingInlineSnapshot( `"Cannot add property b, object is not extensible"`, ); expect(() => { - (actual[0].groups.set as any).y = 2; + (actual[0].groups[0].set as any).y = 2; }).toThrowErrorMatchingInlineSnapshot( `"Cannot add property y, object is not extensible"`, ); expect(() => { - (actual[0].groups.set as any).x.b = 2; + (actual[0].groups[0].set as any).x.b = 2; }).toThrowErrorMatchingInlineSnapshot( `"Cannot add property b, object is not extensible"`, ); }); + + it('supports users/groups config as list', () => { + const config = { + catalog: { + providers: { + ldapOrg: { + default: { + target: 'target', + users: [ + { + dn: 'udn1', + }, + { + dn: 'udn2', + }, + ], + groups: [ + { + dn: 'gdn1', + }, + { + dn: 'gdn2', + }, + ], + }, + }, + }, + }, + }; + const actual = readProviderConfigs(new ConfigReader(config)); + + expect(actual[0].users).toHaveLength(2); + expect(actual[0].groups).toHaveLength(2); + }); + + it('supports users/groups config as undefined', () => { + const config = { + catalog: { + providers: { + ldapOrg: { + default: { + target: 'target', + }, + }, + }, + }, + }; + const actual = readProviderConfigs(new ConfigReader(config)); + + expect(actual[0].users).toHaveLength(0); + expect(actual[0].groups).toHaveLength(0); + }); }); diff --git a/plugins/catalog-backend-module-ldap/src/ldap/config.ts b/plugins/catalog-backend-module-ldap/src/ldap/config.ts index 2bd21fd166..d7e1035545 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/config.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/config.ts @@ -15,9 +15,9 @@ */ import { - readTaskScheduleDefinitionFromConfig, - TaskScheduleDefinition, -} from '@backstage/backend-tasks'; + SchedulerServiceTaskScheduleDefinition, + readSchedulerServiceTaskScheduleDefinitionFromConfig, +} from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { JsonValue } from '@backstage/types'; import { SearchOptions } from 'ldapjs'; @@ -42,11 +42,11 @@ export type LdapProviderConfig = { // command is not issued. bind?: BindConfig; // The settings that govern the reading and interpretation of users - users: UserConfig; + users: UserConfig[]; // The settings that govern the reading and interpretation of groups - groups: GroupConfig; + groups: GroupConfig[]; // Schedule configuration for refresh tasks. - schedule?: TaskScheduleDefinition; + schedule?: SchedulerServiceTaskScheduleDefinition; }; /** @@ -161,34 +161,33 @@ export type GroupConfig = { }; }; -const defaultConfig = { - users: { - options: { - scope: 'one', - attributes: ['*', '+'], - }, - map: { - rdn: 'uid', - name: 'uid', - displayName: 'cn', - email: 'mail', - memberOf: 'memberOf', - }, +const defaultUserConfig = { + options: { + scope: 'one', + attributes: ['*', '+'], }, - groups: { - options: { - scope: 'one', - attributes: ['*', '+'], - }, - map: { - rdn: 'cn', - name: 'cn', - description: 'description', - displayName: 'cn', - type: 'groupType', - memberOf: 'memberOf', - members: 'member', - }, + map: { + rdn: 'uid', + name: 'uid', + displayName: 'cn', + email: 'mail', + memberOf: 'memberOf', + }, +}; + +const defaultGroupConfig = { + options: { + scope: 'one', + attributes: ['*', '+'], + }, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + displayName: 'cn', + type: 'groupType', + memberOf: 'memberOf', + members: 'member', }, }; @@ -272,9 +271,7 @@ function readSetConfig( return c.get(); } -function readUserMapConfig( - c: Config | undefined, -): Partial { +function readUserMapConfig(c: Config | undefined): Partial { if (!c) { return {}; } @@ -292,7 +289,7 @@ function readUserMapConfig( function readGroupMapConfig( c: Config | undefined, -): Partial { +): Partial { if (!c) { return {}; } @@ -311,8 +308,18 @@ function readGroupMapConfig( } function readUserConfig( - c: Config, + c: Config | Config[] | undefined, ): RecursivePartial { + if (!c) { + return []; + } + if (Array.isArray(c)) { + return c.map(it => readSingleUserConfig(it)); + } + return [readSingleUserConfig(c)]; +} + +function readSingleUserConfig(c: Config): RecursivePartial { return { dn: c.getString('dn'), options: readOptionsConfig(c.getOptionalConfig('options')), @@ -322,8 +329,18 @@ function readUserConfig( } function readGroupConfig( - c: Config, + c: Config | Config[] | undefined, ): RecursivePartial { + if (!c) { + return []; + } + if (Array.isArray(c)) { + return c.map(it => readSingleGroupConfig(it)); + } + return [readSingleGroupConfig(c)]; +} + +function readSingleGroupConfig(c: Config): RecursivePartial { return { dn: c.getString('dn'), options: readOptionsConfig(c.getOptionalConfig('options')), @@ -352,14 +369,15 @@ export function readLdapLegacyConfig(config: Config): LdapProviderConfig[] { target: trimEnd(c.getString('target'), '/'), tls: readTlsConfig(c.getOptionalConfig('tls')), bind: readBindConfig(c.getOptionalConfig('bind')), - users: readUserConfig(c.getConfig('users')), - groups: readGroupConfig(c.getConfig('groups')), + users: readUserConfig(c.getConfig('users')).map(it => { + return mergeWith({}, defaultUserConfig, it, replaceArraysIfPresent); + }), + groups: readGroupConfig(c.getConfig('groups')).map(it => { + return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent); + }), }; - const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => { - // Replace arrays instead of merging, otherwise default behavior - return Array.isArray(from) ? from : undefined; - }); - return freeze(merged) as LdapProviderConfig; + + return freeze(newConfig) as LdapProviderConfig; }); } @@ -380,22 +398,41 @@ export function readProviderConfigs(config: Config): LdapProviderConfig[] { const c = providersConfig.getConfig(id); const schedule = c.has('schedule') - ? readTaskScheduleDefinitionFromConfig(c.getConfig('schedule')) + ? readSchedulerServiceTaskScheduleDefinitionFromConfig( + c.getConfig('schedule'), + ) : undefined; + const isUserList = Array.isArray(c.getOptional('users')); + const isGroupList = Array.isArray(c.getOptional('groups')); + const newConfig = { id, target: trimEnd(c.getString('target'), '/'), tls: readTlsConfig(c.getOptionalConfig('tls')), bind: readBindConfig(c.getOptionalConfig('bind')), - users: readUserConfig(c.getConfig('users')), - groups: readGroupConfig(c.getConfig('groups')), + users: readUserConfig( + isUserList + ? c.getOptionalConfigArray('users') + : c.getOptionalConfig('users'), + ).map(it => { + return mergeWith({}, defaultUserConfig, it, replaceArraysIfPresent); + }), + groups: readGroupConfig( + isGroupList + ? c.getOptionalConfigArray('groups') + : c.getOptionalConfig('groups'), + ).map(it => { + return mergeWith({}, defaultGroupConfig, it, replaceArraysIfPresent); + }), schedule, }; - const merged = mergeWith({}, defaultConfig, newConfig, (_into, from) => { - // Replace arrays instead of merging, otherwise default behavior - return Array.isArray(from) ? from : undefined; - }); - return freeze(merged) as LdapProviderConfig; + + return freeze(newConfig) as LdapProviderConfig; }); } + +function replaceArraysIfPresent(_into: any, from: any) { + // Replace arrays instead of merging, otherwise default behavior + return Array.isArray(from) ? from : undefined; +} diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts index fc451fde97..1c3dfb179a 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.test.ts @@ -99,19 +99,21 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig = { - dn: 'ddd', - options: {}, - map: { - rdn: 'uid', - name: 'uid', - description: 'description', - displayName: 'cn', - email: 'mail', - picture: 'avatarUrl', - memberOf: 'memberOf', + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, }, - }; + ]; const { users, userMemberOf } = await readLdapUsers(client, config); expect(users).toEqual([ expect.objectContaining({ @@ -160,19 +162,21 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig = { - dn: 'ddd', - options: {}, - map: { - rdn: 'uid', - name: 'uid', - description: 'description', - displayName: 'cn', - email: 'mail', - picture: 'avatarUrl', - memberOf: 'memberOf', + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, }, - }; + ]; const { users, userMemberOf } = await readLdapUsers(client, config); expect(users).toEqual([ expect.objectContaining({ @@ -216,19 +220,21 @@ describe('readLdapUsers', () => { }), ); }); - const config: UserConfig = { - dn: 'ddd', - options: {}, - map: { - rdn: 'uid', - name: 'uid', - description: 'description', - displayName: 'cn', - email: 'mail', - picture: 'avatarUrl', - memberOf: 'memberOf', + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, }, - }; + ]; const { users, userMemberOf } = await readLdapUsers(client, config); expect(users).toEqual([ expect.objectContaining({ @@ -255,6 +261,58 @@ describe('readLdapUsers', () => { new Map([['dn-value', new Set(['x', 'y', 'z'])]]), ); }); + it('can process a list of UserConfigs', async () => { + client.getVendor.mockResolvedValue(DefaultLdapVendor); + client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { + await fn( + searchEntry({ + uid: ['uid-value'], + description: ['description-value'], + cn: ['cn-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + entryDN: ['dn-value'], + entryUUID: ['uuid-value'], + }), + ); + }); + const config: UserConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, + }, + { + dn: 'ddd', + options: {}, + map: { + rdn: 'uid', + name: 'uid', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + memberOf: 'memberOf', + }, + }, + ]; + const { users } = await readLdapUsers(client, config); + expect(users).toHaveLength(2); + }); + it('can process no UserConfigs', async () => { + const config: UserConfig[] = []; + const { users } = await readLdapUsers(client, config); + expect(users).toHaveLength(0); + }); }); describe('readLdapGroups', () => { @@ -282,21 +340,23 @@ describe('readLdapGroups', () => { }), ); }); - const config: GroupConfig = { - dn: 'ddd', - options: {}, - map: { - rdn: 'cn', - name: 'cn', - description: 'description', - displayName: 'cn', - email: 'mail', - picture: 'avatarUrl', - type: 'tt', - memberOf: 'memberOf', - members: 'member', + const config: GroupConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + type: 'tt', + memberOf: 'memberOf', + members: 'member', + }, }, - }; + ]; const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, config, @@ -353,21 +413,23 @@ describe('readLdapGroups', () => { }), ); }); - const config: GroupConfig = { - dn: 'ddd', - options: {}, - map: { - rdn: 'cn', - name: 'cn', - description: 'description', - displayName: 'cn', - email: 'mail', - picture: 'avatarUrl', - type: 'tt', - memberOf: 'memberOf', - members: 'member', + const config: GroupConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + type: 'tt', + memberOf: 'memberOf', + members: 'member', + }, }, - }; + ]; const { groups, groupMember, groupMemberOf } = await readLdapGroups( client, config, @@ -401,6 +463,65 @@ describe('readLdapGroups', () => { new Map([['dn-value', new Set(['x', 'y', 'z'])]]), ); }); + + it('can process a list of GroupConfigs', async () => { + client.getVendor.mockResolvedValue(DefaultLdapVendor); + client.searchStreaming.mockImplementation(async (_dn, _opts, fn) => { + await fn( + searchEntry({ + cn: ['cn-value'], + description: ['description-value'], + tt: ['type-value'], + mail: ['mail-value'], + avatarUrl: ['avatarUrl-value'], + memberOf: ['x', 'y', 'z'], + member: ['e', 'f', 'g'], + entryDN: ['dn-value'], + entryUUID: ['uuid-value'], + }), + ); + }); + const config: GroupConfig[] = [ + { + dn: 'ddd', + options: {}, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + type: 'tt', + memberOf: 'memberOf', + members: 'member', + }, + }, + { + dn: 'ddd', + options: {}, + map: { + rdn: 'cn', + name: 'cn', + description: 'description', + displayName: 'cn', + email: 'mail', + picture: 'avatarUrl', + type: 'tt', + memberOf: 'memberOf', + members: 'member', + }, + }, + ]; + const { groups } = await readLdapGroups(client, config); + expect(groups).toHaveLength(2); + }); + + it('can process no GroupConfigs', async () => { + const config: GroupConfig[] = []; + const { groups } = await readLdapGroups(client, config); + expect(groups).toHaveLength(0); + }); }); describe('resolveRelations', () => { diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index 50036b94b0..9e6e11a0bc 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -104,32 +104,36 @@ export async function defaultUserTransformer( */ export async function readLdapUsers( client: LdapClient, - config: UserConfig, + config: UserConfig[], opts?: { transformer?: UserTransformer }, ): Promise<{ users: UserEntity[]; // With all relations empty userMemberOf: Map>; // DN -> DN or UUID of groups }> { - const { dn, options, map } = config; - const vendor = await client.getVendor(); - + if (config.length === 0) { + return { users: [], userMemberOf: new Map() }; + } const entities: UserEntity[] = []; const userMemberOf: Map> = new Map(); + const vendor = await client.getVendor(); const transformer = opts?.transformer ?? defaultUserTransformer; - await client.searchStreaming(dn, options, async user => { - const entity = await transformer(vendor, config, user); + for (const cfg of config) { + const { dn, options, map } = cfg; + await client.searchStreaming(dn, options, async user => { + const entity = await transformer(vendor, cfg, user); - if (!entity) { - return; - } + if (!entity) { + return; + } - mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => { - ensureItems(userMemberOf, myDn, vs); + mapReferencesAttr(user, vendor, map.memberOf, (myDn, vs) => { + ensureItems(userMemberOf, myDn, vs); + }); + entities.push(entity); }); - entities.push(entity); - }); + } return { users: entities, userMemberOf }; } @@ -206,7 +210,7 @@ export async function defaultGroupTransformer( */ export async function readLdapGroups( client: LdapClient, - config: GroupConfig, + config: GroupConfig[], opts?: { transformer?: GroupTransformer; }, @@ -215,35 +219,40 @@ export async function readLdapGroups( groupMemberOf: Map>; // DN -> DN or UUID of groups groupMember: Map>; // DN -> DN or UUID of groups & users }> { + if (config.length === 0) { + return { groups: [], groupMemberOf: new Map(), groupMember: new Map() }; + } const groups: GroupEntity[] = []; const groupMemberOf: Map> = new Map(); const groupMember: Map> = new Map(); - const { dn, map, options } = config; const vendor = await client.getVendor(); - const transformer = opts?.transformer ?? defaultGroupTransformer; - await client.searchStreaming(dn, options, async entry => { - if (!entry) { - return; - } + for (const cfg of config) { + const { dn, map, options } = cfg; - const entity = await transformer(vendor, config, entry); + await client.searchStreaming(dn, options, async entry => { + if (!entry) { + return; + } - if (!entity) { - return; - } + const entity = await transformer(vendor, cfg, entry); - mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { - ensureItems(groupMemberOf, myDn, vs); + if (!entity) { + return; + } + + mapReferencesAttr(entry, vendor, map.memberOf, (myDn, vs) => { + ensureItems(groupMemberOf, myDn, vs); + }); + mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => { + ensureItems(groupMember, myDn, vs); + }); + + groups.push(entity); }); - mapReferencesAttr(entry, vendor, map.members, (myDn, vs) => { - ensureItems(groupMember, myDn, vs); - }); - - groups.push(entity); - }); + } return { groups, @@ -264,8 +273,8 @@ export async function readLdapGroups( */ export async function readLdapOrg( client: LdapClient, - userConfig: UserConfig, - groupConfig: GroupConfig, + userConfig: UserConfig[], + groupConfig: GroupConfig[], options: { groupTransformer?: GroupTransformer; userTransformer?: UserTransformer; diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts index fb8f27d685..0e5fae55bb 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { PluginTaskScheduler, TaskRunner } from '@backstage/backend-tasks'; import { ANNOTATION_LOCATION, ANNOTATION_ORIGIN_LOCATION, @@ -35,7 +34,11 @@ import { readLdapOrg, UserTransformer, } from '../ldap'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + LoggerService, + SchedulerService, + SchedulerServiceTaskRunner, +} from '@backstage/backend-plugin-api'; import { readLdapLegacyConfig, readProviderConfigs } from '../ldap'; /** @@ -60,16 +63,16 @@ export type LdapOrgEntityProviderOptions = * manually at some interval. * * But more commonly you will pass in the result of - * {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner} + * {@link @backstage/backend-plugin-api#SchedulerService.createScheduledTaskRunner} * to enable automatic scheduling of tasks. */ - schedule?: 'manual' | TaskRunner; + schedule?: 'manual' | SchedulerServiceTaskRunner; /** * Scheduler used to schedule refreshes based on * the schedule config. */ - scheduler?: PluginTaskScheduler; + scheduler?: SchedulerService; /** * The function that transforms a user entry in msgraph to an entity. @@ -122,10 +125,10 @@ export interface LdapOrgEntityProviderLegacyOptions { * manually at some interval. * * But more commonly you will pass in the result of - * {@link @backstage/backend-tasks#PluginTaskScheduler.createScheduledTaskRunner} + * {@link @backstage/backend-plugin-api#SchedulerService.createScheduledTaskRunner} * to enable automatic scheduling of tasks. */ - schedule: 'manual' | TaskRunner; + schedule: 'manual' | SchedulerServiceTaskRunner; /** * The function that transforms a user entry in LDAP to an entity. @@ -311,7 +314,7 @@ export class LdapOrgEntityProvider implements EntityProvider { markCommitComplete(); } - private schedule(taskRunner: TaskRunner) { + private schedule(taskRunner: SchedulerServiceTaskRunner) { this.scheduleFn = async () => { const id = `${this.getProviderName()}:refresh`; await taskRunner.run({ diff --git a/plugins/catalog-backend-module-logs/.eslintrc.js b/plugins/catalog-backend-module-logs/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/catalog-backend-module-logs/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/catalog-backend-module-logs/CHANGELOG.md b/plugins/catalog-backend-module-logs/CHANGELOG.md new file mode 100644 index 0000000000..4637f19052 --- /dev/null +++ b/plugins/catalog-backend-module-logs/CHANGELOG.md @@ -0,0 +1,65 @@ +# @backstage/plugin-catalog-backend-module-logs + +## 0.0.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## 0.0.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.0.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.0.1 + +### Patch Changes + +- 97caf55: Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them. + + See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install + and configure the plugin. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-catalog-backend@1.24.0 + +## 0.0.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.0.1-next.0 + +### Patch Changes + +- 97caf55: Creates a new module to make logging catalog errors simple. This module subscribes to catalog events and logs them. + + See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install + and configure the plugin. + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 diff --git a/plugins/catalog-backend-module-logs/README.md b/plugins/catalog-backend-module-logs/README.md new file mode 100644 index 0000000000..1771dbe02c --- /dev/null +++ b/plugins/catalog-backend-module-logs/README.md @@ -0,0 +1,8 @@ +# backstage-plugin-catalog-backend-module-logs + +A module that subscribes to catalog related events and logs them. + +## Getting started + +See [Backstage documentation](https://backstage.io/docs/features/software-catalog/configuration#subscribing-to-catalog-errors) for details on how to install +and configure the plugin. diff --git a/plugins/catalog-backend-module-logs/api-report.md b/plugins/catalog-backend-module-logs/api-report.md new file mode 100644 index 0000000000..0be2197c9a --- /dev/null +++ b/plugins/catalog-backend-module-logs/api-report.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-catalog-backend-module-logs" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; + +// @public +const catalogModuleLogs: BackendFeatureCompat; +export default catalogModuleLogs; +``` diff --git a/plugins/catalog-backend-module-logs/catalog-info.yaml b/plugins/catalog-backend-module-logs/catalog-info.yaml new file mode 100644 index 0000000000..f2223174c3 --- /dev/null +++ b/plugins/catalog-backend-module-logs/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-catalog-backend-module-logs + title: '@backstage/plugin-catalog-backend-module-logs' + description: A module that subscribes to catalog releated events and logs them. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/catalog-backend-module-logs/package.json b/plugins/catalog-backend-module-logs/package.json new file mode 100644 index 0000000000..a6609c7227 --- /dev/null +++ b/plugins/catalog-backend-module-logs/package.json @@ -0,0 +1,45 @@ +{ + "name": "@backstage/plugin-catalog-backend-module-logs", + "version": "0.0.2-next.2", + "description": "A module that subscribes to catalog releated events and logs them.", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-backend-module-logs" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-catalog-backend": "workspace:^", + "@backstage/plugin-events-node": "workspace:^" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-events-backend-test-utils": "workspace:^" + } +} diff --git a/plugins/catalog-backend-module-logs/src/index.ts b/plugins/catalog-backend-module-logs/src/index.ts new file mode 100644 index 0000000000..77a7d424eb --- /dev/null +++ b/plugins/catalog-backend-module-logs/src/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2024 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. + */ + +/** + * A catalog module that logs catalog errors using the logger service. + * + * @packageDocumentation + */ +export { catalogModuleLogs as default } from './module'; diff --git a/plugins/catalog-backend-module-logs/src/module.test.ts b/plugins/catalog-backend-module-logs/src/module.test.ts new file mode 100644 index 0000000000..79f1b8c295 --- /dev/null +++ b/plugins/catalog-backend-module-logs/src/module.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2024 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 { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { catalogModuleLogs } from './module'; +import { createServiceFactory } from '@backstage/backend-plugin-api'; +import { TestEventsService } from '@backstage/plugin-events-backend-test-utils'; +import { eventsServiceRef } from '@backstage/plugin-events-node'; + +describe('catalogModuleLogs', () => { + it('should be correctly wired and set up', async () => { + const events = new TestEventsService(); + const eventsServiceFactory = createServiceFactory({ + service: eventsServiceRef, + deps: {}, + async factory({}) { + return events; + }, + }); + + await startTestBackend({ + features: [ + mockServices.logger.factory(), + eventsServiceFactory(), + catalogModuleLogs(), + ], + }); + + expect(events.subscribed).toHaveLength(1); + expect(events.subscribed[0].id).toEqual('catalog'); + }); +}); diff --git a/plugins/catalog-backend-module-logs/src/module.ts b/plugins/catalog-backend-module-logs/src/module.ts new file mode 100644 index 0000000000..bdc916e958 --- /dev/null +++ b/plugins/catalog-backend-module-logs/src/module.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 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 { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { CATALOG_ERRORS_TOPIC } from '@backstage/plugin-catalog-backend'; +import { eventsServiceRef, EventParams } from '@backstage/plugin-events-node'; + +interface EventsPayload { + entity: string; + location?: string; + errors: Error[]; +} + +interface EventsParamsWithPayload extends EventParams { + eventPayload: EventsPayload; +} + +/** + * A catalog module that logs catalog errors using the logger service. + * + * @packageDocumentation + * @public + */ +export const catalogModuleLogs = createBackendModule({ + pluginId: 'catalog', + moduleId: 'logs', + register(env) { + env.registerInit({ + deps: { + events: eventsServiceRef, + logger: coreServices.logger, + }, + async init({ events, logger }) { + events.subscribe({ + id: 'catalog', + topics: [CATALOG_ERRORS_TOPIC], + async onEvent(params: EventParams): Promise { + const event = params as EventsParamsWithPayload; + const { entity, location, errors } = event.eventPayload; + for (const error of errors) { + logger.warn(error.message, { + entity, + location, + }); + } + }, + }); + }, + }); + }, +}); diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index 998f9aacd0..2efe171ec1 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.5.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.5.31-next.0 + +### Patch Changes + +- 58dff4d: Added option to ingest groups based on their group membership in Azure Entra ID +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.25 + +## 0.5.30 + +### Patch Changes + +- f7bdcea: Adds a dynamic provider for the plugin-catalog-backend-module-msgraph. Configuration is now runtime configurable through the ProviderConfigTransformer. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + +## 0.5.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.5.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.5.27 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/api-report-alpha.md b/plugins/catalog-backend-module-msgraph/api-report-alpha.md index 9b769dba1e..84f56a99bf 100644 --- a/plugins/catalog-backend-module-msgraph/api-report-alpha.md +++ b/plugins/catalog-backend-module-msgraph/api-report-alpha.md @@ -7,6 +7,7 @@ import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { GroupTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; import { OrganizationTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; +import { ProviderConfigTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; import { UserTransformer } from '@backstage/plugin-catalog-backend-module-msgraph'; // @alpha @@ -26,6 +27,11 @@ export interface MicrosoftGraphOrgEntityProviderTransformsExtensionPoint { | OrganizationTransformer | Record, ): void; + setProviderConfigTransformer( + transformer: + | ProviderConfigTransformer + | Record, + ): void; setUserTransformer( transformer: UserTransformer | Record, ): void; diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 86170e2910..57f50fbd18 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -39,10 +39,10 @@ export function defaultUserTransformer( // @public export type GroupMember = | (MicrosoftGraph.Group & { - '@odata.type': '#microsoft.graph.user'; + '@odata.type': '#microsoft.graph.group'; }) | (MicrosoftGraph.User & { - '@odata.type': '#microsoft.graph.group'; + '@odata.type': '#microsoft.graph.user'; }); // @public @@ -126,6 +126,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; + providerConfigTransformer?: ProviderConfigTransformer; }); // (undocumented) connect(connection: EntityProviderConnection): Promise; @@ -145,6 +146,7 @@ export interface MicrosoftGraphOrgEntityProviderLegacyOptions { id: string; logger: LoggerService; organizationTransformer?: OrganizationTransformer; + providerConfigTransformer?: ProviderConfigTransformer; schedule: 'manual' | TaskRunner; target: string; userTransformer?: UserTransformer; @@ -162,6 +164,9 @@ export type MicrosoftGraphOrgEntityProviderOptions = organizationTransformer?: | OrganizationTransformer | Record; + providerConfigTransformer?: + | ProviderConfigTransformer + | Record; }; // @public @deprecated @@ -210,6 +215,7 @@ export type MicrosoftGraphProviderConfig = { groupFilter?: string; groupSearch?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; queryMode?: 'basic' | 'advanced'; loadUserPhotos?: boolean; schedule?: TaskScheduleDefinition; @@ -233,6 +239,11 @@ export type OrganizationTransformer = ( organization: MicrosoftGraph.Organization, ) => Promise; +// @public +export type ProviderConfigTransformer = ( + provider: MicrosoftGraphProviderConfig, +) => Promise; + // @public @deprecated export function readMicrosoftGraphConfig( config: Config, @@ -253,6 +264,7 @@ export function readMicrosoftGraphOrg( groupSearch?: string; groupFilter?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; queryMode?: 'basic' | 'advanced'; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; diff --git a/plugins/catalog-backend-module-msgraph/config.d.ts b/plugins/catalog-backend-module-msgraph/config.d.ts index 5a1f56a03f..a470c075f0 100644 --- a/plugins/catalog-backend-module-msgraph/config.d.ts +++ b/plugins/catalog-backend-module-msgraph/config.d.ts @@ -192,6 +192,11 @@ export interface Config { * E.g. ["id", "displayName", "description"] */ select?: string[]; + /** + * Whether to ingest groups that are members of the found/filtered/searched groups. + * Default value is `false`. + */ + includeSubGroups?: boolean; }; userGroupMember?: { diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index bdaa470dea..daba6c4b9f 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", - "version": "0.5.27", + "version": "0.5.31-next.2", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", "backstage": { "role": "backend-plugin-module", @@ -62,7 +62,7 @@ "@microsoft/microsoft-graph-types": "^2.6.0", "@types/node-fetch": "^2.5.12", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.0.2", "qs": "^6.9.4", "uuid": "^9.0.0" diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index 6d63537b54..624b6e42b0 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -15,7 +15,7 @@ */ import { TokenCredential } from '@azure/identity'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { MicrosoftGraphClient } from './client'; @@ -27,7 +27,7 @@ describe('MicrosoftGraphClient', () => { let client: MicrosoftGraphClient; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { tokenCredential.getToken.mockResolvedValue({ diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 1233b1e2f9..c190e2a959 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -64,8 +64,8 @@ export type ODataQuery = { * @public */ export type GroupMember = - | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.user' }) - | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.group' }); + | (MicrosoftGraph.Group & { '@odata.type': '#microsoft.graph.group' }) + | (MicrosoftGraph.User & { '@odata.type': '#microsoft.graph.user' }); /** * A HTTP Client that communicates with Microsoft Graph API. diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index b02894dfd9..0ec56f134d 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -15,7 +15,6 @@ */ import { ConfigReader } from '@backstage/config'; -import { Duration } from 'luxon'; import { readMicrosoftGraphConfig, readProviderConfigs } from './config'; describe('readMicrosoftGraphConfig', () => { @@ -175,6 +174,7 @@ describe('readProviderConfigs', () => { expand: 'member', filter: 'securityEnabled eq false', select: ['id', 'displayName', 'description'], + includeSubGroups: true, }, schedule: { frequency: 'PT30M', @@ -203,8 +203,9 @@ describe('readProviderConfigs', () => { groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', + groupIncludeSubGroups: true, schedule: { - frequency: Duration.fromISO('PT30M'), + frequency: { minutes: 30 }, timeout: { minutes: 3, }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 1545114e45..257a7d5736 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -116,6 +116,12 @@ export type MicrosoftGraphProviderConfig = { */ groupSelect?: string[]; + /** + * Whether to ingest groups that are members of the found/filtered/searched groups. + * Default value is `false`. + */ + groupIncludeSubGroups?: boolean; + /** * By default, the Microsoft Graph API only provides the basic feature set * for querying. Certain features are limited to advanced query capabilities @@ -292,6 +298,9 @@ export function readProviderConfig( const groupFilter = config.getOptionalString('group.filter'); const groupSearch = config.getOptionalString('group.search'); const groupSelect = config.getOptionalStringArray('group.select'); + const groupIncludeSubGroups = config.getOptionalBoolean( + 'group.includeSubGroups', + ); const queryMode = config.getOptionalString('queryMode'); if ( @@ -347,6 +356,7 @@ export function readProviderConfig( groupFilter, groupSearch, groupSelect, + groupIncludeSubGroups, queryMode, userGroupMemberFilter, userGroupMemberSearch, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts index 4db3fb69c6..81f6f21ce2 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/index.ts @@ -39,4 +39,5 @@ export type { GroupTransformer, OrganizationTransformer, UserTransformer, + ProviderConfigTransformer, } from './types'; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 8821ffd2db..b83597653f 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -90,6 +90,9 @@ describe('read microsoft graph', () => { yield { '@odata.type': '#microsoft.graph.group', id: 'childgroupid', + displayName: 'Child Group Name', + description: 'Child Group Description', + mail: 'childgroup@example.com', }; yield { '@odata.type': '#microsoft.graph.user', @@ -770,6 +773,117 @@ describe('read microsoft graph', () => { top: 999, }); }); + + it('should read groups and their sub groups', async () => { + async function* getExampleGroupMembersForSubGroup(): AsyncIterable { + yield { + '@odata.type': '#microsoft.graph.user', + id: 'userid2', + }; + } + + client.getGroups.mockImplementation(getExampleGroups); + client.getGroupMembers.mockImplementationOnce(getExampleGroupMembers); + client.getGroupMembers.mockImplementationOnce( + getExampleGroupMembersForSubGroup, + ); + client.getOrganization.mockResolvedValue({ + id: 'tenantid', + displayName: 'Organization Name', + }); + client.getGroupPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { groups, groupMember, groupMemberOf, rootGroup } = + await readMicrosoftGraphGroups(client, 'tenantid', { + groupIncludeSubGroups: true, + }); + + const expectedRootGroup = group({ + metadata: { + annotations: { + 'graph.microsoft.com/tenant-id': 'tenantid', + }, + name: 'organization_name', + description: 'Organization Name', + }, + spec: { + type: 'root', + profile: { + displayName: 'Organization Name', + }, + children: [], + }, + }); + expect(groups).toEqual([ + expectedRootGroup, + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'childgroupid', + }, + name: 'child_group_name', + description: 'Child Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Child Group Name', + email: 'childgroup@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + group({ + metadata: { + annotations: { + 'graph.microsoft.com/group-id': 'groupid', + }, + name: 'group_name', + description: 'Group Description', + }, + spec: { + type: 'team', + profile: { + displayName: 'Group Name', + email: 'group@example.com', + // TODO: Loading groups photos doesn't work right now as Microsoft + // Graph doesn't allows this yet + /* picture: 'data:image/jpeg;base64,...',*/ + }, + children: [], + }, + }), + ]); + expect(rootGroup).toEqual(expectedRootGroup); + expect(groupMember.get('groupid')).toEqual(new Set(['childgroupid'])); + expect(groupMemberOf.get('userid')).toEqual(new Set(['groupid'])); + expect(groupMemberOf.get('userid2')).toEqual(new Set(['childgroupid'])); + expect(groupMember.get('organization_name')).toEqual(new Set()); + + expect(client.getGroups).toHaveBeenCalledTimes(1); + expect(client.getGroups).toHaveBeenCalledWith( + { + top: 999, + }, + undefined, + ); + expect(client.getGroupMembers).toHaveBeenCalledTimes(2); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); + expect(client.getGroupMembers).toHaveBeenCalledWith('childgroupid', { + top: 999, + }); + // TODO: Loading groups photos doesn't work right now as Microsoft Graph + // doesn't allows this yet + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); + // expect(client.getGroupPhotoWithSizeLimit).toBeCalledWith('groupid', 120); + }); }); describe('resolveRelations', () => { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 2ca21fca9c..60e5c3d79b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -178,6 +178,7 @@ export async function readMicrosoftGraphGroups( groupFilter?: string; groupSearch?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; }, @@ -244,6 +245,31 @@ export async function readMicrosoftGraphGroups( if (member['@odata.type'] === '#microsoft.graph.group') { ensureItem(groupMember, group.id!, member.id); + + if (options?.groupIncludeSubGroups) { + const groupMemberEntity = await transformer(member); + + if (groupMemberEntity) { + groups.push(groupMemberEntity); + + for await (const subMember of client.getGroupMembers( + member.id!, + { top: PAGE_SIZE }, + )) { + if (!subMember.id) { + continue; + } + + if (subMember['@odata.type'] === '#microsoft.graph.user') { + ensureItem(groupMemberOf, subMember.id, member.id!); + } + + if (subMember['@odata.type'] === '#microsoft.graph.group') { + ensureItem(groupMember, member.id!, subMember.id); + } + } + } + } } } @@ -377,6 +403,7 @@ export async function readMicrosoftGraphOrg( groupSearch?: string; groupFilter?: string; groupSelect?: string[]; + groupIncludeSubGroups?: boolean; queryMode?: 'basic' | 'advanced'; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; @@ -421,6 +448,7 @@ export async function readMicrosoftGraphOrg( groupFilter: options.groupFilter, groupSearch: options.groupSearch, groupSelect: options.groupSelect, + groupIncludeSubGroups: options.groupIncludeSubGroups, groupTransformer: options.groupTransformer, organizationTransformer: options.organizationTransformer, }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts index ef60d7b018..bc9270238b 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/types.ts @@ -16,7 +16,7 @@ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; - +import { MicrosoftGraphProviderConfig } from './config'; /** * Customize the ingested User entity * @@ -45,3 +45,15 @@ export type GroupTransformer = ( group: MicrosoftGraph.Group, groupPhoto?: string, ) => Promise; + +/** + * Customize the MSGraph Provider Config Dynamically + * + * Transforming fields that are not used for each scheduled ingestion (e.g., id, schedule) + * will have no effect. + * + * @public + */ +export type ProviderConfigTransformer = ( + provider: MicrosoftGraphProviderConfig, +) => Promise; diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts index 5742fbbc6c..2a3e66a9b3 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.test.ts @@ -17,7 +17,6 @@ import { TaskScheduleDefinition } from '@backstage/backend-tasks'; import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/alpha'; -import { Duration } from 'luxon'; import { catalogModuleMicrosoftGraphOrgEntityProvider } from './catalogModuleMicrosoftGraphOrgEntityProvider'; import { MicrosoftGraphOrgEntityProvider } from '../processors'; @@ -67,8 +66,8 @@ describe('catalogModuleMicrosoftGraphOrgEntityProvider', () => { ], }); - expect(usedSchedule?.frequency).toEqual(Duration.fromISO('PT30M')); - expect(usedSchedule?.timeout).toEqual(Duration.fromISO('PT3M')); + expect(usedSchedule?.frequency).toEqual({ minutes: 30 }); + expect(usedSchedule?.timeout).toEqual({ minutes: 3 }); expect(addedProviders?.length).toEqual(1); expect(addedProviders?.pop()?.getProviderName()).toEqual( 'MicrosoftGraphOrgEntityProvider:customProviderId', diff --git a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts index 193f07889f..e1a15a3468 100644 --- a/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/module/catalogModuleMicrosoftGraphOrgEntityProvider.ts @@ -23,6 +23,7 @@ import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node/ import { GroupTransformer, OrganizationTransformer, + ProviderConfigTransformer, UserTransformer, } from '@backstage/plugin-catalog-backend-module-msgraph'; import { MicrosoftGraphOrgEntityProvider } from '../processors'; @@ -58,6 +59,18 @@ export interface MicrosoftGraphOrgEntityProviderTransformsExtensionPoint { | OrganizationTransformer | Record, ): void; + + /** + * Set the function that transforms provider config dynamically. + * Optionally, you can pass separate transformers per provider ID. + * Note: adjusting fields that are not used on each scheduled ingestion + * (e.g., id, schedule) will have no effect. + */ + setProviderConfigTransformer( + transformer: + | ProviderConfigTransformer + | Record, + ): void; } /** @@ -94,6 +107,10 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( | OrganizationTransformer | Record | undefined; + let providerConfigTransformer: + | ProviderConfigTransformer + | Record + | undefined; env.registerExtensionPoint( microsoftGraphOrgEntityProviderTransformExtensionPoint, @@ -116,6 +133,12 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( } organizationTransformer = transformer; }, + setProviderConfigTransformer(transformer) { + if (providerConfigTransformer) { + throw new Error('Provider transformer may only be set once'); + } + providerConfigTransformer = transformer; + }, }, ); @@ -134,6 +157,7 @@ export const catalogModuleMicrosoftGraphOrgEntityProvider = createBackendModule( userTransformer: userTransformer, groupTransformer: groupTransformer, organizationTransformer: organizationTransformer, + providerConfigTransformer: providerConfigTransformer, }), ); }, diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts index 3c8b903e69..a019ac79a3 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgEntityProvider.ts @@ -34,6 +34,7 @@ import { MICROSOFT_GRAPH_USER_ID_ANNOTATION, MicrosoftGraphClient, MicrosoftGraphProviderConfig, + ProviderConfigTransformer, OrganizationTransformer, readMicrosoftGraphConfig, readMicrosoftGraphOrg, @@ -94,6 +95,13 @@ export type MicrosoftGraphOrgEntityProviderOptions = organizationTransformer?: | OrganizationTransformer | Record; + + /** + * The function that transforms provider config dynamically. + */ + providerConfigTransformer?: + | ProviderConfigTransformer + | Record; }; /** @@ -152,6 +160,11 @@ export interface MicrosoftGraphOrgEntityProviderLegacyOptions { * The function that transforms an organization entry in msgraph to an entity. */ organizationTransformer?: OrganizationTransformer; + + /** + * The function that transforms provider config dynamically. + */ + providerConfigTransformer?: ProviderConfigTransformer; } /** @@ -216,6 +229,10 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { providerConfig.id, options.organizationTransformer, ), + providerConfigTransformer: getTransformer( + providerConfig.id, + options.providerConfigTransformer, + ), }); if (taskRunner !== 'manual') { @@ -257,6 +274,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer: options.userTransformer, groupTransformer: options.groupTransformer, organizationTransformer: options.organizationTransformer, + providerConfigTransformer: options.providerConfigTransformer, logger, provider, }); @@ -276,6 +294,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; organizationTransformer?: OrganizationTransformer; + providerConfigTransformer?: ProviderConfigTransformer; }, ) {} @@ -300,7 +319,9 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { } const logger = options?.logger ?? this.options.logger; - const provider = this.options.provider; + const provider = this.options.providerConfigTransformer + ? await this.options.providerConfigTransformer(this.options.provider) + : this.options.provider; const { markReadComplete } = trackProgress(logger); const client = MicrosoftGraphClient.create(this.options.provider); const { users, groups } = await readMicrosoftGraphOrg( @@ -317,6 +338,7 @@ export class MicrosoftGraphOrgEntityProvider implements EntityProvider { groupFilter: provider.groupFilter, groupSearch: provider.groupSearch, groupSelect: provider.groupSelect, + groupIncludeSubGroups: provider.groupIncludeSubGroups, queryMode: provider.queryMode, groupTransformer: this.options.groupTransformer, userTransformer: this.options.userTransformer, diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index f60ec12844..eb0dbcf08d 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,95 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.41-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-catalog-backend@1.24.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.1.41-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.1.41-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-backend@1.24.1-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + +## 0.1.40 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-backend@1.24.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.1.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.24.0-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.1.39-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-backend@1.23.2-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.1.37 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index c05d8dcec4..aa4ec5a803 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", - "version": "0.1.37", + "version": "0.1.41-next.2", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md index e990331ee9..209160b4e1 100644 --- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md +++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md @@ -1,5 +1,90 @@ # @backstage/plugin-catalog-backend-module-puppetdb +## 0.1.29-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.1.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.1.28 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.1.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + +## 0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.1.25 ### Patch Changes diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json index 3407a6dedd..977379fefa 100644 --- a/plugins/catalog-backend-module-puppetdb/package.json +++ b/plugins/catalog-backend-module-puppetdb/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-puppetdb", - "version": "0.1.25", + "version": "0.1.29-next.2", "description": "A Backstage catalog backend module that helps integrate towards PuppetDB", "backstage": { "role": "backend-plugin-module", @@ -63,7 +63,7 @@ "@backstage/types": "workspace:^", "lodash": "^4.17.21", "luxon": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts index 8da0fb4150..86335cb97d 100644 --- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProviderConfig.test.ts @@ -16,7 +16,6 @@ import { ConfigReader } from '@backstage/config'; import { readProviderConfigs } from './PuppetDbEntityProviderConfig'; -import { Duration } from 'luxon'; describe('readProviderConfigs', () => { afterEach(() => jest.resetAllMocks()); @@ -120,10 +119,8 @@ describe('readProviderConfigs', () => { expect(providerConfigs).toHaveLength(1); expect(providerConfigs[0].schedule).toEqual({ - frequency: Duration.fromISO('PT30M'), - timeout: { - minutes: 10, - }, + frequency: { minutes: 30 }, + timeout: { minutes: 10 }, }); }); }); diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts index ac28f04503..8ad4c2b850 100644 --- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts +++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.test.ts @@ -20,14 +20,14 @@ import { PuppetDbEntityProviderConfig, } from '../providers'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ANNOTATION_PUPPET_CERTNAME, ENDPOINT_FACTSETS } from './constants'; describe('readPuppetNodes', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md index a73ea9fbaf..0813e4b6ab 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md +++ b/plugins/catalog-backend-module-scaffolder-entity-model/CHANGELOG.md @@ -1,5 +1,71 @@ # @backstage/plugin-catalog-backend-module-scaffolder-entity-model +## 0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-model@1.5.0 + +## 0.1.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-model@1.5.0 + +## 0.1.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + +## 0.1.20 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-model@1.5.0 + +## 0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-scaffolder-common@1.5.3 + +## 0.1.19-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-scaffolder-common@1.5.3 + ## 0.1.17 ### Patch Changes diff --git a/plugins/catalog-backend-module-scaffolder-entity-model/package.json b/plugins/catalog-backend-module-scaffolder-entity-model/package.json index 247dc999db..02c8a09ae9 100644 --- a/plugins/catalog-backend-module-scaffolder-entity-model/package.json +++ b/plugins/catalog-backend-module-scaffolder-entity-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-scaffolder-entity-model", - "version": "0.1.17", + "version": "0.1.21-next.2", "description": "Adds support for the scaffolder specific entity model (e.g. the Template kind) to the catalog backend plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md index c07c7ef7a3..37413e5f02 100644 --- a/plugins/catalog-backend-module-unprocessed/CHANGELOG.md +++ b/plugins/catalog-backend-module-unprocessed/CHANGELOG.md @@ -1,5 +1,89 @@ # @backstage/plugin-catalog-backend-module-unprocessed +## 0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## 0.4.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.4-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.4.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.3 + - @backstage/plugin-permission-common@0.8.0 + +## 0.4.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + +## 0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.2 + - @backstage/plugin-permission-common@0.7.14 + +## 0.4.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-unprocessed-entities-common@0.0.2 + - @backstage/plugin-permission-common@0.7.14 + ## 0.4.6 ### Patch Changes diff --git a/plugins/catalog-backend-module-unprocessed/README.md b/plugins/catalog-backend-module-unprocessed/README.md index 6b350e5ef8..f01347912d 100644 --- a/plugins/catalog-backend-module-unprocessed/README.md +++ b/plugins/catalog-backend-module-unprocessed/README.md @@ -17,7 +17,7 @@ yarn --cwd packages/backend add @backstage/plugin-catalog-backend-module-unproce In `packages/backend/src/index.ts` add the module: ```ts title="packages/backend/src/index.ts" -backend.add(catalogModuleUnprocessedEntities()); +backend.add(import('@backstage/plugin-catalog-backend-module-unprocessed')); ``` ### Legacy Backend diff --git a/plugins/catalog-backend-module-unprocessed/package.json b/plugins/catalog-backend-module-unprocessed/package.json index 845f5aa9c6..eaf86d75ed 100644 --- a/plugins/catalog-backend-module-unprocessed/package.json +++ b/plugins/catalog-backend-module-unprocessed/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-unprocessed", - "version": "0.4.6", + "version": "0.4.10-next.2", "description": "Backstage Catalog module to view unprocessed entities", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 7b83c21930..fe707b2a9a 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,148 @@ # @backstage/plugin-catalog-backend +## 1.24.1-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/backend-openapi-utils@0.1.16-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.24.1-next.1 + +### Patch Changes + +- 51240ee: Preserve default `allowedLocationTypes` when `setAllowedLocationTypes()` of `CatalogLocationsExtensionPoint` is not called. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.1 + - @backstage/backend-openapi-utils@0.1.16-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 1.24.1-next.0 + +### Patch Changes + +- a629fb2: Added setAllowedLocationTypes while introducing a new extension point called CatalogLocationsExtensionPoint +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-openapi-utils@0.1.16-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.29-next.0 + +## 1.24.0 + +### Minor Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/backend-openapi-utils@0.1.15 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-search-backend-module-catalog@0.1.28 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.24.0-next.1 + +### Minor Changes + +- b9ed1bb: bumped better-sqlite3 from ^9.0.0 to ^11.0.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-openapi-utils@0.1.15-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-search-backend-module-catalog@0.1.28-next.1 + +## 1.23.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/backend-openapi-utils@0.1.14-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-search-backend-module-catalog@0.1.27-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + ## 1.23.0 ### Minor Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 574e1c99ea..0a046b43e4 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "1.23.0", + "version": "1.24.1-next.2", "description": "The Backstage backend plugin that provides the Backstage catalog", "backstage": { "role": "backend-plugin", @@ -90,7 +90,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "minimatch": "^9.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "p-limit": "^3.0.2", "prom-client": "^15.0.0", "uuid": "^9.0.0", @@ -110,7 +110,7 @@ "@types/lodash": "^4.14.151", "@types/supertest": "^2.0.8", "@types/uuid": "^9.0.0", - "better-sqlite3": "^9.0.0", + "better-sqlite3": "^11.0.0", "luxon": "^3.0.0", "msw": "^1.0.0", "supertest": "^6.1.3", diff --git a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts index dd80f8f0c4..7d1578db00 100644 --- a/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/UrlReaderProcessor.test.ts @@ -17,7 +17,7 @@ import { UrlReader, UrlReaders } from '@backstage/backend-common'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { rest } from 'msw'; @@ -39,7 +39,7 @@ describe('UrlReaderProcessor', () => { set: jest.fn(), }; const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts index 4c1673b785..aee65bd2e1 100644 --- a/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts +++ b/plugins/catalog-backend/src/search/DefaultCatalogCollator.test.ts @@ -18,7 +18,7 @@ import { PluginEndpointDiscovery, TokenManager, } from '@backstage/backend-common'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { Entity } from '@backstage/catalog-model'; import { DefaultCatalogCollator } from './DefaultCatalogCollator'; import { setupServer } from 'msw/node'; @@ -62,7 +62,7 @@ describe('DefaultCatalogCollator', () => { let mockTokenManager: jest.Mocked; let collator: DefaultCatalogCollator; - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeAll(() => { mockDiscoveryApi = { getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007'), diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index aebd969701..269100e586 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -28,6 +28,8 @@ import { catalogPermissionExtensionPoint, CatalogProcessingExtensionPoint, catalogProcessingExtensionPoint, + CatalogLocationsExtensionPoint, + catalogLocationsExtensionPoint, } from '@backstage/plugin-catalog-node/alpha'; import { CatalogProcessor, @@ -41,6 +43,20 @@ import { merge } from 'lodash'; import { Permission } from '@backstage/plugin-permission-common'; import { ForwardedError } from '@backstage/errors'; +class CatalogLocationsExtensionPointImpl + implements CatalogLocationsExtensionPoint +{ + #locationTypes: string[] | undefined; + + setAllowedLocationTypes(locationTypes: Array) { + this.#locationTypes = locationTypes; + } + + get allowedLocationTypes() { + return this.#locationTypes; + } +} + class CatalogProcessingExtensionPointImpl implements CatalogProcessingExtensionPoint { @@ -199,6 +215,12 @@ export const catalogPlugin = createBackendPlugin({ const modelExtensions = new CatalogModelExtensionPointImpl(); env.registerExtensionPoint(catalogModelExtensionPoint, modelExtensions); + const locationTypeExtensions = new CatalogLocationsExtensionPointImpl(); + env.registerExtensionPoint( + catalogLocationsExtensionPoint, + locationTypeExtensions, + ); + env.registerInit({ deps: { logger: coreServices.logger, @@ -271,6 +293,12 @@ export const catalogPlugin = createBackendPlugin({ builder.addPermissionRules(...permissionExtensions.permissionRules); builder.setFieldFormatValidators(modelExtensions.fieldValidators); + if (locationTypeExtensions.allowedLocationTypes) { + builder.setAllowedLocationTypes( + locationTypeExtensions.allowedLocationTypes, + ); + } + const { processingEngine, router } = await builder.build(); lifecycle.addStartupHook(async () => { diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index fd3c714f97..2d0fde55da 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-catalog-common +## 1.0.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/catalog-model@1.5.0 + +## 1.0.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-model@1.5.0 + +## 1.0.25 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-search-common@1.2.13 + - @backstage/catalog-model@1.5.0 + ## 1.0.24 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index 0ea4ce5e3e..cfcfee74d8 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-common", - "version": "1.0.24", + "version": "1.0.26-next.1", "description": "Common functionalities for the catalog plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index 9f39daa276..695af8d4fe 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,115 @@ # @backstage/plugin-catalog-graph +## 0.4.8-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## 0.4.8-next.2 + +### Patch Changes + +- 4a529c2: Use `entityPresentationApi` for the node title and the icon. +- Updated dependencies + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-react@1.12.3-next.1 + +## 0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## 0.4.8-next.0 + +### Patch Changes + +- fba7537: Memoize entity graph nodes when applying an `entityFilter` to prevent repeated redraws +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## 0.4.7 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/types@1.1.1 + +## 0.4.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.4.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/types@1.1.1 + +## 0.4.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.4.6 ### Patch Changes diff --git a/plugins/catalog-graph/api-report-alpha.md b/plugins/catalog-graph/api-report-alpha.md index 3895f12fcf..e651946463 100644 --- a/plugins/catalog-graph/api-report-alpha.md +++ b/plugins/catalog-graph/api-report-alpha.md @@ -13,15 +13,13 @@ const _default: BackstagePlugin< catalogGraph: RouteRef; }, { - catalogEntity: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; - } + catalogEntity: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, + {} >; export default _default; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 17013d199a..08a5497dc0 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.4.6", + "version": "0.4.8-next.3", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-graph", diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.test.tsx index 1f3d77e1ae..10aa62f946 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.test.tsx @@ -47,6 +47,7 @@ describe('', () => { ); expect(screen.getByText('kind:namespace/name')).toBeInTheDocument(); + expect(screen.getByText('namespace/name')).toBeInTheDocument(); }); test('renders node, skips default namespace', async () => { @@ -73,7 +74,7 @@ describe('', () => { , ); - expect(screen.getByText('kind:name')).toBeInTheDocument(); + expect(screen.getByText('name')).toBeInTheDocument(); }); test('renders node with onClick', async () => { @@ -102,8 +103,8 @@ describe('', () => { , ); - expect(screen.getByText('kind:namespace/name')).toBeInTheDocument(); - await userEvent.click(screen.getByText('kind:namespace/name')); + expect(screen.getByText('namespace/name')).toBeInTheDocument(); + await userEvent.click(screen.getByText('namespace/name')); expect(onClick).toHaveBeenCalledTimes(1); }); @@ -134,5 +135,6 @@ describe('', () => { ); expect(screen.getByText('Custom Title')).toBeInTheDocument(); + expect(screen.getByText('kind:namespace/name')).toBeInTheDocument(); }); }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx index d55f4c015b..bc3311d33f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/DefaultRenderNode.tsx @@ -14,12 +14,12 @@ * limitations under the License. */ import { DependencyGraphTypes } from '@backstage/core-components'; -import { useApp } from '@backstage/core-plugin-api'; -import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; +import { IconComponent } from '@backstage/core-plugin-api'; +import { useEntityPresentation } from '@backstage/plugin-catalog-react'; import { makeStyles } from '@material-ui/core/styles'; import classNames from 'classnames'; import React, { useLayoutEffect, useRef, useState } from 'react'; -import { EntityKindIcon } from './EntityKindIcon'; +import { EntityIcon } from './EntityIcon'; import { EntityNodeData } from './types'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; @@ -64,8 +64,10 @@ export function DefaultRenderNode({ const classes = useStyles(); const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); - const app = useApp(); const idRef = useRef(null); + const entityRefPresentationSnapshot = useEntityPresentation(entity, { + defaultNamespace: DEFAULT_NAMESPACE, + }); useLayoutEffect(() => { // set the width to the length of the ID @@ -82,25 +84,14 @@ export function DefaultRenderNode({ } }, [width, height]); - const { - kind, - metadata: { name, namespace = DEFAULT_NAMESPACE, title }, - } = entity; - - const hasKindIcon = app.getSystemIcon( - `kind:${kind.toLocaleLowerCase('en-US')}`, - ); + const hasKindIcon = !!entityRefPresentationSnapshot.Icon; const padding = 10; const iconSize = height; const paddedIconWidth = hasKindIcon ? iconSize + padding : 0; const paddedWidth = paddedIconWidth + width + padding * 2; const paddedHeight = height + padding * 2; - const displayTitle = - title ?? - (kind && name && namespace - ? humanizeEntityRef({ kind, name, namespace }) - : id); + const displayTitle = entityRefPresentationSnapshot.primaryTitle ?? id; return ( @@ -115,8 +106,8 @@ export function DefaultRenderNode({ rx={10} /> {hasKindIcon && ( - {displayTitle} + {entityRefPresentationSnapshot.entityRef} ); } diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityIcon.test.tsx similarity index 81% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx rename to plugins/catalog-graph/src/components/EntityRelationsGraph/EntityIcon.test.tsx index cc47c8850b..8683af9c1e 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityIcon.test.tsx @@ -16,12 +16,14 @@ import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; -import { EntityKindIcon } from './EntityKindIcon'; +import { EntityIcon } from './EntityIcon'; +import MuiMemoryIcon from '@material-ui/icons/Memory'; +import { IconComponent } from '@backstage/core-plugin-api'; describe('', () => { it('renders without exploding', async () => { const { baseElement } = await renderInTestApp( - , + , ); expect(baseElement.querySelector('svg')).toBeInTheDocument(); @@ -29,7 +31,7 @@ describe('', () => { it('renders without exploding for unknown kind', async () => { const { baseElement } = await renderInTestApp( - , + , ); expect(baseElement.querySelector('svg')).toBeInTheDocument(); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityIcon.tsx similarity index 69% rename from plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.tsx rename to plugins/catalog-graph/src/components/EntityRelationsGraph/EntityIcon.tsx index 7263e25eae..a46a8bcc9d 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityIcon.tsx @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useApp } from '@backstage/core-plugin-api'; import React from 'react'; import SvgIcon from '@material-ui/core/SvgIcon'; +import { OverridableComponent } from '@material-ui/core/OverridableComponent'; +import { SvgIconTypeMap } from '@material-ui/core/SvgIcon/SvgIcon'; +import { IconComponent } from '@backstage/core-plugin-api'; -export function EntityKindIcon({ - kind, +export function EntityIcon({ + icon, ...props }: { - kind: string; + icon: IconComponent | undefined; x?: number; y?: number; width?: number; height?: number; className?: string; }) { - const app = useApp(); - const Icon = - app.getSystemIcon(`kind:${kind.toLocaleLowerCase('en-US')}`) ?? SvgIcon; + const Icon = (icon as OverridableComponent) ?? SvgIcon; return ; } diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index af2a491ada..07e3085f1f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useEntityStore } from './useEntityStore'; import { pickBy } from 'lodash'; @@ -104,9 +104,13 @@ export function useEntityRelationGraph({ requestEntities, ]); - const filteredEntities = entityFilter - ? pickBy(entities, (value, _key) => entityFilter(value)) - : entities; + const filteredEntities = useMemo( + () => + entityFilter + ? pickBy(entities, (value, _key) => entityFilter(value)) + : entities, + [entities, entityFilter], + ); return { entities: filteredEntities, diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 9bee65367a..2063f9e2f9 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,124 @@ # @backstage/plugin-catalog-import +## 0.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + +## 0.12.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + +## 0.12.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/plugin-catalog-common@1.0.25 + +## 0.12.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/integration@1.13.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.12.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.12.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-common@1.0.24 + +## 0.12.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.12.0 ### Minor Changes diff --git a/plugins/catalog-import/api-report-alpha.md b/plugins/catalog-import/api-report-alpha.md index 63e2cda339..51e3d99fd7 100644 --- a/plugins/catalog-import/api-report-alpha.md +++ b/plugins/catalog-import/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { importPage: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 2b93e0a7c0..44076a3fbc 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.12.0", + "version": "0.12.2-next.2", "description": "A Backstage plugin the helps you import entities into your catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts index a8fbe9db76..9f058dc744 100644 --- a/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts +++ b/plugins/catalog-import/src/api/AzureRepoApiClient.test.ts @@ -26,7 +26,7 @@ import { RepoApiClient, } from './AzureRepoApiClient'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; function mockRepoEndpoint() { return rest.get( @@ -153,7 +153,7 @@ function mockPrEndpoint() { describe('RepoApiClient', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const testToken = new Date().toString(); const sut = new RepoApiClient({ project: 'project', diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 1f16840c33..fa646e3ca4 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -57,7 +57,7 @@ import { ConfigReader, UrlPatternDiscovery } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; import { ScmAuthApi } from '@backstage/integration-react'; import { CatalogApi } from '@backstage/plugin-catalog-react'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { Octokit } from '@octokit/rest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -70,7 +70,7 @@ import { describe('CatalogImportClient', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage:9191/api/catalog'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index f476ca301a..24f1446c6d 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,91 @@ # @backstage/plugin-catalog-node +## 1.12.5-next.2 + +### Patch Changes + +- 7c5f3b0: Explicit declare if the service ref accepts `single` or `multiple` implementations. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.12.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.12.5-next.0 + +### Patch Changes + +- a629fb2: Added setAllowedLocationTypes while introducing a new extension point called CatalogLocationsExtensionPoint +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 1.12.4 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.12.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 1.12.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + ## 1.12.1 ### Patch Changes diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index b61d1b1d27..ba02079ceb 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -34,6 +34,14 @@ export interface CatalogAnalysisExtensionPoint { // @alpha (undocumented) export const catalogAnalysisExtensionPoint: ExtensionPoint; +// @alpha (undocumented) +export interface CatalogLocationsExtensionPoint { + setAllowedLocationTypes(locationTypes: Array): void; +} + +// @alpha (undocumented) +export const catalogLocationsExtensionPoint: ExtensionPoint; + // @alpha (undocumented) export interface CatalogModelExtensionPoint { setEntityDataParser(parser: CatalogProcessorParser): void; @@ -88,7 +96,7 @@ export interface CatalogProcessingExtensionPoint { export const catalogProcessingExtensionPoint: ExtensionPoint; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index c6bb7854b6..330427c7b5 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-node", - "version": "1.12.1", + "version": "1.12.5-next.2", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", "backstage": { "role": "node-library", diff --git a/plugins/catalog-node/src/alpha.ts b/plugins/catalog-node/src/alpha.ts index 91b5bb99b6..b75ec48a7f 100644 --- a/plugins/catalog-node/src/alpha.ts +++ b/plugins/catalog-node/src/alpha.ts @@ -15,6 +15,8 @@ */ export { catalogServiceRef } from './catalogService'; +export type { CatalogLocationsExtensionPoint } from './extensions'; +export { catalogLocationsExtensionPoint } from './extensions'; export type { CatalogProcessingExtensionPoint } from './extensions'; export { catalogProcessingExtensionPoint } from './extensions'; export type { CatalogAnalysisExtensionPoint } from './extensions'; diff --git a/plugins/catalog-node/src/extensions.ts b/plugins/catalog-node/src/extensions.ts index 7c1fe6c30d..a971e1588d 100644 --- a/plugins/catalog-node/src/extensions.ts +++ b/plugins/catalog-node/src/extensions.ts @@ -31,6 +31,25 @@ import { } from '@backstage/plugin-permission-common'; import { PermissionRule } from '@backstage/plugin-permission-node'; +/** + * @alpha + */ +export interface CatalogLocationsExtensionPoint { + /** + * Allows setting custom location types, such as showcased in: https://backstage.io/docs/features/software-catalog/external-integrations/#creating-a-catalog-data-reader-processor + * @param locationTypes - List of location types to allow, default is "url" and "file" + */ + setAllowedLocationTypes(locationTypes: Array): void; +} + +/** + * @alpha + */ +export const catalogLocationsExtensionPoint = + createExtensionPoint({ + id: 'catalog.locations', + }); + /** * @alpha */ diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index f4e0b0c708..24a1cf6572 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,132 @@ # @backstage/plugin-catalog-react +## 1.12.3-next.2 + +### Patch Changes + +- 012e3eb: Entity page extensions created for the new frontend system via the `/alpha` exports will now be enabled by default. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.12.3-next.1 + +### Patch Changes + +- 7ca331c: Correct `EntityDisplayName`'s icon alignment with the text. +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.12.3-next.0 + +### Patch Changes + +- 31bfc44: Updated alpha definitions of extension data references. +- 9b89b82: Internal refactor to remove unnecessary `routable` prop in the implementation of the `createEntityContentExtension` alpha export. +- bebd569: Fix extra divider displayed on user list picker component +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-react@0.4.24 + +## 1.12.2 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- 2030962: Make EntityOwnerPicker display metadata.title or spec.profile.displayName for mode=only-owners instead of metadata.name +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/core-components@0.14.9 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.12.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + +## 1.12.2-next.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + +## 1.12.2-next.0 + +### Patch Changes + +- 2030962: Make EntityOwnerPicker display metadata.title or spec.profile.displayName for mode=only-owners instead of metadata.name +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-react@0.4.23 + ## 1.12.1 ### Patch Changes diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index fb807d947d..edfa183903 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -13,17 +13,85 @@ import { PortableSchema } from '@backstage/frontend-plugin-api'; import { ResolvedExtensionInputs } from '@backstage/frontend-plugin-api'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/core-plugin-api/alpha'; // @alpha (undocumented) export const catalogExtensionData: { - entityContentTitle: ConfigurableExtensionDataRef; - entityFilterFunction: ConfigurableExtensionDataRef< - (entity: Entity) => boolean, + entityContentTitle: ConfigurableExtensionDataRef< + string, + 'catalog.entity-content-title', + {} + >; + entityFilterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + entityFilterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', {} >; - entityFilterExpression: ConfigurableExtensionDataRef; }; +// @alpha (undocumented) +export const catalogReactTranslationRef: TranslationRef< + 'catalog-react', + { + readonly 'catalogFilter.title': 'Filters'; + readonly 'catalogFilter.buttonTitle': 'Filters'; + readonly 'entityKindPicker.title': 'Kind'; + readonly 'entityKindPicker.errorMessage': 'Failed to load entity kinds'; + readonly 'entityLifecyclePicker.title': 'Lifecycle'; + readonly 'entityNamespacePicker.title': 'Namespace'; + readonly 'entityOwnerPicker.title': 'Owner'; + readonly 'entityProcessingStatusPicker.title': 'Processing Status'; + readonly 'entityTagPicker.title': 'Tags'; + readonly 'entityPeekAheadPopover.title': 'Drill into the entity to see all of the tags.'; + readonly 'entityPeekAheadPopover.entityCardActionsTitle': 'Show details'; + readonly 'entityPeekAheadPopover.emailCardAction.title': 'Email {{email}}'; + readonly 'entityPeekAheadPopover.emailCardAction.subTitle': 'mailto {{email}}'; + readonly 'entitySearchBar.placeholder': 'Search'; + readonly 'entityTypePicker.title': 'Type'; + readonly 'entityTypePicker.errorMessage': 'Failed to load entity types'; + readonly 'entityTypePicker.optionAllTitle': 'all'; + readonly 'favoriteEntity.addToFavorites': 'Add to favorites'; + readonly 'favoriteEntity.removeFromFavorites': 'Remove from favorites'; + readonly 'inspectEntityDialog.title': 'Entity Inspector'; + readonly 'inspectEntityDialog.closeButtonTitle': 'Close'; + readonly 'inspectEntityDialog.ancestryPage.title': 'Ancestry'; + readonly 'inspectEntityDialog.colocatedPage.title': 'Colocated'; + readonly 'inspectEntityDialog.colocatedPage.description': 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).'; + readonly 'inspectEntityDialog.colocatedPage.alertNoLocation': 'Entity had no location information.'; + readonly 'inspectEntityDialog.colocatedPage.alertNoEntity': 'There were no other entities on this location.'; + readonly 'inspectEntityDialog.jsonPage.title': 'Entity as JSON'; + readonly 'inspectEntityDialog.jsonPage.description': 'This is the raw entity data as received from the catalog, on JSON form.'; + readonly 'inspectEntityDialog.overviewPage.title': 'Overview'; + readonly 'inspectEntityDialog.yamlPage.title': 'Entity as YAML'; + readonly 'inspectEntityDialog.yamlPage.description': 'This is the raw entity data as received from the catalog, on YAML form.'; + readonly 'unregisterEntityDialog.title': 'Are you sure you want to unregister this entity?'; + readonly 'unregisterEntityDialog.cancelButtonTitle': 'Cancel'; + readonly 'unregisterEntityDialog.deleteButtonTitle': 'Delete Entity'; + readonly 'unregisterEntityDialog.deleteEntitySuccessMessage': 'Removed entity {{entityName}}'; + readonly 'unregisterEntityDialog.onlyDeleteStateTitle': 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.'; + readonly 'unregisterEntityDialog.errorStateTitle': 'Internal error: Unknown state'; + readonly 'unregisterEntityDialog.bootstrapState.title': 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedDescription': 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.bootstrapState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.title': 'This action will unregister the following entities:'; + readonly 'unregisterEntityDialog.unregisterState.description': 'To undo, just re-register the entity in {{appTitle}}.'; + readonly 'unregisterEntityDialog.unregisterState.subTitle': 'Located at the following location:'; + readonly 'unregisterEntityDialog.unregisterState.advancedDescription': 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.'; + readonly 'unregisterEntityDialog.unregisterState.advancedOptions': 'Advanced Options'; + readonly 'unregisterEntityDialog.unregisterState.unregisterButtonTitle': 'Unregister Location'; + readonly 'userListPicker.defaultOrgName': 'Company'; + readonly 'userListPicker.orgFilterAllLabel': 'All'; + readonly 'userListPicker.personalFilter.title': 'Personal'; + readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; + readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + } +>; + // @alpha (undocumented) export function createEntityCardExtension< TConfig extends { @@ -47,7 +115,15 @@ export function createEntityCardExtension< config: TConfig; inputs: Expand>; }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export function createEntityContentExtension< @@ -70,11 +146,23 @@ export function createEntityContentExtension< loader: (options: { inputs: Expand>; }) => Promise; -}): ExtensionDefinition<{ - title: string; - path: string; - filter?: string | undefined; -}>; +}): ExtensionDefinition< + { + title: string; + path: string; + filter?: string | undefined; + }, + { + filter?: string | undefined; + title?: string | undefined; + path?: string | undefined; + }, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 961dad0e0e..7300729db5 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "1.12.1", + "version": "1.12.3-next.2", "description": "A frontend library that helps other Backstage plugins interact with the catalog", "backstage": { "role": "web-library", diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index f5f4dfdb74..42d69726b6 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -32,18 +32,19 @@ import { Expand } from '../../../packages/frontend-plugin-api/src/types'; export { useEntityPermission } from './hooks/useEntityPermission'; export { isOwnerOf } from './utils'; +export * from './translation'; /** @alpha */ export const catalogExtensionData = { - entityContentTitle: createExtensionDataRef( - 'catalog.entity-content-title', - ), - entityFilterFunction: createExtensionDataRef<(entity: Entity) => boolean>( - 'catalog.entity-filter-function', - ), - entityFilterExpression: createExtensionDataRef( - 'catalog.entity-filter-expression', - ), + entityContentTitle: createExtensionDataRef().with({ + id: 'catalog.entity-content-title', + }), + entityFilterFunction: createExtensionDataRef< + (entity: Entity) => boolean + >().with({ id: 'catalog.entity-filter-function' }), + entityFilterExpression: createExtensionDataRef().with({ + id: 'catalog.entity-filter-expression', + }), }; // TODO: Figure out how to merge with provided config schema @@ -82,7 +83,7 @@ export function createEntityCardExtension< id: 'entity-content:catalog/overview', input: 'cards', }, - disabled: options.disabled ?? true, + disabled: options.disabled, output: { element: coreExtensionData.reactElement, filterFunction: catalogExtensionData.entityFilterFunction.optional(), @@ -136,7 +137,7 @@ export function createEntityContentExtension< id: 'page:catalog/entity', input: 'contents', }, - disabled: options.disabled ?? true, + disabled: options.disabled, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath, @@ -165,7 +166,7 @@ export function createEntityContentExtension< title: config.title, routeRef: options.routeRef, element: ( - + ), diff --git a/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx b/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx index 7583520412..ee51020cbb 100644 --- a/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx +++ b/plugins/catalog-react/src/components/CatalogFilterLayout/CatalogFilterLayout.tsx @@ -23,6 +23,8 @@ import Typography from '@material-ui/core/Typography'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { Theme, useTheme } from '@material-ui/core/styles'; import FilterListIcon from '@material-ui/icons/FilterList'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export const Filters = (props: { @@ -37,6 +39,7 @@ export const Filters = (props: { ); const theme = useTheme(); const [filterDrawerOpen, setFilterDrawerOpen] = useState(false); + const { t } = useTranslationRef(catalogReactTranslationRef); return isScreenSmallerThanBreakpoint ? ( <> @@ -45,7 +48,7 @@ export const Filters = (props: { onClick={() => setFilterDrawerOpen(true)} startIcon={} > - Filters + {t('catalogFilter.buttonTitle')} - Filters + {t('catalogFilter.title')} {props.children} diff --git a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx index e953f21afb..68d5ecefdb 100644 --- a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx +++ b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx @@ -38,7 +38,9 @@ const useStyles = makeStyles( icon: { marginRight: theme.spacing(0.5), color: theme.palette.text.secondary, - lineHeight: 0, + '& svg': { + verticalAlign: 'middle', + }, }, }), { name: 'CatalogReactEntityDisplayName' }, diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx index 2513f8dca9..612c515d28 100644 --- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx @@ -18,7 +18,7 @@ import { GetEntityFacetsResponse } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef } from '@backstage/core-plugin-api'; -import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { fireEvent, waitFor, screen, within } from '@testing-library/react'; import { capitalize } from 'lodash'; import { default as React } from 'react'; @@ -75,7 +75,7 @@ describe('', () => { ); it('renders available entity kinds', async () => { - await renderWithEffects( + await renderInTestApp( ', () => { it('sets the selected kind filter', async () => { const updateFilters = jest.fn(); - await renderWithEffects( + await renderInTestApp( ', () => { it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { kind: 'group' }; - await renderWithEffects( + await renderInTestApp( ', () => { }); it('renders unknown kinds provided in query parameters', async () => { - await renderWithEffects( + await renderInTestApp( ', () => { }); it('limits kinds when allowedKinds is set', async () => { - await renderWithEffects( + await renderInTestApp( @@ -183,7 +183,7 @@ describe('', () => { }); it('renders kind from the query parameter even when not in allowedKinds', async () => { - await renderWithEffects( + await renderInTestApp( { const { allowedKinds, hidden, initialFilter = 'component' } = props; + const { t } = useTranslationRef(catalogReactTranslationRef); const alertApi = useApi(alertApiRef); @@ -106,11 +109,11 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => { useEffect(() => { if (error) { alertApi.post({ - message: `Failed to load entity kinds`, + message: t('entityKindPicker.errorMessage'), severity: 'error', }); } - }, [error, alertApi]); + }, [error, alertApi, t]); if (error) return null; @@ -124,7 +127,7 @@ export const EntityKindPicker = (props: EntityKindPickerProps) => { return hidden ? null : ( 1 ? selectedTypes[0] : undefined) ?? 'all'} onChange={value => diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx index fd0a656317..8848857976 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.tsx @@ -22,6 +22,8 @@ import Star from '@material-ui/icons/Star'; import StarBorder from '@material-ui/icons/StarBorder'; import React, { ComponentProps } from 'react'; import { useStarredEntity } from '../../hooks/useStarredEntity'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type FavoriteEntityProps = ComponentProps & { @@ -43,6 +45,7 @@ export const FavoriteEntity = (props: FavoriteEntityProps) => { const { toggleStarredEntity, isStarredEntity } = useStarredEntity( props.entity, ); + const { t } = useTranslationRef(catalogReactTranslationRef); return ( { onClick={() => toggleStarredEntity()} > {isStarredEntity ? : } diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx index f155c3f164..644167acbb 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/InspectEntityDialog.tsx @@ -30,6 +30,8 @@ import { ColocatedPage } from './components/ColocatedPage'; import { JsonPage } from './components/JsonPage'; import { OverviewPage } from './components/OverviewPage'; import { YamlPage } from './components/YamlPage'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ fullHeightDialog: { @@ -95,6 +97,7 @@ export function InspectEntityDialog(props: { }) { const classes = useStyles(); const [activeTab, setActiveTab] = React.useState(0); + const { t } = useTranslationRef(catalogReactTranslationRef); useEffect(() => { setActiveTab(0); @@ -114,7 +117,7 @@ export function InspectEntityDialog(props: { PaperProps={{ className: classes.fullHeightDialog }} > - Entity Inspector + {t('inspectEntityDialog.title')}
    @@ -152,7 +155,7 @@ export function InspectEntityDialog(props: { diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx index e08bac06e8..98bd27bd77 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx @@ -38,6 +38,8 @@ import { catalogApiRef } from '../../../api'; import { humanizeEntityRef } from '../../EntityRefLink'; import { entityRouteRef } from '../../../routes'; import { EntityKindIcon } from './EntityKindIcon'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ node: { @@ -198,6 +200,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) { export function AncestryPage(props: { entity: Entity }) { const { loading, error, nodes, edges } = useAncestry(props.entity); + const { t } = useTranslationRef(catalogReactTranslationRef); if (loading) { return ; } else if (error) { @@ -206,7 +209,9 @@ export function AncestryPage(props: { entity: Entity }) { return ( <> - Ancestry + + {t('inspectEntityDialog.ancestryPage.title')} + This is the ancestry of entities above the current one - as in, the chain(s) of entities down to the current one, where{' '} diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx index 97bf0f3cb8..6f89ff2c9f 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/ColocatedPage.tsx @@ -32,6 +32,8 @@ import useAsync from 'react-use/esm/useAsync'; import { catalogApiRef } from '../../../api'; import { EntityRefLink } from '../../EntityRefLink'; import { KeyValueListItem, ListItemText } from './common'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles({ root: { @@ -95,6 +97,7 @@ function EntityList(props: { entities: Entity[]; header?: [string, string] }) { function Contents(props: { entity: Entity }) { const { entity } = props; + const { t } = useTranslationRef(catalogReactTranslationRef); const { loading, error, location, originLocation, colocatedEntities } = useColocated(entity); @@ -106,12 +109,14 @@ function Contents(props: { entity: Entity }) { if (!location && !originLocation) { return ( - Entity had no location information. + + {t('inspectEntityDialog.colocatedPage.alertNoLocation')} + ); } else if (!colocatedEntities?.length) { return ( - There were no other entities on this location. + {t('inspectEntityDialog.colocatedPage.alertNoEntity')} ); } @@ -148,13 +153,14 @@ function Contents(props: { entity: Entity }) { export function ColocatedPage(props: { entity: Entity }) { const classes = useStyles(); + const { t } = useTranslationRef(catalogReactTranslationRef); return ( <> - Colocated + + {t('inspectEntityDialog.colocatedPage.title')} + - These are the entities that are colocated with this entity - as in, they - originated from the same data source (e.g. came from the same YAML - file), or from the same origin (e.g. the originally registered URL). + {t('inspectEntityDialog.colocatedPage.description')}
    diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx index b6bda65229..af8a724729 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/JsonPage.tsx @@ -19,13 +19,18 @@ import { CodeSnippet } from '@backstage/core-components'; import DialogContentText from '@material-ui/core/DialogContentText'; import React from 'react'; import { sortKeys } from './util'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export function JsonPage(props: { entity: Entity }) { + const { t } = useTranslationRef(catalogReactTranslationRef); return ( <> - Entity as JSON + + {t('inspectEntityDialog.jsonPage.title')} + - This is the raw entity data as received from the catalog, on JSON form. + {t('inspectEntityDialog.jsonPage.description')}
    diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx index ad742459de..361910b2cd 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/OverviewPage.tsx @@ -36,6 +36,8 @@ import { } from './common'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { CopyTextButton } from '@backstage/core-components'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles({ root: { @@ -59,11 +61,14 @@ export function OverviewPage(props: { entity: AlphaEntity }) { sortBy(relations, r => r.targetRef), 'type', ); + const { t } = useTranslationRef(catalogReactTranslationRef); const entityRef = stringifyEntityRef(props.entity); return ( <> - Overview + + {t('inspectEntityDialog.overviewPage.title')} +
    diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx index ad74611ec5..4e56b9b42a 100644 --- a/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx +++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/YamlPage.tsx @@ -20,13 +20,18 @@ import DialogContentText from '@material-ui/core/DialogContentText'; import React from 'react'; import YAML from 'yaml'; import { sortKeys } from './util'; +import { catalogReactTranslationRef } from '../../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; export function YamlPage(props: { entity: Entity }) { + const { t } = useTranslationRef(catalogReactTranslationRef); return ( <> - Entity as YAML + + {t('inspectEntityDialog.yamlPage.title')} + - This is the raw entity data as received from the catalog, on YAML form. + {t('inspectEntityDialog.yamlPage.description')}
    diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 7f41badca5..e2ed9387e1 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -32,6 +32,8 @@ import { useUnregisterEntityDialogState } from './useUnregisterEntityDialogState import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api'; import { Progress, ResponseErrorPanel } from '@backstage/core-components'; import { assertError } from '@backstage/errors'; +import { catalogReactTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles({ advancedButton: { @@ -58,6 +60,7 @@ const Contents = ({ const [showDelete, setShowDelete] = useState(false); const [busy, setBusy] = useState(false); const appTitle = configApi.getOptionalString('app.title') ?? 'Backstage'; + const { t } = useTranslationRef(catalogReactTranslationRef); const onUnregister = useCallback( async function onUnregisterFn() { @@ -86,7 +89,9 @@ const Contents = ({ const entityName = entity.metadata.title ?? entity.metadata.name; onConfirm(); alertApi.post({ - message: `Removed entity ${entityName}`, + message: t('unregisterEntityDialog.deleteEntitySuccessMessage', { + entityName, + }), severity: 'success', display: 'transient', }); @@ -98,13 +103,13 @@ const Contents = ({ } } }, - [alertApi, onConfirm, state, entity], + [alertApi, onConfirm, state, entity, t], ); const DialogActionsPanel = () => ( ); @@ -121,10 +126,10 @@ const Contents = ({ return ( <> - You cannot unregister this entity, since it originates from a - protected Backstage configuration (location "{state.location}"). If - you believe this is in error, please contact the {appTitle}{' '} - integrator. + {t('unregisterEntityDialog.bootstrapState.title', { + appTitle, + location: state.location, + })} @@ -137,7 +142,7 @@ const Contents = ({ className={classes.advancedButton} onClick={() => setShowDelete(true)} > - Advanced Options + {t('unregisterEntityDialog.bootstrapState.advancedOptions')} @@ -146,11 +151,7 @@ const Contents = ({ {showDelete && ( <> - You have the option to delete the entity itself from the - catalog. Note that this should only be done if you know that the - catalog file has been deleted at, or moved from, its origin - location. If that is not the case, the entity will reappear - shortly as the next refresh round is performed by the catalog. + {t('unregisterEntityDialog.bootstrapState.advancedDescription')} @@ -172,8 +173,7 @@ const Contents = ({ return ( <> - This entity does not seem to originate from a registered location. You - therefore only have the option to delete it outright from the catalog. + {t('unregisterEntityDialog.onlyDeleteStateTitle')} @@ -192,7 +192,7 @@ const Contents = ({ return ( <> - This action will unregister the following entities: + {t('unregisterEntityDialog.unregisterState.title')} {state.colocatedEntities.map(e => ( @@ -202,13 +202,15 @@ const Contents = ({ ))} - Located at the following location: + {t('unregisterEntityDialog.unregisterState.subTitle')}
  • {state.location}
  • - To undo, just re-register the entity in {appTitle}. + {t('unregisterEntityDialog.unregisterState.description', { + appTitle, + })} {!showDelete && ( @@ -228,7 +230,7 @@ const Contents = ({ className={classes.advancedButton} onClick={() => setShowDelete(true)} > - Advanced Options + {t('unregisterEntityDialog.unregisterState.advancedOptions')} )} @@ -240,11 +242,7 @@ const Contents = ({ - You also have the option to delete the entity itself from the - catalog. Note that this should only be done if you know that the - catalog file has been deleted at, or moved from, its origin - location. If that is not the case, the entity will reappear - shortly as the next refresh round is performed by the catalog. + {t('unregisterEntityDialog.unregisterState.advancedDescription')} )} @@ -260,7 +258,11 @@ const Contents = ({ ); } - return Internal error: Unknown state; + return ( + + {t('unregisterEntityDialog.errorStateTitle')} + + ); }; /** @public */ @@ -274,10 +276,11 @@ export type UnregisterEntityDialogProps = { /** @public */ export const UnregisterEntityDialog = (props: UnregisterEntityDialogProps) => { const { open, onConfirm, onClose, entity } = props; + const { t } = useTranslationRef(catalogReactTranslationRef); return ( - Are you sure you want to unregister this entity? + {t('unregisterEntityDialog.title')} diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 5667447503..0b101e8174 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { fireEvent, render, waitFor, screen } from '@testing-library/react'; +import { fireEvent, waitFor, screen } from '@testing-library/react'; import { UserEntity } from '@backstage/catalog-model'; import { UserListPicker, UserListPickerProps } from './UserListPicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; @@ -30,7 +30,11 @@ import { QueryEntitiesInitialRequest, } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; -import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; +import { + MockStorageApi, + TestApiRegistry, + renderInTestApp, +} from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; import { ConfigApi, @@ -145,7 +149,7 @@ describe('', () => { }); it('renders filter groups', async () => { - render( + await renderInTestApp( @@ -164,7 +168,7 @@ describe('', () => { }); it('renders filters', async () => { - render( + await renderInTestApp( ', () => { }); it('respects other frontend filters in counts', async () => { - render( + await renderInTestApp( ', () => { it('respects the query parameter filter value', async () => { const updateFilters = jest.fn(); const queryParameters = { user: 'owned', kind: 'component' }; - render( + await renderInTestApp( ', () => { it('updates user filter when a menuitem is selected', async () => { const updateFilters = jest.fn(); - render( + await renderInTestApp( ', () => { it('responds to external queryParameters changes', async () => { const updateFilters = jest.fn(); - const rendered = render( + const rendered = await renderInTestApp( ', () => { it('does not reset the filter while entities are loading', async () => { mockCatalogApi.queryEntities?.mockReturnValue(new Promise(() => {})); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), @@ -433,7 +437,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -444,7 +448,7 @@ describe('', () => { }); it('does not reset the filter when request is empty', async () => { - render(); + await renderInTestApp(); await waitFor(() => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(1); @@ -473,7 +477,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -489,7 +493,7 @@ describe('', () => { () => new Promise(() => {}), ); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled(), @@ -512,7 +516,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -537,7 +541,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(updateFilters).toHaveBeenLastCalledWith({ @@ -563,7 +567,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -574,7 +578,7 @@ describe('', () => { }); it('does not reset the filter when entities are loaded', async () => { - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -588,7 +592,7 @@ describe('', () => { }); it('does not reset the filter when request is empty xxxx', async () => { - render(); + await renderInTestApp(); await waitFor(() => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(1); @@ -619,7 +623,7 @@ describe('', () => { return mockQueryEntitiesImplementation(request); }); - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), @@ -630,7 +634,7 @@ describe('', () => { }); it('does not reset the filter when entities are loaded', async () => { - render(); + await renderInTestApp(); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledTimes(3), diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx index 1e46258885..c6b515aa67 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.tsx @@ -36,6 +36,11 @@ import { UserListFilterKind } from '../../types'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; import { useAllEntitiesCount } from './useAllEntitiesCount'; import { useStarredEntitiesCount } from './useStarredEntitiesCount'; +import { + TranslationFunction, + useTranslationRef, +} from '@backstage/core-plugin-api/alpha'; +import { catalogReactTranslationRef } from '../../translation'; /** @public */ export type CatalogReactUserListPickerClassKey = @@ -83,29 +88,32 @@ export type ButtonGroup = { }[]; }; -function getFilterGroups(orgName: string | undefined): ButtonGroup[] { +function getFilterGroups( + orgName: string, + t: TranslationFunction, +): ButtonGroup[] { return [ { - name: 'Personal', + name: t('userListPicker.personalFilter.title'), items: [ { id: 'owned', - label: 'Owned', + label: t('userListPicker.personalFilter.ownedLabel'), icon: SettingsIcon, }, { id: 'starred', - label: 'Starred', + label: t('userListPicker.personalFilter.starredLabel'), icon: StarIcon, }, ], }, { - name: orgName ?? 'Company', + name: orgName, items: [ { id: 'all', - label: 'All', + label: t('userListPicker.orgFilterAllLabel'), }, ], }, @@ -123,7 +131,10 @@ export const UserListPicker = (props: UserListPickerProps) => { const { initialFilter, availableFilters } = props; const classes = useStyles(); const configApi = useApi(configApiRef); - const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; + const { t } = useTranslationRef(catalogReactTranslationRef); + const orgName = + configApi.getOptionalString('organization.name') ?? + t('userListPicker.defaultOrgName'); const { filters, updateFilters, @@ -133,7 +144,7 @@ export const UserListPicker = (props: UserListPickerProps) => { // Remove group items that aren't in availableFilters and exclude // any now-empty groups. const userAndGroupFilterIds = ['starred', 'all']; - const filterGroups = getFilterGroups(orgName) + const filterGroups = getFilterGroups(orgName, t) .map(filterGroup => ({ ...filterGroup, items: filterGroup.items.filter(({ id }) => @@ -236,11 +247,11 @@ export const UserListPicker = (props: UserListPickerProps) => { - {group.items.map(item => ( + {group.items.map((item, index) => ( setSelectedUserFilter(item.id)} selected={item.id === filters.user?.value} className={classes.menuItem} diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index bc8e6b6038..9afa81e2dd 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -20,6 +20,7 @@ import { alertApiRef, ConfigApi, configApiRef, + errorApiRef, IdentityApi, identityApiRef, storageApiRef, @@ -39,6 +40,8 @@ import { } from '../filters'; import { EntityListProvider, useEntityList } from './useEntityListProvider'; import { useMountEffect } from '@react-hookz/web'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; +import { MockTranslationApi } from '@backstage/test-utils/alpha'; const entities: Entity[] = [ { @@ -110,6 +113,8 @@ const createWrapper = [storageApiRef, MockStorageApi.create()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], [alertApiRef, { post: jest.fn() }], + [translationApiRef, MockTranslationApi.create()], + [errorApiRef, { error$: jest.fn(), post: jest.fn() }], ]} > diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts new file mode 100644 index 0000000000..7f9661380f --- /dev/null +++ b/plugins/catalog-react/src/translation.ts @@ -0,0 +1,128 @@ +/* + * Copyright 2024 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 { createTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @alpha */ +export const catalogReactTranslationRef = createTranslationRef({ + id: 'catalog-react', + messages: { + catalogFilter: { + title: 'Filters', + buttonTitle: 'Filters', + }, + entityKindPicker: { + title: 'Kind', + errorMessage: 'Failed to load entity kinds', + }, + entityLifecyclePicker: { + title: 'Lifecycle', + }, + entityNamespacePicker: { + title: 'Namespace', + }, + entityOwnerPicker: { + title: 'Owner', + }, + entityProcessingStatusPicker: { + title: 'Processing Status', + }, + entityTagPicker: { + title: 'Tags', + }, + entityPeekAheadPopover: { + title: 'Drill into the entity to see all of the tags.', + emailCardAction: { + title: 'Email {{email}}', + subTitle: 'mailto {{email}}', + }, + entityCardActionsTitle: 'Show details', + }, + entitySearchBar: { + placeholder: 'Search', + }, + entityTypePicker: { + title: 'Type', + errorMessage: 'Failed to load entity types', + optionAllTitle: 'all', + }, + favoriteEntity: { + addToFavorites: 'Add to favorites', + removeFromFavorites: 'Remove from favorites', + }, + inspectEntityDialog: { + title: 'Entity Inspector', + closeButtonTitle: 'Close', + ancestryPage: { + title: 'Ancestry', + }, + colocatedPage: { + title: 'Colocated', + description: + 'These are the entities that are colocated with this entity - as in, they originated from the same data source (e.g. came from the same YAML file), or from the same origin (e.g. the originally registered URL).', + alertNoLocation: 'Entity had no location information.', + alertNoEntity: 'There were no other entities on this location.', + }, + jsonPage: { + title: 'Entity as JSON', + description: + 'This is the raw entity data as received from the catalog, on JSON form.', + }, + overviewPage: { + title: 'Overview', + }, + yamlPage: { + title: 'Entity as YAML', + description: + 'This is the raw entity data as received from the catalog, on YAML form.', + }, + }, + unregisterEntityDialog: { + title: 'Are you sure you want to unregister this entity?', + cancelButtonTitle: 'Cancel', + deleteButtonTitle: 'Delete Entity', + deleteEntitySuccessMessage: 'Removed entity {{entityName}}', + bootstrapState: { + title: + 'You cannot unregister this entity, since it originates from a protected Backstage configuration (location "{{location}}"). If you believe this is in error, please contact the {{appTitle}} integrator.', + advancedDescription: + 'You have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.', + advancedOptions: 'Advanced Options', + }, + onlyDeleteStateTitle: + 'This entity does not seem to originate from a registered location. You therefore only have the option to delete it outright from the catalog.', + unregisterState: { + title: 'This action will unregister the following entities:', + subTitle: 'Located at the following location:', + description: 'To undo, just re-register the entity in {{appTitle}}.', + unregisterButtonTitle: 'Unregister Location', + advancedOptions: 'Advanced Options', + advancedDescription: + 'You also have the option to delete the entity itself from the catalog. Note that this should only be done if you know that the catalog file has been deleted at, or moved from, its origin location. If that is not the case, the entity will reappear shortly as the next refresh round is performed by the catalog.', + }, + errorStateTitle: 'Internal error: Unknown state', + }, + userListPicker: { + defaultOrgName: 'Company', + personalFilter: { + title: 'Personal', + ownedLabel: 'Owned', + starredLabel: 'Starred', + }, + orgFilterAllLabel: 'All', + }, + }, +}); diff --git a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md index f2f489b6a0..b299631a21 100644 --- a/plugins/catalog-unprocessed-entities-common/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-catalog-unprocessed-entities-common +## 0.0.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + +## 0.0.3 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + ## 0.0.2 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities-common/package.json b/plugins/catalog-unprocessed-entities-common/package.json index f491d27157..e5f88ebba4 100644 --- a/plugins/catalog-unprocessed-entities-common/package.json +++ b/plugins/catalog-unprocessed-entities-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities-common", - "version": "0.0.2", + "version": "0.0.4-next.1", "description": "Common functionalities for the catalog-unprocessed-entities plugin", "backstage": { "role": "common-library", diff --git a/plugins/catalog-unprocessed-entities/CHANGELOG.md b/plugins/catalog-unprocessed-entities/CHANGELOG.md index 8d39848d54..2c2eb03f5e 100644 --- a/plugins/catalog-unprocessed-entities/CHANGELOG.md +++ b/plugins/catalog-unprocessed-entities/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-catalog-unprocessed-entities +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.2.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + ## 0.2.5 ### Patch Changes diff --git a/plugins/catalog-unprocessed-entities/package.json b/plugins/catalog-unprocessed-entities/package.json index 32abaa1bbb..8ce8c8b0d3 100644 --- a/plugins/catalog-unprocessed-entities/package.json +++ b/plugins/catalog-unprocessed-entities/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-unprocessed-entities", - "version": "0.2.5", + "version": "0.2.7-next.0", "backstage": { "role": "frontend-plugin", "pluginId": "catalog-unprocessed-entities", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index f74c71e66c..48daaa7774 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,153 @@ # @backstage/plugin-catalog +## 1.22.0-next.2 + +### Minor Changes + +- 6925dcb: Introduces the HasSubdomainsCard component that displays the subdomains of a given domain + +### Patch Changes + +- 604a504: The entity relation cards available for the new frontend system via `/alpha` now have more accurate and granular default filters. +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-search-common@1.2.14-next.1 + - @backstage/plugin-search-react@1.7.14-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## 1.21.2-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-search-react@1.7.14-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/plugin-search-common@1.2.14-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + +## 1.21.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/plugin-search-react@1.7.14-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/integration-react@1.1.30-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-search-common@1.2.13 + +## 1.21.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- d133eaa: Added small notes to AboutCard to discourage customizability PRs +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-search-common@1.2.13 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/integration-react@1.1.29 + - @backstage/plugin-search-react@1.7.13 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.21.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-search-react@1.7.13-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## 1.21.1-next.1 + +### Patch Changes + +- 06c0956: Support i18n for catalog and catalog-react plugins +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + - @backstage/plugin-search-common@1.2.12 + +## 1.21.1-next.0 + +### Patch Changes + +- d133eaa: Added small notes to AboutCard to discourage customizability PRs +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/integration-react@1.1.29-next.0 + - @backstage/plugin-search-react@1.7.13-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-catalog-common@1.0.24 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + - @backstage/plugin-search-common@1.2.12 + ## 1.21.0 ### Minor Changes diff --git a/plugins/catalog/api-report-alpha.md b/plugins/catalog/api-report-alpha.md index 75d7e53873..5019fc25ea 100644 --- a/plugins/catalog/api-report-alpha.md +++ b/plugins/catalog/api-report-alpha.md @@ -17,22 +17,106 @@ import { TranslationRef } from '@backstage/core-plugin-api/alpha'; export const catalogTranslationRef: TranslationRef< 'catalog', { + readonly 'deleteEntity.description': 'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.'; + readonly 'deleteEntity.cancelButtonTitle': 'Cancel'; + readonly 'deleteEntity.deleteButtonTitle': 'Delete'; + readonly 'deleteEntity.dialogTitle': 'Are you sure you want to delete this entity?'; readonly 'indexPage.title': '{{orgName}} Catalog'; readonly 'indexPage.createButtonTitle': 'Create'; + readonly 'indexPage.supportButtonContent': 'All your software catalog entities'; + readonly 'aboutCard.title': 'About'; + readonly 'aboutCard.refreshButtonTitle': 'Schedule entity refresh'; + readonly 'aboutCard.editButtonTitle': 'Edit Metadata'; + readonly 'aboutCard.createSimilarButtonTitle': 'Create something similar'; + readonly 'aboutCard.refreshScheduledMessage': 'Refresh scheduled'; + readonly 'aboutCard.launchTemplate': 'Launch Template'; + readonly 'aboutCard.viewTechdocs': 'View TechDocs'; + readonly 'aboutCard.viewSource': 'View Source'; + readonly 'aboutCard.descriptionField.value': 'No description'; + readonly 'aboutCard.descriptionField.label': 'Description'; + readonly 'aboutCard.ownerField.value': 'No Owner'; + readonly 'aboutCard.ownerField.label': 'Owner'; + readonly 'aboutCard.domainField.value': 'No Domain'; + readonly 'aboutCard.domainField.label': 'Domain'; + readonly 'aboutCard.systemField.value': 'No System'; + readonly 'aboutCard.systemField.label': 'System'; + readonly 'aboutCard.parentComponentField.value': 'No Parent Component'; + readonly 'aboutCard.parentComponentField.label': 'Parent Component'; + readonly 'aboutCard.typeField.label': 'Type'; + readonly 'aboutCard.lifecycleField.label': 'Lifecycle'; + readonly 'aboutCard.tagsField.value': 'No Tags'; + readonly 'aboutCard.tagsField.label': 'Tags'; + readonly 'aboutCard.targetsField.label': 'Targets'; + readonly 'searchResultItem.lifecycle': 'Lifecycle'; + readonly 'searchResultItem.Owner': 'Owner'; + readonly 'catalogTable.warningPanelTitle': 'Could not fetch catalog entities.'; + readonly 'catalogTable.viewActionTitle': 'View'; + readonly 'catalogTable.editActionTitle': 'Edit'; + readonly 'catalogTable.starActionTitle': 'Add to favorites'; + readonly 'catalogTable.unStarActionTitle': 'Remove from favorites'; + readonly 'dependencyOfComponentsCard.title': 'Dependency of components'; + readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component'; + readonly 'dependsOnComponentsCard.title': 'Depends on components'; + readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component'; + readonly 'dependsOnResourcesCard.title': 'Depends on resources'; + readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component'; + readonly 'entityContextMenu.copiedMessage': 'Copied!'; + readonly 'entityContextMenu.moreButtonTitle': 'More'; + readonly 'entityContextMenu.inspectMenuTitle': 'Inspect entity'; + readonly 'entityContextMenu.copyURLMenuTitle': 'Copy entity URL'; + readonly 'entityContextMenu.unregisterMenuTitle': 'Unregister entity'; + readonly 'entityLabelsCard.title': 'Labels'; + readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:'; + readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLabels.warningPanelTitle': 'Entity not found'; + readonly 'entityLabels.ownerLabel': 'Owner'; + readonly 'entityLabels.lifecycleLabel': 'Lifecycle'; + readonly 'entityLinksCard.title': 'Links'; + readonly 'entityLinksCard.emptyDescription': 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:'; + readonly 'entityLinksCard.readMoreButtonTitle': 'Read more'; + readonly 'entityNotFound.title': 'Entity was not found'; + readonly 'entityNotFound.description': 'Want to help us build this? Check out our Getting Started documentation.'; + readonly 'entityNotFound.docButtonTitle': 'DOCS'; + readonly entityProcessingErrorsDescription: 'The error below originates from'; + readonly entityRelationWarningDescription: "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: "; + readonly 'hasComponentsCard.title': 'Has components'; + readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system'; + readonly 'hasResourcesCard.title': 'Has resources'; + readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system'; + readonly 'hasSubcomponentsCard.title': 'Has subcomponents'; + readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component'; + readonly 'hasSubdomainsCard.title': 'Has subdomains'; + readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain'; + readonly 'hasSystemsCard.title': 'Has systems'; + readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain'; + readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.'; + readonly 'systemDiagramCard.title': 'System Diagram'; + readonly 'systemDiagramCard.description': 'Use pinch & zoo to move around the diagram.'; + readonly 'systemDiagramCard.edgeLabels.dependsOn': 'depends on'; + readonly 'systemDiagramCard.edgeLabels.partOf': 'part of'; + readonly 'systemDiagramCard.edgeLabels.provides': 'provides'; } >; // @alpha (undocumented) export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, - TConfig = never, + TConfig, >(options: { namespace?: string; name?: string; inputs?: TInputs; configSchema?: PortableSchema; loader: (options: { config: TConfig }) => Promise; -}): ExtensionDefinition; +}): ExtensionDefinition< + TConfig, + TConfig, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) const _default: BackstagePlugin< @@ -45,24 +129,19 @@ const _default: BackstagePlugin< }>; }, { - viewTechDoc: ExternalRouteRef< - { - name: string; - kind: string; - namespace: string; - }, - true - >; - createComponent: ExternalRouteRef; - createFromTemplate: ExternalRouteRef< - { - namespace: string; - templateName: string; - }, - true - >; - unregisterRedirect: ExternalRouteRef; - } + viewTechDoc: ExternalRouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + createComponent: ExternalRouteRef; + createFromTemplate: ExternalRouteRef<{ + namespace: string; + templateName: string; + }>; + unregisterRedirect: ExternalRouteRef; + }, + {} >; export default _default; diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index c1fb52fb94..95333a6451 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -370,6 +370,11 @@ export const EntityHasSubcomponentsCard: ( props: HasSubcomponentsCardProps, ) => JSX.Element; +// @public (undocumented) +export const EntityHasSubdomainsCard: ( + props: HasSubdomainsCardProps, +) => JSX.Element; + // @public (undocumented) export const EntityHasSystemsCard: (props: HasSystemsCardProps) => JSX.Element; @@ -551,6 +556,16 @@ export interface HasSubcomponentsCardProps { variant?: InfoCardVariants; } +// @public (undocumented) +export interface HasSubdomainsCardProps { + // (undocumented) + tableOptions?: TableOptions; + // (undocumented) + title?: string; + // (undocumented) + variant?: InfoCardVariants; +} + // @public (undocumented) export interface HasSystemsCardProps { // (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 5b7aea1449..0208271d1a 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "1.21.0", + "version": "1.22.0-next.2", "description": "The Backstage plugin for browsing the Backstage catalog", "backstage": { "role": "frontend-plugin", diff --git a/plugins/catalog/src/alpha.ts b/plugins/catalog/src/alpha.ts index 927d5362b4..b78a5e6225 100644 --- a/plugins/catalog/src/alpha.ts +++ b/plugins/catalog/src/alpha.ts @@ -17,3 +17,4 @@ export * from './alpha/index'; export { default } from './alpha/index'; export { catalogTranslationRef } from './translation'; +export * from './translation'; diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx index 48ee647b81..d8a218f8f3 100644 --- a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx +++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx @@ -26,7 +26,7 @@ import { /** @alpha */ export function createCatalogFilterExtension< TInputs extends AnyExtensionInputMap, - TConfig = never, + TConfig, >(options: { namespace?: string; name?: string; diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index da6a6a669e..8a18efabde 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -46,6 +46,7 @@ export const catalogLabelsEntityCard = createEntityCardExtension({ export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ name: 'depends-on-components', + filter: 'kind:component', loader: async () => import('../components/DependsOnComponentsCard').then(m => compatWrapper(), @@ -54,6 +55,7 @@ export const catalogDependsOnComponentsEntityCard = createEntityCardExtension({ export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ name: 'depends-on-resources', + filter: 'kind:component', loader: async () => import('../components/DependsOnResourcesCard').then(m => compatWrapper(), @@ -62,6 +64,7 @@ export const catalogDependsOnResourcesEntityCard = createEntityCardExtension({ export const catalogHasComponentsEntityCard = createEntityCardExtension({ name: 'has-components', + filter: 'kind:system', loader: async () => import('../components/HasComponentsCard').then(m => compatWrapper(), @@ -70,6 +73,7 @@ export const catalogHasComponentsEntityCard = createEntityCardExtension({ export const catalogHasResourcesEntityCard = createEntityCardExtension({ name: 'has-resources', + filter: 'kind:system', loader: async () => import('../components/HasResourcesCard').then(m => compatWrapper(), @@ -78,14 +82,25 @@ export const catalogHasResourcesEntityCard = createEntityCardExtension({ export const catalogHasSubcomponentsEntityCard = createEntityCardExtension({ name: 'has-subcomponents', + filter: 'kind:component', loader: async () => import('../components/HasSubcomponentsCard').then(m => compatWrapper(), ), }); +export const catalogHasSubdomainsEntityCard = createEntityCardExtension({ + name: 'has-subdomains', + filter: 'kind:domain', + loader: async () => + import('../components/HasSubdomainsCard').then(m => + compatWrapper(), + ), +}); + export const catalogHasSystemsEntityCard = createEntityCardExtension({ name: 'has-systems', + filter: 'kind:domain', loader: async () => import('../components/HasSystemsCard').then(m => compatWrapper(), @@ -101,5 +116,6 @@ export default [ catalogHasComponentsEntityCard, catalogHasResourcesEntityCard, catalogHasSubcomponentsEntityCard, + catalogHasSubdomainsEntityCard, catalogHasSystemsEntityCard, ]; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index ce975f28ad..ff5d7fc801 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -64,6 +64,8 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common import { useSourceTemplateCompoundEntityRef } from './hooks'; import { taskCreatePermission } from '@backstage/plugin-scaffolder-common/alpha'; import { usePermission } from '@backstage/plugin-permission-react'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref'; @@ -120,6 +122,7 @@ export function AboutCard(props: AboutCardProps) { const { allowed: canRefresh } = useEntityPermission( catalogEntityRefreshPermission, ); + const { t } = useTranslationRef(catalogTranslationRef); const { allowed: canCreateTemplateTask } = usePermission({ permission: taskCreatePermission, @@ -146,13 +149,13 @@ export function AboutCard(props: AboutCardProps) { } const viewInSource: IconLinkVerticalProps = { - label: 'View Source', + label: t('aboutCard.viewSource'), disabled: !entitySourceLocation, icon: , href: entitySourceLocation?.locationTargetUrl, }; const viewInTechDocs: IconLinkVerticalProps = { - label: 'View TechDocs', + label: t('aboutCard.viewTechdocs'), disabled: !( entity.metadata.annotations?.[TECHDOCS_ANNOTATION] || @@ -180,7 +183,7 @@ export function AboutCard(props: AboutCardProps) { const Icon = app.getSystemIcon('scaffolder') ?? CreateComponentIcon; const launchTemplate: IconLinkVerticalProps = { - label: 'Launch Template', + label: t('aboutCard.launchTemplate'), icon: , disabled: !templateRoute || !canCreateTemplateTask, href: @@ -216,25 +219,25 @@ export function AboutCard(props: AboutCardProps) { try { await catalogApi.refreshEntity(stringifyEntityRef(entity)); alertApi.post({ - message: 'Refresh scheduled', + message: t('aboutCard.refreshScheduledMessage'), severity: 'info', display: 'transient', }); } catch (e) { errorApi.post(e); } - }, [catalogApi, alertApi, errorApi, entity]); + }, [catalogApi, entity, alertApi, t, errorApi]); return ( {allowRefresh && canRefresh && ( @@ -244,7 +247,7 @@ export function AboutCard(props: AboutCardProps) { component={Link} aria-label="Edit" disabled={!entityMetadataEditUrl} - title="Edit Metadata" + title={t('aboutCard.editButtonTitle')} to={entityMetadataEditUrl ?? '#'} > @@ -252,7 +255,7 @@ export function AboutCard(props: AboutCardProps) { {sourceTemplateRef && templateRoute && ( - + {ownedByRelations.length > 0 && ( @@ -130,8 +140,8 @@ export function AboutContent(props: AboutContentProps) { {(isSystem || partOfDomainRelations.length > 0) && ( {partOfDomainRelations.length > 0 && ( @@ -147,8 +157,8 @@ export function AboutContent(props: AboutContentProps) { isResource || partOfSystemRelations.length > 0) && ( {partOfSystemRelations.length > 0 && ( @@ -161,8 +171,8 @@ export function AboutContent(props: AboutContentProps) { )} {isComponent && partOfComponentRelations.length > 0 && ( @@ -188,22 +198,25 @@ export function AboutContent(props: AboutContentProps) { isComponent || typeof entity?.spec?.lifecycle === 'string') && ( )} - {(entity?.metadata?.tags || []).map(t => ( - + {(entity?.metadata?.tags || []).map(tag => ( + ))} {isLocation && (entity?.spec?.targets || entity?.spec?.target) && ( - + { ], }, })), + getEntitiesByRefs: jest.fn().mockImplementation(async () => ({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'not-tools', + namespace: 'default', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'tools', + namespace: 'default', + }, + }, + ], + })), queryEntities: jest .fn() .mockImplementation(async (request: QueryEntitiesInitialRequest) => { diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index 44e1022042..db765b9145 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -69,7 +69,7 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) { to={createComponentLink && createComponentLink()} /> )} - All your software catalog entities + {t('indexPage.supportButtonContent')} diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index 32c86f38b7..1a596f8b9b 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -27,6 +27,8 @@ import { ResultHighlight, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; const useStyles = makeStyles( { @@ -66,6 +68,7 @@ export function CatalogSearchResultListItem( const highlight = props.highlight as ResultHighlight; const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); if (!result) return null; @@ -121,10 +124,16 @@ export function CatalogSearchResultListItem( {result.kind && } {result.type && } {result.lifecycle && ( - + )} {result.owner && ( - + )}
    diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index b1590bdff3..e18467249a 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -48,6 +48,8 @@ import { columnFactories } from './columns'; import { CatalogTableColumnsFunc, CatalogTableRow } from './types'; import { PaginatedCatalogTable } from './PaginatedCatalogTable'; import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogTranslationRef } from '../../translation'; /** * Props for {@link CatalogTable}. @@ -96,13 +98,14 @@ export const CatalogTable = (props: CatalogTableProps) => { typeof columns === 'function' ? columns(entityListContext) : columns, [columns, entityListContext], ); + const { t } = useTranslationRef(catalogTranslationRef); if (error) { return (
    @@ -113,7 +116,7 @@ export const CatalogTable = (props: CatalogTableProps) => { const defaultActions: TableProps['actions'] = [ ({ entity }) => { const url = entity.metadata.annotations?.[ANNOTATION_VIEW_URL]; - const title = 'View'; + const title = t('catalogTable.viewActionTitle'); return { icon: () => ( @@ -132,7 +135,7 @@ export const CatalogTable = (props: CatalogTableProps) => { }, ({ entity }) => { const url = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; - const title = 'Edit'; + const title = t('catalogTable.editActionTitle'); return { icon: () => ( @@ -151,7 +154,9 @@ export const CatalogTable = (props: CatalogTableProps) => { }, ({ entity }) => { const isStarred = isStarredEntity(entity); - const title = isStarred ? 'Remove from favorites' : 'Add to favorites'; + const title = isStarred + ? t('catalogTable.unStarActionTitle') + : t('catalogTable.starActionTitle'); return { cellStyle: { paddingLeft: '1em' }, diff --git a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx index 976cdff667..c96fe0e067 100644 --- a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React, { ReactNode } from 'react'; -import { fireEvent, render, waitFor } from '@testing-library/react'; +import { fireEvent, waitFor } from '@testing-library/react'; import { PaginatedCatalogTable } from './PaginatedCatalogTable'; import { screen } from '@testing-library/react'; import { CatalogTableRow } from './types'; @@ -63,8 +63,8 @@ describe('PaginatedCatalogTable', () => { ); }; - it('should display all the items', () => { - render( + it('should display all the items', async () => { + await renderInTestApp( wrapInContext(), ); @@ -74,7 +74,7 @@ describe('PaginatedCatalogTable', () => { }); it('should display and invoke the next button', async () => { - const { rerender } = render( + const { rerender } = await renderInTestApp( wrapInContext( { }); it('should display and invoke the prev button', async () => { - const { rerender } = render( + const { rerender } = await renderInTestApp( wrapInContext( diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index f4ea850b9b..fe56ea582b 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -27,6 +27,8 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface DependsOnComponentsCardProps { @@ -37,9 +39,10 @@ export interface DependsOnComponentsCardProps { } export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', - title = 'Depends on components', + title = t('dependsOnComponentsCard.title'), columns = componentEntityColumns, tableOptions = {}, } = props; @@ -50,7 +53,7 @@ export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) { entityKind="Component" relationType={RELATION_DEPENDS_ON} columns={columns} - emptyMessage="No component is a dependency of this component" + emptyMessage={t('dependsOnComponentsCard.emptyMessage')} emptyHelpLink={componentEntityHelpLink} asRenderableEntities={asComponentEntities} tableOptions={tableOptions} diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index d99a9df387..281bfd8766 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -27,6 +27,8 @@ import { RelatedEntitiesCard, resourceEntityColumns, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface DependsOnResourcesCardProps { @@ -37,9 +39,10 @@ export interface DependsOnResourcesCardProps { } export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', - title = 'Depends on resources', + title = t('dependsOnResourcesCard.title'), columns = resourceEntityColumns, tableOptions = {}, } = props; @@ -50,7 +53,7 @@ export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) { entityKind="Resource" relationType={RELATION_DEPENDS_ON} columns={columns} - emptyMessage="No resource is a dependency of this component" + emptyMessage={t('dependsOnResourcesCard.emptyMessage')} emptyHelpLink={componentEntityHelpLink} asRenderableEntities={asResourceEntities} tableOptions={tableOptions} diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index a173773e35..5b740038a1 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -33,6 +33,8 @@ import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common/ import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity'; import { useApi, alertApiRef } from '@backstage/core-plugin-api'; import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type EntityContextMenuClassKey = 'button'; @@ -70,6 +72,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { onUnregisterEntity, onInspectEntity, } = props; + const { t } = useTranslationRef(catalogTranslationRef); const [anchorEl, setAnchorEl] = useState(); const classes = useStyles(); const unregisterPermission = useEntityPermission( @@ -90,12 +93,12 @@ export function EntityContextMenu(props: EntityContextMenuProps) { useEffect(() => { if (!copyState.error && copyState.value) { alertApi.post({ - message: 'Copied!', + message: t('entityContextMenu.copiedMessage'), severity: 'info', display: 'transient', }); } - }, [copyState, alertApi]); + }, [copyState, alertApi, t]); const extraItems = UNSTABLE_extraContextMenuItems && [ ...UNSTABLE_extraContextMenuItems.map(item => ( @@ -117,7 +120,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { return ( <> - + - + { @@ -168,7 +171,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { - + diff --git a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx index 3de890097b..08496d4819 100644 --- a/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/UnregisterEntity.tsx @@ -19,6 +19,8 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import MenuItem from '@material-ui/core/MenuItem'; import CancelIcon from '@material-ui/icons/Cancel'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; type VisibleType = 'visible' | 'hidden' | 'disable'; @@ -40,6 +42,7 @@ export function UnregisterEntity(props: UnregisterEntityProps) { onUnregisterEntity, onClose, } = props; + const { t } = useTranslationRef(catalogTranslationRef); const isBoolean = typeof unregisterEntityOptions?.disableUnregister === 'boolean'; @@ -65,7 +68,7 @@ export function UnregisterEntity(props: UnregisterEntityProps) { - + ); } diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx index 0b1448bb1f..d48b45f054 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx @@ -25,6 +25,8 @@ import { import { EntityLabelsEmptyState } from './EntityLabelsEmptyState'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface EntityLabelsCardProps { @@ -42,6 +44,7 @@ export const EntityLabelsCard = (props: EntityLabelsCardProps) => { const { variant, title } = props; const { entity } = useEntity(); const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); const columns: TableColumn<{ key: string; value: string }>[] = [ { @@ -61,7 +64,7 @@ export const EntityLabelsCard = (props: EntityLabelsCardProps) => { const labels = entity?.metadata?.labels; return ( - + {!labels || Object.keys(labels).length === 0 ? ( ) : ( diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx index 5f5db0baa5..16d0a3a897 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsEmptyState.tsx @@ -19,6 +19,8 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const ENTITY_YAML = `metadata: name: example @@ -39,12 +41,12 @@ const useStyles = makeStyles( export function EntityLabelsEmptyState() { const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); return ( <> - No labels defined for this entity. You can add labels to your entity - YAML as shown in the highlighted example below: + {t('entityLabelsCard.emptyDescription')}
    - Read more + {t('entityLabelsCard.readMoreButtonTitle')} ); diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 274972283f..bbc7792acb 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -53,6 +53,8 @@ import React, { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { rootRouteRef, unregisterRedirectRouteRef } from '../../routes'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type EntityLayoutRouteProps = { @@ -117,11 +119,12 @@ function headerProps( function EntityLabels(props: { entity: Entity }) { const { entity } = props; const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + const { t } = useTranslationRef(catalogTranslationRef); return ( <> {ownedByRelations.length > 0 && ( )} @@ -234,6 +237,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { const navigate = useNavigate(); const catalogRoute = useRouteRef(rootRouteRef); const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef); + const { t } = useTranslationRef(catalogTranslationRef); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); @@ -286,7 +290,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { {NotFoundComponent ? ( NotFoundComponent ) : ( - + There is no {kind} with the requested{' '} kind, namespace, and name diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index ebc551c29f..b5d8269a0b 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -22,6 +22,8 @@ import { LinksGridList } from './LinksGridList'; import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; import { InfoCard, InfoCardVariants } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogTranslationRef } from '../../translation'; /** @public */ export interface EntityLinksCardProps { @@ -33,6 +35,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { const { cols = undefined, variant } = props; const { entity } = useEntity(); const app = useApp(); + const { t } = useTranslationRef(catalogTranslationRef); const iconResolver = (key?: string): IconComponent => key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; @@ -40,7 +43,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { const links = entity?.metadata?.links; return ( - + {!links || links.length === 0 ? ( ) : ( diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx index 81009c5393..09362385f9 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksEmptyState.tsx @@ -19,6 +19,8 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import React from 'react'; import { CodeSnippet } from '@backstage/core-components'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { catalogTranslationRef } from '../../translation'; const ENTITY_YAML = `metadata: name: example @@ -44,12 +46,12 @@ const useStyles = makeStyles( export function EntityLinksEmptyState() { const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); return ( <> - No links defined for this entity. You can add links to your entity YAML - as shown in the highlighted example below: + {t('entityLinksCard.emptyDescription')}
    - Read more + {t('entityLinksCard.readMoreButtonTitle')} ); diff --git a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx index 479edf3984..252005dff5 100644 --- a/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx +++ b/plugins/catalog/src/components/EntityNotFound/EntityNotFound.tsx @@ -20,6 +20,8 @@ import Button from '@material-ui/core/Button'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import { Illo } from './Illo'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; const useStyles = makeStyles(theme => ({ container: { @@ -45,24 +47,24 @@ const useStyles = makeStyles(theme => ({ export function EntityNotFound() { const classes = useStyles(); + const { t } = useTranslationRef(catalogTranslationRef); return ( - Entity was not found + {t('entityNotFound.title')} - Want to help us build this? Check out our Getting Started - documentation. + {t('entityNotFound.description')} diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx index 603f2040d9..c9c24a14a1 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.tsx @@ -23,6 +23,8 @@ import DialogTitle from '@material-ui/core/DialogTitle'; import React, { useState } from 'react'; import { alertApiRef, useApi } from '@backstage/core-plugin-api'; import { assertError } from '@backstage/errors'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; interface DeleteEntityDialogProps { open: boolean; @@ -36,6 +38,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) { const [busy, setBusy] = useState(false); const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); + const { t } = useTranslationRef(catalogTranslationRef); const onDelete = async () => { setBusy(true); @@ -54,7 +57,7 @@ export function DeleteEntityDialog(props: DeleteEntityDialogProps) { return ( - Are you sure you want to delete this entity? + {t('deleteEntity.dialogTitle')} diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx index 8a17559681..10a10e54fd 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.tsx @@ -22,6 +22,8 @@ import { useNavigate } from 'react-router-dom'; import { DeleteEntityDialog } from './DeleteEntityDialog'; import { useRouteRef } from '@backstage/core-plugin-api'; import { rootRouteRef } from '../../routes'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** * Returns true if the given entity has the orphan annotation given by the @@ -44,6 +46,7 @@ export function EntityOrphanWarning() { const catalogLink = useRouteRef(rootRouteRef); const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); const { entity } = useEntity(); + const { t } = useTranslationRef(catalogTranslationRef); const cleanUpAfterRemoval = async () => { setConfirmationDialogOpen(false); @@ -53,8 +56,7 @@ export function EntityOrphanWarning() { return ( <> setConfirmationDialogOpen(true)}> - This entity is not referenced by any location and is therefore not - receiving updates. Click here to delete. + {t('deleteEntity.description')} i.error && @@ -95,6 +97,7 @@ export function EntityProcessingErrorsPanel() { const { loading, error, value } = useAsync(async () => { return getOwnAndAncestorsErrors(entityRef, catalogApi); }, [entityRef, catalogApi]); + const { t } = useTranslationRef(catalogTranslationRef); if (error) { return ( @@ -115,7 +118,7 @@ export function EntityProcessingErrorsPanel() { {stringifyEntityRef(entity) !== stringifyEntityRef(ancestorError.entity) && ( - The error below originates from{' '} + {t('entityProcessingErrorsDescription')}{' '} )} diff --git a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx index 13542a32b4..2ce9120bf0 100644 --- a/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx +++ b/plugins/catalog/src/components/EntityRelationWarning/EntityRelationWarning.tsx @@ -26,6 +26,8 @@ import useAsync from 'react-use/esm/useAsync'; import Box from '@material-ui/core/Box'; import { ResponseErrorPanel } from '@backstage/core-components'; import { useApi, ApiHolder } from '@backstage/core-plugin-api'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; async function getRelationWarnings(entity: Entity, catalogApi: CatalogApi) { const entityRefRelations = entity.relations?.map( @@ -80,6 +82,7 @@ export function EntityRelationWarning() { const { loading, error, value } = useAsync(async () => { return getRelationWarnings(entity, catalogApi); }, [entity, catalogApi]); + const { t } = useTranslationRef(catalogTranslationRef); if (error) { return ( @@ -95,10 +98,8 @@ export function EntityRelationWarning() { return ( <> - - This entity has relations to other entities, which can't be found in the - catalog.
    - Entities not found are: {value.join(', ')} + + {t('entityRelationWarningDescription')} {value.join(', ')} ); diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 9071dac030..917543dd12 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -23,6 +23,8 @@ import { componentEntityHelpLink, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasComponentsCardProps { @@ -31,7 +33,8 @@ export interface HasComponentsCardProps { } export function HasComponentsCard(props: HasComponentsCardProps) { - const { variant = 'gridItem', title = 'Has components' } = props; + const { t } = useTranslationRef(catalogTranslationRef); + const { variant = 'gridItem', title = t('hasComponentsCard.title') } = props; return ( diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index 5af7eec258..d11f10c882 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -23,6 +23,8 @@ import { resourceEntityColumns, resourceEntityHelpLink, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasResourcesCardProps { @@ -31,7 +33,8 @@ export interface HasResourcesCardProps { } export function HasResourcesCard(props: HasResourcesCardProps) { - const { variant = 'gridItem', title = 'Has resources' } = props; + const { t } = useTranslationRef(catalogTranslationRef); + const { variant = 'gridItem', title = t('hasResourcesCard.title') } = props; return ( ); diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index ac40c82435..d81349794a 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -22,6 +22,8 @@ import { componentEntityColumns, RelatedEntitiesCard, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasSubcomponentsCardProps { @@ -31,10 +33,11 @@ export interface HasSubcomponentsCardProps { } export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); const { variant = 'gridItem', tableOptions = {}, - title = 'Has subcomponents', + title = t('hasSubcomponentsCard.title'), } = props; return ( diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx new file mode 100644 index 0000000000..6c145b3228 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.test.tsx @@ -0,0 +1,122 @@ +/* + * 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 { Entity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, + entityRouteRef, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { waitFor, screen } from '@testing-library/react'; +import React from 'react'; +import { HasSubdomainsCard } from './HasSubdomainsCard'; + +describe('', () => { + const getEntitiesByRefs: jest.MockedFunction< + CatalogApi['getEntitiesByRefs'] + > = jest.fn(); + let Wrapper: React.ComponentType>; + + beforeEach(() => { + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [], + }; + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('Has subdomains')).toBeInTheDocument(); + expect( + screen.getByText(/No subdomain is part of this domain/i), + ).toBeInTheDocument(); + }); + + it('shows related subdomains', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [ + { + targetRef: 'domain:my-namespace/target-name', + type: RELATION_HAS_PART, + }, + ], + }; + getEntitiesByRefs.mockResolvedValue({ + items: [ + { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }, + ], + }); + + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + await waitFor(() => { + expect(screen.getByText('Has subdomains')).toBeInTheDocument(); + expect(screen.getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx new file mode 100644 index 0000000000..811f74d820 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx @@ -0,0 +1,55 @@ +/* + * 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 { RELATION_HAS_PART } from '@backstage/catalog-model'; +import { InfoCardVariants, TableOptions } from '@backstage/core-components'; +import React from 'react'; +import { + asComponentEntities, + componentEntityColumns, + RelatedEntitiesCard, +} from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; + +/** @public */ +export interface HasSubdomainsCardProps { + variant?: InfoCardVariants; + tableOptions?: TableOptions; + title?: string; +} + +export function HasSubdomainsCard(props: HasSubdomainsCardProps) { + const { t } = useTranslationRef(catalogTranslationRef); + const { + variant = 'gridItem', + tableOptions = {}, + title = t('hasSubdomainsCard.title'), + } = props; + return ( + + ); +} diff --git a/plugins/catalog/src/components/HasSubdomainsCard/index.ts b/plugins/catalog/src/components/HasSubdomainsCard/index.ts new file mode 100644 index 0000000000..a740abbcc2 --- /dev/null +++ b/plugins/catalog/src/components/HasSubdomainsCard/index.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export { HasSubdomainsCard } from './HasSubdomainsCard'; +export type { HasSubdomainsCardProps } from './HasSubdomainsCard'; diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index d6da465ffa..cee45a95c5 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -23,6 +23,8 @@ import { systemEntityColumns, systemEntityHelpLink, } from '../RelatedEntitiesCard'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export interface HasSystemsCardProps { @@ -31,7 +33,8 @@ export interface HasSystemsCardProps { } export function HasSystemsCard(props: HasSystemsCardProps) { - const { variant = 'gridItem', title = 'Has systems' } = props; + const { t } = useTranslationRef(catalogTranslationRef); + const { variant = 'gridItem', title = t('hasSystemsCard.title') } = props; return ( ); diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index eb37973c27..dd1359482d 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -31,6 +31,8 @@ import { TableColumn, TableOptions, } from '@backstage/core-components'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type RelatedEntitiesCardProps = { @@ -72,6 +74,7 @@ export function RelatedEntitiesCard( tableOptions = {}, } = props; + const { t } = useTranslationRef(catalogTranslationRef); const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { type: relationType, @@ -102,7 +105,9 @@ export function RelatedEntitiesCard(
    {emptyMessage} - Learn how to change this. + + {t('relatedEntitiesCard.emptyHelpLinkTitle')} +
    } diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx index 22466b1464..5e485d672b 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.tsx @@ -46,6 +46,8 @@ import { } from '@backstage/core-components'; import { useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { catalogTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; /** @public */ export type SystemDiagramCardClassKey = @@ -164,6 +166,7 @@ function RenderNode(props: DependencyGraphTypes.RenderNodeProps) { export function SystemDiagramCard() { const { entity } = useEntity(); const theme = useTheme(); + const { t } = useTranslationRef(catalogTranslationRef); const currentSystemName = entity.metadata.name; const currentSystemNode = stringifyEntityRef(entity); const systemNodes = new Array<{ id: string; kind: string; name: string }>(); @@ -211,7 +214,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: currentSystemNode, to: stringifyEntityRef(foundDomain), - label: 'part of', + label: t('systemDiagramCard.edgeLabels.partOf'), }), ); @@ -235,7 +238,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: stringifyEntityRef(catalogItem), to: stringifyEntityRef(foundRelation), - label: 'part of', + label: t('systemDiagramCard.edgeLabels.partOf'), }), ); @@ -247,7 +250,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: stringifyEntityRef(catalogItem), to: stringifyEntityRef(foundRelation), - label: 'provides', + label: t('systemDiagramCard.edgeLabels.provides'), }), ); @@ -259,7 +262,7 @@ export function SystemDiagramCard() { systemEdges.push({ from: stringifyEntityRef(catalogItem), to: stringifyEntityRef(foundRelation), - label: 'depends on', + label: t('systemDiagramCard.edgeLabels.dependsOn'), }), ); } @@ -272,7 +275,7 @@ export function SystemDiagramCard() { } return ( - + - Use pinch & zoom - to move around the diagram. + + {t('systemDiagramCard.description')} ); diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index a691011c96..806ea40c66 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -49,6 +49,7 @@ export { EntityHasComponentsCard, EntityHasResourcesCard, EntityHasSubcomponentsCard, + EntityHasSubdomainsCard, EntityHasSystemsCard, EntityLinksCard, EntityLabelsCard, @@ -71,6 +72,7 @@ export type { EntityContextMenuClassKey } from './components/EntityContextMenu'; export type { HasComponentsCardProps } from './components/HasComponentsCard'; export type { HasResourcesCardProps } from './components/HasResourcesCard'; export type { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +export type { HasSubdomainsCardProps } from './components/HasSubdomainsCard'; export type { HasSystemsCardProps } from './components/HasSystemsCard'; export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 33ceaef632..a339ff31a8 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -50,6 +50,7 @@ import { DependsOnResourcesCardProps } from './components/DependsOnResourcesCard import { HasComponentsCardProps } from './components/HasComponentsCard'; import { HasResourcesCardProps } from './components/HasResourcesCard'; import { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard'; +import { HasSubdomainsCardProps } from './components/HasSubdomainsCard'; import { HasSystemsCardProps } from './components/HasSystemsCard'; import { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard'; import { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem'; @@ -199,6 +200,19 @@ export const EntityHasSubcomponentsCard: ( }), ); +/** @public */ +export const EntityHasSubdomainsCard: ( + props: HasSubdomainsCardProps, +) => JSX.Element = catalogPlugin.provide( + createComponentExtension({ + name: 'EntityHasSubdomainsCard', + component: { + lazy: () => + import('./components/HasSubdomainsCard').then(m => m.HasSubdomainsCard), + }, + }), +); + /** @public */ export const EntityHasResourcesCard: ( props: HasResourcesCardProps, diff --git a/plugins/catalog/src/translation.ts b/plugins/catalog/src/translation.ts index bd27069fde..f38a34be84 100644 --- a/plugins/catalog/src/translation.ts +++ b/plugins/catalog/src/translation.ts @@ -23,6 +23,145 @@ export const catalogTranslationRef = createTranslationRef({ indexPage: { title: `{{orgName}} Catalog`, createButtonTitle: 'Create', + supportButtonContent: 'All your software catalog entities', + }, + aboutCard: { + title: 'About', + refreshButtonTitle: 'Schedule entity refresh', + editButtonTitle: 'Edit Metadata', + createSimilarButtonTitle: 'Create something similar', + refreshScheduledMessage: 'Refresh scheduled', + launchTemplate: 'Launch Template', + viewTechdocs: 'View TechDocs', + viewSource: 'View Source', + descriptionField: { + label: 'Description', + value: 'No description', + }, + ownerField: { + label: 'Owner', + value: 'No Owner', + }, + domainField: { + label: 'Domain', + value: 'No Domain', + }, + systemField: { + label: 'System', + value: 'No System', + }, + parentComponentField: { + label: 'Parent Component', + value: 'No Parent Component', + }, + typeField: { + label: 'Type', + }, + lifecycleField: { + label: 'Lifecycle', + }, + tagsField: { + label: 'Tags', + value: 'No Tags', + }, + targetsField: { + label: 'Targets', + }, + }, + searchResultItem: { + lifecycle: 'Lifecycle', + Owner: 'Owner', + }, + catalogTable: { + warningPanelTitle: 'Could not fetch catalog entities.', + viewActionTitle: 'View', + editActionTitle: 'Edit', + starActionTitle: 'Add to favorites', + unStarActionTitle: 'Remove from favorites', + }, + dependencyOfComponentsCard: { + title: 'Dependency of components', + emptyMessage: 'No component depends on this component', + }, + dependsOnComponentsCard: { + title: 'Depends on components', + emptyMessage: 'No component is a dependency of this component', + }, + dependsOnResourcesCard: { + title: 'Depends on resources', + emptyMessage: 'No resource is a dependency of this component', + }, + entityContextMenu: { + copiedMessage: 'Copied!', + moreButtonTitle: 'More', + inspectMenuTitle: 'Inspect entity', + copyURLMenuTitle: 'Copy entity URL', + unregisterMenuTitle: 'Unregister entity', + }, + entityLabelsCard: { + title: 'Labels', + emptyDescription: + 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:', + readMoreButtonTitle: 'Read more', + }, + entityLabels: { + warningPanelTitle: 'Entity not found', + ownerLabel: 'Owner', + lifecycleLabel: 'Lifecycle', + }, + entityLinksCard: { + title: 'Links', + emptyDescription: + 'No links defined for this entity. You can add links to your entity YAML as shown in the highlighted example below:', + readMoreButtonTitle: 'Read more', + }, + entityNotFound: { + title: 'Entity was not found', + description: + 'Want to help us build this? Check out our Getting Started documentation.', + docButtonTitle: 'DOCS', + }, + deleteEntity: { + dialogTitle: 'Are you sure you want to delete this entity?', + deleteButtonTitle: 'Delete', + cancelButtonTitle: 'Cancel', + description: + 'This entity is not referenced by any location and is therefore not receiving updates. Click here to delete.', + }, + entityProcessingErrorsDescription: 'The error below originates from', + entityRelationWarningDescription: + "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: ", + hasComponentsCard: { + title: 'Has components', + emptyMessage: 'No component is part of this system', + }, + hasResourcesCard: { + title: 'Has resources', + emptyMessage: 'No resource is part of this system', + }, + hasSubcomponentsCard: { + title: 'Has subcomponents', + emptyMessage: 'No subcomponent is part of this component', + }, + hasSubdomainsCard: { + title: 'Has subdomains', + emptyMessage: 'No subdomain is part of this domain', + }, + hasSystemsCard: { + title: 'Has systems', + emptyMessage: 'No system is part of this domain', + }, + relatedEntitiesCard: { + emptyHelpLinkTitle: 'Learn how to change this.', + }, + systemDiagramCard: { + title: 'System Diagram', + description: 'Use pinch & zoo to move around the diagram.', + edgeLabels: { + partOf: 'part of', + provides: 'provides', + dependsOn: 'depends on', + }, }, }, }); diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index 582368aca4..c11499f589 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-config-schema +## 0.1.58-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.1.57 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.1.57-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.1.57-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.1.56 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 0aa4d1dc6c..ad98b78b98 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-config-schema", - "version": "0.1.56", + "version": "0.1.58-next.0", "description": "A Backstage plugin that lets you browse the configuration schema of your app", "backstage": { "role": "frontend-plugin", diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md index fbcae07d37..db4d04fc6e 100644 --- a/plugins/devtools-backend/CHANGELOG.md +++ b/plugins/devtools-backend/CHANGELOG.md @@ -1,5 +1,103 @@ # @backstage/plugin-devtools-backend +## 0.3.9-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config-loader@1.9.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/config-loader@1.9.0-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-devtools-common@0.1.12-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.3.9-next.0 + +### Patch Changes + +- 32a38e1: Removed unused code for lockfile analysis. +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/config-loader@1.8.2-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/config-loader@1.8.1 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.10 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/cli-common@0.1.14 + - @backstage/config@1.2.0 + - @backstage/config-loader@1.8.1 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-devtools-common@0.1.10 + - @backstage/plugin-permission-common@0.7.14 + ## 0.3.5 ### Patch Changes diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json index 106950a4a4..64cfc4f883 100644 --- a/plugins/devtools-backend/package.json +++ b/plugins/devtools-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-backend", - "version": "0.3.5", + "version": "0.3.9-next.2", "backstage": { "role": "backend-plugin", "pluginId": "devtools", @@ -56,7 +56,7 @@ "express-promise-router": "^4.1.0", "fs-extra": "^11.0.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "ping": "^0.4.1", "semver": "^7.5.3", "yn": "^4.0.0" diff --git a/plugins/devtools-backend/src/util/Lockfile.ts b/plugins/devtools-backend/src/util/Lockfile.ts index 52dbb847bc..7f5497e948 100644 --- a/plugins/devtools-backend/src/util/Lockfile.ts +++ b/plugins/devtools-backend/src/util/Lockfile.ts @@ -15,7 +15,6 @@ */ import fs from 'fs-extra'; -import semver from 'semver'; import { parseSyml, stringifySyml } from '@yarnpkg/parsers'; import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile'; @@ -35,35 +34,6 @@ type LockfileQueryEntry = { version: string; }; -/** Entries that have an invalid version range, for example an npm tag */ -type AnalyzeResultInvalidRange = { - name: string; - range: string; -}; - -/** Entries that can be deduplicated by bumping to an existing higher version */ -type AnalyzeResultNewVersion = { - name: string; - range: string; - oldVersion: string; - newVersion: string; -}; - -/** Entries that would need a dependency update in package.json to be deduplicated */ -type AnalyzeResultNewRange = { - name: string; - oldRange: string; - newRange: string; - oldVersion: string; - newVersion: string; -}; - -type AnalyzeResult = { - invalidRanges: AnalyzeResultInvalidRange[]; - newVersions: AnalyzeResultNewVersion[]; - newRanges: AnalyzeResultNewRange[]; -}; - function parseLockfile(lockfileContents: string) { try { return { @@ -135,11 +105,10 @@ export class Lockfile { queries.push({ range, version: value.version }); } - return new Lockfile(path, packages, data, legacy); + return new Lockfile(packages, data, legacy); } private constructor( - private readonly path: string, private readonly packages: Map, private readonly data: LockfileData, private readonly legacy: boolean = false, @@ -155,162 +124,6 @@ export class Lockfile { return this.packages.keys(); } - /** Analyzes the lockfile to identify possible actions and warnings for the entries */ - analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult { - const { filter } = options ?? {}; - const result: AnalyzeResult = { - invalidRanges: [], - newVersions: [], - newRanges: [], - }; - - for (const [name, allEntries] of this.packages) { - if (filter && !filter(name)) { - continue; - } - - // Get rid of and signal any invalid ranges upfront - const invalid = allEntries.filter(e => !semver.validRange(e.range)); - result.invalidRanges.push( - ...invalid.map(({ range }) => ({ name, range })), - ); - - // Grab all valid entries, if there aren't at least 2 different valid ones we're done - const entries = allEntries.filter(e => semver.validRange(e.range)); - if (entries.length < 2) { - continue; - } - - // Find all versions currently in use - const versions = Array.from(new Set(entries.map(e => e.version))).sort( - (v1, v2) => semver.rcompare(v1, v2), - ); - - // If we're not using at least 2 different versions we're done - if (versions.length < 2) { - continue; - } - - const acceptedVersions = new Set(); - for (const { version, range } of entries) { - // Finds the highest matching version from the the known versions - // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one - const acceptedVersion = versions.find(v => semver.satisfies(v, range)); - if (!acceptedVersion) { - throw new Error( - `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`, - ); - } - - if (acceptedVersion !== version) { - result.newVersions.push({ - name, - range, - newVersion: acceptedVersion, - oldVersion: version, - }); - } - - acceptedVersions.add(acceptedVersion); - } - - // If all ranges were able to accept the same version, we're done - if (acceptedVersions.size === 1) { - continue; - } - - // Find the max version that we may want bump older packages to - const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0]; - // Find all existing ranges that satisfy the new max version, and pick the one that - // results in the highest minimum allowed version, usually being the more specific one - const maxEntry = entries - .filter(e => semver.satisfies(maxVersion, e.range)) - .map(e => ({ e, min: semver.minVersion(e.range) })) - .filter(p => p.min) - .sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e; - if (!maxEntry) { - throw new Error( - `No entry found that satisfies max version '${maxVersion}'`, - ); - } - - // Find all entries that don't satisfy the max version - for (const { version, range } of entries) { - if (semver.satisfies(maxVersion, range)) { - continue; - } - - result.newRanges.push({ - name, - oldRange: range, - newRange: maxEntry.range, - oldVersion: version, - newVersion: maxVersion, - }); - } - } - - return result; - } - - remove(name: string, range: string): boolean { - const query = `${name}@${range}`; - const existed = Boolean(this.data[query]); - delete this.data[query]; - - const newEntries = this.packages.get(name)?.filter(e => e.range !== range); - if (newEntries) { - this.packages.set(name, newEntries); - } - - return existed; - } - - /** Modifies the lockfile by bumping packages to the suggested versions */ - replaceVersions(results: AnalyzeResultNewVersion[]) { - for (const { name, range, oldVersion, newVersion } of results) { - const query = `${name}@${range}`; - - // Update the backing data - const entryData = this.data[query]; - if (!entryData) { - throw new Error(`No entry data for ${query}`); - } - if (entryData.version !== oldVersion) { - throw new Error( - `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, - ); - } - - // Modifying the data in the entry is not enough, we need to reference an existing version object - const matchingEntry = Object.entries(this.data).find( - ([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion, - ); - if (!matchingEntry) { - throw new Error( - `No matching entry found for ${name} at version ${newVersion}`, - ); - } - this.data[query] = matchingEntry[1]; - - // Update our internal data structure - const entry = this.packages.get(name)?.find(e => e.range === range); - if (!entry) { - throw new Error(`No entry data for ${query}`); - } - if (entry.version !== oldVersion) { - throw new Error( - `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`, - ); - } - entry.version = newVersion; - } - } - - async save() { - await fs.writeFile(this.path, this.toString(), 'utf8'); - } - toString() { return stringifyLockfile(this.data, this.legacy); } diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md index 3e060571ad..80d0584c77 100644 --- a/plugins/devtools-common/CHANGELOG.md +++ b/plugins/devtools-common/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-devtools-common +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/types@1.1.1 + +## 0.1.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/types@1.1.1 + +## 0.1.11 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/types@1.1.1 + ## 0.1.10 ### Patch Changes diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json index 24b17d6742..d8a87753ca 100644 --- a/plugins/devtools-common/package.json +++ b/plugins/devtools-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools-common", - "version": "0.1.10", + "version": "0.1.12-next.1", "description": "Common functionalities for the devtools plugin", "backstage": { "role": "common-library", diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md index db8e1701f1..afebfeefe2 100644 --- a/plugins/devtools/CHANGELOG.md +++ b/plugins/devtools/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/plugin-devtools +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-devtools-common@0.1.12-next.1 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-devtools-common@0.1.12-next.0 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/plugin-permission-react@0.4.24 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-devtools-common@0.1.11 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/plugin-devtools-common@0.1.10 + - @backstage/plugin-permission-react@0.4.23 + ## 0.1.15 ### Patch Changes diff --git a/plugins/devtools/api-report-alpha.md b/plugins/devtools/api-report-alpha.md index 59aefd3698..23ede8144c 100644 --- a/plugins/devtools/api-report-alpha.md +++ b/plugins/devtools/api-report-alpha.md @@ -11,6 +11,7 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json index 3e9d697965..264f7af95b 100644 --- a/plugins/devtools/package.json +++ b/plugins/devtools/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-devtools", - "version": "0.1.15", + "version": "0.1.17-next.2", "backstage": { "role": "frontend-plugin", "pluginId": "devtools", diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index 0c80bac266..a4a4da2b17 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.3.9-next.0 + +### Patch Changes + +- ba8571e: Setup user agent header for AWS sdk clients, this enables users to better track API calls made from Backstage to AWS APIs through things like CloudTrail. +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.3.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.3.5 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 6d58fe4436..7b1da53130 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.3.5", + "version": "0.3.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts index 26eba88c18..de55dee017 100644 --- a/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts +++ b/plugins/events-backend-module-aws-sqs/src/publisher/AwsSqsConsumingEventPublisher.ts @@ -76,6 +76,7 @@ export class AwsSqsConsumingEventPublisher { }; this.sqs = new SQSClient({ + customUserAgent: 'backstage-aws-events-sqs-publisher', region: config.region, endpoint: config.endpoint, }); diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 22171cbd33..804293b869 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-events-backend-module-azure +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index bb0bce7753..5d7fc3f1bf 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.2.5", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index b71ef0dadf..edef56d946 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 83e5f3528b..a58bacdf8c 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.2.5", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 4ff865f60b..fcdda38e9b 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,53 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 5fbc339191..91e598511f 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.2.5", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index 77882e3711..38b1e277e8 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-events-backend-module-github +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index cf1bb2c7a5..f77c2c9df8 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.2.5", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index ec96ba8f73..a7986891fa 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,59 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## 0.2.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.2.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.2.8 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + +## 0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.2.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + ## 0.2.5 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index e6ed2d9b16..c5a0abfeb8 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.2.5", + "version": "0.2.9-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "events", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index 4f04a9c277..f408883694 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,47 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.33-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.2 + +## 0.1.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.1.33-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.1.32 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.8 + +## 0.1.32-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.3.7-next.0 + ## 0.1.29 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index 89dffd2e2c..0cff95bf35 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-test-utils", - "version": "0.1.29", + "version": "0.1.33-next.2", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", "backstage": { "role": "node-library", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index a5699c8d9f..8c2e2f767b 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-events-backend +## 0.3.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/config@1.2.0 + +## 0.3.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.1 + +## 0.3.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.9-next.0 + +## 0.3.9 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/config@1.2.0 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/plugin-events-node@0.3.8-next.1 + +## 0.3.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/config@1.2.0 + ## 0.3.6 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index d3ff69d254..01310be5d8 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.3.6", + "version": "0.3.10-next.2", "backstage": { "role": "backend-plugin", "pluginId": "events", diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index 18da615383..ec7c864d23 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/plugin-events-node +## 0.3.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + +## 0.3.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + +## 0.3.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + +## 0.3.8 + +### Patch Changes + +- b05e1e1: Service factories exported by this package have been updated to use the new service factory format that doesn't use a callback. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + +## 0.3.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + ## 0.3.5 ### Patch Changes diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 46646d2ce8..729f526dee 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -4,7 +4,7 @@ ```ts import { LoggerService } from '@backstage/backend-plugin-api'; -import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @public @@ -63,13 +63,15 @@ export interface EventsService { export type EventsServiceEventHandler = (params: EventParams) => Promise; // @public (undocumented) -export const eventsServiceFactory: () => ServiceFactory< +export const eventsServiceFactory: ServiceFactoryCompat< EventsService, - 'plugin' + 'plugin', + 'singleton', + undefined >; // @public -export const eventsServiceRef: ServiceRef; +export const eventsServiceRef: ServiceRef; // @public (undocumented) export type EventsServiceSubscribeOptions = { diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 5e0e375d48..db2a6f4271 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-node", - "version": "0.3.5", + "version": "0.3.9-next.2", "description": "The plugin-events-node module for @backstage/plugin-events-backend", "backstage": { "role": "node-library", diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index c3bb326175..370053d097 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,65 @@ # @internal/plugin-todo-list-backend +## 1.0.30-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/errors@1.2.4 + +## 1.0.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 1.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + +## 1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/errors@1.2.4 + +## 1.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + +## 1.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/errors@1.2.4 + ## 1.0.28 ### Patch Changes diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index 36bd9f86c6..b9b04c178f 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.28", + "version": "1.0.30-next.2", "backstage": { "role": "backend-plugin", "pluginId": "todo-list", diff --git a/plugins/example-todo-list-common/CHANGELOG.md b/plugins/example-todo-list-common/CHANGELOG.md index 3761ead6a3..b2f2175573 100644 --- a/plugins/example-todo-list-common/CHANGELOG.md +++ b/plugins/example-todo-list-common/CHANGELOG.md @@ -1,5 +1,26 @@ # @internal/plugin-todo-list-common +## 1.0.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + +## 1.0.21-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + +## 1.0.20 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + ## 1.0.19 ### Patch Changes diff --git a/plugins/example-todo-list-common/package.json b/plugins/example-todo-list-common/package.json index 317cfa991d..cca7f5aa4b 100644 --- a/plugins/example-todo-list-common/package.json +++ b/plugins/example-todo-list-common/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-common", - "version": "1.0.19", + "version": "1.0.21-next.1", "backstage": { "role": "common-library", "pluginId": "todo-list", diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 43873757cf..51e2b93ef9 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,36 @@ # @internal/plugin-todo-list +## 1.0.30-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 1.0.29 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + +## 1.0.29-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 1.0.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 1.0.28 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index 5e5f76ec6b..d950d8c406 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.28", + "version": "1.0.30-next.0", "backstage": { "role": "frontend-plugin", "pluginId": "todo-list", diff --git a/plugins/home-react/CHANGELOG.md b/plugins/home-react/CHANGELOG.md index 4ac5fa4b93..d810195e7f 100644 --- a/plugins/home-react/CHANGELOG.md +++ b/plugins/home-react/CHANGELOG.md @@ -1,5 +1,52 @@ # @backstage/plugin-home-react +## 0.1.16-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.15 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.1.15-next.1 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.1.14 ### Patch Changes diff --git a/plugins/home-react/catalog-info.yaml b/plugins/home-react/catalog-info.yaml index 28109b9ddc..7022206fb9 100644 --- a/plugins/home-react/catalog-info.yaml +++ b/plugins/home-react/catalog-info.yaml @@ -9,4 +9,4 @@ metadata: spec: lifecycle: experimental type: backstage-web-library - owner: discoverability-maintainers + owner: home-maintainers diff --git a/plugins/home-react/package.json b/plugins/home-react/package.json index ad2bf6c7b2..00b0f29fca 100644 --- a/plugins/home-react/package.json +++ b/plugins/home-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home-react", - "version": "0.1.14", + "version": "0.1.16-next.0", "description": "A Backstage plugin that contains react components helps you build a home page", "backstage": { "role": "web-library", @@ -46,7 +46,7 @@ "@backstage/core-plugin-api": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", - "@rjsf/utils": "5.18.4", + "@rjsf/utils": "5.18.5", "@types/react": "^16.13.1 || ^17.0.0" }, "devDependencies": { diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index fc163b8757..08cf9bec5f 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,128 @@ # @backstage/plugin-home +## 0.7.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## 0.7.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## 0.7.8-next.0 + +### Patch Changes + +- 31bfc44: Updated alpha definitions of extension data references. +- fdcc059: Fixed a bug on the WelcomeTitle component where the welcome message wasn't correct when the language was set to Spanish +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/plugin-home-react@0.1.16-next.0 + +## 0.7.7 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-home-react@0.1.15 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + +## 0.7.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/plugin-home-react@0.1.15-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.7.7-next.1 + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/plugin-home-react@0.1.15-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/theme@0.5.6 + +## 0.7.6-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-home-react@0.1.15-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/theme@0.5.6 + ## 0.7.5 ### Patch Changes diff --git a/plugins/home/api-report-alpha.md b/plugins/home/api-report-alpha.md index 35ce0bb86c..0f9b980a78 100644 --- a/plugins/home/api-report-alpha.md +++ b/plugins/home/api-report-alpha.md @@ -7,11 +7,15 @@ import { BackstagePlugin } from '@backstage/frontend-plugin-api'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; // @alpha (undocumented) -const _default: BackstagePlugin<{}, {}>; +const _default: BackstagePlugin<{}, {}, {}>; export default _default; // @alpha (undocumented) -export const titleExtensionDataRef: ConfigurableExtensionDataRef; +export const titleExtensionDataRef: ConfigurableExtensionDataRef< + string, + 'title', + {} +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/home/catalog-info.yaml b/plugins/home/catalog-info.yaml index 1b23d4b83b..eefc114313 100644 --- a/plugins/home/catalog-info.yaml +++ b/plugins/home/catalog-info.yaml @@ -7,4 +7,4 @@ metadata: spec: lifecycle: experimental type: backstage-frontend-plugin - owner: discoverability-maintainers + owner: home-maintainers diff --git a/plugins/home/package.json b/plugins/home/package.json index 6dda6cbfc2..e566b46e78 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-home", - "version": "0.7.5", + "version": "0.7.9-next.2", "description": "A Backstage plugin that helps you build a home page", "backstage": { "role": "frontend-plugin", @@ -70,10 +70,10 @@ "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", - "@rjsf/core": "5.18.4", - "@rjsf/material-ui": "5.18.4", - "@rjsf/utils": "5.18.4", - "@rjsf/validator-ajv8": "5.18.4", + "@rjsf/core": "5.18.5", + "@rjsf/material-ui": "5.18.5", + "@rjsf/utils": "5.18.5", + "@rjsf/validator-ajv8": "5.18.5", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "lodash": "^4.17.21", "luxon": "^3.4.3", diff --git a/plugins/home/src/alpha.tsx b/plugins/home/src/alpha.tsx index e9ff26c9e1..b13c2a2e0e 100644 --- a/plugins/home/src/alpha.tsx +++ b/plugins/home/src/alpha.tsx @@ -31,7 +31,9 @@ const rootRouteRef = createRouteRef(); /** * @alpha */ -export const titleExtensionDataRef = createExtensionDataRef('title'); +export const titleExtensionDataRef = createExtensionDataRef().with({ + id: 'title', +}); const homePage = createPageExtension({ defaultPath: '/home', diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json index 9010ba4359..ddb51654fa 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json +++ b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodAfternoon.locales.json @@ -103,6 +103,7 @@ "Sinhala": "සුභ දවාලක්", "Slovenian": "Dober dan", "Somali": "Galab wanaagsan", + "Spanish": "Buenas tardes", "Southern Sámi": "Buerie biejjie", "Sumerian": "𒄭𒌓𒁀", "Swahili": "Habari ya mchana", diff --git a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json index 5a98abafaa..c8d8c7334e 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json +++ b/plugins/home/src/homePageComponents/WelcomeTitle/locales/goodEvening.locales.json @@ -85,7 +85,7 @@ "Slovak": "Dober vecher", "Slovenian": "Dober vecher", "Somali": "Habeen wanaagsan", - "Spanish": "Buenas tardes", + "Spanish": "Buenas noches", "Sumerian": "𒄭𒈪", "Swedish": "God afton", "Tagalog": "Magandang gabi", diff --git a/plugins/home/src/plugin.test.ts b/plugins/home/src/plugin.test.ts index 1526bd0c61..904481c98e 100644 --- a/plugins/home/src/plugin.test.ts +++ b/plugins/home/src/plugin.test.ts @@ -24,7 +24,7 @@ describe('home', () => { // Temporarily ensure we are installing a working version of the react-grid-layout library // For more details, see: https://github.com/react-grid-layout/react-grid-layout/issues/1959 - // TODO(@backstage/discoverability-maintainers): Delete this once the sub-dependency issue has been resolved. + // TODO(@backstage/home-maintainers): Delete this once the sub-dependency issue has been resolved. it('should pin react-grid-layout version to 1.3.4', async () => { expect(packageJson.dependencies['react-grid-layout']).toBe('1.3.4'); }); diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 5d0b4c6f37..1255d6d0fd 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,128 @@ # @backstage/plugin-kubernetes-backend +## 0.18.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 8c1aa06: Add `kubernetes.clusterLocatorMethods[].clusters[].customResources` to the configuration schema. + This was already documented and supported by the plugin. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-kubernetes-node@0.1.17-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.18.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-kubernetes-node@0.1.17-next.1 + +## 0.18.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/plugin-kubernetes-node@0.1.17-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 0.18.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-kubernetes-node@0.1.16 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.18.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-kubernetes-common@0.8.0 + - @backstage/plugin-kubernetes-node@0.1.16-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 0.18.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-kubernetes-node@0.1.15-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + - @backstage/plugin-permission-common@0.7.14 + ## 0.18.0 ### Minor Changes diff --git a/plugins/kubernetes-backend/config.d.ts b/plugins/kubernetes-backend/config.d.ts index 777a3392db..d6c5f1d32b 100644 --- a/plugins/kubernetes-backend/config.d.ts +++ b/plugins/kubernetes-backend/config.d.ts @@ -68,6 +68,11 @@ export interface Config { caData?: string; /** @visibility secret */ caFile?: string; + customResources?: Array<{ + group: string; + apiVersion: string; + plural: string; + }>; }>; } | { diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index ad05c481c0..70f640e0ba 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.18.0", + "version": "0.18.4-next.2", "description": "A Backstage backend plugin that integrates towards Kubernetes", "backstage": { "role": "backend-plugin", @@ -92,7 +92,7 @@ "lodash": "^4.17.21", "luxon": "^3.0.0", "morgan": "^1.10.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 1ac653bb58..5edb8845d4 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -38,7 +38,7 @@ import { ServiceMock, mockCredentials, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; @@ -508,7 +508,7 @@ describe('API integration tests', () => { describe('/proxy', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { worker.use( diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index a433374a2c..3491268d39 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -28,7 +28,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { FetchResponse, @@ -1128,7 +1128,7 @@ describe('KubernetesFanOutHandler', () => { describe('with a real fetcher', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('fetch error short-circuits requests to a single cluster, recovering across the fleet', async () => { const pods = [{ metadata: { name: 'pod-name' } }]; diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index dafb073bb1..8666e0ef16 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -28,7 +28,7 @@ import { setupServer } from 'msw/node'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { Config } from '@kubernetes/client-node'; @@ -71,7 +71,7 @@ const POD_METRICS_FIXTURE = [ describe('KubernetesFetcher', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const labels = (req: MockedRequest): object => { const selectorParam = req.url.searchParams.get('labelSelector'); diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts index 02eeb8c7ed..fe4feaee11 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts @@ -20,7 +20,7 @@ import { errorHandler } from '@backstage/backend-common'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { NotFoundError } from '@backstage/errors'; import { @@ -90,7 +90,7 @@ describe('KubernetesProxy', () => { getExternalBaseUrl: jest.fn(), }; - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const buildMockRequest = (clusterName: any, path: string): Request => { const req = getMockReq({ diff --git a/plugins/kubernetes-cluster/CHANGELOG.md b/plugins/kubernetes-cluster/CHANGELOG.md index e27d874ccc..801c2018c9 100644 --- a/plugins/kubernetes-cluster/CHANGELOG.md +++ b/plugins/kubernetes-cluster/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-kubernetes-cluster +## 0.0.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## 0.0.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.1 + +## 0.0.14-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/plugin-kubernetes-react@0.4.2-next.0 + +## 0.0.13 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-kubernetes-react@0.4.1 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.0.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## 0.0.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## 0.0.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + ## 0.0.12 ### Patch Changes diff --git a/plugins/kubernetes-cluster/package.json b/plugins/kubernetes-cluster/package.json index 9555fdbf79..75c0758ac5 100644 --- a/plugins/kubernetes-cluster/package.json +++ b/plugins/kubernetes-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-cluster", - "version": "0.0.12", + "version": "0.0.14-next.2", "description": "A Backstage plugin that shows details of Kubernetes clusters", "backstage": { "role": "frontend-plugin", diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index e4de39bfd9..f8ac4b4768 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-kubernetes-common +## 0.8.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 0.8.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 0.8.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 0.8.0 ### Minor Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 9005c5ffe9..1e05ed873e 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-common", - "version": "0.8.0", + "version": "0.8.2-next.1", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/kubernetes-node/CHANGELOG.md b/plugins/kubernetes-node/CHANGELOG.md index 47a7fbba76..42e5885c49 100644 --- a/plugins/kubernetes-node/CHANGELOG.md +++ b/plugins/kubernetes-node/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-kubernetes-node +## 0.1.17-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 0.1.17-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 0.1.17-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.1 + +## 0.1.16 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 0.1.16-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + ## 0.1.13 ### Patch Changes diff --git a/plugins/kubernetes-node/package.json b/plugins/kubernetes-node/package.json index 90fb172405..5377f26b4d 100644 --- a/plugins/kubernetes-node/package.json +++ b/plugins/kubernetes-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-node", - "version": "0.1.13", + "version": "0.1.17-next.2", "description": "Node.js library for the kubernetes plugin", "backstage": { "role": "node-library", @@ -43,7 +43,7 @@ "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/types": "workspace:^", "@kubernetes/client-node": "^0.20.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "winston": "^3.2.1" }, "devDependencies": { diff --git a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts index fdfea030a5..24cd459fde 100644 --- a/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts +++ b/plugins/kubernetes-node/src/auth/PinnipedHelper.test.ts @@ -18,7 +18,7 @@ import { ExtendedHttpServer } from '@backstage/backend-app-api'; import { ClusterDetails } from '../types'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { createBackendModule } from '@backstage/backend-plugin-api'; @@ -43,7 +43,7 @@ describe('Pinniped - tokenCredentialRequest', () => { const logger = loggerToWinstonLogger(mockServices.logger.mock()); let httpsRequest: jest.SpyInstance; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeAll(() => { httpsRequest = jest.spyOn( diff --git a/plugins/kubernetes-react/CHANGELOG.md b/plugins/kubernetes-react/CHANGELOG.md index 8ebe85459a..34e9b259be 100644 --- a/plugins/kubernetes-react/CHANGELOG.md +++ b/plugins/kubernetes-react/CHANGELOG.md @@ -1,5 +1,75 @@ # @backstage/plugin-kubernetes-react +## 0.4.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.1 + +## 0.4.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- 2414d86: Fix the `HTML` markup of the `FixDialog` component, `ul` and `li` are not allowed inside a `p` tag. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.1-next.1 + +### Patch Changes + +- e3cb6ab: Add a namespace label to RolloutDrawer +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-kubernetes-common@0.8.0 + ## 0.4.0 ### Minor Changes diff --git a/plugins/kubernetes-react/package.json b/plugins/kubernetes-react/package.json index 481b264d27..dbf89ae0cf 100644 --- a/plugins/kubernetes-react/package.json +++ b/plugins/kubernetes-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-react", - "version": "0.4.0", + "version": "0.4.2-next.2", "description": "Web library for the kubernetes-react plugin", "backstage": { "role": "web-library", diff --git a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts index 82ac7c84e2..4698f01dfe 100644 --- a/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts +++ b/plugins/kubernetes-react/src/api/KubernetesBackendClient.test.ts @@ -19,7 +19,7 @@ import { KubernetesBackendClient } from './KubernetesBackendClient'; import { rest } from 'msw'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { setupServer } from 'msw/node'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { CustomObjectsByEntityRequest, KubernetesRequestBody, @@ -36,7 +36,7 @@ describe('KubernetesBackendClient', () => { }; let mockResponse: ObjectsByEntityResponse; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const identityApi = { getCredentials: jest.fn(), diff --git a/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx b/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx index 00063afa99..a4748cdae7 100644 --- a/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx +++ b/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/Rollout.test.tsx @@ -35,6 +35,7 @@ describe('Rollout', () => { expect(screen.getByText('dice-roller')).toBeInTheDocument(); expect(screen.getByText('Rollout')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); expect(screen.getByText('2 pods')).toBeInTheDocument(); expect(screen.getByText('No pods with errors')).toBeInTheDocument(); expect(screen.queryByText('Paused')).toBeNull(); @@ -52,6 +53,7 @@ describe('Rollout', () => { expect(screen.getByText('dice-roller')).toBeInTheDocument(); expect(screen.getByText('Rollout')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); expect(screen.getByText('2 pods')).toBeInTheDocument(); expect(screen.getByText('1 pod with errors')).toBeInTheDocument(); expect(screen.queryByText('Paused')).toBeNull(); @@ -71,6 +73,7 @@ describe('Rollout', () => { expect(screen.getByText('dice-roller')).toBeInTheDocument(); expect(screen.getByText('Rollout')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); expect(screen.getByText('2 pods')).toBeInTheDocument(); expect(screen.getByText('No pods with errors')).toBeInTheDocument(); expect(screen.getByText('Paused (45 minutes ago)')).toBeInTheDocument(); @@ -90,6 +93,7 @@ describe('Rollout', () => { expect(screen.getByText('dice-roller')).toBeInTheDocument(); expect(screen.getByText('Rollout')).toBeInTheDocument(); + expect(screen.getByText('namespace: default')).toBeInTheDocument(); expect(screen.getByText('2 pods')).toBeInTheDocument(); expect(screen.getByText('No pods with errors')).toBeInTheDocument(); expect(screen.queryByText('Paused')).toBeNull(); diff --git a/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx b/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx index 304c8bdf78..65d4840bf0 100644 --- a/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx +++ b/plugins/kubernetes-react/src/components/CustomResources/ArgoRollouts/RolloutDrawer.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { KubernetesStructuredMetadataTableDrawer } from '../../KubernetesDrawer'; import Typography from '@material-ui/core/Typography'; import Grid from '@material-ui/core/Grid'; +import Chip from '@material-ui/core/Chip'; export const RolloutDrawer = ({ rollout, @@ -26,6 +27,7 @@ export const RolloutDrawer = ({ rollout: any; expanded?: boolean; }) => { + const namespace = rollout.metadata?.namespace; return ( + {namespace && ( + + + + )} ); diff --git a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx index fd7440870f..99386b3d40 100644 --- a/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx +++ b/plugins/kubernetes-react/src/components/Pods/FixDialog/FixDialog.tsx @@ -33,6 +33,8 @@ import { DetectedError } from '@backstage/plugin-kubernetes-common'; import { PodLogs } from '../PodLogs'; import { Events } from '../Events'; import { LinkButton } from '@backstage/core-components'; +import List from '@material-ui/core/List'; +import ListItem from '@material-ui/core/ListItem'; const useStyles = makeStyles((theme: Theme) => createStyles({ @@ -96,17 +98,15 @@ export const FixDialog: React.FC = ({ Fix: - -
      - {(error.proposedFix?.actions ?? []).map((fix, i) => { - return ( -
    • - {fix} -
    • - ); - })} -
    -
    + + {(error.proposedFix?.actions ?? []).map((fix, i) => { + return ( + + {fix} + + ); + })} +
    {pf && pf.type === 'logs' && ( diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 2320219483..1b5ca07506 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,91 @@ # @backstage/plugin-kubernetes +## 0.11.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-kubernetes-common@0.8.2-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.2 + +## 0.11.13-next.1 + +### Patch Changes + +- e6c15cc: Adds support for Backstage's new frontend system, available via the `/alpha` sub-path export. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-kubernetes-common@0.8.2-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.2-next.1 + +## 0.11.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/plugin-kubernetes-react@0.4.2-next.0 + +## 0.11.12 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-kubernetes-react@0.4.1 + - @backstage/plugin-kubernetes-common@0.8.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.11.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-kubernetes-react@0.4.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## 0.11.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + +## 0.11.12-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/plugin-kubernetes-react@0.4.1-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-kubernetes-common@0.8.0 + ## 0.11.11 ### Patch Changes diff --git a/plugins/kubernetes/README.md b/plugins/kubernetes/README.md index 15dfd16b17..4ffde719f2 100644 --- a/plugins/kubernetes/README.md +++ b/plugins/kubernetes/README.md @@ -26,8 +26,38 @@ For more information, see the [formal documentation about the Kubernetes feature ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/kubernetes](http://localhost:3000/kubernetes). +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/kubernetes](http://localhost:3000/catalog/default/component/:component-name/kubernetes). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. + +### Integrating with `EntityPage` (New Frontend System) + +Follow this section if you are using Backstage's [new frontend system](https://backstage.io/docs/frontend-system/). + +1. Import `kubernetesPlugin` in your `App.tsx` and add it to your app's `features` array: + +```typescript +import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha'; + +// ... + +export const app = createApp({ + features: [ + // ... + kubernetesPlugin, + // ... + ], +}); +``` + +2. Next, enable your desired extensions in `app-config.yaml` + +```yaml +app: + extensions: + - entity-content:kubernetes/kubernetes +``` + +Now, the extension appears on your entity page as one of the tabs, which is called `KUBERNETES` diff --git a/plugins/kubernetes/api-report-alpha.md b/plugins/kubernetes/api-report-alpha.md new file mode 100644 index 0000000000..befbcf379a --- /dev/null +++ b/plugins/kubernetes/api-report-alpha.md @@ -0,0 +1,20 @@ +## API Report File for "@backstage/plugin-kubernetes" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; +import { RouteRef } from '@backstage/frontend-plugin-api'; + +// @public (undocumented) +const _default: BackstagePlugin< + { + kubernetes: RouteRef; + }, + {}, + {} +>; +export default _default; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index fbc666ae13..2de5a17f25 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -33,6 +33,7 @@ import fixture1 from '../src/__fixtures__/1-deployments.json'; import fixture2 from '../src/__fixtures__/2-deployments.json'; import fixture3 from '../src/__fixtures__/1-cronjobs.json'; import fixture4 from '../src/__fixtures__/2-cronjobs.json'; +import fixture5 from '../src/__fixtures__/1-rollouts.json'; import { TestApiProvider } from '@backstage/test-utils'; const mockEntity: Entity = { @@ -186,5 +187,18 @@ createDevApp() ), }) + .addPage({ + path: '/fixture-5', + title: 'Fixture 5', + element: ( + + + + + + ), + }) .registerPlugin(kubernetesPlugin) .render(); diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 339e26a246..ab8fe84d9b 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.11.11", + "version": "0.11.13-next.2", "description": "A Backstage plugin that integrates towards Kubernetes", "backstage": { "role": "frontend-plugin", @@ -14,9 +14,7 @@ ] }, "publishConfig": { - "access": "public", - "main": "dist/index.esm.js", - "types": "dist/index.d.ts" + "access": "public" }, "keywords": [ "backstage", @@ -30,8 +28,23 @@ }, "license": "Apache-2.0", "sideEffects": false, + "exports": { + ".": "./src/index.ts", + "./alpha": "./src/alpha.ts", + "./package.json": "./package.json" + }, "main": "src/index.ts", "types": "src/index.ts", + "typesVersions": { + "*": { + "alpha": [ + "src/alpha.ts" + ], + "package.json": [ + "package.json" + ] + } + }, "files": [ "dist" ], @@ -46,8 +59,10 @@ }, "dependencies": { "@backstage/catalog-model": "workspace:^", + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", "@backstage/plugin-kubernetes-common": "workspace:^", "@backstage/plugin-kubernetes-react": "workspace:^", diff --git a/plugins/kubernetes/src/__fixtures__/1-rollouts.json b/plugins/kubernetes/src/__fixtures__/1-rollouts.json new file mode 100644 index 0000000000..66929b9129 --- /dev/null +++ b/plugins/kubernetes/src/__fixtures__/1-rollouts.json @@ -0,0 +1,2908 @@ +{ + "pods": [ + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.11\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-2m5hv", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593216", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-2m5hv", + "uid": "aadb71c0-36fa-43e3-b38a-162f134d4359" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://aa4489297c34c48bb33c18474a8d2b33854a82ed42155680b259f635f556ce70", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.11", + "podIPs": [ + { + "ip": "172.17.0.11" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.7\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-4v6s8", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593221", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-4v6s8", + "uid": "32e56490-6f20-4757-991f-a0c7fb407358" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://d91cc76c41249b8d3dfcf538d180b471b2a7966aae0d17a818307c8a9b4af897", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.7", + "podIPs": [ + { + "ip": "172.17.0.7" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-24T11:39:26.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-24T11:39:26.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.6\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-24T11:39:27.000Z" + } + ], + "name": "dice-roller-6c8646bfd-b9zt5", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "503886", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-b9zt5", + "uid": "8b6601b6-469e-4e89-8fd0-abb2f1a567e4" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:26.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:26.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://c50e0d0fa96f09a2ed5df7dd5a7f7de88e6b7c7addb8f002f299d6afc52d82ce", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-24T11:39:27.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.6", + "podIPs": [ + { + "ip": "172.17.0.6" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-24T11:39:26.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.13\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-cfxqc", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593211", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-cfxqc", + "uid": "e87e1776-0ca7-41fb-aeea-e1f648387545" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:51.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://c1641d986aae424429b7c2c1117baeb17d3794f73206bd57292cdf8264f929b2", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.13", + "podIPs": [ + { + "ip": "172.17.0.13" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:51.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.15\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:54.000Z" + } + ], + "name": "dice-roller-6c8646bfd-dtv5z", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593190", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-dtv5z", + "uid": "1638a41b-e47e-4c17-a1f7-7b447df248b6" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:51.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://e62c32db58dbfe0a74b2d2cba0e0a97b7f222693c68e930037ac8738c4758ca3", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.15", + "podIPs": [ + { + "ip": "172.17.0.15" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:51.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.12\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:53.000Z" + } + ], + "name": "dice-roller-6c8646bfd-hhts2", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593186", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-hhts2", + "uid": "ea0b147a-c56a-46e6-9445-7a0b1b0ed3c0" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://bcdbc153630f90556d57ff0d62f04d847ca63a5f0a7e5d9230683623d33d4b8d", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.12", + "podIPs": [ + { + "ip": "172.17.0.12" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.14\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-j68lm", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593226", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-j68lm", + "uid": "b230ff1d-7a13-479b-9c7b-77c26bd79f62" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://30fa8a3429f86ab8e9a4cec472b79d74266609082703f48950f4aae021e9d6a2", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.14", + "podIPs": [ + { + "ip": "172.17.0.14" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.9\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:53.000Z" + } + ], + "name": "dice-roller-6c8646bfd-m6f9w", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593181", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-m6f9w", + "uid": "9b0079f6-ff29-4619-bf6e-71cc10199ac5" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:53.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://f78767ee90eedffe46ff64bfe76d7f69426800dd89f3df012daf0baf3a9c5bdd", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.9", + "podIPs": [ + { + "ip": "172.17.0.9" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-24T11:39:27.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-24T11:39:27.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.4\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-24T11:39:28.000Z" + } + ], + "name": "dice-roller-6c8646bfd-nv9pk", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "503918", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-nv9pk", + "uid": "acf7f77f-78c6-4d4b-bc73-637211770ffc" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:28.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:28.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-24T11:39:27.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://63dec9b32942c4b09ae43d09d6978261f674a3ee623823b8f1d20da8044c12ac", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-24T11:39:28.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.4", + "podIPs": [ + { + "ip": "172.17.0.4" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-24T11:39:27.000Z" + } + }, + { + "metadata": { + "creationTimestamp": "2020-09-25T09:58:50.000Z", + "generateName": "dice-roller-6c8646bfd-", + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:generateName": {}, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"5126c354-4310-4e23-a9e4-c9b87cb69792\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:enableServiceLinks": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:status": { + "f:conditions": { + "k:{\"type\":\"ContainersReady\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Initialized\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Ready\"}": { + ".": {}, + "f:lastProbeTime": {}, + "f:lastTransitionTime": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:containerStatuses": {}, + "f:hostIP": {}, + "f:phase": {}, + "f:podIP": {}, + "f:podIPs": { + ".": {}, + "k:{\"ip\":\"172.17.0.10\"}": { + ".": {}, + "f:ip": {} + } + }, + "f:startTime": {} + } + }, + "manager": "kubelet", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd-pjhfj", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + } + ], + "resourceVersion": "593205", + "selfLink": "/api/v1/namespaces/default/pods/dice-roller-6c8646bfd-pjhfj", + "uid": "78775c3a-7af2-4ebe-a676-bc2e5dfa2bcc" + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-5gctn", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "minikube", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-5gctn", + "secret": { + "defaultMode": 420, + "secretName": "default-token-5gctn" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2020-09-25T09:58:50.000Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://cbb83dda250db74285dcbe0b81bd0896acf402bac9710af090311f7360f824aa", + "image": "nginx:1.14.2", + "imageID": "docker-pullable://nginx@sha256:f7988fb6c02e0ce69257d9bd9cf37ae20a60f1df7563c3a2a6abe24160306b8d", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2020-09-25T09:58:53.000Z" + } + } + } + ], + "hostIP": "192.168.64.2", + "phase": "Running", + "podIP": "172.17.0.10", + "podIPs": [ + { + "ip": "172.17.0.10" + } + ], + "qosClass": "BestEffort", + "startTime": "2020-09-25T09:58:50.000Z" + } + } + ], + "replicaSets": [ + { + "metadata": { + "annotations": { + "deployment.kubernetes.io/desired-replicas": "10", + "deployment.kubernetes.io/max-replicas": "13", + "deployment.kubernetes.io/revision": "2" + }, + "creationTimestamp": "2020-09-24T11:39:26.000Z", + "generation": 3, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:deployment.kubernetes.io/desired-replicas": {}, + "f:deployment.kubernetes.io/max-replicas": {}, + "f:deployment.kubernetes.io/revision": {} + }, + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + }, + "f:ownerReferences": { + ".": {}, + "k:{\"uid\":\"7551e949-42d1-4061-83c5-9da107186e47\"}": { + ".": {}, + "f:apiVersion": {}, + "f:blockOwnerDeletion": {}, + "f:controller": {}, + "f:kind": {}, + "f:name": {}, + "f:uid": {} + } + } + }, + "f:spec": { + "f:replicas": {}, + "f:selector": { + "f:matchLabels": { + ".": {}, + "f:app": {}, + "f:pod-template-hash": {} + } + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {}, + "f:pod-template-hash": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:fullyLabeledReplicas": {}, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller-6c8646bfd", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "Deployment", + "name": "dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + } + ], + "resourceVersion": "593228", + "selfLink": "/apis/apps/v1/namespaces/default/replicasets/dice-roller-6c8646bfd", + "uid": "5126c354-4310-4e23-a9e4-c9b87cb69792" + }, + "spec": { + "replicas": 10, + "selector": { + "matchLabels": { + "app": "dice-roller", + "pod-template-hash": "6c8646bfd" + } + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller", + "pod-template-hash": "6c8646bfd" + } + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 10, + "fullyLabeledReplicas": 10, + "observedGeneration": 3, + "readyReplicas": 10, + "replicas": 10 + } + } + ], + "customResources": [ + { + "apiVersion": "argoproj.io/v1alpha1", + "kind": "Rollout", + "metadata": { + "annotations": { + "deployment.kubernetes.io/revision": "2", + "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"annotations\":{},\"labels\":{\"backstage.io/kubernetes-id\":\"dice-roller\"},\"name\":\"dice-roller\",\"namespace\":\"default\"},\"spec\":{\"replicas\":10,\"selector\":{\"matchLabels\":{\"app\":\"dice-roller\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"dice-roller\",\"backstage.io/kubernetes-id\":\"dice-roller\"}},\"spec\":{\"containers\":[{\"image\":\"nginx:1.14.2\",\"name\":\"nginx\",\"ports\":[{\"containerPort\":80}]}]}}}}\n" + }, + "creationTimestamp": "2020-09-23T12:00:55.000Z", + "generation": 3, + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "managedFields": [ + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + ".": {}, + "f:kubectl.kubernetes.io/last-applied-configuration": {} + }, + "f:labels": { + ".": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:progressDeadlineSeconds": {}, + "f:replicas": {}, + "f:revisionHistoryLimit": {}, + "f:selector": { + "f:matchLabels": { + ".": {}, + "f:app": {} + } + }, + "f:strategy": { + "f:rollingUpdate": { + ".": {}, + "f:maxSurge": {}, + "f:maxUnavailable": {} + }, + "f:type": {} + }, + "f:template": { + "f:metadata": { + "f:labels": { + ".": {}, + "f:app": {}, + "f:backstage.io/kubernetes-id": {} + } + }, + "f:spec": { + "f:containers": { + "k:{\"name\":\"nginx\"}": { + ".": {}, + "f:image": {}, + "f:imagePullPolicy": {}, + "f:name": {}, + "f:ports": { + ".": {}, + "k:{\"containerPort\":80,\"protocol\":\"TCP\"}": { + ".": {}, + "f:containerPort": {}, + "f:protocol": {} + } + }, + "f:resources": {}, + "f:terminationMessagePath": {}, + "f:terminationMessagePolicy": {} + } + }, + "f:dnsPolicy": {}, + "f:restartPolicy": {}, + "f:schedulerName": {}, + "f:securityContext": {}, + "f:terminationGracePeriodSeconds": {} + } + } + } + }, + "manager": "kubectl", + "operation": "Update", + "time": "2020-09-25T09:58:50.000Z" + }, + { + "apiVersion": "apps/v1", + "fieldsType": "FieldsV1", + "fieldsV1": { + "f:metadata": { + "f:annotations": { + "f:deployment.kubernetes.io/revision": {} + } + }, + "f:status": { + "f:availableReplicas": {}, + "f:conditions": { + ".": {}, + "k:{\"type\":\"Available\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + }, + "k:{\"type\":\"Progressing\"}": { + ".": {}, + "f:lastTransitionTime": {}, + "f:lastUpdateTime": {}, + "f:message": {}, + "f:reason": {}, + "f:status": {}, + "f:type": {} + } + }, + "f:observedGeneration": {}, + "f:readyReplicas": {}, + "f:replicas": {}, + "f:updatedReplicas": {} + } + }, + "manager": "kube-controller-manager", + "operation": "Update", + "time": "2020-09-25T09:58:55.000Z" + } + ], + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "593230", + "selfLink": "/apis/apps/v1/namespaces/default/deployments/dice-roller", + "uid": "7551e949-42d1-4061-83c5-9da107186e47" + }, + "spec": { + "progressDeadlineSeconds": 600, + "replicas": 10, + "revisionHistoryLimit": 10, + "selector": { + "matchLabels": { + "app": "dice-roller" + } + }, + "strategy": { + "rollingUpdate": { + "maxSurge": "25%", + "maxUnavailable": "25%" + }, + "type": "RollingUpdate" + }, + "template": { + "metadata": { + "creationTimestamp": null, + "labels": { + "app": "dice-roller", + "backstage.io/kubernetes-id": "dice-roller" + } + }, + "spec": { + "containers": [ + { + "image": "nginx:1.14.2", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": {}, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File" + } + ], + "dnsPolicy": "ClusterFirst", + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "terminationGracePeriodSeconds": 30 + } + } + }, + "status": { + "availableReplicas": 10, + "conditions": [ + { + "lastTransitionTime": "2020-09-23T12:00:55.000Z", + "lastUpdateTime": "2020-09-24T11:39:28.000Z", + "message": "ReplicaSet \"dice-roller-6c8646bfd\" has successfully progressed.", + "reason": "NewReplicaSetAvailable", + "status": "True", + "type": "Progressing" + }, + { + "lastTransitionTime": "2020-09-25T09:58:55.000Z", + "lastUpdateTime": "2020-09-25T09:58:55.000Z", + "message": "Deployment has minimum availability.", + "reason": "MinimumReplicasAvailable", + "status": "True", + "type": "Available" + } + ], + "observedGeneration": 3, + "readyReplicas": 10, + "replicas": 10, + "updatedReplicas": 10 + } + } + ], + "horizontalPodAutoscalers": [ + { + "apiVersion": "autoscaling/v2", + "kind": "HorizontalPodAutoscaler", + "metadata": { + "annotations": {}, + "creationTimestamp": "2024-02-13T20:13:52Z", + "labels": { + "backstage.io/kubernetes-id": "dice-roller" + }, + "name": "dice-roller", + "namespace": "default", + "resourceVersion": "6717s736", + "uid": "a34c90e1-8e8f-407f-b4c5-b4543bd56c1b" + }, + "spec": { + "maxReplicas": 2, + "metrics": [ + { + "resource": { + "name": "cpu", + "target": { + "averageUtilization": 50, + "type": "Utilization" + } + }, + "type": "Resource" + } + ], + "minReplicas": 1, + "scaleTargetRef": { + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": "dice-roller" + } + }, + "status": { + "conditions": [ + { + "lastTransitionTime": "2024-05-17T19:50:35Z", + "message": "recent recommendations were higher than current one, applying the highest recent recommendation", + "reason": "ScaleDownStabilized", + "status": "True", + "type": "AbleToScale" + }, + { + "lastTransitionTime": "2024-05-16T06:21:01Z", + "message": "the HPA was able to successfully calculate a replica count from cpu resource utilization (percentage of request)", + "reason": "ValidMetricFound", + "status": "True", + "type": "ScalingActive" + }, + { + "lastTransitionTime": "2024-05-16T06:21:01Z", + "message": "the desired replica count is more than the maximum replica count", + "reason": "TooManyReplicas", + "status": "True", + "type": "ScalingLimited" + } + ], + "currentMetrics": [ + { + "resource": { + "current": { + "averageUtilization": 100, + "averageValue": "50m", + "value": "100m" + }, + "name": "cpu" + }, + "type": "Resource" + } + ], + "currentReplicas": 2, + "desiredReplicas": 2, + "lastScaleTime": "2024-02-13T20:14:23Z" + } + } + ] +} diff --git a/plugins/kubernetes/src/alpha.ts b/plugins/kubernetes/src/alpha.ts new file mode 100644 index 0000000000..e80f131817 --- /dev/null +++ b/plugins/kubernetes/src/alpha.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 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. + */ + +export * from './alpha/index'; +export { default } from './alpha/index'; diff --git a/plugins/kubernetes/src/alpha/KubernetesContentPage.tsx b/plugins/kubernetes/src/alpha/KubernetesContentPage.tsx new file mode 100644 index 0000000000..948a492e63 --- /dev/null +++ b/plugins/kubernetes/src/alpha/KubernetesContentPage.tsx @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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 { useEntity } from '@backstage/plugin-catalog-react'; +import { KubernetesContent } from '../KubernetesContent'; +import React from 'react'; + +export function KubernetesContentPage() { + const { entity } = useEntity(); + return ; +} diff --git a/plugins/kubernetes/src/alpha/apis.tsx b/plugins/kubernetes/src/alpha/apis.tsx new file mode 100644 index 0000000000..6c880d2f53 --- /dev/null +++ b/plugins/kubernetes/src/alpha/apis.tsx @@ -0,0 +1,119 @@ +/* + * Copyright 2024 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 { + createApiExtension, + createApiFactory, + discoveryApiRef, + fetchApiRef, +} from '@backstage/frontend-plugin-api'; +import { + KubernetesBackendClient, + kubernetesApiRef, + kubernetesProxyApiRef, + kubernetesAuthProvidersApiRef, + KubernetesAuthProviders, + KubernetesProxyClient, + kubernetesClusterLinkFormatterApiRef, + getDefaultFormatters, + KubernetesClusterLinkFormatter, + DEFAULT_FORMATTER_NAME, +} from '@backstage/plugin-kubernetes-react'; +import { + gitlabAuthApiRef, + googleAuthApiRef, + microsoftAuthApiRef, + oktaAuthApiRef, + oneloginAuthApiRef, +} from '@backstage/core-plugin-api'; + +export const kubernetesApiExtension = createApiExtension({ + factory: createApiFactory({ + api: kubernetesApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + kubernetesAuthProvidersApi: kubernetesAuthProvidersApiRef, + }, + factory: ({ discoveryApi, fetchApi, kubernetesAuthProvidersApi }) => + new KubernetesBackendClient({ + discoveryApi, + fetchApi, + kubernetesAuthProvidersApi, + }), + }), +}); + +export const kubernetesProxyApi = createApiExtension({ + factory: createApiFactory({ + api: kubernetesProxyApiRef, + deps: { + kubernetesApi: kubernetesApiRef, + }, + factory: ({ kubernetesApi }) => + new KubernetesProxyClient({ + kubernetesApi, + }), + }), +}); + +export const kubernetesAuthProvidersApi = createApiExtension({ + factory: createApiFactory({ + api: kubernetesAuthProvidersApiRef, + deps: { + gitlabAuthApi: gitlabAuthApiRef, + googleAuthApi: googleAuthApiRef, + microsoftAuthApi: microsoftAuthApiRef, + oktaAuthApi: oktaAuthApiRef, + oneloginAuthApi: oneloginAuthApiRef, + }, + factory: ({ + gitlabAuthApi, + googleAuthApi, + microsoftAuthApi, + oktaAuthApi, + oneloginAuthApi, + }) => { + const oidcProviders = { + gitlab: gitlabAuthApi, + google: googleAuthApi, + microsoft: microsoftAuthApi, + okta: oktaAuthApi, + onelogin: oneloginAuthApi, + }; + + return new KubernetesAuthProviders({ + microsoftAuthApi, + googleAuthApi, + oidcProviders, + }); + }, + }), +}); + +export const kubernetesClusterLinkFormatterApi = createApiExtension({ + factory: createApiFactory({ + api: kubernetesClusterLinkFormatterApiRef, + deps: { googleAuthApi: googleAuthApiRef }, + factory: deps => { + const formatters = getDefaultFormatters(deps); + return new KubernetesClusterLinkFormatter({ + formatters, + defaultFormatterName: DEFAULT_FORMATTER_NAME, + }); + }, + }), +}); diff --git a/plugins/kubernetes/src/alpha/entityContents.tsx b/plugins/kubernetes/src/alpha/entityContents.tsx new file mode 100644 index 0000000000..9ad0b6843f --- /dev/null +++ b/plugins/kubernetes/src/alpha/entityContents.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2024 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 React from 'react'; +import { + compatWrapper, + convertLegacyRouteRef, +} from '@backstage/core-compat-api'; +import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; +import { rootCatalogKubernetesRouteRef } from '../plugin'; + +export const entityKubernetesContent = createEntityContentExtension({ + defaultPath: 'kubernetes', + defaultTitle: 'Kubernetes', + name: 'kubernetes', + routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), + loader: () => + import('./KubernetesContentPage').then(m => + compatWrapper(), + ), +}); diff --git a/packages/backend-common/src/middleware/index.ts b/plugins/kubernetes/src/alpha/index.ts similarity index 87% rename from packages/backend-common/src/middleware/index.ts rename to plugins/kubernetes/src/alpha/index.ts index f1754ef4d8..2f137f09ee 100644 --- a/packages/backend-common/src/middleware/index.ts +++ b/plugins/kubernetes/src/alpha/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2023 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. @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './statusCheckHandler'; +export { default } from './plugin'; diff --git a/plugins/kubernetes/src/alpha/pages.tsx b/plugins/kubernetes/src/alpha/pages.tsx new file mode 100644 index 0000000000..8b5db150bd --- /dev/null +++ b/plugins/kubernetes/src/alpha/pages.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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 React from 'react'; // Add this line to import React + +import { createPageExtension } from '@backstage/frontend-plugin-api'; +import { + compatWrapper, + convertLegacyRouteRef, +} from '@backstage/core-compat-api'; +import { rootCatalogKubernetesRouteRef } from '../plugin'; + +export const kubernetesPage = createPageExtension({ + defaultPath: '/kubernetes', + // you can reuse the existing routeRef + // by wrapping into the convertLegacyRouteRef. + routeRef: convertLegacyRouteRef(rootCatalogKubernetesRouteRef), + // these inputs usually match the props required by the component. + loader: () => import('../Router').then(m => compatWrapper()), +}); diff --git a/plugins/kubernetes/src/alpha/plugin.tsx b/plugins/kubernetes/src/alpha/plugin.tsx new file mode 100644 index 0000000000..47433bce35 --- /dev/null +++ b/plugins/kubernetes/src/alpha/plugin.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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 { convertLegacyRouteRefs } from '@backstage/core-compat-api'; +import { createPlugin } from '@backstage/frontend-plugin-api'; +import { kubernetesPage } from './pages'; +import { entityKubernetesContent } from './entityContents'; +import { rootCatalogKubernetesRouteRef } from '../plugin'; +import { + kubernetesApiExtension as kubernetesApi, + kubernetesAuthProvidersApi, + kubernetesClusterLinkFormatterApi, + kubernetesProxyApi, +} from './apis'; + +export default createPlugin({ + id: 'kubernetes', + extensions: [ + kubernetesPage, + entityKubernetesContent, + kubernetesApi, + kubernetesProxyApi, + kubernetesAuthProvidersApi, + kubernetesClusterLinkFormatterApi, + ], + routes: convertLegacyRouteRefs({ kubernetes: rootCatalogKubernetesRouteRef }), +}); diff --git a/plugins/notifications-backend-module-email/CHANGELOG.md b/plugins/notifications-backend-module-email/CHANGELOG.md index 30862ed64e..a0b4bc8caa 100644 --- a/plugins/notifications-backend-module-email/CHANGELOG.md +++ b/plugins/notifications-backend-module-email/CHANGELOG.md @@ -1,5 +1,142 @@ # @backstage/plugin-notifications-backend-module-email +## 0.2.0-next.2 + +### Patch Changes + +- cdb630d: Add support for stream transport for debugging purposes +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.2.0-next.1 + +### Minor Changes + +- def53a7: **BREAKING** Following `NotificationTemplateRenderer` methods now return a Promise and **must** be awaited: `getSubject`, `getText` and `getHtml`. + + Required changes and example usage: + + ```diff + import { notificationsEmailTemplateExtensionPoint } from '@backstage/plugin-notifications-backend-module-email'; + import { Notification } from '@backstage/plugin-notifications-common'; + +import { getNotificationSubject, getNotificationTextContent, getNotificationHtmlContent } from 'my-notification-processing-library` + export const notificationsModuleEmailDecorator = createBackendModule({ + pluginId: 'notifications', + moduleId: 'email.templates', + register(reg) { + reg.registerInit({ + deps: { + emailTemplates: notificationsEmailTemplateExtensionPoint, + }, + async init({ emailTemplates }) { + emailTemplates.setTemplateRenderer({ + - getSubject(notification) { + + async getSubject(notification) { + - return `New notification from ${notification.source}`; + + const subject = await getNotificationSubject(notification); + + return `New notification from ${subject}`; + }, + - getText(notification) { + + async getText(notification) { + - return notification.content; + + const text = await getNotificationTextContent(notification); + + return text; + }, + - getHtml(notification) { + + async getHtml(notification) { + - return `

    ${notification.content}

    `; + + const html = await getNotificationHtmlContent(notification); + + return html; + }, + }); + }, + }); + }, + }); + ``` + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration-aws-node@0.1.12 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + +## 0.1.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + +## 0.1.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + +## 0.1.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + +## 0.1.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/integration-aws-node@0.1.12 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + ## 0.1.0 ### Minor Changes diff --git a/plugins/notifications-backend-module-email/README.md b/plugins/notifications-backend-module-email/README.md index 7bbac05b62..1defad74b4 100644 --- a/plugins/notifications-backend-module-email/README.md +++ b/plugins/notifications-backend-module-email/README.md @@ -2,7 +2,7 @@ Adds support for sending Backstage notifications as emails to users. -Supports sending emails using `SMTP`, `SES`, or `sendmail`. +Supports sending emails using `SMTP`, `SES`, `sendmail`, or `stream` (for debugging purposes). ## Customizing email content diff --git a/plugins/notifications-backend-module-email/api-report.md b/plugins/notifications-backend-module-email/api-report.md index 7e8eac6cb7..b105f15cba 100644 --- a/plugins/notifications-backend-module-email/api-report.md +++ b/plugins/notifications-backend-module-email/api-report.md @@ -23,10 +23,10 @@ export default notificationsModuleEmail; // @public (undocumented) export interface NotificationTemplateRenderer { // (undocumented) - getHtml?(notification: Notification_2): string; + getHtml?(notification: Notification_2): Promise; // (undocumented) - getSubject?(notification: Notification_2): string; + getSubject?(notification: Notification_2): Promise; // (undocumented) - getText?(notification: Notification_2): string; + getText?(notification: Notification_2): Promise; } ``` diff --git a/plugins/notifications-backend-module-email/config.d.ts b/plugins/notifications-backend-module-email/config.d.ts index 92f3385d1b..04e6f7910f 100644 --- a/plugins/notifications-backend-module-email/config.d.ts +++ b/plugins/notifications-backend-module-email/config.d.ts @@ -79,6 +79,10 @@ export interface Config { * Newline style, defaults to 'unix' */ newline?: 'unix' | 'windows'; + } + | { + /** Only for debugging, disables the actual sending of emails */ + transport: 'stream'; }; /** * Sender email address @@ -132,6 +136,14 @@ export interface Config { */ excludedTopics?: string[]; }; + /** + * White list of addresses to send email to + */ + allowlistEmailAddresses?: string[]; + /** + * Black list of addresses to not send email to + */ + denylistEmailAddresses?: string[]; }; }; }; diff --git a/plugins/notifications-backend-module-email/package.json b/plugins/notifications-backend-module-email/package.json index 4f2c8a6876..33451341ea 100644 --- a/plugins/notifications-backend-module-email/package.json +++ b/plugins/notifications-backend-module-email/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend-module-email", - "version": "0.1.0", + "version": "0.2.0-next.2", "description": "The email backend module for the notifications plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/notifications-backend-module-email/src/extensions.ts b/plugins/notifications-backend-module-email/src/extensions.ts index 454fe9b384..49795eadcf 100644 --- a/plugins/notifications-backend-module-email/src/extensions.ts +++ b/plugins/notifications-backend-module-email/src/extensions.ts @@ -20,9 +20,9 @@ import { Notification } from '@backstage/plugin-notifications-common'; * @public */ export interface NotificationTemplateRenderer { - getSubject?(notification: Notification): string; - getText?(notification: Notification): string; - getHtml?(notification: Notification): string; + getSubject?(notification: Notification): Promise; + getText?(notification: Notification): Promise; + getHtml?(notification: Notification): Promise; } /** diff --git a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts index a78ebd3260..03388a77a2 100644 --- a/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts +++ b/plugins/notifications-backend-module-email/src/processor/NotificationsEmailProcessor.ts @@ -15,7 +15,6 @@ */ import { NotificationProcessor, - NotificationProcessorFilters, NotificationSendOptions, } from '@backstage/plugin-notifications-node'; import { @@ -30,14 +29,15 @@ import { CatalogClient, } from '@backstage/catalog-client'; import { + getProcessorFiltersFromConfig, Notification, - notificationSeverities, - NotificationSeverity, + NotificationProcessorFilters, } from '@backstage/plugin-notifications-common'; import { createSendmailTransport, createSesTransport, createSmtpTransport, + createStreamTransport, } from './transports'; import { UserEntity } from '@backstage/catalog-model'; import { compact } from 'lodash'; @@ -57,6 +57,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { private readonly throttleInterval: number; private readonly frontendBaseUrl: string; private readonly filter: NotificationProcessorFilters; + private readonly allowlistEmailAddresses?: string[]; + private readonly denylistEmailAddresses?: string[]; constructor( private readonly logger: LoggerService, @@ -86,30 +88,13 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ? durationToMilliseconds(readDurationFromConfig(cacheConfig)) : 3_600_000; this.frontendBaseUrl = config.getString('app.baseUrl'); - this.filter = {}; - const minSeverity = emailProcessorConfig.getOptionalString( - 'filter.minSeverity', - ) as NotificationSeverity; - if (minSeverity) { - if (notificationSeverities.includes(minSeverity)) { - this.filter.minSeverity = minSeverity; - } else { - throw new Error(`Invalid minSeverity: ${minSeverity}`); - } - } - const maxSeverity = emailProcessorConfig.getOptionalString( - 'filter.maxSeverity', - ) as NotificationSeverity; - if (maxSeverity) { - if (notificationSeverities.includes(maxSeverity)) { - this.filter.maxSeverity = maxSeverity; - } else { - throw new Error(`Invalid maxSeverity: ${maxSeverity}`); - } - } - this.filter.excludedTopics = emailProcessorConfig.getOptionalStringArray( - 'filter.excludedTopics', + this.allowlistEmailAddresses = emailProcessorConfig.getOptionalStringArray( + 'allowlistEmailAddresses', ); + this.denylistEmailAddresses = emailProcessorConfig.getOptionalStringArray( + 'denylistEmailAddresses', + ); + this.filter = getProcessorFiltersFromConfig(emailProcessorConfig); } private async getTransporter() { @@ -129,6 +114,8 @@ export class NotificationsEmailProcessor implements NotificationProcessor { ); } else if (transport === 'sendmail') { this.transporter = createSendmailTransport(this.transportConfig); + } else if (transport === 'stream') { + this.transporter = createStreamTransport(); } else { throw new Error(`Unsupported transport: ${transport}`); } @@ -221,14 +208,30 @@ export class NotificationsEmailProcessor implements NotificationProcessor { notification: Notification, options: NotificationSendOptions, ) { + let emails: string[]; if (options.recipients.type === 'broadcast' || notification.user === null) { - return await this.getBroadcastEmails(); + emails = await this.getBroadcastEmails(); + } else { + emails = await this.getUserEmail(notification.user); } - return await this.getUserEmail(notification.user); + + if (this.allowlistEmailAddresses) { + emails = emails.filter(email => + this.allowlistEmailAddresses?.includes(email), + ); + } + + if (this.denylistEmailAddresses) { + emails = emails.filter( + email => !this.denylistEmailAddresses?.includes(email), + ); + } + return emails; } private async sendMail(options: Mail.Options) { try { + this.logger.debug(`Sending notification email to ${options.to}`); await this.transporter.sendMail(options); } catch (e) { this.logger.error(`Failed to send email to ${options.to}: ${e}`); @@ -304,10 +307,10 @@ export class NotificationsEmailProcessor implements NotificationProcessor { const mailOptions = { from: this.sender, subject: - this.templateRenderer?.getSubject?.(notification) ?? + (await this.templateRenderer?.getSubject?.(notification)) ?? notification.payload.title, - html: this.templateRenderer?.getHtml?.(notification), - text: this.templateRenderer?.getText?.(notification), + html: await this.templateRenderer?.getHtml?.(notification), + text: await this.templateRenderer?.getText?.(notification), replyTo: this.replyTo, }; diff --git a/plugins/notifications-backend-module-email/src/processor/transports/index.ts b/plugins/notifications-backend-module-email/src/processor/transports/index.ts index ccea9f77cc..9a679f3723 100644 --- a/plugins/notifications-backend-module-email/src/processor/transports/index.ts +++ b/plugins/notifications-backend-module-email/src/processor/transports/index.ts @@ -16,3 +16,4 @@ export { createSmtpTransport } from './smtp'; export { createSesTransport } from './ses'; export { createSendmailTransport } from './sendmail'; +export { createStreamTransport } from './stream'; diff --git a/plugins/notifications-backend-module-email/src/processor/transports/stream.ts b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts new file mode 100644 index 0000000000..26ed401366 --- /dev/null +++ b/plugins/notifications-backend-module-email/src/processor/transports/stream.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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 { createTransport } from 'nodemailer'; + +export const createStreamTransport = () => { + return createTransport({ streamTransport: true }); +}; diff --git a/plugins/notifications-backend/CHANGELOG.md b/plugins/notifications-backend/CHANGELOG.md index 66885a07bd..61cba9e3a6 100644 --- a/plugins/notifications-backend/CHANGELOG.md +++ b/plugins/notifications-backend/CHANGELOG.md @@ -1,5 +1,110 @@ # @backstage/plugin-notifications-backend +## 0.3.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-events-node@0.3.9-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.3.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-events-node@0.3.9-next.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## 0.3.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-events-node@0.3.9-next.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + +## 0.3.3 + +### Patch Changes + +- d7b8ca5: Added an option to filter notifications by topic +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-events-node@0.3.8 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.3.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-events-node@0.3.8-next.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## 0.3.2-next.0 + +### Patch Changes + +- d7b8ca5: Added an option to filter notifications by topic +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-events-node@0.3.7-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-notifications-common@0.0.4 + ## 0.3.0 ### Minor Changes diff --git a/plugins/notifications-backend/README.md b/plugins/notifications-backend/README.md index 60c9a4e078..a6b6615d9f 100644 --- a/plugins/notifications-backend/README.md +++ b/plugins/notifications-backend/README.md @@ -4,13 +4,7 @@ Welcome to the notifications backend plugin! ## Getting started -Add the notifications to your backend: - -```ts -const backend = createBackend(); -// ... -backend.add(import('@backstage/plugin-notifications-backend')); -``` +To install, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. For users to be able to see notifications in real-time, you have to install also the signals plugin (`@backstage/plugin-signals-node`, `@backstage/plugin-signals-backend`, and @@ -18,58 +12,8 @@ the signals plugin (`@backstage/plugin-signals-node`, `@backstage/plugin-signals ## Extending Notifications -The notifications can be extended with `NotificationProcessor`. These processors allow to decorate notifications -before they are sent or/and send the notifications to external services. - -Start off by creating a notification processor: - -```ts -import { Notification } from '@backstage/plugin-notifications-common'; -import { NotificationProcessor } from '@backstage/plugin-notifications-node'; - -class MyNotificationProcessor implements NotificationProcessor { - async decorate(notification: Notification): Promise { - if (notification.origin === 'plugin-my-plugin') { - notification.payload.icon = 'my-icon'; - } - return notification; - } - - async send(notification: Notification): Promise { - nodemailer.sendEmail({ - from: 'backstage', - to: 'user', - subject: notification.payload.title, - text: notification.payload.description, - }); - } -} -``` - -Both of the processing functions are optional, and you can implement only one of them. - -Add the notification processor to the notification system by: - -```ts -import { notificationsProcessingExtensionPoint } from '@backstage/plugin-notifications-node'; -import { Notification } from '@backstage/plugin-notifications-common'; - -export const myPlugin = createBackendPlugin({ - pluginId: 'myPlugin', - register(env) { - env.registerInit({ - deps: { - notifications: notificationsProcessingExtensionPoint, - // ... - }, - async init({ notifications }) { - // ... - notifications.addProcessor(new MyNotificationProcessor()); - }, - }); - }, -}); -``` +When a notification is created, it's processing can be customized via `processors`. +Please refer [Backstage documentation](https://backstage.io/docs/notifications) for further details. ## Sending Notifications By Backend Plugins diff --git a/plugins/notifications-backend/package.json b/plugins/notifications-backend/package.json index 34fe472f42..9bc26f7a1f 100644 --- a/plugins/notifications-backend/package.json +++ b/plugins/notifications-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-backend", - "version": "0.3.0", + "version": "0.3.4-next.2", "backstage": { "role": "backend-plugin", "pluginId": "notifications", @@ -52,7 +52,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "knex": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0", "winston": "^3.2.1", "yn": "^4.0.0" diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts index 6204629821..c6df60c56e 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.test.ts @@ -271,6 +271,20 @@ describe.each(databases.eachSupportedId())( expect(notifications.length).toBe(1); expect(notifications.at(0)?.id).toEqual(id2); }); + + it('should filter notifications based on topic', async () => { + await storage.saveNotification(testNotification1); + await storage.saveNotification(testNotification2); + await storage.saveNotification(testNotification3); + + const notifications = await storage.getNotifications({ + user, + topic: 'efgh-topic', + }); + + expect(notifications.length).toBe(1); + expect(notifications.at(0)?.id).toEqual(id1); + }); }); describe('getNotifications filters on severity', () => { diff --git a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts index f4bd25d1ba..3d027128ec 100644 --- a/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts +++ b/plugins/notifications-backend/src/database/DatabaseNotificationsStore.ts @@ -204,6 +204,10 @@ export class DatabaseNotificationsStore implements NotificationsStore { query.whereNull('read'); } // or match both if undefined + if (options.topic) { + query.where('topic', '=', options.topic); + } + if (options.saved) { query.whereNotNull('saved'); } else if (options.saved === false) { diff --git a/plugins/notifications-backend/src/database/NotificationsStore.ts b/plugins/notifications-backend/src/database/NotificationsStore.ts index 901ea1695d..3c57d0ac4a 100644 --- a/plugins/notifications-backend/src/database/NotificationsStore.ts +++ b/plugins/notifications-backend/src/database/NotificationsStore.ts @@ -35,6 +35,7 @@ export type NotificationGetOptions = { limit?: number; search?: string; orderField?: EntityOrder[]; + topic?: string; read?: boolean; saved?: boolean; createdAfter?: Date; diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts new file mode 100644 index 0000000000..b06d6f203b --- /dev/null +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2024 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 { mockServices } from '@backstage/backend-test-utils'; +import { getUsersForEntityRef } from './getUsersForEntityRef'; +import { CatalogApi } from '@backstage/catalog-client'; +import { + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, +} from '@backstage/catalog-model'; + +describe('getUsersForEntityRef', () => { + const catalogApiMock = { + getEntitiesByRefs: jest.fn(), + getEntityByRef: jest.fn(), + }; + const authMock = mockServices.auth(); + + it('should return empty array if entityRef is null', async () => { + await expect( + getUsersForEntityRef(null, [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual([]); + }); + + it('should resolve users without calling catalog', async () => { + await expect( + getUsersForEntityRef(['user:foo', 'user:ignored'], ['user:ignored'], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:foo']); + expect(catalogApiMock.getEntitiesByRefs).not.toHaveBeenCalled(); + }); + + it('should resolve group entities to users', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'parent_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + { + type: RELATION_PARENT_OF, + targetRef: 'group:default/child_group', + }, + ], + }, + ], + }); + + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'child_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/bar', + }, + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/ignored', + }, + ], + }, + ], + }); + + await expect( + getUsersForEntityRef( + 'group:default/parent_group', + ['user:default/ignored'], + { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }, + ), + ).resolves.toEqual(['user:default/foo', 'user:default/bar']); + }); + + it('should resolve user owner of entity from entity ref', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'user:default/foo', + }, + ], + }, + ], + }); + + await expect( + getUsersForEntityRef('component:default/test_component', [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:default/foo']); + }); + + it('should resolve group owner of entity from entity ref', async () => { + catalogApiMock.getEntitiesByRefs.mockResolvedValueOnce({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test_component', + }, + relations: [ + { + type: RELATION_OWNED_BY, + targetRef: 'group:default/owner_group', + }, + ], + }, + ], + }); + + catalogApiMock.getEntityByRef.mockResolvedValueOnce({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'owner_group', + }, + relations: [ + { + type: RELATION_HAS_MEMBER, + targetRef: 'user:default/foo', + }, + ], + }); + + await expect( + getUsersForEntityRef('component:default/test_component', [], { + auth: authMock, + catalogClient: catalogApiMock as unknown as CatalogApi, + }), + ).resolves.toEqual(['user:default/foo']); + }); +}); diff --git a/plugins/notifications-backend/src/service/getUsersForEntityRef.ts b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts new file mode 100644 index 0000000000..fd564d6528 --- /dev/null +++ b/plugins/notifications-backend/src/service/getUsersForEntityRef.ts @@ -0,0 +1,161 @@ +/* + * Copyright 2024 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 { + Entity, + isGroupEntity, + isUserEntity, + parseEntityRef, + RELATION_HAS_MEMBER, + RELATION_OWNED_BY, + RELATION_PARENT_OF, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { AuthService } from '@backstage/backend-plugin-api'; +import { CatalogApi } from '@backstage/catalog-client'; + +const isUserEntityRef = (ref: string) => + parseEntityRef(ref).kind.toLocaleLowerCase() === 'user'; + +// Partitions array of entity references to two arrays; user entity refs and other entity refs +const partitionEntityRefs = (refs: string[]): string[][] => + refs.reduce( + ([userEntityRefs, otherEntityRefs]: string[][], ref: string) => { + return isUserEntityRef(ref) + ? [[...userEntityRefs, ref], otherEntityRefs] + : [userEntityRefs, [...otherEntityRefs, ref]]; + }, + [[], []], + ); + +export const getUsersForEntityRef = async ( + entityRef: string | string[] | null, + excludeEntityRefs: string | string[], + options: { + auth: AuthService; + catalogClient: CatalogApi; + }, +): Promise => { + const { auth, catalogClient } = options; + + if (entityRef === null) { + return []; + } + + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', + }); + + const excluded = Array.isArray(excludeEntityRefs) + ? excludeEntityRefs + : [excludeEntityRefs]; + + const refsArr = Array.isArray(entityRef) ? entityRef : [entityRef]; + const [userEntityRefs, otherEntityRefs] = partitionEntityRefs(refsArr); + const users: string[] = userEntityRefs.filter(ref => !excluded.includes(ref)); + const entityRefs = otherEntityRefs.filter(ref => !excluded.includes(ref)); + + const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; + let entities: Array = []; + if (entityRefs.length > 0) { + const fetchedEntities = await catalogClient.getEntitiesByRefs( + { + entityRefs, + fields, + }, + { token }, + ); + entities = fetchedEntities.items; + } + + const mapEntity = async (entity: Entity | undefined): Promise => { + if (!entity) { + return []; + } + + const currentEntityRef = stringifyEntityRef(entity); + if (excluded.includes(currentEntityRef)) { + return []; + } + + if (isUserEntity(entity)) { + return [currentEntityRef]; + } + + if (isGroupEntity(entity)) { + if (!entity.relations?.length) { + return []; + } + + const groupUsers = entity.relations + .filter( + relation => + relation.type === RELATION_HAS_MEMBER && + isUserEntityRef(relation.targetRef), + ) + .map(r => r.targetRef); + + const childGroupRefs = entity.relations + .filter(relation => relation.type === RELATION_PARENT_OF) + .map(r => r.targetRef); + + let childGroupUsers: string[][] = []; + if (childGroupRefs.length > 0) { + const childGroups = await catalogClient.getEntitiesByRefs( + { + entityRefs: childGroupRefs, + fields, + }, + { token }, + ); + childGroupUsers = await Promise.all(childGroups.items.map(mapEntity)); + } + + return [...groupUsers, ...childGroupUsers.flat(2)].filter( + ref => !excluded.includes(ref), + ); + } + + if (entity.relations?.length) { + const ownerRef = entity.relations.find( + relation => relation.type === RELATION_OWNED_BY, + )?.targetRef; + + if (!ownerRef) { + return []; + } + + if (isUserEntityRef(ownerRef)) { + if (excluded.includes(ownerRef)) { + return []; + } + return [ownerRef]; + } + + const owner = await catalogClient.getEntityByRef(ownerRef, { token }); + return mapEntity(owner); + } + + return []; + }; + + for (const entity of entities) { + const u = await mapEntity(entity); + users.push(...u); + } + + return [...new Set(users)]; +}; diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index bb5c9e06ac..abd7e2dc13 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -23,15 +23,6 @@ import { } from '../database'; import { v4 as uuid } from 'uuid'; import { CatalogApi, CatalogClient } from '@backstage/catalog-client'; -import { - Entity, - isGroupEntity, - isUserEntity, - RELATION_HAS_MEMBER, - RELATION_OWNED_BY, - RELATION_PARENT_OF, - stringifyEntityRef, -} from '@backstage/catalog-model'; import { NotificationProcessor, NotificationSendOptions, @@ -54,6 +45,7 @@ import { NotificationStatus, } from '@backstage/plugin-notifications-common'; import { parseEntityOrderFieldParams } from './parseEntityOrderFieldParams'; +import { getUsersForEntityRef } from './getUsersForEntityRef'; /** @internal */ export interface RouterOptions { @@ -94,91 +86,6 @@ export async function createRouter( return info.userEntityRef; }; - const getUsersForEntityRef = async ( - entityRef: string | string[] | null, - excludeEntityRefs: string | string[], - ): Promise => { - const { token } = await auth.getPluginRequestToken({ - onBehalfOf: await auth.getOwnServiceCredentials(), - targetPluginId: 'catalog', - }); - - if (entityRef === null) { - return []; - } - - const fields = ['kind', 'metadata.name', 'metadata.namespace', 'relations']; - - const refs = Array.isArray(entityRef) ? entityRef : [entityRef]; - const entities = await catalogClient.getEntitiesByRefs( - { - entityRefs: refs, - fields, - }, - { token }, - ); - - const excluded = Array.isArray(excludeEntityRefs) - ? excludeEntityRefs - : [excludeEntityRefs]; - - const mapEntity = async (entity: Entity | undefined): Promise => { - if (!entity) { - return []; - } - - const currentEntityRef = stringifyEntityRef(entity); - if (excluded.includes(currentEntityRef)) { - return []; - } - - if (isUserEntity(entity)) { - return [currentEntityRef]; - } else if (isGroupEntity(entity) && entity.relations) { - const users = entity.relations - .filter(relation => relation.type === RELATION_HAS_MEMBER) - .map(r => r.targetRef); - - const childGroupRefs = entity.relations - .filter(relation => relation.type === RELATION_PARENT_OF) - .map(r => r.targetRef); - - const childGroups = await catalogClient.getEntitiesByRefs( - { - entityRefs: childGroupRefs, - fields, - }, - { token }, - ); - const childGroupUsers = await Promise.all( - childGroups.items.map(mapEntity), - ); - return [...users, ...childGroupUsers.flat(2)]; - } else if (entity.relations) { - const ownerRef = entity.relations.find( - relation => relation.type === RELATION_OWNED_BY, - )?.targetRef; - - if (ownerRef) { - const owner = await catalogClient.getEntityByRef(ownerRef, { token }); - if (owner) { - return mapEntity(owner); - } - } - } - - return []; - }; - - const users: string[] = []; - for (const entity of entities.items) { - const u = await mapEntity(entity); - users.push(...u); - } - - return users; - }; - const filterProcessors = (payload: NotificationPayload) => { const result: NotificationProcessor[] = []; @@ -302,6 +209,11 @@ export async function createRouter( opts.read = false; // or keep undefined } + + if (req.query.topic) { + opts.topic = req.query.topic.toString(); + } + if (req.query.saved === 'true') { opts.saved = true; } else if (req.query.saved === 'false') { @@ -538,6 +450,7 @@ export async function createRouter( users = await getUsersForEntityRef( entityRef, recipients.excludeEntityRef ?? [], + { auth, catalogClient }, ); } catch (e) { logger.error(`Failed to resolve notification receivers: ${e}`); diff --git a/plugins/notifications-common/CHANGELOG.md b/plugins/notifications-common/CHANGELOG.md index 17231f0147..6e10272c36 100644 --- a/plugins/notifications-common/CHANGELOG.md +++ b/plugins/notifications-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-notifications-common +## 0.0.5 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/config@1.2.0 + ## 0.0.4 ### Patch Changes diff --git a/plugins/notifications-common/api-report.md b/plugins/notifications-common/api-report.md index 3ddce24393..0f59a85849 100644 --- a/plugins/notifications-common/api-report.md +++ b/plugins/notifications-common/api-report.md @@ -3,6 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { Config } from '@backstage/config'; + +// @public (undocumented) +export const getProcessorFiltersFromConfig: ( + config: Config, +) => NotificationProcessorFilters; + // @public (undocumented) export type NewNotificationSignal = { action: 'new_notification'; @@ -33,6 +40,13 @@ export type NotificationPayload = { icon?: string; }; +// @public (undocumented) +export type NotificationProcessorFilters = { + minSeverity?: NotificationSeverity; + maxSeverity?: NotificationSeverity; + excludedTopics?: string[]; +}; + // @public (undocumented) export type NotificationReadSignal = { action: 'notification_read' | 'notification_unread'; diff --git a/plugins/notifications-common/package.json b/plugins/notifications-common/package.json index 744cf52cba..139681e6c6 100644 --- a/plugins/notifications-common/package.json +++ b/plugins/notifications-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-common", - "version": "0.0.4", + "version": "0.0.5", "description": "Common functionalities for the notifications plugin", "backstage": { "role": "common-library", @@ -39,6 +39,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/config": "workspace:^", "@material-ui/icons": "^4.9.1" }, "devDependencies": { diff --git a/plugins/notifications-common/src/filters.ts b/plugins/notifications-common/src/filters.ts new file mode 100644 index 0000000000..725cfcf714 --- /dev/null +++ b/plugins/notifications-common/src/filters.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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 { Config } from '@backstage/config'; +import { NotificationProcessorFilters, NotificationSeverity } from './types'; +import { notificationSeverities } from './constants'; + +/** @public */ +export const getProcessorFiltersFromConfig = (config: Config) => { + const filter: NotificationProcessorFilters = {}; + const minSeverity = config.getOptionalString( + 'filter.minSeverity', + ) as NotificationSeverity; + if (minSeverity) { + if (notificationSeverities.includes(minSeverity)) { + filter.minSeverity = minSeverity; + } else { + throw new Error(`Invalid minSeverity: ${minSeverity}`); + } + } + const maxSeverity = config.getOptionalString( + 'filter.maxSeverity', + ) as NotificationSeverity; + if (maxSeverity) { + if (notificationSeverities.includes(maxSeverity)) { + filter.maxSeverity = maxSeverity; + } else { + throw new Error(`Invalid maxSeverity: ${maxSeverity}`); + } + } + filter.excludedTopics = config.getOptionalStringArray( + 'filter.excludedTopics', + ); + return filter; +}; diff --git a/plugins/notifications-common/src/index.ts b/plugins/notifications-common/src/index.ts index 7d74c75be8..9fc7bde2b8 100644 --- a/plugins/notifications-common/src/index.ts +++ b/plugins/notifications-common/src/index.ts @@ -22,3 +22,4 @@ export * from './types'; export * from './constants'; +export * from './filters'; diff --git a/plugins/notifications-common/src/types.ts b/plugins/notifications-common/src/types.ts index b9c9f63489..a4be3e4255 100644 --- a/plugins/notifications-common/src/types.ts +++ b/plugins/notifications-common/src/types.ts @@ -116,3 +116,12 @@ export type NotificationReadSignal = { /** @public */ export type NotificationSignal = NewNotificationSignal | NotificationReadSignal; + +/** + * @public + */ +export type NotificationProcessorFilters = { + minSeverity?: NotificationSeverity; + maxSeverity?: NotificationSeverity; + excludedTopics?: string[]; +}; diff --git a/plugins/notifications-node/CHANGELOG.md b/plugins/notifications-node/CHANGELOG.md index 5611c7c6c4..627cb53ea0 100644 --- a/plugins/notifications-node/CHANGELOG.md +++ b/plugins/notifications-node/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/plugin-notifications-node +## 0.2.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.2.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.9-next.1 + +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.9-next.0 + +## 0.2.3 + +### Patch Changes + +- 4e4ef2b: Move notification processor filter parsing to common package +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + +## 0.2.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-signals-node@0.1.8-next.1 + +## 0.2.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-notifications-common@0.0.4 + ## 0.2.0 ### Minor Changes diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 27e6422f8a..995b98bc21 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -8,7 +8,7 @@ import { DiscoveryService } from '@backstage/backend-plugin-api'; import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Notification as Notification_2 } from '@backstage/plugin-notifications-common'; import { NotificationPayload } from '@backstage/plugin-notifications-common'; -import { NotificationSeverity } from '@backstage/plugin-notifications-common'; +import { NotificationProcessorFilters as NotificationProcessorFilters_2 } from '@backstage/plugin-notifications-common'; import { ServiceRef } from '@backstage/backend-plugin-api'; // @public (undocumented) @@ -38,12 +38,8 @@ export interface NotificationProcessor { ): Promise; } -// @public (undocumented) -export type NotificationProcessorFilters = { - minSeverity?: NotificationSeverity; - maxSeverity?: NotificationSeverity; - excludedTopics?: string[]; -}; +// @public @deprecated (undocumented) +export type NotificationProcessorFilters = NotificationProcessorFilters_2; // @public (undocumented) export type NotificationRecipients = @@ -69,7 +65,11 @@ export interface NotificationService { } // @public (undocumented) -export const notificationService: ServiceRef; +export const notificationService: ServiceRef< + NotificationService, + 'plugin', + 'singleton' +>; // @public (undocumented) export type NotificationServiceOptions = { diff --git a/plugins/notifications-node/package.json b/plugins/notifications-node/package.json index 973182be4e..533ebe7ce2 100644 --- a/plugins/notifications-node/package.json +++ b/plugins/notifications-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications-node", - "version": "0.2.0", + "version": "0.2.4-next.2", "description": "Node.js library for the notifications plugin", "backstage": { "role": "node-library", @@ -44,7 +44,7 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-signals-node": "workspace:^", "knex": "^3.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/plugins/notifications-node/src/extensions.ts b/plugins/notifications-node/src/extensions.ts index 7adb6caf50..03ce47fea6 100644 --- a/plugins/notifications-node/src/extensions.ts +++ b/plugins/notifications-node/src/extensions.ts @@ -16,7 +16,7 @@ import { createExtensionPoint } from '@backstage/backend-plugin-api'; import { Notification, - NotificationSeverity, + NotificationProcessorFilters as NotificationProcessorFiltersCommon, } from '@backstage/plugin-notifications-common'; import { NotificationSendOptions } from './service'; @@ -119,9 +119,6 @@ export const notificationsProcessingExtensionPoint = /** * @public + * @deprecated Please import from `@backstage/plugin-notifications-common` instead */ -export type NotificationProcessorFilters = { - minSeverity?: NotificationSeverity; - maxSeverity?: NotificationSeverity; - excludedTopics?: string[]; -}; +export type NotificationProcessorFilters = NotificationProcessorFiltersCommon; diff --git a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts index d5c327e788..3872f898ee 100644 --- a/plugins/notifications-node/src/service/DefaultNotificationService.test.ts +++ b/plugins/notifications-node/src/service/DefaultNotificationService.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { NotificationPayload } from '@backstage/plugin-notifications-common'; @@ -32,7 +32,7 @@ const testNotification: NotificationPayload = { describe('DefaultNotificationService', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const discovery = mockServices.discovery.mock({ getBaseUrl: jest.fn().mockResolvedValue('http://example.com'), diff --git a/plugins/notifications/CHANGELOG.md b/plugins/notifications/CHANGELOG.md index 3b3def49cc..42c55615b2 100644 --- a/plugins/notifications/CHANGELOG.md +++ b/plugins/notifications/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-notifications +## 0.2.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-signals-react@0.0.4 + +## 0.2.3 + +### Patch Changes + +- 3bf0697: The toolbar on the Notifications page is hidden when there are no listed notifications. +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + +## 0.2.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + +## 0.2.3-next.1 + +### Patch Changes + +- 3bf0697: The toolbar on the Notifications page is hidden when there are no listed notifications. +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-signals-react@0.0.4 + +## 0.2.3-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-signals-react@0.0.4 + ## 0.2.2 ### Patch Changes diff --git a/plugins/notifications/README.md b/plugins/notifications/README.md index 63212d2c16..78bcf4e753 100644 --- a/plugins/notifications/README.md +++ b/plugins/notifications/README.md @@ -2,38 +2,11 @@ Welcome to the notifications plugin! -_This plugin was created through the Backstage CLI_ - ## Getting started -First, install the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages. -See the documentation for installation instructions. +To install, please refer the [Getting Started](https://backstage.io/docs/notifications) Backstage Notifications and Signals documentation section. -To add the notifications main menu, add the following to your `packages/app/src/components/Root/Root.tsx`: - -```tsx -import { NotificationsSidebarItem } from '@backstage/plugin-notifications'; - - - - - // ... - - - -; -``` - -Also add the route to notifications to `packages/app/src/App.tsx`: - -```tsx -import { NotificationsPage } from '@backstage/plugin-notifications'; - - - // ... - } /> -; -``` +Please mind installing the `@backstage/plugin-notifications-backend` and `@backstage/plugin-notifications-node` packages before this frontend plugin. ## Real-time notifications diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 9240bdb1c2..5e06ae2616 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -176,20 +176,5 @@ export function useNotificationsApi( value: T; }; -// @public (undocumented) -export function useTitleCounter(): { - setNotificationCount: (newCount: number) => void; -}; - -// @public (undocumented) -export function useWebNotifications(enabled: boolean): { - sendWebNotification: (options: { - id: string; - title: string; - description: string; - link?: string; - }) => Notification | null; -}; - // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index f375ae702e..5dbfd83255 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-notifications", - "version": "0.2.2", + "version": "0.2.4-next.0", "backstage": { "role": "frontend-plugin", "pluginId": "notifications", diff --git a/plugins/notifications/src/api/NotificationsClient.test.ts b/plugins/notifications/src/api/NotificationsClient.test.ts index e9acd04fc9..f378269e45 100644 --- a/plugins/notifications/src/api/NotificationsClient.test.ts +++ b/plugins/notifications/src/api/NotificationsClient.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { NotificationsClient } from './NotificationsClient'; @@ -33,7 +33,7 @@ const testNotification: Partial = { }; describe('NotificationsClient', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage/api/notifications'; const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; const fetchApi = new MockFetchApi(); diff --git a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx index 7b55c63747..457893b016 100644 --- a/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx +++ b/plugins/notifications/src/components/NotificationsSideBarItem/NotificationsSideBarItem.tsx @@ -114,7 +114,9 @@ export const NotificationsSidebarItem = (props?: { const notificationsRoute = useRouteRef(rootRouteRef); // TODO: Do we want to add long polling in case signals are not available const { lastSignal } = useSignal('notifications'); - const { sendWebNotification } = useWebNotifications(webNotificationsEnabled); + const { sendWebNotification, requestUserPermission } = useWebNotifications( + webNotificationsEnabled, + ); const [refresh, setRefresh] = React.useState(false); const { setNotificationCount } = useTitleCounter(); @@ -126,6 +128,19 @@ export const NotificationsSidebarItem = (props?: { component={Link} to={notification.payload.link ?? notificationsRoute()} onClick={() => { + if (notification.payload.link) { + notificationsApi + .updateNotifications({ + ids: [notification.id], + read: true, + }) + .catch(() => { + alertApi.post({ + message: 'Failed to mark notification as read', + severity: 'error', + }); + }); + } closeSnackbar(snackBarId); }} > @@ -259,6 +274,9 @@ export const NotificationsSidebarItem = (props?: { )} { + requestUserPermission(); + }} hasNotifications={!error && !!unreadCount} text={text} icon={icon} diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx index 72a27975f6..d7c4c40781 100644 --- a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx +++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx @@ -173,12 +173,14 @@ export const NotificationsTable = ({ } }, [notifications, selectedNotifications]); - const compactColumns = React.useMemo( - (): TableColumn[] => [ + const compactColumns = React.useMemo((): TableColumn[] => { + const showToolbar = notifications.length > 0; + + return [ { /* selection column */ width: '1rem', - title: ( + title: showToolbar ? ( - ), + ) : undefined, render: (notification: Notification) => ( - ), + ) : undefined, render: (notification: Notification) => ( ), }, - ], - [ - markAsReadOnLinkOpen, - selectedNotifications, - notifications, - isUnread, - onSwitchReadStatus, - onSwitchSavedStatus, - onMarkAllRead, - onNotificationsSelectChange, - classes.severityItem, - classes.description, - ], - ); + ]; + }, [ + markAsReadOnLinkOpen, + selectedNotifications, + notifications, + isUnread, + onSwitchReadStatus, + onSwitchSavedStatus, + onMarkAllRead, + onNotificationsSelectChange, + classes.severityItem, + classes.description, + ]); return ( diff --git a/plugins/notifications/src/hooks/useTitleCounter.ts b/plugins/notifications/src/hooks/useTitleCounter.ts index 0793cd58d6..399bfb84f4 100644 --- a/plugins/notifications/src/hooks/useTitleCounter.ts +++ b/plugins/notifications/src/hooks/useTitleCounter.ts @@ -25,7 +25,7 @@ const throttledSetTitle = throttle((shownTitle: string) => { document.title = shownTitle; }, 100); -/** @public */ +/** @internal */ export function useTitleCounter() { const [title, setTitle] = useState(document.title); const [count, setCount] = useState(0); diff --git a/plugins/notifications/src/hooks/useWebNotifications.ts b/plugins/notifications/src/hooks/useWebNotifications.ts index 728d992130..ec120a614d 100644 --- a/plugins/notifications/src/hooks/useWebNotifications.ts +++ b/plugins/notifications/src/hooks/useWebNotifications.ts @@ -13,19 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useState } from 'react'; import { rootRouteRef } from '../routes'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { useApi, useRouteRef } from '@backstage/core-plugin-api'; import { useNavigate } from 'react-router-dom'; +import { notificationsApiRef } from '../api'; -/** @public */ +/** @internal */ export function useWebNotifications(enabled: boolean) { const [webNotificationPermission, setWebNotificationPermission] = useState('default'); const notificationsRoute = useRouteRef(rootRouteRef); + const notificationsApi = useApi(notificationsApiRef); const navigate = useNavigate(); - useEffect(() => { + const requestUserPermission = useCallback(() => { if ( enabled && 'Notification' in window && @@ -57,6 +59,10 @@ export function useWebNotifications(enabled: boolean) { event.preventDefault(); if (options.link) { window.open(options.link, '_blank'); + notificationsApi.updateNotifications({ + ids: [options.id], + read: true, + }); } else { navigate(notificationsRoute()); } @@ -65,8 +71,8 @@ export function useWebNotifications(enabled: boolean) { return notification; }, - [webNotificationPermission, navigate, notificationsRoute], + [webNotificationPermission, notificationsApi, navigate, notificationsRoute], ); - return { sendWebNotification }; + return { sendWebNotification, requestUserPermission }; } diff --git a/plugins/notifications/src/index.ts b/plugins/notifications/src/index.ts index bb5d76a0cf..25565f1ae1 100644 --- a/plugins/notifications/src/index.ts +++ b/plugins/notifications/src/index.ts @@ -15,5 +15,5 @@ */ export { notificationsPlugin, NotificationsPage } from './plugin'; export * from './api'; -export * from './hooks'; +export { useNotificationsApi } from './hooks'; export * from './components'; diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 9d0ffacd97..4bbf343849 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,79 @@ # @backstage/plugin-org-react +## 0.1.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.27-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.27-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## 0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + +## 0.1.26-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + ## 0.1.25 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index 32d46ef1a3..d91d73c4ca 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.25", + "version": "0.1.27-next.2", "backstage": { "role": "web-library", "pluginId": "org", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index e14b1afcec..923d5d1d23 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,103 @@ # @backstage/plugin-org +## 0.6.28-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-catalog-common@1.0.26-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.6.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/plugin-catalog-common@1.0.26-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.6.28-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/plugin-catalog-common@1.0.25 + +## 0.6.27 + +### Patch Changes + +- 5132d28: The `useGetEntities` hook could result in requests to `/api/catalog/entities` where the headers exceed the default maximum Node.js header size of 16KB. The hook logic has been adjusted to batch the requests. +- c307ef4: Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`. + + Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property. + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-catalog-common@1.0.25 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/catalog-model@1.5.0 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + +## 0.6.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.6.27-next.1 + +### Patch Changes + +- 5132d28: The `useGetEntities` hook could result in requests to `/api/catalog/entities` where the headers exceed the default maximum Node.js header size of 16KB. The hook logic has been adjusted to batch the requests. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/plugin-catalog-common@1.0.24 + +## 0.6.27-next.0 + +### Patch Changes + +- c307ef4: Added `relationType` property to EntityMembersListCard component that allows for display users related to a group via some other relationship aside from `memberOf`. + + Also, as a side effect, the `relationsType` property has been deprecated in favor of a more accurately named `relationAggregation` property. + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-model@1.5.0 + - @backstage/plugin-catalog-common@1.0.24 + ## 0.6.26 ### Patch Changes diff --git a/plugins/org/api-report-alpha.md b/plugins/org/api-report-alpha.md index 8c5391d857..be74cde141 100644 --- a/plugins/org/api-report-alpha.md +++ b/plugins/org/api-report-alpha.md @@ -10,8 +10,9 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; const _default: BackstagePlugin< {}, { - catalogIndex: ExternalRouteRef; - } + catalogIndex: ExternalRouteRef; + }, + {} >; export default _default; diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index a6926d71f2..e958e8e29a 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -23,7 +23,9 @@ export const EntityMembersListCard: (props: { memberDisplayTitle?: string | undefined; pageSize?: number | undefined; showAggregateMembersToggle?: boolean | undefined; + relationType?: string | undefined; relationsType?: EntityRelationAggregation | undefined; + relationAggregation?: EntityRelationAggregation | undefined; }) => JSX_2.Element; // @public (undocumented) @@ -32,6 +34,7 @@ export const EntityOwnershipCard: (props: { entityFilterKind?: string[] | undefined; hideRelationsToggle?: boolean | undefined; relationsType?: EntityRelationAggregation | undefined; + relationAggregation?: EntityRelationAggregation | undefined; entityLimit?: number | undefined; }) => JSX_2.Element; @@ -55,7 +58,9 @@ export const MembersListCard: (props: { memberDisplayTitle?: string; pageSize?: number; showAggregateMembersToggle?: boolean; + relationType?: string; relationsType?: EntityRelationAggregation; + relationAggregation?: EntityRelationAggregation; }) => React_2.JSX.Element; // @public @@ -82,6 +87,7 @@ export const OwnershipCard: (props: { entityFilterKind?: string[]; hideRelationsToggle?: boolean; relationsType?: EntityRelationAggregation; + relationAggregation?: EntityRelationAggregation; entityLimit?: number; }) => React_2.JSX.Element; diff --git a/plugins/org/package.json b/plugins/org/package.json index af3c54f1a2..44ae3d10f6 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.6.26", + "version": "0.6.28-next.2", "description": "A Backstage plugin that helps you create entity pages for your organization", "backstage": { "role": "frontend-plugin", diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 793ffe274b..69d6090687 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -99,6 +99,7 @@ describe('MemberTab Test', () => { ] as Entity[], }), }; + const getEntitiesSpy = jest.spyOn(catalogApi, 'getEntities'); it('Display Profile Card', async () => { await renderInTestApp( @@ -115,6 +116,12 @@ describe('MemberTab Test', () => { }, }, ); + expect(getEntitiesSpy).toHaveBeenCalledWith({ + filter: { + kind: 'User', + 'relations.memberof': ['group:default/team-d'], + }, + }); expect(screen.getByAltText('Tara MacGovern')).toHaveAttribute( 'src', @@ -149,6 +156,29 @@ describe('MemberTab Test', () => { expect(screen.getByText('Testers (1)')).toBeInTheDocument(); }); + it('Can query a different relationship', async () => { + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/catalog': rootRouteRef, + }, + }, + ); + + expect(getEntitiesSpy).toHaveBeenCalledWith({ + filter: { + kind: 'User', + 'relations.leaderof': ['group:default/team-d'], + }, + }); + }); + describe('Aggregate members toggle', () => { it('Does not show the aggregate members toggle if the showAggregateMembersToggle prop is undefined', async () => { await renderInTestApp( @@ -346,7 +376,7 @@ describe('MemberTab Test', () => { @@ -384,7 +414,7 @@ describe('MemberTab Test', () => { - + diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 418a83f5b2..f27b071114 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -138,14 +138,19 @@ export const MembersListCard = (props: { memberDisplayTitle?: string; pageSize?: number; showAggregateMembersToggle?: boolean; + relationType?: string; + /** @deprecated Please use `relationAggregation` instead */ relationsType?: EntityRelationAggregation; + relationAggregation?: EntityRelationAggregation; }) => { const { memberDisplayTitle = 'Members', pageSize = 50, showAggregateMembersToggle, - relationsType = 'direct', + relationType = 'memberof', } = props; + const relationAggregation = + props.relationAggregation ?? props.relationsType ?? 'direct'; const classes = useListStyles(); const { entity: groupEntity } = useEntity(); @@ -165,7 +170,7 @@ export const MembersListCard = (props: { }; const [showAggregateMembers, setShowAggregateMembers] = useState( - relationsType === 'aggregated', + relationAggregation === 'aggregated', ); const { loading: loadingDescendantMembers, value: descendantMembers } = @@ -177,6 +182,7 @@ export const MembersListCard = (props: { return await getAllDesendantMembersForGroupEntity( groupEntity, catalogApi, + relationType, ); }, [catalogApi, groupEntity, showAggregateMembers]); const { @@ -187,7 +193,7 @@ export const MembersListCard = (props: { const membersList = await catalogApi.getEntities({ filter: { kind: 'User', - 'relations.memberof': [ + [`relations.${relationType.toLocaleLowerCase('en-US')}`]: [ stringifyEntityRef({ kind: 'group', namespace: groupNamespace.toLocaleLowerCase('en-US'), diff --git a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx index 69ad5bf8bb..a7decfb592 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx @@ -114,19 +114,27 @@ export const ComponentsGrid = ({ className, entity, relationsType, + relationAggregation, entityFilterKind, entityLimit = 6, }: { className?: string; entity: Entity; - relationsType: EntityRelationAggregation; + /** @deprecated Please use relationAggregation instead */ + relationsType?: EntityRelationAggregation; + relationAggregation?: EntityRelationAggregation; entityFilterKind?: string[]; entityLimit?: number; }) => { const catalogLink = useRouteRef(catalogIndexRouteRef); + if (!relationsType && !relationAggregation) { + throw new Error( + 'The relationAggregation property must be set as an EntityRelationAggregation type.', + ); + } const { componentsWithCounters, loading, error } = useGetEntities( entity, - relationsType, + (relationAggregation ?? relationsType)!, // we can safely use the non-null assertion here because of the run-time check above entityFilterKind, entityLimit, ); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index f2b949f768..004fb8b964 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -288,7 +288,7 @@ describe('OwnershipCard', () => { const { getByText } = await renderInTestApp( - + , { diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index a5b0b7bb19..fce47845bc 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -67,31 +67,34 @@ export const OwnershipCard = (props: { variant?: InfoCardVariants; entityFilterKind?: string[]; hideRelationsToggle?: boolean; + /** @deprecated Please use relationAggregation instead */ relationsType?: EntityRelationAggregation; + relationAggregation?: EntityRelationAggregation; entityLimit?: number; }) => { const { variant, entityFilterKind, hideRelationsToggle, - relationsType, entityLimit = 6, } = props; + const relationAggregation = props.relationAggregation ?? props.relationsType; const relationsToggle = hideRelationsToggle === undefined ? false : hideRelationsToggle; const classes = useStyles(); const { entity } = useEntity(); - const defaultRelationsType = entity.kind === 'User' ? 'aggregated' : 'direct'; - const [getRelationsType, setRelationsType] = useState( - relationsType ?? defaultRelationsType, + const defaultRelationAggregation = + entity.kind === 'User' ? 'aggregated' : 'direct'; + const [getRelationAggregation, setRelationAggregation] = useState( + relationAggregation ?? defaultRelationAggregation, ); useEffect(() => { - if (!relationsType) { - setRelationsType(defaultRelationsType); + if (!relationAggregation) { + setRelationAggregation(defaultRelationAggregation); } - }, [setRelationsType, defaultRelationsType, relationsType]); + }, [setRelationAggregation, defaultRelationAggregation, relationAggregation]); return ( { - const updatedRelationsType = - getRelationsType === 'direct' ? 'aggregated' : 'direct'; - setRelationsType(updatedRelationsType); + const updatedRelationAggregation = + getRelationAggregation === 'direct' + ? 'aggregated' + : 'direct'; + setRelationAggregation(updatedRelationAggregation); }} name="pin" inputProps={{ 'aria-label': 'Ownership Type Switch' }} @@ -136,7 +141,7 @@ export const OwnershipCard = (props: { className={classes.grid} entity={entity} entityLimit={entityLimit} - relationsType={getRelationsType} + relationAggregation={getRelationAggregation} entityFilterKind={entityFilterKind} /> diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts index eac5a171ba..65a8dec226 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.test.ts @@ -64,7 +64,7 @@ describe('useGetEntities', () => { ]), }); - describe('given aggregated relationsType', () => { + describe('given aggregated relationAggregation', () => { const whenHookIsCalledWith = async (_entity: Entity) => { const { result } = renderHook( ({ entity }) => useGetEntities(entity, 'aggregated'), @@ -205,7 +205,7 @@ describe('useGetEntities', () => { }); }); - describe('given direct relationsType', () => { + describe('given direct relationAggregation', () => { const whenHookIsCalledWith = async (_entity: Entity) => { const { result } = renderHook( ({ entity }) => useGetEntities(entity, 'direct'), @@ -231,6 +231,114 @@ describe('useGetEntities', () => { ); }); }); + + describe('useGetEntities with 500+ relations', () => { + const manyGroups = Array.from({ length: 555 }, (_, i) => + createGroupEntityFromName(`group${i + 1}`), + ); + + const givenUserWithManyGroups = { + kind: 'User', + metadata: { + name: givenUser, + }, + spec: { + memberOf: manyGroups.map( + group => `group:default/${group.metadata.name}`, + ), + }, + } as Partial as Entity; + + beforeEach(() => { + getEntityRelationsMock.mockImplementation(entity => + entity?.kind === 'User' + ? manyGroups.map(group => createGroupRefFromName(group.metadata.name)) + : [], + ); + (catalogApiMock.getEntities as jest.Mock).mockClear(); + }); + + it('should handle 500+ relations without exceeding URL length limits', async () => { + const { result } = renderHook( + ({ entity }) => useGetEntities(entity, 'aggregated'), + { + initialProps: { entity: givenUserWithManyGroups }, + }, + ); + + await waitFor(() => expect(result.current.loading).toBe(false), { + timeout: 5000, + }); + + const callArgs = (catalogApiMock.getEntities as jest.Mock).mock + .calls[0][0]; + + expect( + callArgs.filter[0]['relations.ownedBy'].length, + ).toBeLessThanOrEqual(100); + + const owners = callArgs.filter[0]['relations.ownedBy']; + + expect(Array.isArray(owners)).toBeTruthy(); + expect(owners.length).toBeLessThanOrEqual(100); + }); + }); + + describe('useGetEntities exceeding default 16384 bytes header size', () => { + const largeNumberOfGroups = Array.from({ length: 600 }, (_, i) => + createGroupEntityFromName(`very-long-group-name-${i + 1}`), + ); + + const givenUserWithLargeNumberOfGroups = { + kind: 'User', + metadata: { + name: givenUser, + }, + spec: { + memberOf: largeNumberOfGroups.map( + group => `group:default/${group.metadata.name}`, + ), + }, + } as Partial as Entity; + + beforeEach(() => { + getEntityRelationsMock.mockImplementation(entity => + entity?.kind === 'User' + ? largeNumberOfGroups.map(group => + createGroupRefFromName(group.metadata.name), + ) + : [], + ); + (catalogApiMock.getEntities as jest.Mock).mockClear(); + }); + + it('should batch the request to avoid exceeding header size limits', async () => { + const { result } = renderHook( + ({ entity }) => useGetEntities(entity, 'aggregated'), + { + initialProps: { entity: givenUserWithLargeNumberOfGroups }, + }, + ); + + await waitFor(() => expect(result.current.loading).toBe(false), { + timeout: 5000, + }); + const callArgs = (catalogApiMock.getEntities as jest.Mock).mock + .calls[0][0]; + + const url = new URL( + `http://localhost/api/catalog/entities?${new URLSearchParams( + callArgs as any, + )}`, + ); + const headerSize = url.href.length; + expect(headerSize).toBeLessThanOrEqual(16384); + + const owners = callArgs.filter[0]['relations.ownedBy']; + expect(Array.isArray(owners)).toBeTruthy(); + expect(owners.length).toBeLessThanOrEqual(100); + }); + }); }); function createGroupEntityFromName(name: string): Entity { diff --git a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index 5a708bcfee..a5a5363192 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -32,7 +32,7 @@ import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/esm/useAsync'; import qs from 'qs'; import { EntityRelationAggregation } from '../types'; -import { uniq } from 'lodash'; +import { uniq, uniqBy } from 'lodash'; const limiter = limiterFactory(5); @@ -125,11 +125,11 @@ const getChildOwnershipEntityRefs = async ( const getOwners = async ( entity: Entity, - relations: EntityRelationAggregation, + relationAggregation: EntityRelationAggregation, catalogApi: CatalogApi, ): Promise => { const isGroup = entity.kind === 'Group'; - const isAggregated = relations === 'aggregated'; + const isAggregated = relationAggregation === 'aggregated'; const isUserEntity = entity.kind === 'User'; if (isAggregated && isGroup) { @@ -143,30 +143,46 @@ const getOwners = async ( return [stringifyEntityRef(entity)]; }; -const getOwnedEntitiesByOwners = ( +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const batchGetOwnedEntitiesByOwners = async ( owners: string[], kinds: string[], catalogApi: CatalogApi, -) => - catalogApi.getEntities({ - filter: [ - { - kind: kinds, - 'relations.ownedBy': owners, - }, - ], - fields: [ - 'kind', - 'metadata.name', - 'metadata.namespace', - 'spec.type', - 'relations', - ], - }); + batchSize: number = 100, + delayMs: number = 100, +) => { + const results = []; + + for (let i = 0; i < owners.length; i += batchSize) { + const batch = owners.slice(i, i + batchSize); + const response = await catalogApi.getEntities({ + filter: [ + { + kind: kinds, + 'relations.ownedBy': batch, + }, + ], + fields: [ + 'kind', + 'metadata.name', + 'metadata.namespace', + 'spec.type', + 'relations', + ], + }); + + results.push(...response.items); + + if (i + batchSize < owners.length) await delay(delayMs); + } + + return uniqBy(results, stringifyEntityRef); +}; export function useGetEntities( entity: Entity, - relations: EntityRelationAggregation, + relationAggregation: EntityRelationAggregation, entityFilterKind?: string[], entityLimit = 6, ): { @@ -189,15 +205,15 @@ export function useGetEntities( error, value: componentsWithCounters, } = useAsync(async () => { - const owners = await getOwners(entity, relations, catalogApi); + const owners = await getOwners(entity, relationAggregation, catalogApi); - const ownedEntitiesList = await getOwnedEntitiesByOwners( + const ownedEntitiesList = await batchGetOwnedEntitiesByOwners( owners, kinds, catalogApi, ); - const counts = ownedEntitiesList.items.reduce( + const counts = ownedEntitiesList.reduce( (acc: EntityTypeProps[], ownedEntity) => { const match = acc.find( x => x.kind === ownedEntity.kind && x.type === ownedEntity.spec?.type, @@ -230,7 +246,7 @@ export function useGetEntities( kind: string; queryParams: string; }>; - }, [catalogApi, entity, relations]); + }, [catalogApi, entity, relationAggregation]); return { componentsWithCounters, diff --git a/plugins/org/src/helpers/helpers.ts b/plugins/org/src/helpers/helpers.ts index 7da4ee32d8..465f89afa1 100644 --- a/plugins/org/src/helpers/helpers.ts +++ b/plugins/org/src/helpers/helpers.ts @@ -31,6 +31,7 @@ import { export const getMembersFromGroups = async ( groups: CompoundEntityRef[], catalogApi: CatalogApi, + relationship = 'memberof', ) => { const membersList = groups.length === 0 @@ -38,13 +39,14 @@ export const getMembersFromGroups = async ( : await catalogApi.getEntities({ filter: { kind: 'User', - 'relations.memberof': groups.map(group => - stringifyEntityRef({ - kind: 'group', - namespace: group.namespace.toLocaleLowerCase('en-US'), - name: group.name.toLocaleLowerCase('en-US'), - }), - ), + [`relations.${relationship.toLocaleLowerCase('en-US')}`]: + groups.map(group => + stringifyEntityRef({ + kind: 'group', + namespace: group.namespace.toLocaleLowerCase('en-US'), + name: group.name.toLocaleLowerCase('en-US'), + }), + ), }, }); @@ -99,10 +101,12 @@ export const getDescendantGroupsFromGroup = async ( export const getAllDesendantMembersForGroupEntity = async ( groupEntity: GroupEntity, catalogApi: CatalogApi, + relationship = 'memberof', ) => getMembersFromGroups( await getDescendantGroupsFromGroup(groupEntity, catalogApi), catalogApi, + relationship, ); export const removeDuplicateEntitiesFrom = (entityArray: Entity[]) => { diff --git a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md index 3c0a8d1bcb..7c0765ad43 100644 --- a/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md +++ b/plugins/permission-backend-module-policy-allow-all/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-permission-backend-module-allow-all-policy +## 0.1.20-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + +## 0.1.20-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.1.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 0.1.19 + +### Patch Changes + +- ed10fd2: Internal refactor to use new `PolicyQueryUser` type. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + +## 0.1.19-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 0.1.18-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-permission-common@0.7.14 + ## 0.1.16 ### Patch Changes diff --git a/plugins/permission-backend-module-policy-allow-all/package.json b/plugins/permission-backend-module-policy-allow-all/package.json index e0108c6541..0ba16a61f7 100644 --- a/plugins/permission-backend-module-policy-allow-all/package.json +++ b/plugins/permission-backend-module-policy-allow-all/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend-module-allow-all-policy", - "version": "0.1.16", + "version": "0.1.20-next.2", "description": "Allow all policy backend module for the permission plugin.", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/permission-backend-module-policy-allow-all/src/policy.ts b/plugins/permission-backend-module-policy-allow-all/src/policy.ts index da853e47db..90ae21f341 100644 --- a/plugins/permission-backend-module-policy-allow-all/src/policy.ts +++ b/plugins/permission-backend-module-policy-allow-all/src/policy.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { AuthorizeResult, PolicyDecision, @@ -22,12 +21,13 @@ import { import { PermissionPolicy, PolicyQuery, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; export class AllowAllPermissionPolicy implements PermissionPolicy { async handle( _request: PolicyQuery, - _user?: BackstageIdentityResponse, + _user?: PolicyQueryUser, ): Promise { return { result: AuthorizeResult.ALLOW, diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index baaaefef70..89955c0f69 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,91 @@ # @backstage/plugin-permission-backend +## 0.5.47-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.5.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.5.47-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + +## 0.5.46 + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.5.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + +## 0.5.45-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + ## 0.5.43 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index a3020e1dd3..7a331c1ccd 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.43", + "version": "0.5.47-next.2", "backstage": { "role": "backend-plugin", "pluginId": "permission", @@ -63,7 +63,7 @@ "express": "^4.17.1", "express-promise-router": "^4.1.0", "lodash": "^4.17.21", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yn": "^4.0.0", "zod": "^3.22.4" }, diff --git a/plugins/permission-backend/src/service/router.test.ts b/plugins/permission-backend/src/service/router.test.ts index be90c5f628..f12bacc61d 100644 --- a/plugins/permission-backend/src/service/router.test.ts +++ b/plugins/permission-backend/src/service/router.test.ts @@ -186,6 +186,13 @@ describe('createRouter', () => { mockCredentials.user().principal.userEntityRef, ], }, + info: { + userEntityRef: mockCredentials.user().principal.userEntityRef, + ownershipEntityRefs: [ + mockCredentials.user().principal.userEntityRef, + ], + }, + credentials: mockCredentials.user(), }, ); expect(response.body).toEqual({ diff --git a/plugins/permission-backend/src/service/router.ts b/plugins/permission-backend/src/service/router.ts index cc45d0b41a..fd33f0b629 100644 --- a/plugins/permission-backend/src/service/router.ts +++ b/plugins/permission-backend/src/service/router.ts @@ -22,10 +22,7 @@ import { errorHandler, } from '@backstage/backend-common'; import { InputError } from '@backstage/errors'; -import { - BackstageIdentityResponse, - IdentityApi, -} from '@backstage/plugin-auth-node'; +import { IdentityApi } from '@backstage/plugin-auth-node'; import { AuthorizeResult, EvaluatePermissionRequest, @@ -40,6 +37,7 @@ import { ApplyConditionsRequestEntry, ApplyConditionsResponseEntry, PermissionPolicy, + PolicyQueryUser, } from '@backstage/plugin-permission-node'; import { PermissionIntegrationClient } from './PermissionIntegrationClient'; import { memoize } from 'lodash'; @@ -130,9 +128,9 @@ const handleRequest = async ( ); }); - let user: BackstageIdentityResponse | undefined; + let user: PolicyQueryUser | undefined; if (auth.isPrincipal(credentials, 'user')) { - const { ownershipEntityRefs } = await userInfo.getUserInfo(credentials); + const info = await userInfo.getUserInfo(credentials); const { token } = await auth.getPluginRequestToken({ onBehalfOf: credentials, targetPluginId: 'catalog', // TODO: unknown at this point @@ -141,9 +139,11 @@ const handleRequest = async ( identity: { type: 'user', userEntityRef: credentials.principal.userEntityRef, - ownershipEntityRefs, + ownershipEntityRefs: info.ownershipEntityRefs, }, token, + credentials, + info, }; } diff --git a/plugins/permission-common/CHANGELOG.md b/plugins/permission-common/CHANGELOG.md index e6b637a092..8ef1eb314f 100644 --- a/plugins/permission-common/CHANGELOG.md +++ b/plugins/permission-common/CHANGELOG.md @@ -1,5 +1,39 @@ # @backstage/plugin-permission-common +## 0.8.1-next.1 + +### Patch Changes + +- df784fe: Add the MetadataResponse type from @backstage/plugin-permission-node, since this + type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.8.1-next.0 + +### Patch Changes + +- 137fa34: Add the MetadataResponseSerializedRule type from @backstage/plugin-permission-node, since this type might be used in frontend code. +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.8.0 + +### Minor Changes + +- f4085b8: **BREAKING**: Removed the deprecated and unused `token` option from `EvaluatorRequestOptions`. The `PermissionsClient` now has its own `PermissionClientRequestOptions` type that declares the `token` option instead. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.7.14 ### Patch Changes diff --git a/plugins/permission-common/api-report.md b/plugins/permission-common/api-report.md index 1c2476d20d..465c43846f 100644 --- a/plugins/permission-common/api-report.md +++ b/plugins/permission-common/api-report.md @@ -5,6 +5,7 @@ ```ts import { Config } from '@backstage/config'; import { JsonPrimitive } from '@backstage/types'; +import zodToJsonSchema from 'zod-to-json-schema'; // @public export type AllOfCriteria = { @@ -94,9 +95,7 @@ export type EvaluatePermissionResponseBatch = PermissionMessageBatch; // @public -export type EvaluatorRequestOptions = { - token?: string; -}; +export interface EvaluatorRequestOptions {} // @public export type IdentifiedPermissionMessage = T & { @@ -127,6 +126,20 @@ export function isResourcePermission( // @public export function isUpdatePermission(permission: Permission): boolean; +// @public +export type MetadataResponse = { + permissions?: Permission[]; + rules: MetadataResponseSerializedRule[]; +}; + +// @public +export type MetadataResponseSerializedRule = { + name: string; + description: string; + resourceType: string; + paramsSchema?: ReturnType; +}; + // @public export type NotCriteria = { not: PermissionCriteria; @@ -162,14 +175,19 @@ export class PermissionClient implements PermissionEvaluator { constructor(options: { discovery: DiscoveryApi; config: Config }); authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise; authorizeConditional( queries: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise; } +// @public +export type PermissionClientRequestOptions = { + token?: string; +}; + // @public export type PermissionCondition< TResourceType extends string = string, @@ -191,11 +209,15 @@ export type PermissionCriteria = export interface PermissionEvaluator { authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { + _ignored?: never; + }, ): Promise; authorizeConditional( requests: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { + _ignored?: never; + }, ): Promise; } diff --git a/plugins/permission-common/package.json b/plugins/permission-common/package.json index ee54459589..faeaf4a972 100644 --- a/plugins/permission-common/package.json +++ b/plugins/permission-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-common", - "version": "0.7.14", + "version": "0.8.1-next.1", "description": "Isomorphic types and client for Backstage permissions and authorization", "backstage": { "role": "common-library", @@ -53,7 +53,8 @@ "@backstage/types": "workspace:^", "cross-fetch": "^4.0.0", "uuid": "^9.0.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "zod-to-json-schema": "^3.20.4" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/permission-common/src/PermissionClient.ts b/plugins/permission-common/src/PermissionClient.ts index a5d08205d8..2414774775 100644 --- a/plugins/permission-common/src/PermissionClient.ts +++ b/plugins/permission-common/src/PermissionClient.ts @@ -27,7 +27,6 @@ import { PermissionEvaluator, QueryPermissionRequest, AuthorizePermissionRequest, - EvaluatorRequestOptions, AuthorizePermissionResponse, QueryPermissionResponse, } from './types/api'; @@ -93,6 +92,15 @@ const responseSchema = ( ), }); +/** + * Options for {@link PermissionClient} requests. + * + * @public + */ +export type PermissionClientRequestOptions = { + token?: string; +}; + /** * An isomorphic client for requesting authorization for Backstage permissions. * @public @@ -112,7 +120,7 @@ export class PermissionClient implements PermissionEvaluator { */ async authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise { return this.makeRequest( requests, @@ -126,7 +134,7 @@ export class PermissionClient implements PermissionEvaluator { */ async authorizeConditional( queries: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: PermissionClientRequestOptions, ): Promise { return this.makeRequest(queries, queryPermissionResponseSchema, options); } diff --git a/plugins/permission-common/src/types/api.ts b/plugins/permission-common/src/types/api.ts index e597384a56..072615989b 100644 --- a/plugins/permission-common/src/types/api.ts +++ b/plugins/permission-common/src/types/api.ts @@ -244,7 +244,7 @@ export interface PermissionEvaluator { */ authorize( requests: AuthorizePermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { _ignored?: never }, // Since the options are empty we add this placeholder to reject all options ): Promise; /** @@ -257,22 +257,20 @@ export interface PermissionEvaluator { */ authorizeConditional( requests: QueryPermissionRequest[], - options?: EvaluatorRequestOptions, + options?: EvaluatorRequestOptions & { _ignored?: never }, // Since the options are empty we add this placeholder to reject all options ): Promise; } +// Note(Rugvip): I kept the below type around in case we want to add new options +// in the future, for example a signal. It also helps out enabling API +// constraints, as without this we can't have the permissions service implement +// the evaluator interface due to the mismatch in parameter count. + /** * Options for {@link PermissionEvaluator} requests. * + * This is currently empty, as there are no longer any common options for the permission evaluator. + * * @public */ -export type EvaluatorRequestOptions = { - /** - * @deprecated Backend plugins should no longer depend on the - * `PermissionEvaluator`, but instead use the `PermissionService` from - * `@backstage/backend-plugin-api`. Frontend plugins should not need to inject - * this token at all, but instead implicitly rely on underlying fetchApi to do - * it for them. - */ - token?: string; -}; +export interface EvaluatorRequestOptions {} diff --git a/plugins/permission-common/src/types/index.ts b/plugins/permission-common/src/types/index.ts index 5702f47c1c..f5380541df 100644 --- a/plugins/permission-common/src/types/index.ts +++ b/plugins/permission-common/src/types/index.ts @@ -40,6 +40,10 @@ export type { NotCriteria, } from './api'; export type { DiscoveryApi } from './discovery'; +export type { + MetadataResponse, + MetadataResponseSerializedRule, +} from './integration'; export type { BasicPermission, PermissionAttributes, diff --git a/plugins/permission-common/src/types/integration.ts b/plugins/permission-common/src/types/integration.ts new file mode 100644 index 0000000000..d531140923 --- /dev/null +++ b/plugins/permission-common/src/types/integration.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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 zodToJsonSchema from 'zod-to-json-schema'; +import { Permission } from './permission'; + +/** + * Serialized permission rules, with the paramsSchema + * converted from a ZodSchema to a JsonSchema. + * + * @public + */ +export type MetadataResponseSerializedRule = { + name: string; + description: string; + resourceType: string; + paramsSchema?: ReturnType; +}; + +/** + * Response type for the .metadata endpoint in + * {@link @backstage/plugin-permission-node#createPermissionIntegrationRouter} + * + * @public + */ +export type MetadataResponse = { + permissions?: Permission[]; + rules: MetadataResponseSerializedRule[]; +}; diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 2e12639437..282ea5fda8 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,95 @@ # @backstage/plugin-permission-node +## 0.8.1-next.2 + +### Patch Changes + +- df784fe: The MetadataResponse type has been moved to @backstage/plugin-permission-common + to match the recent move of MetadataResponseSerializedRule, and should be + imported from there going forward. To avoid an immediate breaking change, this + type is still re-exported from this package, but is marked as deprecated and + will be removed in a future release. +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.8.1-next.1 + +### Patch Changes + +- 5cd9878: The MetadataResponseSerializedRule type has been moved to @backstage/plugin-permission-common, and should be imported from there going forward. To avoid an immediate breaking change, this type is still re-exported from this package, but is marked as deprecated and will be removed in a future release. +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.1 + +## 0.8.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-permission-common@0.8.0 + +## 0.8.0 + +### Minor Changes + +- 36f91e8: **BREAKING**: Updated the `ServerPermissionClient` to match the new `PermissionsService` interface, where the deprecated `token` option has been removed and the options are now required. + +### Patch Changes + +- ed10fd2: The `PermissionPolicy` interface has been updated to align with the recent changes to the Backstage auth system. The second argument to the `handle` method is now of the new `PolicyQueryUser` type. This type maintains the old fields from the `BackstageIdentityResponse`, which are now all deprecated. Instead, two new fields have been added, which allows access to the same information: + + - `credentials` - A `BackstageCredentials` object, which is useful for making requests to other services on behalf of the user as part of evaluating the policy. This replaces the deprecated `token` field. See the [Auth Service documentation](https://backstage.io/docs/backend-system/core-services/auth#creating-request-tokens) for information about how to create a token using these credentials. + - `info` - A `BackstageUserInfo` object, which contains the same information as the deprecated `identity`, except for the `type` field that was redundant. + + Most existing policies can be updated by replacing the `BackstageIdentityResponse` type with `PolicyQueryUser`, which is exported from `@backstage/plugin-permission-node`, as well as replacing any occurrences of `user?.identity` with `user?.info`. + +- 28b2cfb: Fix invalid cross-reference in API Reference docs +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.7.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-permission-common@0.7.14 + +## 0.7.32-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-permission-common@0.7.14 + ## 0.7.30 ### Patch Changes diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 290c586f2c..36483bd342 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -8,13 +8,17 @@ import { AnyOfCriteria } from '@backstage/plugin-permission-common'; import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common'; import { AuthorizePermissionResponse } from '@backstage/plugin-permission-common'; import { AuthService } from '@backstage/backend-plugin-api'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { BackstageUserInfo } from '@backstage/backend-plugin-api'; import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common'; import { Config } from '@backstage/config'; import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common'; import { DiscoveryService } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentifiedPermissionMessage } from '@backstage/plugin-permission-common'; +import { MetadataResponse as MetadataResponse_2 } from '@backstage/plugin-permission-common'; +import { MetadataResponseSerializedRule as MetadataResponseSerializedRule_2 } from '@backstage/plugin-permission-common'; import { NotCriteria } from '@backstage/plugin-permission-common'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionCondition } from '@backstage/plugin-permission-common'; @@ -27,7 +31,6 @@ import { QueryPermissionRequest } from '@backstage/plugin-permission-common'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { z } from 'zod'; -import zodToJsonSchema from 'zod-to-json-schema'; // @public export type ApplyConditionsRequest = { @@ -186,19 +189,11 @@ export const makeCreatePermissionRule: < rule: PermissionRule, ) => PermissionRule; -// @public -export type MetadataResponse = { - permissions?: Permission[]; - rules: MetadataResponseSerializedRule[]; -}; +// @public @deprecated +export type MetadataResponse = MetadataResponse_2; -// @public -export type MetadataResponseSerializedRule = { - name: string; - description: string; - resourceType: string; - paramsSchema?: ReturnType; -}; +// @public @deprecated +export type MetadataResponseSerializedRule = MetadataResponseSerializedRule_2; // @public export type PermissionIntegrationRouterOptions< @@ -246,10 +241,7 @@ export type PermissionIntegrationRouterOptions< // @public export interface PermissionPolicy { // (undocumented) - handle( - request: PolicyQuery, - user?: BackstageIdentityResponse, - ): Promise; + handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } // @public @@ -272,6 +264,15 @@ export type PolicyQuery = { permission: Permission; }; +// @public +export type PolicyQueryUser = { + token: string; + expiresInSeconds?: number; + identity: BackstageUserIdentity; + credentials: BackstageCredentials; + info: BackstageUserInfo; +}; + // @public export class ServerPermissionClient implements PermissionsService { // (undocumented) diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 1c29b97db0..b2c2d06f11 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-node", - "version": "0.7.30", + "version": "0.8.1-next.2", "description": "Common permission and authorization utilities for backend plugins", "backstage": { "role": "node-library", diff --git a/plugins/permission-node/src/ServerPermissionClient.test.ts b/plugins/permission-node/src/ServerPermissionClient.test.ts index 6e99ff71ab..5c6328ae44 100644 --- a/plugins/permission-node/src/ServerPermissionClient.test.ts +++ b/plugins/permission-node/src/ServerPermissionClient.test.ts @@ -25,7 +25,7 @@ import { import { mockCredentials, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; import { setupServer } from 'msw/node'; @@ -54,7 +54,7 @@ const config = new ConfigReader({ }); describe('ServerPermissionClient', () => { - setupRequestMockHandlers(server); + registerMswTestHooks(server); const discovery = mockServices.discovery.mock(); @@ -83,18 +83,22 @@ describe('ServerPermissionClient', () => { const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { discovery, tokenManager: mockServices.tokenManager.mock(), + auth: mockServices.auth(), }); - await client.authorize([ - { - permission: testBasicPermission, - }, - ]); + await client.authorize( + [ + { + permission: testBasicPermission, + }, + ], + { credentials: mockCredentials.none() }, + ); expect(mockAuthorizeHandler).not.toHaveBeenCalled(); }); - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { + it('should bypass the permission backend if permissions are enabled and request has valid server credentials', async () => { const client = ServerPermissionClient.fromConfig(config, { discovery, tokenManager: mockServices.tokenManager.mock(), @@ -102,13 +106,13 @@ describe('ServerPermissionClient', () => { }); await client.authorize([{ permission: testBasicPermission }], { - token: mockCredentials.service.token(), + credentials: mockCredentials.service(), }); expect(mockAuthorizeHandler).not.toHaveBeenCalled(); }); - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { + it('should call the permission backend if permissions are enabled and request does not have valid server credentials', async () => { const client = ServerPermissionClient.fromConfig(config, { discovery, tokenManager: mockServices.tokenManager.mock(), @@ -116,10 +120,18 @@ describe('ServerPermissionClient', () => { }); await client.authorize([{ permission: testBasicPermission }], { - token: mockCredentials.user.token(), + credentials: mockCredentials.user(), }); expect(mockAuthorizeHandler).toHaveBeenCalled(); + expect( + mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), + ).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'permission', + }), + ); }); }); @@ -145,201 +157,59 @@ describe('ServerPermissionClient', () => { const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { discovery, tokenManager: mockServices.tokenManager.mock(), - }); - - await client.authorizeConditional([ - { permission: testResourcePermission }, - ]); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), + auth: mockServices.auth(), }); await client.authorizeConditional( [{ permission: testResourcePermission }], { - token: mockCredentials.service.token(), + credentials: mockCredentials.none(), }, ); expect(mockAuthorizeHandler).not.toHaveBeenCalled(); }); - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { - const auth = mockServices.auth(); + it('should bypass the permission backend if permissions are enabled and request has valid server credentials', async () => { const client = ServerPermissionClient.fromConfig(config, { discovery, tokenManager: mockServices.tokenManager.mock(), - auth, + auth: mockServices.auth(), }); await client.authorizeConditional( [{ permission: testResourcePermission }], { - token: mockCredentials.user.token(), + credentials: mockCredentials.service(), + }, + ); + + expect(mockAuthorizeHandler).not.toHaveBeenCalled(); + }); + + it('should call the permission backend if permissions are enabled and request does not have valid server credentials', async () => { + const client = ServerPermissionClient.fromConfig(config, { + discovery, + tokenManager: mockServices.tokenManager.mock(), + auth: mockServices.auth(), + }); + + await client.authorizeConditional( + [{ permission: testResourcePermission }], + { + credentials: mockCredentials.user(), }, ); expect(mockAuthorizeHandler).toHaveBeenCalled(); - }); - }); - - describe('with credentials', () => { - describe('authorize', () => { - let mockAuthorizeHandler: jest.Mock; - - beforeEach(() => { - mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { - const responses = req.body.items.map( - (r: IdentifiedPermissionMessage) => ({ - id: r.id, - result: AuthorizeResult.ALLOW, - }), - ); - - return res(json({ items: responses })); - }); - - server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); - }); - - it('should bypass the permission backend if permissions are disabled', async () => { - const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorize( - [ - { - permission: testBasicPermission, - }, - ], - { credentials: mockCredentials.none() }, - ); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorize([{ permission: testBasicPermission }], { - credentials: mockCredentials.service(), - }); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorize([{ permission: testBasicPermission }], { - credentials: mockCredentials.user(), - }); - - expect(mockAuthorizeHandler).toHaveBeenCalled(); - expect( - mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), - ).toBe( - mockCredentials.service.header({ - onBehalfOf: mockCredentials.user(), - targetPluginId: 'permission', - }), - ); - }); - }); - - describe('authorizeConditional', () => { - let mockAuthorizeHandler: jest.Mock; - - beforeEach(() => { - mockAuthorizeHandler = jest.fn((req, res, { json }: RestContext) => { - const responses = req.body.items.map( - (r: IdentifiedPermissionMessage) => ({ - id: r.id, - result: AuthorizeResult.ALLOW, - }), - ); - - return res(json({ items: responses })); - }); - - server.use(rest.post(`${mockBaseUrl}/authorize`, mockAuthorizeHandler)); - }); - - it('should bypass the permission backend if permissions are disabled', async () => { - const client = ServerPermissionClient.fromConfig(new ConfigReader({}), { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorizeConditional( - [{ permission: testResourcePermission }], - { - credentials: mockCredentials.none(), - }, - ); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should bypass the permission backend if permissions are enabled and request has valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorizeConditional( - [{ permission: testResourcePermission }], - { - credentials: mockCredentials.service(), - }, - ); - - expect(mockAuthorizeHandler).not.toHaveBeenCalled(); - }); - - it('should call the permission backend if permissions are enabled and request does not have valid server token', async () => { - const client = ServerPermissionClient.fromConfig(config, { - discovery, - tokenManager: mockServices.tokenManager.mock(), - auth: mockServices.auth(), - }); - - await client.authorizeConditional( - [{ permission: testResourcePermission }], - { - credentials: mockCredentials.user(), - }, - ); - - expect(mockAuthorizeHandler).toHaveBeenCalled(); - expect( - mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), - ).toBe( - mockCredentials.service.header({ - onBehalfOf: mockCredentials.user(), - targetPluginId: 'permission', - }), - ); - }); + expect( + mockAuthorizeHandler.mock.calls[0][0].headers.get('authorization'), + ).toBe( + mockCredentials.service.header({ + onBehalfOf: mockCredentials.user(), + targetPluginId: 'permission', + }), + ); }); }); diff --git a/plugins/permission-node/src/ServerPermissionClient.ts b/plugins/permission-node/src/ServerPermissionClient.ts index cf54f3b894..e21067bfa1 100644 --- a/plugins/permission-node/src/ServerPermissionClient.ts +++ b/plugins/permission-node/src/ServerPermissionClient.ts @@ -146,14 +146,6 @@ export class ServerPermissionClient implements PermissionsService { return options.credentials; } - if (options?.token) { - try { - return await this.#auth.authenticate(options.token); - } catch { - // ignore - } - } - return undefined; } diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 4a52c33f18..40d215d515 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -24,6 +24,8 @@ import { AuthorizeResult, DefinitivePolicyDecision, IdentifiedPermissionMessage, + MetadataResponse as CommonMetadataResponse, + MetadataResponseSerializedRule as CommonMetadataResponseSerializedRule, Permission, PermissionCondition, PermissionCriteria, @@ -109,23 +111,18 @@ export type ApplyConditionsResponse = { * converted from a ZodSchema to a JsonSchema. * * @public + * @deprecated Please import from `@backstage/plugin-permission-common` instead. */ -export type MetadataResponseSerializedRule = { - name: string; - description: string; - resourceType: string; - paramsSchema?: ReturnType; -}; +export type MetadataResponseSerializedRule = + CommonMetadataResponseSerializedRule; /** * Response type for the .metadata endpoint. * * @public + * @deprecated Please import from `@backstage/plugin-permission-common` instead. */ -export type MetadataResponse = { - permissions?: Permission[]; - rules: MetadataResponseSerializedRule[]; -}; +export type MetadataResponse = CommonMetadataResponse; const applyConditions = ( criteria: PermissionCriteria>, diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts index f151cfb4fb..fd47ed7e62 100644 --- a/plugins/permission-node/src/policy/index.ts +++ b/plugins/permission-node/src/policy/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export type { PermissionPolicy, PolicyQuery } from './types'; +export type { PermissionPolicy, PolicyQuery, PolicyQueryUser } from './types'; diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 0f469cfbfe..a7b8b75218 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -18,7 +18,11 @@ import { Permission, PolicyDecision, } from '@backstage/plugin-permission-common'; -import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; +import { BackstageUserIdentity } from '@backstage/plugin-auth-node'; +import { + BackstageCredentials, + BackstageUserInfo, +} from '@backstage/backend-plugin-api'; /** * A query to be evaluated by the {@link PermissionPolicy}. @@ -34,6 +38,44 @@ export type PolicyQuery = { permission: Permission; }; +/** + * The context within which a policy query is evaluated. + * + * @public + */ +export type PolicyQueryUser = { + /** + * The token used to authenticate the user within Backstage. + * + * @deprecated User the `credentials` field in combination with `coreServices.auth` to generate a request token instead. + */ + token: string; + + /** + * The number of seconds until the token expires. If not set, it can be assumed that the token does not expire. + * + * @deprecated This field is deprecated and will be removed in a future release. + */ + expiresInSeconds?: number; + + /** + * A plaintext description of the identity that is encapsulated within the token. + * + * @deprecated Use the `info` field instead. + */ + identity: BackstageUserIdentity; + + /** + * The credentials of the user making the request. + */ + credentials: BackstageCredentials; + + /** + * The information for the user making the request. + */ + info: BackstageUserInfo; +}; + /** * A policy to evaluate authorization requests for any permissioned action performed in Backstage. * @@ -51,8 +93,5 @@ export type PolicyQuery = { * @public */ export interface PermissionPolicy { - handle( - request: PolicyQuery, - user?: BackstageIdentityResponse, - ): Promise; + handle(request: PolicyQuery, user?: PolicyQueryUser): Promise; } diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 36e30714f6..0a7e06ed91 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -23,7 +23,7 @@ import { NoInfer } from './integration/util'; /** * A conditional rule that can be provided in an - * {@link @backstage/permission-common#AuthorizeDecision} response to an authorization request. + * {@link @backstage/plugin-permission-common#AuthorizeDecision} response to an authorization request. * * @remarks * diff --git a/plugins/permission-react/CHANGELOG.md b/plugins/permission-react/CHANGELOG.md index 1eb951e625..83f4dea7ed 100644 --- a/plugins/permission-react/CHANGELOG.md +++ b/plugins/permission-react/CHANGELOG.md @@ -1,5 +1,33 @@ # @backstage/plugin-permission-react +## 0.4.25-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.4.25-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + +## 0.4.24 + +### Patch Changes + +- 28b2cfb: Improve API Reference documentation +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/config@1.2.0 + - @backstage/core-plugin-api@1.9.3 + ## 0.4.23 ### Patch Changes diff --git a/plugins/permission-react/api-report.md b/plugins/permission-react/api-report.md index 3c1961c92c..4c51d16b80 100644 --- a/plugins/permission-react/api-report.md +++ b/plugins/permission-react/api-report.md @@ -100,6 +100,4 @@ export function usePermission( resourceRef: string | undefined; }, ): AsyncPermissionResult; - -// (No @packageDocumentation comment for this package) ``` diff --git a/plugins/permission-react/package.json b/plugins/permission-react/package.json index 9ebb13e633..394f56ff02 100644 --- a/plugins/permission-react/package.json +++ b/plugins/permission-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-react", - "version": "0.4.23", + "version": "0.4.25-next.1", "backstage": { "role": "web-library", "pluginId": "permission", diff --git a/plugins/permission-react/src/index.ts b/plugins/permission-react/src/index.ts index 57352c5cdd..de6958b3e5 100644 --- a/plugins/permission-react/src/index.ts +++ b/plugins/permission-react/src/index.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +/** + * Components and hooks to help implement permissions in Backstage frontend plugins. + * + * @packageDocumentation + */ export * from './components'; export * from './hooks'; export * from './apis'; diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index deeaf2621b..138b98902b 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-proxy-backend +## 0.5.4-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.5.4-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.5.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.5.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.5.3-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + +## 0.5.2-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/config@1.2.0 + - @backstage/types@1.1.1 + ## 0.5.0 ### Minor Changes diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index 4b990e57f7..091e637886 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.5.0", + "version": "0.5.4-next.2", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", "backstage": { "role": "backend-plugin", @@ -79,7 +79,7 @@ "@types/uuid": "^9.0.0", "@types/yup": "^0.32.0", "msw": "^2.0.0", - "node-fetch": "^2.6.7" + "node-fetch": "^2.7.0" }, "configSchema": "config.d.ts" } diff --git a/plugins/proxy-backend/src/service/router.config.test.ts b/plugins/proxy-backend/src/service/router.config.test.ts index ba81553a4f..be26ddb9bc 100644 --- a/plugins/proxy-backend/src/service/router.config.test.ts +++ b/plugins/proxy-backend/src/service/router.config.test.ts @@ -19,7 +19,7 @@ import { createServiceFactory, } from '@backstage/backend-plugin-api'; import { - setupRequestMockHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { @@ -36,7 +36,7 @@ import fetch from 'node-fetch'; describe('createRouter reloadable configuration', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); it('should be able to observe the config', async () => { // Grab the subscriber function and use mutable config data to mock a config file change diff --git a/plugins/proxy-backend/src/service/router.credentials.test.ts b/plugins/proxy-backend/src/service/router.credentials.test.ts index 54c865ba11..ac32179ef3 100644 --- a/plugins/proxy-backend/src/service/router.credentials.test.ts +++ b/plugins/proxy-backend/src/service/router.credentials.test.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-app-api'; import { mockServices, - setupRequestMockHandlers, + registerMswTestHooks, startTestBackend, } from '@backstage/backend-test-utils'; import { ResponseError } from '@backstage/errors'; @@ -34,7 +34,7 @@ import fetch from 'node-fetch'; describe('credentials', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); it('handles all valid credentials settings', async () => { const config = { diff --git a/plugins/scaffolder-backend-module-azure/CHANGELOG.md b/plugins/scaffolder-backend-module-azure/CHANGELOG.md index 7c1115e7fa..90541db310 100644 --- a/plugins/scaffolder-backend-module-azure/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-azure/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-scaffolder-backend-module-azure +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.14 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.11 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-azure/package.json b/plugins/scaffolder-backend-module-azure/package.json index ece29a8866..92d64ec7f0 100644 --- a/plugins/scaffolder-backend-module-azure/package.json +++ b/plugins/scaffolder-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-azure", - "version": "0.1.11", + "version": "0.1.15-next.2", "description": "The azure module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts index 35868274fd..dda65a76ea 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.test.ts @@ -19,7 +19,10 @@ import { ConfigReader } from '@backstage/config'; import { createPublishAzureAction } from './azure'; import { ScmIntegrations } from '@backstage/integration'; import { WebApi } from 'azure-devops-node-api'; -import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { + getRepoSourceDirectory, + initRepoAndPush, +} from '@backstage/plugin-scaffolder-node'; import { examples } from './azure.examples'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -48,6 +51,10 @@ describe('publish:azure examples', () => { host: 'dev.azure.com', credentials: [{ personalAccessToken: 'tokenlols' }], }, + { + host: 'test.azure.com', + credentials: [{ personalAccessToken: 'tokenlols' }], + }, ], }, }); @@ -114,4 +121,172 @@ describe('publish:azure examples', () => { gitAuthorInfo: {}, }); }); + + it(`should ${examples[3].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: input.gitCommitMessage, + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[4].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { + name: input.gitAuthorName, + email: input.gitAuthorEmail, + }, + }); + }); + + it(`should ${examples[5].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath), + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[6].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + webUrl: 'https://dev.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://dev.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: input.token }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); + + it(`should ${examples[7].description}`, async () => { + mockGitClient.createRepository.mockResolvedValue({ + remoteUrl: 'https://test.azure.com/organization/project/_git/repo', + webUrl: 'https://test.azure.com/organization/project/_git/repo', + id: '709e891c-dee7-4f91-b963-534713c0737f', + }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://test.azure.com/organization/project/_git/repo', + defaultBranch: 'master', + auth: { username: 'notempty', password: 'tokenlols' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: {}, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts index d1459d1820..ae5bfece2b 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.examples.ts @@ -29,7 +29,7 @@ export const examples: TemplateExample[] = [ name: 'Publish to Azure', input: { repoUrl: - 'dev.azure.com?organization=organization&owner=project&repo=repo', + 'dev.azure.com?organization=organization&project=project&repo=repo', }, }, ], @@ -45,7 +45,7 @@ export const examples: TemplateExample[] = [ name: 'Publish to Azure', input: { repoUrl: - 'dev.azure.com?organization=organization&owner=project&repo=repo', + 'dev.azure.com?organization=organization&project=project&repo=repo', description: 'Initialize a git repository', }, }, @@ -63,11 +63,101 @@ export const examples: TemplateExample[] = [ name: 'Publish to Azure', input: { repoUrl: - 'dev.azure.com?organization=organization&owner=project&repo=repo', + 'dev.azure.com?organization=organization&project=project&repo=repo', defaultBranch: 'main', }, }, ], }), }, + { + description: + 'Initializes an Azure DevOps repository with a custom commit message', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + gitCommitMessage: 'Initial setup and configuration', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository with a custom author name and email', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + gitAuthorName: 'John Doe', + gitAuthorEmail: 'john.doe@example.com', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using a specific source path', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + sourcePath: 'path/to/source', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using an authentication token', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'dev.azure.com?organization=organization&project=project&repo=repo', + token: 'personal-access-token', + }, + }, + ], + }), + }, + { + description: + 'Initializes an Azure DevOps repository using an custom repo url', + example: yaml.stringify({ + steps: [ + { + id: 'publish', + action: 'publish:azure', + name: 'Publish to Azure', + input: { + repoUrl: + 'test.azure.com?organization=organization&project=project&repo=repo', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts index a21520f751..140c77fb52 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.test.ts @@ -55,7 +55,9 @@ describe('publish:azure', () => { const action = createPublishAzureAction({ integrations, config }); const mockContext = createMockActionContext({ - input: { repoUrl: 'dev.azure.com?repo=repo&owner=owner&organization=org' }, + input: { + repoUrl: 'dev.azure.com?repo=repo&project=project&organization=org', + }, }); const mockGitClient = { @@ -77,19 +79,19 @@ describe('publish:azure', () => { ...mockContext, input: { repoUrl: 'dev.azure.com?repo=bob' }, }), - ).rejects.toThrow(/missing owner/); + ).rejects.toThrow(/missing project/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'dev.azure.com?owner=owner' }, + input: { repoUrl: 'dev.azure.com?project=project' }, }), ).rejects.toThrow(/missing repo/); await expect( action.handler({ ...mockContext, - input: { repoUrl: 'dev.azure.com?owner=owner&repo=repo' }, + input: { repoUrl: 'dev.azure.com?project=project&repo=repo' }, }), ).rejects.toThrow(/missing organization/); }); @@ -98,7 +100,9 @@ describe('publish:azure', () => { await expect( action.handler({ ...mockContext, - input: { repoUrl: 'azure.com?repo=bob&owner=owner&organization=org' }, + input: { + repoUrl: 'azure.com?repo=bob&project=project&organization=org', + }, }), ).rejects.toThrow(/No matching integration configuration/); }); @@ -109,7 +113,7 @@ describe('publish:azure', () => { ...mockContext, input: { repoUrl: - 'myazurehostnotoken.com?repo=bob&owner=owner&organization=org', + 'myazurehostnotoken.com?repo=bob&project=project&organization=org', }, }), ).rejects.toThrow( @@ -122,7 +126,7 @@ describe('publish:azure', () => { action.handler({ ...mockContext, input: { - repoUrl: 'dev.azure.com?repo=bob&owner=owner&organization=org', + repoUrl: 'dev.azure.com?repo=bob&project=project&organization=org', }, }), ).rejects.toThrow(/Unable to create the repository/); @@ -138,7 +142,8 @@ describe('publish:azure', () => { await action.handler({ ...mockContext, input: { - repoUrl: 'myazurehostnotoken.com?repo=bob&owner=owner&organization=org', + repoUrl: + 'myazurehostnotoken.com?repo=bob&project=project&organization=org', token: 'lols', }, }); @@ -152,7 +157,7 @@ describe('publish:azure', () => { { name: 'bob', }, - 'owner', + 'project', ); }); @@ -166,7 +171,7 @@ describe('publish:azure', () => { action.handler({ ...mockContext, input: { - repoUrl: 'dev.azure.com?repo=bob&owner=owner&organization=org', + repoUrl: 'dev.azure.com?repo=bob&project=project&organization=org', }, }), ).rejects.toThrow(/No remote URL returned/); @@ -182,7 +187,7 @@ describe('publish:azure', () => { action.handler({ ...mockContext, input: { - repoUrl: 'dev.azure.com?repo=bob&owner=owner&organization=org', + repoUrl: 'dev.azure.com?repo=bob&project=project&organization=org', }, }), ).rejects.toThrow(/No Id returned/); @@ -198,7 +203,7 @@ describe('publish:azure', () => { action.handler({ ...mockContext, input: { - repoUrl: 'dev.azure.com?repo=bob&owner=owner&organization=org', + repoUrl: 'dev.azure.com?repo=bob&project=project&organization=org', }, }), ).rejects.toThrow(/No web URL returned/); @@ -214,7 +219,7 @@ describe('publish:azure', () => { await action.handler({ ...mockContext, input: { - repoUrl: 'dev.azure.com?repo=bob&owner=owner&organization=org', + repoUrl: 'dev.azure.com?repo=bob&project=project&organization=org', }, }); @@ -227,7 +232,7 @@ describe('publish:azure', () => { { name: 'bob', }, - 'owner', + 'project', ); }); diff --git a/plugins/scaffolder-backend-module-azure/src/actions/azure.ts b/plugins/scaffolder-backend-module-azure/src/actions/azure.ts index bd75cedc65..a284f463a6 100644 --- a/plugins/scaffolder-backend-module-azure/src/actions/azure.ts +++ b/plugins/scaffolder-backend-module-azure/src/actions/azure.ts @@ -136,7 +136,7 @@ export function createPublishAzureAction(options: { gitAuthorEmail, } = ctx.input; - const { owner, repo, host, organization } = parseRepoUrl( + const { project, repo, host, organization } = parseRepoUrl( repoUrl, integrations, ); @@ -166,11 +166,14 @@ export function createPublishAzureAction(options: { const webApi = new WebApi(url, authHandler); const client = await webApi.getGitApi(); const createOptions: GitRepositoryCreateOptions = { name: repo }; - const returnedRepo = await client.createRepository(createOptions, owner); + const returnedRepo = await client.createRepository( + createOptions, + project, + ); if (!returnedRepo) { throw new InputError( - `Unable to create the repository with Organization ${organization}, Project ${owner} and Repo ${repo}. + `Unable to create the repository with Organization ${organization}, Project ${project} and Repo ${repo}. Please make sure that both the Org and Project are typed corrected and exist.`, ); } diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md index 01bcce124f..604b44d3da 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,81 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-cloud +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 3fca643: Added autocompletion support for resource `branches` +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.12 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.11-next.0 + +### Patch Changes + +- b5deed0: Add support for `autocomplete` handler to provide autocomplete options for `RepoUrlPicker` +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.9 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json index 8e1fd1facd..1806189dd4 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud", - "version": "0.1.9", + "version": "0.1.13-next.2", "description": "The Bitbucket Cloud module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -46,9 +46,10 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", + "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts index 5e95f41751..6b4b77879e 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketCloudAction } from './bitbucketCloud'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -66,7 +66,7 @@ describe('publish:bitbucketCloud', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts index 6a079a3f9b..6cac8a0e1c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloud.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketCloudAction } from './bitbucketCloud'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -56,7 +56,7 @@ describe('publish:bitbucketCloud', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts index 472c7a6a03..3e306c4065 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.examples.test.ts @@ -21,7 +21,7 @@ import yaml from 'yaml'; import { examples } from './bitbucketCloudPipelinesRun.examples'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('bitbucket:pipelines:run', () => { @@ -48,7 +48,7 @@ describe('bitbucket:pipelines:run', () => { build_number: 1, }; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts index 9386246868..edabe2a7ba 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/actions/bitbucketCloudPipelinesRun.test.ts @@ -16,7 +16,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createBitbucketPipelinesRunAction } from './bitbucketCloudPipelinesRun'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; @@ -48,7 +48,7 @@ describe('bitbucket:pipelines:run', () => { build_number: 1, }; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts new file mode 100644 index 0000000000..d99a8358b9 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright 2024 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 { BitbucketCloudClient } from '@backstage/plugin-bitbucket-cloud-common'; +import { handleAutocompleteRequest } from './autocomplete'; +import { InputError } from '@backstage/errors'; + +describe('handleAutocompleteRequest', () => { + const client: Partial = { + listWorkspaces: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ slug: 'workspace1' }] }]), + }), + listProjectsByWorkspace: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ key: 'project1' }] }]), + }), + listRepositoriesByWorkspace: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ slug: 'repository1' }] }]), + }), + listBranchesByRepository: jest.fn().mockReturnValue({ + iteratePages: jest + .fn() + .mockReturnValue([{ values: [{ name: 'branch1' }] }]), + }), + }; + + const fromConfig = jest + .spyOn(BitbucketCloudClient, 'fromConfig') + .mockReturnValue(client as BitbucketCloudClient); + + it('should pass the token to the client', async () => { + const accessToken = 'foo'; + await handleAutocompleteRequest({ + token: accessToken, + context: {}, + resource: 'workspaces', + }); + + expect(fromConfig).toHaveBeenCalledWith( + expect.objectContaining({ token: accessToken }), + ); + }); + + it('should return workspaces', async () => { + const result = await handleAutocompleteRequest({ + token: 'foo', + context: {}, + resource: 'workspaces', + }); + + expect(result).toEqual({ results: [{ title: 'workspace1' }] }); + }); + + it('should return projects', async () => { + const result = await handleAutocompleteRequest({ + token: 'foo', + context: { + workspace: 'workspace1', + }, + resource: 'projects', + }); + + expect(result).toEqual({ results: [{ title: 'project1' }] }); + }); + + it('should return repositories', async () => { + const result = await handleAutocompleteRequest({ + token: 'foo', + resource: 'repositories', + context: { + workspace: 'workspace1', + project: 'project1', + }, + }); + + expect(result).toEqual({ results: [{ title: 'repository1' }] }); + }); + + it('should return branches', async () => { + const result = await handleAutocompleteRequest({ + token: 'foo', + resource: 'branches', + context: { + workspace: 'workspace1', + repository: 'repository1', + }, + }); + + expect(result).toEqual({ results: [{ title: 'branch1' }] }); + }); + + it('should throw an error when passing an invalid resource', async () => { + await expect( + handleAutocompleteRequest({ + token: 'token', + resource: 'invalid', + context: {}, + }), + ).rejects.toThrow(InputError); + }); + + it('should throw an error when there are missing parameters', async () => { + await expect( + handleAutocompleteRequest({ + token: 'token', + resource: 'projects', + context: {}, + }), + ).rejects.toThrow(InputError); + }); +}); diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts new file mode 100644 index 0000000000..75cc170a30 --- /dev/null +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/autocomplete/autocomplete.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2024 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 { InputError } from '@backstage/errors'; +import { BitbucketCloudClient } from '@backstage/plugin-bitbucket-cloud-common'; + +export async function handleAutocompleteRequest({ + resource, + token, + context, +}: { + resource: string; + token: string; + context: Record; +}): Promise<{ results: { title: string }[] }> { + const client = BitbucketCloudClient.fromConfig({ + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + token, + }); + + switch (resource) { + case 'workspaces': { + const results: { title: string }[] = []; + + for await (const page of client.listWorkspaces().iteratePages()) { + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); + } + + return { results }; + } + case 'projects': { + if (!context.workspace) + throw new InputError('Missing workspace context parameter'); + + const results: { title: string }[] = []; + + for await (const page of client + .listProjectsByWorkspace(context.workspace) + .iteratePages()) { + const keys = [...page.values!].map(p => ({ title: p.key! })); + results.push(...keys); + } + + return { results }; + } + case 'repositories': { + if (!context.workspace || !context.project) + throw new InputError( + 'Missing workspace and/or project context parameter', + ); + + const results: { title: string }[] = []; + + for await (const page of client + .listRepositoriesByWorkspace(context.workspace, { + q: `project.key="${context.project}"`, + }) + .iteratePages()) { + const slugs = [...page.values!].map(p => ({ title: p.slug! })); + results.push(...slugs); + } + + return { results }; + } + case 'branches': { + if (!context.workspace || !context.repository) + throw new InputError( + 'Missing workspace and/or repository context parameter', + ); + + const results: { title: string }[] = []; + + for await (const page of client + .listBranchesByRepository(context.repository, context.workspace) + .iteratePages()) { + const names = [...page.values!].map(p => ({ title: p.name! })); + results.push(...names); + } + + return { results }; + } + default: + throw new InputError(`Invalid resource: ${resource}`); + } +} diff --git a/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts b/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts index 81afbf8c88..5bf49f7cd9 100644 --- a/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts +++ b/plugins/scaffolder-backend-module-bitbucket-cloud/src/module.ts @@ -17,12 +17,16 @@ import { coreServices, createBackendModule, } from '@backstage/backend-plugin-api'; -import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { + scaffolderActionsExtensionPoint, + scaffolderAutocompleteExtensionPoint, +} from '@backstage/plugin-scaffolder-node/alpha'; import { createBitbucketPipelinesRunAction, createPublishBitbucketCloudAction, } from './actions'; import { ScmIntegrations } from '@backstage/integration'; +import { handleAutocompleteRequest } from './autocomplete/autocomplete'; /** * @public @@ -35,15 +39,21 @@ export const bitbucketCloudModule = createBackendModule({ registerInit({ deps: { scaffolder: scaffolderActionsExtensionPoint, + autocomplete: scaffolderAutocompleteExtensionPoint, config: coreServices.rootConfig, }, - async init({ scaffolder, config }) { + async init({ scaffolder, config, autocomplete }) { const integrations = ScmIntegrations.fromConfig(config); scaffolder.addActions( createPublishBitbucketCloudAction({ integrations, config }), createBitbucketPipelinesRunAction({ integrations }), ); + + autocomplete.addAutocompleteProvider({ + id: 'bitbucket-cloud', + handler: handleAutocompleteRequest, + }); }, }); }, diff --git a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md index 3eefd69ab7..221a626169 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,77 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket-server +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.13-next.1 + +### Patch Changes + +- e3b64be: Added examples for publish:bitbucketServer action and improve its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.12 + +### Patch Changes + +- 6a4ad4e: Instead of using hardcoded `targetBranch` now fetch the default branch from Bitbucket repository. + This prevents from errors when no `targetBranch` is provided and the default repository branch is different from `master`, for example: `main`. +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.11-next.0 + +### Patch Changes + +- 6a4ad4e: Instead of using hardcoded `targetBranch` now fetch the default branch from Bitbucket repository. + This prevents from errors when no `targetBranch` is provided and the default repository branch is different from `master`, for example: `main`. +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.9 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket-server/package.json b/plugins/scaffolder-backend-module-bitbucket-server/package.json index cc321ccd33..5c08e23b6f 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/package.json +++ b/plugins/scaffolder-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket-server", - "version": "0.1.9", + "version": "0.1.13-next.2", "description": "The Bitbucket Server module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -48,7 +48,7 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts index eae8385822..8e222a6876 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.test.ts @@ -29,12 +29,15 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerAction } from './bitbucketServer'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import yaml from 'yaml'; import { examples } from './bitbucketServer.examples'; -import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; +import { + getRepoSourceDirectory, + initRepoAndPush, +} from '@backstage/plugin-scaffolder-node'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; describe('publish:bitbucketServer', () => { @@ -68,7 +71,7 @@ describe('publish:bitbucketServer', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); @@ -266,7 +269,8 @@ describe('publish:bitbucketServer', () => { it(`should ${examples[3].description}`, async () => { expect.assertions(3); - server.use( + + const handlers = [ rest.post( 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', (req, res, ctx) => { @@ -300,7 +304,35 @@ describe('publish:bitbucketServer', () => { ); }, ), - ); + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); await action.handler({ ...mockContext, @@ -334,7 +366,8 @@ describe('publish:bitbucketServer', () => { it(`should ${examples[4].description}`, async () => { expect.assertions(3); - server.use( + + const handlers = [ rest.post( 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', (req, res, ctx) => { @@ -368,7 +401,35 @@ describe('publish:bitbucketServer', () => { ); }, ), - ); + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); await action.handler({ ...mockContext, @@ -399,4 +460,1050 @@ describe('publish:bitbucketServer', () => { gitAuthorInfo: { name: 'Test User', email: 'test.user@example.com' }, }); }); + it(`should ${examples[5].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe(`Bearer thing`); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + // Ensure that the initRepoAndPush function was called with the correct remoteUrl and input properties + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + defaultBranch: 'master', + auth: { token: 'thing' }, + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { + name: 'Custom Author', + email: 'custom.author@example.com', + }, + }); + }); + + it(`should ${examples[6].description}`, async () => { + expect.assertions(5); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + defaultBranch: 'master', + auth: { token: 'thing' }, + logger: mockContext.logger, + commitMessage: 'Initial commit with LFS enabled', + gitAuthorInfo: { + email: input.gitAuthorName, + name: input.gitAuthorEmail, + }, + }); + }); + + it(`should ${examples[7].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath), + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: 'custom-token' }, + defaultBranch: 'master', + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { name: undefined, email: undefined }, + }); + }); + + it(`should ${examples[8].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: 'thing' }, + defaultBranch: 'master', + logger: mockContext.logger, + commitMessage: 'initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[9].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: true, + name: 'repo', + defaultBranch: 'master', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: 'thing' }, + defaultBranch: 'master', + logger: mockContext.logger, + commitMessage: 'Public repository initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[10].description}`, async () => { + expect.assertions(5); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + description: 'A fully customized repository', + defaultBranch: 'development', + }); + + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath), + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: input.token }, + defaultBranch: input.defaultBranch, + logger: mockContext.logger, + commitMessage: input.gitCommitMessage, + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[11].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'main', + }); + + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: input.sourcePath + ? getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath) + : mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: input.token ?? 'thing' }, + defaultBranch: input.defaultBranch, + logger: mockContext.logger, + commitMessage: input.gitCommitMessage ?? 'initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[12].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: true, + name: 'repo', + defaultBranch: 'master', + description: 'A public repository with a custom description', + }); + + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[12].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: input.sourcePath + ? getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath) + : mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: input.token ?? 'thing' }, + defaultBranch: input.defaultBranch ?? 'master', + logger: mockContext.logger, + commitMessage: input.gitCommitMessage ?? 'initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[13].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Bearer custom-auth-token', + ); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[13].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: input.sourcePath + ? getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath) + : mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: input.token ?? 'thing' }, + defaultBranch: input.defaultBranch ?? 'master', + logger: mockContext.logger, + commitMessage: input.gitCommitMessage ?? 'initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[14].description}`, async () => { + expect.assertions(3); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer custom-token'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[14].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: input.sourcePath + ? getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath) + : mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: input.token ?? 'thing' }, + defaultBranch: input.defaultBranch ?? 'master', + logger: mockContext.logger, + commitMessage: input.gitCommitMessage ?? 'initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); + + it(`should ${examples[15].description}`, async () => { + expect.assertions(5); + + const handlers = [ + rest.post( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual({ + public: false, + name: 'repo', + defaultBranch: 'master', + }); + + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + { + href: 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + rest.put( + 'https://hosted.bitbucket.com/rest/git-lfs/admin/projects/project/repos/repo/enabled', + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Bearer thing'); + expect(req.body).toEqual(''); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + links: { + self: [ + { + href: 'https://bitbucket.mycompany.com/projects/project/repos/repo', + }, + ], + clone: [ + { + name: 'http', + href: 'https://bitbucket.mycompany.com/scm/project/repo', + }, + ], + }, + }), + ); + }, + ), + ]; + + server.use(...handlers); + + let input; + try { + input = yaml.parse(examples[15].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: input.sourcePath + ? getRepoSourceDirectory(mockContext.workspacePath, input.sourcePath) + : mockContext.workspacePath, + remoteUrl: 'https://bitbucket.mycompany.com/scm/project/repo', + auth: { token: input.token ?? 'thing' }, + defaultBranch: input.defaultBranch ?? 'master', + logger: mockContext.logger, + commitMessage: input.gitCommitMessage ?? 'initial commit', + gitAuthorInfo: { name: input.gitAuthorName, email: input.gitAuthorEmail }, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.ts index b3a68eed57..e28f9ca48c 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.examples.ts @@ -129,4 +129,207 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: + 'Initialize git repository with default settings and custom author information', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-custom-author', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + gitAuthorName: 'Custom Author', + gitAuthorEmail: 'custom.author@example.com', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with LFS enabled and a specific commit message', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-lfs-commit', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + enableLFS: true, + gitCommitMessage: 'Initial commit with LFS enabled', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with a custom source path and token', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-custom-source', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + sourcePath: 'custom/source/path', + token: 'custom-token', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with private visibility and custom author details', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-private-custom-author', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + repoVisibility: 'private', + gitAuthorName: 'Private Author', + gitAuthorEmail: 'private.author@example.com', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with public visibility and specific commit message', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-public-commit', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + repoVisibility: 'public', + gitCommitMessage: 'Public repository initial commit', + }, + }, + ], + }), + }, + { + description: 'Initialize git repository with all settings customized', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-all-custom', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + description: 'A fully customized repository', + repoVisibility: 'private', + defaultBranch: 'development', + sourcePath: 'src/backend', + enableLFS: true, + token: 'custom-token', + gitCommitMessage: 'Fully customized initial commit', + gitAuthorName: 'Custom Dev', + gitAuthorEmail: 'custom.dev@example.com', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with a specific default branch and no LFS', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-no-lfs', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + defaultBranch: 'main', + enableLFS: false, + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with a custom repository description and public visibility', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-custom-description', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + description: 'A public repository with a custom description', + repoVisibility: 'public', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with a custom token for authentication', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-custom-token', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + token: 'custom-auth-token', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with a different repository root path', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-different-root', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + sourcePath: 'different/root/path', + }, + }, + ], + }), + }, + { + description: + 'Initialize git repository with private visibility and LFS enabled', + example: yaml.stringify({ + steps: [ + { + action: 'publish:bitbucketServer', + id: 'publish-bitbucket-server-private-lfs', + name: 'Publish To Bitbucket Server', + input: { + repoUrl: 'hosted.bitbucket.com?project=project&repo=repo', + repoVisibility: 'private', + enableLFS: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts index 1d42e7368c..250b3f9b54 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServer.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerAction } from './bitbucketServer'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -66,7 +66,7 @@ describe('publish:bitbucketServer', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts index 1d3556fc88..b83ba2be55 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import yaml from 'yaml'; @@ -191,7 +191,7 @@ describe('publish:bitbucketServer:pull-request', () => { }; const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts index 9d2dd915f8..37df535fec 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; @@ -103,6 +103,14 @@ describe('publish:bitbucketServer:pull-request', () => { ], start: 0, }; + const responseOfDefaultBranch = { + id: 'refs/heads/main', + displayId: 'main', + type: 'BRANCH', + latestCommit: '1245346tsdfgdf', + latestChangeset: 'wsdfgdh234', + isDefault: true, + }; const responseOfPullRequests = { id: 19, version: 0, @@ -190,6 +198,16 @@ describe('publish:bitbucketServer:pull-request', () => { ); }, ), + rest.get( + 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos/repo/default-branch', + (_, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(responseOfDefaultBranch), + ); + }, + ), rest.post( 'https://hosted.bitbucket.com/rest/api/1.0/projects/project/repos/repo/pull-requests', (_, res, ctx) => { @@ -203,7 +221,7 @@ describe('publish:bitbucketServer:pull-request', () => { ]; const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.ts b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.ts index 8fddffaba4..440e402764 100644 --- a/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.ts +++ b/plugins/scaffolder-backend-module-bitbucket-server/src/actions/bitbucketServerPullRequest.ts @@ -204,6 +204,39 @@ const createBranch = async (opts: { return await response.json(); }; +const getDefaultBranch = async (opts: { + project: string; + repo: string; + authorization: string; + apiBaseUrl: string; +}) => { + const { project, repo, authorization, apiBaseUrl } = opts; + let response: Response; + + const options: RequestInit = { + method: 'GET', + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + }, + }; + + try { + response = await fetch( + `${apiBaseUrl}/projects/${project}/repos/${repo}/default-branch`, + options, + ); + } catch (error) { + throw error; + } + + const { displayId } = await response.json(); + const defaultBranch = displayId; + if (!defaultBranch) { + throw new Error(`Could not fetch default branch for ${project}/${repo}`); + } + return defaultBranch; +}; /** * Creates a BitbucketServer Pull Request action. * @public @@ -288,7 +321,7 @@ export function createPublishBitbucketServerPullRequestAction(options: { repoUrl, title, description, - targetBranch = 'master', + targetBranch, sourceBranch, gitAuthorName, gitAuthorEmail, @@ -326,10 +359,20 @@ export function createPublishBitbucketServerPullRequestAction(options: { const apiBaseUrl = integrationConfig.config.apiBaseUrl; + let finalTargetBranch = targetBranch; + if (!finalTargetBranch) { + finalTargetBranch = await getDefaultBranch({ + project, + repo, + authorization, + apiBaseUrl, + }); + } + const toRef = await findBranches({ project, repo, - branchName: targetBranch, + branchName: finalTargetBranch!, authorization, apiBaseUrl, }); diff --git a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md index 97b68baf15..062d54226d 100644 --- a/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,84 @@ # @backstage/plugin-scaffolder-backend-module-bitbucket +## 0.2.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.2.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.2.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12-next.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.2.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.11-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.11-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.9 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-bitbucket/package.json b/plugins/scaffolder-backend-module-bitbucket/package.json index bc3a578fdb..a9f499ec74 100644 --- a/plugins/scaffolder-backend-module-bitbucket/package.json +++ b/plugins/scaffolder-backend-module-bitbucket/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-bitbucket", - "version": "0.2.9", + "version": "0.2.13-next.2", "description": "The bitbucket module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -50,7 +50,7 @@ "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts index 6633246d70..d452f2e11f 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.examples.test.ts @@ -29,7 +29,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -67,7 +67,7 @@ describe('publish:bitbucket', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts index a63d657904..654b0fffcb 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/actions/bitbucket.test.ts @@ -28,7 +28,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { import { createPublishBitbucketAction } from './bitbucket'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -63,7 +63,7 @@ describe('publish:bitbucket', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts b/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts index a17c88c3a4..a9daa771f0 100644 --- a/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts +++ b/plugins/scaffolder-backend-module-bitbucket/src/deprecated.ts @@ -19,25 +19,29 @@ import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bi export { createPublishBitbucketAction } from './actions/bitbucket'; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ export const createPublishBitbucketCloudAction = bitbucketCloud.createPublishBitbucketCloudAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ export const createBitbucketPipelinesRunAction = bitbucketCloud.createBitbucketPipelinesRunAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerAction = bitbucketServer.createPublishBitbucketServerAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = bitbucketServer.createPublishBitbucketServerPullRequestAction; diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md index 45cb0bc6fc..2621b1d8f3 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/plugin-scaffolder-backend-module-confluence-to-markdown +## 0.2.24-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.24-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.2.24-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.2.23 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.2.23-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.2.20 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json index 02fa559e56..b4b10f7d4c 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/package.json +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown", - "version": "0.2.20", + "version": "0.2.24-next.2", "description": "The confluence-to-markdown module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -53,7 +53,7 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "fs-extra": "^11.2.0", "git-url-parse": "^14.0.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "node-html-markdown": "^1.3.0", "yaml": "^2.0.0" }, diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts index da82849c1e..36ae750723 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.examples.test.ts @@ -20,7 +20,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -32,7 +32,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('confluence:transform:markdown examples', () => { const baseUrl = `https://confluence.example.com`; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const config = new ConfigReader({ confluence: { diff --git a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts index b19259b3df..4c3c6b891f 100644 --- a/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts +++ b/plugins/scaffolder-backend-module-confluence-to-markdown/src/actions/confluence/confluenceToMarkdown.test.ts @@ -21,7 +21,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { createMockDirectory, mockServices, - setupRequestMockHandlers, + registerMswTestHooks, } from '@backstage/backend-test-utils'; import type { ActionContext } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; @@ -31,7 +31,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test- describe('confluence:transform:markdown', () => { const baseUrl = `https://nodomain.confluence.com`; const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const config = new ConfigReader({ confluence: { diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 8df8ef83e5..160953da23 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.2.47-next.0 + +### Patch Changes + +- dae85df: Add examples for `fetch:cookiecutter` scaffolder action & improve related tests +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.2.46 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.2.46-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.2.45-next.0 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.2.43 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index a6e9cfbf66..7749830304 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", - "version": "0.2.43", + "version": "0.2.47-next.2", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", "backstage": { "role": "backend-plugin-module", @@ -53,6 +53,7 @@ "command-exists": "^1.2.9", "fs-extra": "^11.2.0", "winston": "^3.2.1", + "yaml": "^2.0.0", "yn": "^4.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts new file mode 100644 index 0000000000..ae46ef1bd6 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.test.ts @@ -0,0 +1,274 @@ +/* + * 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 { UrlReader, ContainerRunner } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createFetchCookiecutterAction } from './cookiecutter'; +import { join } from 'path'; +import type { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { examples } from './cookiecutter.examples'; +import yaml from 'yaml'; + +const executeShellCommand = jest.fn(); +const commandExists = jest.fn(); +const fetchContents = jest.fn(); + +jest.mock('@backstage/plugin-scaffolder-node', () => ({ + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + fetchContents: (...args: any[]) => fetchContents(...args), + executeShellCommand: (...args: any[]) => executeShellCommand(...args), +})); + +jest.mock( + 'command-exists', + () => + (...args: any[]) => + commandExists(...args), +); + +describe('fetch:cookiecutter', () => { + const mockDir = createMockDirectory({ mockOsTmpDir: true }); + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }), + ); + + const mockTmpDir = mockDir.path; + + let mockContext: ActionContext<{ + url: string; + targetPath?: string; + values: JsonObject; + copyWithoutRender?: string[]; + extensions?: string[]; + imageName?: string; + }>; + + const containerRunner: jest.Mocked = { + runContainer: jest.fn(), + }; + + const mockReader: UrlReader = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + const action = createFetchCookiecutterAction({ + integrations, + containerRunner, + reader: mockReader, + }); + + beforeEach(() => { + jest.resetAllMocks(); + + mockContext = createMockActionContext({ + input: { + url: 'https://google.com/cookie/cutter', + targetPath: 'something', + values: { + help: 'me', + }, + }, + templateInfo: { + entityRef: 'template:default/cookiecutter', + baseUrl: 'somebase', + }, + workspacePath: mockTmpDir, + }); + mockDir.setContent({ template: {} }); + + commandExists.mockResolvedValue(null); + + // Mock when run container is called it creates some new files in the mock filesystem + containerRunner.runContainer.mockImplementation(async () => { + mockDir.setContent({ + 'intermediate/testfile.json': '{}', + }); + }); + + // Mock when executeShellCommand is called it creates some new files in the mock filesystem + executeShellCommand.mockImplementation(async () => { + mockDir.setContent({ + 'intermediate/testfile.json': '{}', + }); + }); + }); + + it(`should ${examples[0].description}`, async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[1].description}`, async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: input, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[2].description}`, async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[3].description}`, async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); + + it(`should ${examples[4].description}`, async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + commandExists.mockResolvedValue(false); + + fetchContents.mockImplementation(() => Promise.resolve()); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: mockContext.input.url, + outputPath: join( + mockTmpDir, + 'template', + "{{cookiecutter and 'contents'}}", + ), + }), + ); + expect(containerRunner.runContainer).toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts new file mode 100644 index 0000000000..6ebada50f0 --- /dev/null +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.examples.ts @@ -0,0 +1,119 @@ +/* + * 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Fetch and template using cookiecutter with minimal options.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with custom target path.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + targetPath: 'custom-path', + values: { + help: 'me', + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with copyWithoutRender.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + copyWithoutRender: ['.github/*', '*.md'], + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with custom extensions.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + extensions: ['jinja2_time.TimeExtension'], + }, + }, + ], + }), + }, + { + description: + 'Fetch and template using cookiecutter with a custom Docker image.', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:cookiecutter', + name: 'Fetch and Template Using Cookiecutter', + input: { + url: 'https://google.com/cookie/cutter', + values: { + help: 'me', + }, + imageName: 'custom/cookiecutter-image', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index d170acb5d0..efbe0a07cf 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -28,6 +28,7 @@ import { fetchContents, executeShellCommand, } from '@backstage/plugin-scaffolder-node'; +import { examples } from './cookiecutter.examples'; export class CookiecutterRunner { private readonly containerRunner?: ContainerRunner; @@ -156,6 +157,7 @@ export function createFetchCookiecutterAction(options: { id: 'fetch:cookiecutter', description: 'Downloads a template from the given URL into the workspace, and runs cookiecutter on it.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend-module-gcp/.eslintrc.js b/plugins/scaffolder-backend-module-gcp/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-gcp/CHANGELOG.md b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md new file mode 100644 index 0000000000..c759549d90 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/CHANGELOG.md @@ -0,0 +1,77 @@ +# @backstage/plugin-scaffolder-backend-module-gcp + +## 0.1.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.0 + +### Minor Changes + +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.0-next.3 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.0-next.2 + +### Minor Changes + +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.0.1 diff --git a/plugins/scaffolder-backend-module-gcp/README.md b/plugins/scaffolder-backend-module-gcp/README.md new file mode 100644 index 0000000000..a9ce86cc6b --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-scaffolder-backend-module-gcp + +The GCP bucket module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend). + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/scaffolder-backend-module-gcp/api-report.md b/plugins/scaffolder-backend-module-gcp/api-report.md new file mode 100644 index 0000000000..d75a6ce155 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/api-report.md @@ -0,0 +1,11 @@ +## API Report File for "@backstage/plugin-scaffolder-backend-module-gcp" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeatureCompat } from '@backstage/backend-plugin-api'; + +// @public +const gcpBucketModule: BackendFeatureCompat; +export default gcpBucketModule; +``` diff --git a/plugins/scaffolder-backend-module-gcp/catalog-info.yaml b/plugins/scaffolder-backend-module-gcp/catalog-info.yaml new file mode 100644 index 0000000000..175f9860d1 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-scaffolder-backend-module-gcp + title: '@backstage/plugin-scaffolder-backend-module-gcp' + description: The GCP module for @backstage/plugin-scaffolder-backend +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/scaffolder-backend-module-gcp/config.d.ts b/plugins/scaffolder-backend-module-gcp/config.d.ts new file mode 100644 index 0000000000..acd3eebdd9 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/config.d.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export interface Config { + /** Configuration options for the scaffolder plugin */ + scaffolder?: { + /** + * Sets GCP bucket name to store serialized workspace for scaffolder tasks. + */ + EXPERIMENTAL_workspaceSerializationGcpBucketName?: string; + }; +} diff --git a/plugins/scaffolder-backend-module-gcp/knip-report.md b/plugins/scaffolder-backend-module-gcp/knip-report.md new file mode 100644 index 0000000000..2661c35327 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/knip-report.md @@ -0,0 +1,2 @@ +# Knip report + diff --git a/plugins/scaffolder-backend-module-gcp/package.json b/plugins/scaffolder-backend-module-gcp/package.json new file mode 100644 index 0000000000..33459ba5b6 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/package.json @@ -0,0 +1,59 @@ +{ + "name": "@backstage/plugin-scaffolder-backend-module-gcp", + "version": "0.1.1-next.2", + "description": "The GCP Bucket module for @backstage/plugin-scaffolder-backend", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "scaffolder", + "pluginPackage": "@backstage/plugin-scaffolder-backend" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder-backend-module-gcp" + }, + "license": "Apache-2.0", + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "main": "src/index.ts", + "types": "src/index.ts", + "typesVersions": { + "*": { + "package.json": [ + "package.json" + ] + } + }, + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/integration": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", + "@google-cloud/storage": "^7.0.0", + "raw-body": "^2.4.1" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/scaffolder-backend-module-gcp/src/index.ts b/plugins/scaffolder-backend-module-gcp/src/index.ts new file mode 100644 index 0000000000..05498e3edd --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 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. + */ + +/** + * A module for the scaffolder backend that lets you interact with azure + * + * @packageDocumentation + */ + +export { gcpBucketModule as default } from './module'; diff --git a/plugins/scaffolder-backend-module-gcp/src/module.ts b/plugins/scaffolder-backend-module-gcp/src/module.ts new file mode 100644 index 0000000000..0f2f37f5af --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/src/module.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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 { + coreServices, + createBackendModule, +} from '@backstage/backend-plugin-api'; +import { scaffolderWorkspaceProviderExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha'; +import { GcpBucketWorkspaceProvider } from './providers/GcpBucketWorkspaceProvider'; + +/** + * @public + * The Azure Module for the Scaffolder Backend + */ +export const gcpBucketModule = createBackendModule({ + moduleId: 'gcp', + pluginId: 'scaffolder', + register({ registerInit }) { + registerInit({ + deps: { + scaffolderWorkspaceProviders: scaffolderWorkspaceProviderExtensionPoint, + config: coreServices.rootConfig, + logger: coreServices.logger, + }, + async init({ config, logger, scaffolderWorkspaceProviders }) { + scaffolderWorkspaceProviders.addProviders({ + gcpBucket: GcpBucketWorkspaceProvider.create(logger, config), + }); + }, + }); + }, +}); diff --git a/plugins/scaffolder-backend-module-gcp/src/providers/GcpBucketWorkspaceProvider.ts b/plugins/scaffolder-backend-module-gcp/src/providers/GcpBucketWorkspaceProvider.ts new file mode 100644 index 0000000000..3529073648 --- /dev/null +++ b/plugins/scaffolder-backend-module-gcp/src/providers/GcpBucketWorkspaceProvider.ts @@ -0,0 +1,99 @@ +/* + * 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 { Config } from '@backstage/config'; +import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; + +import getRawBody from 'raw-body'; +import { Storage } from '@google-cloud/storage'; +import { LoggerService } from '@backstage/backend-plugin-api'; + +import { + serializeWorkspace, + restoreWorkspace, +} from '@backstage/plugin-scaffolder-node/alpha'; + +export class GcpBucketWorkspaceProvider implements WorkspaceProvider { + static create(logger: LoggerService, config?: Config) { + return new GcpBucketWorkspaceProvider(new Storage(), logger, config); + } + + private constructor( + private readonly storage: Storage, + private readonly logger: LoggerService, + private readonly config?: Config, + ) {} + + public async cleanWorkspace(options: { taskId: string }): Promise { + const file = this.storage + .bucket(this.getGcpBucketName()) + .file(options.taskId); + + const result = await file.exists(); + if (result[0]) { + await file.delete(); + } + } + + public async serializeWorkspace(options: { + path: string; + taskId: string; + }): Promise { + const fileCloud = this.storage + .bucket(this.getGcpBucketName()) + .file(options.taskId); + const { contents: workspace } = await serializeWorkspace(options); + try { + await fileCloud.save(workspace, { + contentType: 'application/x-tar', + }); + } catch (err) { + this.logger.error( + `An error occurred during uploading the workspace of task ${ + options.taskId + } into GCP bucket ${this.getGcpBucketName()}`, + ); + } + this.logger.info( + `Workspace for task ${options.taskId} has been serialized.`, + ); + } + + public async rehydrateWorkspace(options: { + taskId: string; + targetPath: string; + }): Promise { + const bucket = this.storage.bucket(this.getGcpBucketName()); + const file = bucket.file(options.taskId); + const result = await file.exists(); + if (result[0]) { + const workspace = await getRawBody(file.createReadStream()); + await restoreWorkspace({ path: options.targetPath, buffer: workspace }); + } + } + + private getGcpBucketName(): string { + const bucketName = this.config?.getOptionalString( + 'scaffolder.EXPERIMENTAL_workspaceSerializationGcpBucketName', + ); + if (!bucketName) { + throw new Error( + `You've missed to configure scaffolder.EXPERIMENTAL_workspaceSerializationGcpBucketName in app-config.yaml file`, + ); + } + return bucketName; + } +} diff --git a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md index 2703b15871..811e9a80cc 100644 --- a/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-scaffolder-backend-module-gerrit +## 0.1.15-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.15-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.15-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.14 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.14-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.11 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json index 15aac0cceb..0e50124013 100644 --- a/plugins/scaffolder-backend-module-gerrit/package.json +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gerrit", - "version": "0.1.11", + "version": "0.1.15-next.2", "description": "The gerrit module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -47,7 +47,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index 1f069d3835..c102353209 100644 --- a/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -30,7 +30,7 @@ import path from 'path'; import { createPublishGerritAction } from './gerrit'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; @@ -61,7 +61,7 @@ describe('publish:gerrit', () => { }, }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md index d2604729c2..0a05340f77 100644 --- a/plugins/scaffolder-backend-module-gitea/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitea/CHANGELOG.md @@ -1,5 +1,73 @@ # @backstage/plugin-scaffolder-backend-module-gitea +## 0.1.13-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.13-next.1 + +### Patch Changes + +- 24de005: Added test cases for publish:gitea examples +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.13-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.12 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.12-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.11-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.9 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitea/package.json b/plugins/scaffolder-backend-module-gitea/package.json index 55f948b87e..cb888b65ad 100644 --- a/plugins/scaffolder-backend-module-gitea/package.json +++ b/plugins/scaffolder-backend-module-gitea/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitea", - "version": "0.1.9", + "version": "0.1.13-next.2", "description": "The gitea module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", @@ -47,7 +47,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.0.0" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.test.ts new file mode 100644 index 0000000000..5575921c34 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.test.ts @@ -0,0 +1,806 @@ +/* + * Copyright 2024 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 { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { createPublishGiteaAction } from './gitea'; +import { + getRepoSourceDirectory, + initRepoAndPush, +} from '@backstage/plugin-scaffolder-node'; +import { rest } from 'msw'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { setupServer } from 'msw/node'; +import { examples } from './gitea.examples'; +import yaml from 'yaml'; + +jest.mock('@backstage/plugin-scaffolder-node', () => { + return { + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + initRepoAndPush: jest.fn().mockResolvedValue({ + commitHash: '431f19cc36b551763d157f1b5e4a4b446165dbn2', + }), + }; +}); + +describe('publish:gitea', () => { + const config = new ConfigReader({ + integrations: { + gitea: [ + { + host: 'gitea.com', + username: 'sample_user', + password: 'password_token', + }, + ], + }, + }); + + const description = 'gitea description'; + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGiteaAction({ integrations, config }); + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitea.com?repo=repo&owner=owner', + description, + }, + }); + + const server = setupServer(); + registerMswTestHooks(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it(`should ${examples[0].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description: 'Initialize a gitea repository', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: true, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + it(`should ${examples[3].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/staging', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'staging', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + it(`should ${examples[4].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining( + 'Initial Commit Message\n\nChange-Id:', + ), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + it(`should ${examples[5].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: 'John Doe', + }, + }); + }); + + it(`should ${examples[6].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: 'johndoe@email.com', + name: undefined, + }, + }); + }); + + it(`should ${examples[7].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, 'repository/'), + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + it(`should ${examples[8].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/staging', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description: 'Initialize a gitea repository', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: getRepoSourceDirectory(mockContext.workspacePath, 'repository/'), + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'staging', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining( + 'Initial Commit Message\n\nChange-Id:', + ), + gitAuthorInfo: { + email: 'johndoe@email.com', + name: 'John Doe', + }, + }); + }); + + it(`should ${examples[0].description}`, async () => { + server.use( + rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({ + id: 1, + name: 'org1', + visibility: 'public', + repo_admin_change_team_access: false, + username: 'org1', + }), + ); + }), + rest.get( + 'https://gitea.com/org1/repo/src/branch/main', + (_req, res, ctx) => { + return res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }, + ), + rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => { + // Basic auth must match the user and password defined part of the config + expect(req.headers.get('Authorization')).toBe( + 'basic c2FtcGxlX3VzZXI6cGFzc3dvcmRfdG9rZW4=', + ); + expect(req.body).toEqual({ + name: 'repo', + private: false, + description, + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + repoUrl: 'gitea.com?repo=repo&owner=org1', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gitea.com/org1/repo.git', + defaultBranch: 'main', + auth: { username: 'sample_user', password: 'password_token' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: { + email: undefined, + name: undefined, + }, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts index dd4d038ead..5cf640de87 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.examples.ts @@ -78,7 +78,7 @@ export const examples: TemplateExample[] = [ name: 'Publish to Gitea', input: { repoUrl: 'gitea.com?repo=repo&owner=owner', - defaultBranch: 'main', + defaultBranch: 'staging', }, }, ], diff --git a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts index 8a72265df0..7812ded60a 100644 --- a/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts +++ b/plugins/scaffolder-backend-module-gitea/src/actions/gitea.test.ts @@ -18,7 +18,7 @@ import { ConfigReader } from '@backstage/config'; import { createPublishGiteaAction } from './gitea'; import { initRepoAndPush } from '@backstage/plugin-scaffolder-node'; import { rest } from 'msw'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { setupServer } from 'msw/node'; @@ -62,7 +62,7 @@ describe('publish:gitea', () => { }); const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); beforeEach(() => { jest.resetAllMocks(); diff --git a/plugins/scaffolder-backend-module-github/CHANGELOG.md b/plugins/scaffolder-backend-module-github/CHANGELOG.md index 16d121113e..e9d841aa0c 100644 --- a/plugins/scaffolder-backend-module-github/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-github/CHANGELOG.md @@ -1,5 +1,102 @@ # @backstage/plugin-scaffolder-backend-module-github +## 0.4.1-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.4.1-next.1 + +### Patch Changes + +- d21d307: Added examples for github:environment:create action and improve its test cases +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.4.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.4.0 + +### Minor Changes + +- 70c4b36: Adds support for custom tag policies when creating GitHub environments. + +### Patch Changes + +- ccfc9d1: Fixed bug resulting from missing required owner and repo arguments in `getEnvironmentPublicKey` in action `github:environment:create`. + + Adding environment secrets now works as expected. + +- 141f366: Added action to enable GitHub Pages on a repo +- 4410fed: Fixed issue with octokit call missing owner and repo when creating environment variables and secrets using github:environment:create action +- dfaa28d: Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action + + Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.4.0-next.1 + +### Patch Changes + +- 141f366: Added action to enable GitHub Pages on a repo +- dfaa28d: Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:publish` action + + Adds `requireLastPushApproval` input property to configure Branch Protection Settings in `github:repo:push` action + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.4.0-next.0 + +### Minor Changes + +- 70c4b36: Adds support for custom tag policies when creating GitHub environments. + +### Patch Changes + +- 4410fed: Fixed issue with octokit call missing owner and repo when creating environment variables and secrets using github:environment:create action +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.3.0 ### Minor Changes diff --git a/plugins/scaffolder-backend-module-github/api-report.md b/plugins/scaffolder-backend-module-github/api-report.md index beabdfaaf6..4ec5795dd9 100644 --- a/plugins/scaffolder-backend-module-github/api-report.md +++ b/plugins/scaffolder-backend-module-github/api-report.md @@ -77,6 +77,7 @@ export function createGithubEnvironmentAction(options: { } | undefined; customBranchPolicyNames?: string[] | undefined; + customTagPolicyNames?: string[] | undefined; environmentVariables?: | { [key: string]: string; @@ -106,6 +107,21 @@ export function createGithubIssuesLabelAction(options: { JsonObject >; +// @public +export function createGithubPagesEnableAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}): TemplateAction< + { + repoUrl: string; + buildType?: 'legacy' | 'workflow' | undefined; + sourceBranch?: string | undefined; + sourcePath?: '/' | '/docs' | undefined; + token?: string | undefined; + }, + JsonObject +>; + // @public export interface CreateGithubPullRequestActionOptions { clientFactory?: (input: { @@ -256,6 +272,7 @@ export function createGithubRepoPushAction(options: { sourcePath?: string | undefined; token?: string | undefined; requiredCommitSigning?: boolean | undefined; + requireLastPushApproval?: boolean | undefined; }, JsonObject >; @@ -328,6 +345,7 @@ export function createPublishGithubAction(options: { requiredStatusCheckContexts?: string[] | undefined; requireBranchesToBeUpToDate?: boolean | undefined; requiredConversationResolution?: boolean | undefined; + requireLastPushApproval?: boolean | undefined; repoVisibility?: 'internal' | 'private' | 'public' | undefined; collaborators?: | ( diff --git a/plugins/scaffolder-backend-module-github/package.json b/plugins/scaffolder-backend-module-github/package.json index c86a10fb49..c373839d13 100644 --- a/plugins/scaffolder-backend-module-github/package.json +++ b/plugins/scaffolder-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-github", - "version": "0.3.0", + "version": "0.4.1-next.2", "description": "The github module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "backend-plugin-module", diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts index ea92201eec..9989a80724 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHelpers.ts @@ -38,6 +38,7 @@ type BranchProtectionOptions = { }; requireBranchesToBeUpToDate?: boolean; requiredConversationResolution?: boolean; + requireLastPushApproval: boolean; defaultBranch?: string; enforceAdmins?: boolean; dismissStaleReviews?: boolean; @@ -56,6 +57,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, requiredConversationResolution = false, + requireLastPushApproval = false, defaultBranch = 'master', enforceAdmins = true, dismissStaleReviews = false, @@ -88,6 +90,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ require_code_owner_reviews: requireCodeOwnerReviews, bypass_pull_request_allowances: bypassPullRequestAllowances, dismiss_stale_reviews: dismissStaleReviews, + require_last_push_approval: requireLastPushApproval, }, required_conversation_resolution: requiredConversationResolution, }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts index 606424bef0..7c5f7efa3d 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/gitHubEnvironment.examples.ts @@ -96,4 +96,210 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitHub Environment with Custom Tag Policies', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + customTagPolicyNames: ['release/*/*', 'v*.*.*'], + }, + }, + ], + }), + }, + { + description: + 'Create a GitHub Environment with Both Branch and Tag Policies', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customBranchPolicyNames: ['feature/*', 'hotfix/*'], + customTagPolicyNames: ['release/*', 'v*'], + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with Full Configuration', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customBranchPolicyNames: ['dev/*', 'test/*'], + customTagPolicyNames: ['v1.*', 'v2.*'], + environmentVariables: { + API_KEY: '123456789', + NODE_ENV: 'production', + }, + secrets: { + DATABASE_URL: 'supersecretdatabaseurl', + API_SECRET: 'supersecretapisecret', + }, + token: 'ghp_abcdef1234567890', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with Only Token Authentication', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + token: 'ghp_abcdef1234567890', + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with No Deployment Policies', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: null, + }, + }, + ], + }), + }, + { + description: + 'Create a GitHub Environment with Custom Branch and Tag Policies', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customBranchPolicyNames: ['release/*', 'hotfix/*'], + customTagPolicyNames: ['v*', 'release-*'], + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with Environment Variables Only', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + environmentVariables: { + VAR1: 'value1', + VAR2: 'value2', + }, + }, + }, + ], + }), + }, + { + description: 'Create a GitHub Environment with Deployment Secrets Only', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + secrets: { + SECRET1: 'secretvalue1', + SECRET2: 'secretvalue2', + }, + }, + }, + ], + }), + }, + { + description: + 'Create a GitHub Environment with Deployment Branch Policy and Token', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + deploymentBranchPolicy: { + protected_branches: true, + custom_branch_policies: false, + }, + token: 'ghp_abcdef1234567890', + }, + }, + ], + }), + }, + { + description: + 'Create a GitHub Environment with Environment Variables, Secrets, and Token', + example: yaml.stringify({ + steps: [ + { + action: 'github:environment:create', + name: 'Create Environment', + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + environmentVariables: { + VAR1: 'value1', + VAR2: 'value2', + }, + secrets: { + SECRET1: 'secretvalue1', + SECRET2: 'secretvalue2', + }, + token: 'ghp_abcdef1234567890', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts index 9eda9a2eb0..31ec7ecc90 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.test.ts @@ -1010,174 +1010,6 @@ describe('publish:github', () => { ); }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { - mockOctokit.rest.users.getByUsername.mockResolvedValue({ - data: { type: 'User' }, - }); - - mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - name: 'repo', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requireCodeOwnerReviews: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: true, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requireCodeOwnerReviews: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - }); - - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of enforceAdmins', async () => { - mockOctokit.rest.users.getByUsername.mockResolvedValue({ - data: { type: 'User' }, - }); - - mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - name: 'repo', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - protectEnforceAdmins: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: false, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - protectEnforceAdmins: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredStatusCheckContexts and requireBranchesToBeUpToDate', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, @@ -1204,6 +1036,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1232,6 +1065,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1259,6 +1093,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: ['statusCheck'], requireBranchesToBeUpToDate: false, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1285,6 +1120,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1346,255 +1182,6 @@ describe('publish:github', () => { }); }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of dismissStaleReviews', async () => { - mockOctokit.rest.users.getByUsername.mockResolvedValue({ - data: { type: 'User' }, - }); - - mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - name: 'repo', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - dismissStaleReviews: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: true, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - dismissStaleReviews: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredConversationResolution', async () => { - mockOctokit.rest.users.getByUsername.mockResolvedValue({ - data: { type: 'User' }, - }); - - mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - name: 'repo', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredConversationResolution: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: true, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredConversationResolution: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredApprovingReviewCount', async () => { - mockOctokit.rest.users.getByUsername.mockResolvedValue({ - data: { type: 'User' }, - }); - - mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - name: 'repo', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredApprovingReviewCount: 2, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 2, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredApprovingReviewCount: 0, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 0, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - }); it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of restrictions', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, @@ -1621,6 +1208,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1653,6 +1241,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1685,6 +1274,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1719,6 +1309,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1753,6 +1344,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1787,94 +1379,12 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, }); }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredCommitSigning', async () => { - mockOctokit.rest.users.getByUsername.mockResolvedValue({ - data: { type: 'User' }, - }); - - mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ - data: { - name: 'repo', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredCommitSigning: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: false, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredCommitSigning: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repo', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - restrictions: undefined, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - requiredCommitSigning: true, - }); - }); it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of bypassPullRequestAllowances', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, @@ -1901,6 +1411,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1931,6 +1442,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1961,6 +1473,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -1991,6 +1504,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -2025,6 +1539,7 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, @@ -2059,9 +1574,145 @@ describe('publish:github', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, requiredCommitSigning: false, }); }); + + it.each([ + { + inputProperty: 'dismissStaleReviews', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'requiredConversationResolution', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'requireLastPushApproval', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'requiredApprovingReviewCount', + defaultValue: 1, + overrideValue: 2, + }, + { + inputProperty: 'requiredCommitSigning', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'protectEnforceAdmins', + defaultValue: true, + overrideValue: false, + octokitParameter: 'enforceAdmins', + }, + { + inputProperty: 'requireCodeOwnerReviews', + defaultValue: false, + overrideValue: true, + }, + ])( + 'should call enableBranchProtectionOnDefaultRepoBranch with the correct values of $inputProperty', + async ({ + inputProperty, + defaultValue, + overrideValue, + octokitParameter, + }) => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repo', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + requireLastPushApproval: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + [octokitParameter || inputProperty]: defaultValue, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + [inputProperty]: overrideValue, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + requireLastPushApproval: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + [octokitParameter || inputProperty]: overrideValue, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + [inputProperty]: defaultValue, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repo', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + restrictions: undefined, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + requireLastPushApproval: false, + enforceAdmins: true, + dismissStaleReviews: false, + requiredCommitSigning: false, + [octokitParameter || inputProperty]: defaultValue, + }); + }, + ); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/github.ts b/plugins/scaffolder-backend-module-github/src/actions/github.ts index f32b65bc9a..c8b22de7e7 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/github.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/github.ts @@ -86,6 +86,7 @@ export function createPublishGithubAction(options: { requiredStatusCheckContexts?: string[]; requireBranchesToBeUpToDate?: boolean; requiredConversationResolution?: boolean; + requireLastPushApproval?: boolean; repoVisibility?: 'private' | 'internal' | 'public'; collaborators?: Array< | { @@ -137,6 +138,7 @@ export function createPublishGithubAction(options: { requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, requiredConversationResolution: inputProps.requiredConversationResolution, + requireLastPushApproval: inputProps.requireLastPushApproval, repoVisibility: inputProps.repoVisibility, defaultBranch: inputProps.defaultBranch, protectDefaultBranch: inputProps.protectDefaultBranch, @@ -187,6 +189,7 @@ export function createPublishGithubAction(options: { requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, requiredConversationResolution = false, + requireLastPushApproval = false, repoVisibility = 'private', defaultBranch = 'master', protectDefaultBranch = true, @@ -274,6 +277,7 @@ export function createPublishGithubAction(options: { requiredStatusCheckContexts, requireBranchesToBeUpToDate, requiredConversationResolution, + requireLastPushApproval, config, ctx.logger, gitCommitMessage, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts index ecb4e9f092..c8b133859b 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.examples.test.ts @@ -171,6 +171,7 @@ describe('github:environment:create examples', () => { repo: 'repository', environment_name: 'envname', name: 'main', + type: 'branch', }); expect( @@ -180,6 +181,7 @@ describe('github:environment:create examples', () => { repo: 'repository', environment_name: 'envname', name: '*.*.*', + type: 'branch', }); expect( @@ -219,6 +221,8 @@ describe('github:environment:create examples', () => { mockOctokit.rest.actions.createEnvironmentVariable, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', name: 'key1', value: 'val1', @@ -227,6 +231,8 @@ describe('github:environment:create examples', () => { mockOctokit.rest.actions.createEnvironmentVariable, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', name: 'key2', value: 'val2', @@ -239,6 +245,8 @@ describe('github:environment:create examples', () => { mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', secret_name: 'secret1', key_id: 'keyid', @@ -248,10 +256,530 @@ describe('github:environment:create examples', () => { mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', secret_name: 'secret2', key_id: 'keyid', encrypted_value: expect.any(String), }); }); + + it(`should ${examples[4].description}`, async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'release/*/*', + owner: 'owner', + repo: 'repository', + type: 'tag', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'v*.*.*', + owner: 'owner', + repo: 'repository', + type: 'tag', + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[5].description}`, async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + custom_branch_policies: true, + protected_branches: false, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledTimes(4); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'release/*', + owner: 'owner', + repo: 'repository', + type: 'tag', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'feature/*', + owner: 'owner', + repo: 'repository', + type: 'branch', + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[6].description}`, async () => { + const input = yaml.parse(examples[6].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + custom_branch_policies: true, + protected_branches: false, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledTimes(4); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'dev/*', + owner: 'owner', + repo: 'repository', + type: 'branch', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'test/*', + owner: 'owner', + repo: 'repository', + type: 'branch', + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledTimes(2); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'API_KEY', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + value: '123456789', + }); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'NODE_ENV', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + value: 'production', + }); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + encrypted_value: expect.any(String), + environment_name: 'envname', + key_id: 'keyid', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + secret_name: 'API_SECRET', + }); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + encrypted_value: expect.any(String), + environment_name: 'envname', + key_id: 'keyid', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + secret_name: 'DATABASE_URL', + }); + }); + + it(`should ${examples[7].description}`, async () => { + const input = yaml.parse(examples[7].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[8].description}`, async () => { + const input = yaml.parse(examples[8].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[9].description}`, async () => { + const input = yaml.parse(examples[9].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + custom_branch_policies: true, + protected_branches: false, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledTimes(4); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'release/*', + owner: 'owner', + repo: 'repository', + type: 'branch', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'hotfix/*', + owner: 'owner', + repo: 'repository', + type: 'branch', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'v*', + owner: 'owner', + repo: 'repository', + type: 'tag', + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[10].description}`, async () => { + const input = yaml.parse(examples[10].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'VAR1', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + value: 'value1', + }); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'VAR2', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + value: 'value2', + }); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[11].description}`, async () => { + const input = yaml.parse(examples[11].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + encrypted_value: expect.any(String), + environment_name: 'envname', + key_id: 'keyid', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + secret_name: 'SECRET1', + }); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + encrypted_value: expect.any(String), + environment_name: 'envname', + key_id: 'keyid', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + secret_name: 'SECRET2', + }); + }); + + it(`should ${examples[12].description}`, async () => { + const input = yaml.parse(examples[12].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + custom_branch_policies: false, + protected_branches: true, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).not.toHaveBeenCalled(); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).not.toHaveBeenCalled(); + }); + + it(`should ${examples[13].description}`, async () => { + const input = yaml.parse(examples[13].example).steps[0].input; + + await action.handler({ + ...mockContext, + input, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).not.toHaveBeenCalled(); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'VAR1', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + value: 'value1', + }); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + environment_name: 'envname', + name: 'VAR2', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + value: 'value2', + }); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + encrypted_value: expect.any(String), + environment_name: 'envname', + key_id: 'keyid', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + secret_name: 'SECRET1', + }); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + encrypted_value: expect.any(String), + environment_name: 'envname', + key_id: 'keyid', + owner: 'owner', + repo: 'repository', + repository_id: 'repoid', + secret_name: 'SECRET2', + }); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts index 16872525b4..139d8028eb 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.test.ts @@ -121,6 +121,7 @@ describe('github:environment:create', () => { }, }); }); + it('should work specify deploymentBranchPolicy custom', async () => { await action.handler({ ...mockContext, @@ -153,6 +154,7 @@ describe('github:environment:create', () => { repo: 'repository', environment_name: 'envname', name: 'main', + type: 'branch', }); expect( mockOctokit.rest.repos.createDeploymentBranchPolicy, @@ -161,6 +163,52 @@ describe('github:environment:create', () => { repo: 'repository', environment_name: 'envname', name: '*.*.*', + type: 'branch', + }); + }); + + it('should work specify deploymentTagPolicy custom', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customTagPolicyNames: ['main', '*.*.*'], + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: false, + custom_branch_policies: true, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: 'main', + type: 'tag', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: '*.*.*', + type: 'tag', }); }); @@ -192,6 +240,8 @@ describe('github:environment:create', () => { mockOctokit.rest.actions.createEnvironmentVariable, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', name: 'key1', value: 'val1', @@ -200,6 +250,8 @@ describe('github:environment:create', () => { mockOctokit.rest.actions.createEnvironmentVariable, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', name: 'key2', value: 'val2', @@ -234,6 +286,8 @@ describe('github:environment:create', () => { mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', secret_name: 'key1', key_id: 'keyid', @@ -243,10 +297,23 @@ describe('github:environment:create', () => { mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, ).toHaveBeenCalledWith({ repository_id: 'repoid', + owner: 'owner', + repo: 'repository', environment_name: 'envname', secret_name: 'key2', key_id: 'keyid', encrypted_value: expect.any(String), }); + expect( + mockOctokit.rest.actions.getEnvironmentPublicKey, + ).toHaveBeenCalledTimes(1); + expect( + mockOctokit.rest.actions.getEnvironmentPublicKey, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + }); }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts index 7f6f9f7fa3..d3e8d0e5d2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubEnvironment.ts @@ -44,6 +44,7 @@ export function createGithubEnvironmentAction(options: { custom_branch_policies: boolean; }; customBranchPolicyNames?: string[]; + customTagPolicyNames?: string[]; environmentVariables?: { [key: string]: string }; secrets?: { [key: string]: string }; token?: string; @@ -94,6 +95,16 @@ export function createGithubEnvironmentAction(options: { type: 'string', }, }, + customTagPolicyNames: { + title: 'Custom Tag Policy Name', + description: `The name pattern that tags must match in order to deploy to the environment. + + Wildcard characters will not match /. For example, to match tags that begin with release/ and contain an additional single slash, use release/*/*. For more information about pattern matching syntax, see the Ruby File.fnmatch documentation.`, + type: 'array', + items: { + type: 'string', + }, + }, environmentVariables: { title: 'Environment Variables', description: `Environment variables attached to the deployment environment`, @@ -118,6 +129,7 @@ export function createGithubEnvironmentAction(options: { name, deploymentBranchPolicy, customBranchPolicyNames, + customTagPolicyNames, environmentVariables, secrets, token: providedToken, @@ -153,6 +165,19 @@ export function createGithubEnvironmentAction(options: { await client.rest.repos.createDeploymentBranchPolicy({ owner: owner, repo: repo, + type: 'branch', + environment_name: name, + name: item, + }); + } + } + + if (customTagPolicyNames) { + for (const item of customTagPolicyNames) { + await client.rest.repos.createDeploymentBranchPolicy({ + owner: owner, + repo: repo, + type: 'tag', environment_name: name, name: item, }); @@ -162,6 +187,8 @@ export function createGithubEnvironmentAction(options: { for (const [key, value] of Object.entries(environmentVariables ?? {})) { await client.rest.actions.createEnvironmentVariable({ repository_id: repository.data.id, + owner: owner, + repo: repo, environment_name: name, name: key, value, @@ -172,6 +199,8 @@ export function createGithubEnvironmentAction(options: { const publicKeyResponse = await client.rest.actions.getEnvironmentPublicKey({ repository_id: repository.data.id, + owner: owner, + repo: repo, environment_name: name, }); @@ -193,6 +222,8 @@ export function createGithubEnvironmentAction(options: { await client.rest.actions.createOrUpdateEnvironmentSecret({ repository_id: repository.data.id, + owner: owner, + repo: repo, environment_name: name, secret_name: key, encrypted_value: encryptedBase64Secret, diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts new file mode 100644 index 0000000000..f0aa0ea5a2 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.test.ts @@ -0,0 +1,446 @@ +/* + * 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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { createGithubPagesEnableAction } from './githubPagesEnable'; +import { examples } from './githubPagesEnable.examples'; +import yaml from 'yaml'; + +const mockOctokit = { + request: jest.fn(), +}; + +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:pages', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'mock-token' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/', + token: 'gph_YourGitHubToken', + }, + }); + + beforeEach(() => { + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubPagesEnableAction({ + integrations, + githubCredentialsProvider, + }); + }); + + afterEach(jest.resetAllMocks); + + it(`Should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'owner', + repo: 'repo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'customPathRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'legacyOwner', + repo: 'legacyRepo', + build_type: 'legacy', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'branchOwner', + repo: 'customBranchRepo', + build_type: 'workflow', + source: { + branch: 'develop', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'fullCustomRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'minimalOwner', + repo: 'minimalRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + it(`Should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwner', + repo: 'customBuildPathRepo', + build_type: 'legacy', + source: { + branch: 'main', + path: '/custom-path', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'branchPathOwner', + repo: 'customBranchPathRepo', + build_type: 'workflow', + source: { + branch: 'feature-branch', + path: '/project-docs', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'customOwnerName', + repo: 'customRepoName', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'tokenOwner', + repo: 'customTokenRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/site', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[10].description}`, async () => { + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'tokenOwner', + repo: 'specificTokenRepo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); + + it(`Should ${examples[11].description}`, async () => { + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'docsOwner', + repo: 'docSiteRepo', + build_type: 'workflow', + source: { + branch: 'docs-branch', + path: '/docs-site', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts new file mode 100644 index 0000000000..57102c4b92 --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.examples.ts @@ -0,0 +1,245 @@ +/* + * Copyright 2024 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Enables GitHub Pages for a repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages', + name: 'Enable GitHub Pages', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom source path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-path', + name: 'Enable GitHub Pages with Custom Source Path', + input: { + repoUrl: 'github.com?repo=customPathRepo&owner=customOwner', + sourcePath: '/docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository using legacy build type.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-legacy', + name: 'Enable GitHub Pages with Legacy Build Type', + input: { + repoUrl: 'github.com?repo=legacyRepo&owner=legacyOwner', + buildType: 'legacy', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom source branch.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-branch', + name: 'Enable GitHub Pages with Custom Source Branch', + input: { + repoUrl: 'github.com?repo=customBranchRepo&owner=branchOwner', + sourceBranch: 'develop', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + + { + description: + 'Enables GitHub Pages for a repository with full customization.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-full-custom', + name: 'Enable GitHub Pages with Full Customization', + input: { + repoUrl: 'github.com?repo=fullCustomRepo&owner=customOwner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with minimal configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-minimal', + name: 'Enable GitHub Pages with Minimal Configuration', + input: { + repoUrl: 'github.com?repo=minimalRepo&owner=minimalOwner', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with custom build type and source path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-build-path', + name: 'Enable GitHub Pages with Custom Build Type and Source Path', + input: { + repoUrl: 'github.com?repo=customBuildPathRepo&owner=customOwner', + buildType: 'legacy', + sourcePath: '/custom-path', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with custom source branch and path.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-branch-path', + name: 'Enable GitHub Pages with Custom Source Branch and Path', + input: { + repoUrl: + 'github.com?repo=customBranchPathRepo&owner=branchPathOwner', + sourceBranch: 'feature-branch', + sourcePath: '/project-docs', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a custom owner and repository name.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-custom-owner-repo', + name: 'Enable GitHub Pages with Custom Owner and Repository Name', + input: { + repoUrl: 'github.com?repo=customRepoName&owner=customOwnerName', + token: 'gph_YourGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with full customization and a different token.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-full-custom-diff-token', + name: 'Enable GitHub Pages with Full Customization and Different Token', + input: { + repoUrl: 'github.com?repo=customTokenRepo&owner=tokenOwner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/site', + token: 'gph_DifferentGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a repository with a specific token for authorization.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-specific-token', + name: 'Enable GitHub Pages with Specific Token', + input: { + repoUrl: 'github.com?repo=specificTokenRepo&owner=tokenOwner', + token: 'gph_SpecificGitHubToken', + }, + }, + ], + }), + }, + { + description: + 'Enables GitHub Pages for a documentation site with custom configuration.', + example: yaml.stringify({ + steps: [ + { + action: 'github:pages', + id: 'github-pages-doc-site', + name: 'Enable GitHub Pages for Documentation Site', + input: { + repoUrl: 'github.com?repo=docSiteRepo&owner=docsOwner', + buildType: 'workflow', + sourceBranch: 'docs-branch', + sourcePath: '/docs-site', + token: 'gph_DocsGitHubToken', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.test.ts new file mode 100644 index 0000000000..203014b67b --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultGithubCredentialsProvider, + GithubCredentialsProvider, + ScmIntegrations, +} from '@backstage/integration'; +import { createGithubPagesEnableAction } from './githubPagesEnable'; + +const mockOctokit = { + request: jest.fn(), +}; + +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +describe('github:pages', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'mock-token' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let githubCredentialsProvider: GithubCredentialsProvider; + let action: TemplateAction; + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + buildType: 'workflow', + sourceBranch: 'main', + sourcePath: '/', + token: 'gph_YourGitHubToken', + }, + }); + + beforeEach(() => { + githubCredentialsProvider = + DefaultGithubCredentialsProvider.fromIntegrations(integrations); + action = createGithubPagesEnableAction({ + integrations, + githubCredentialsProvider, + }); + }); + + afterEach(jest.resetAllMocks); + + it('should work happy path', async () => { + await action.handler(mockContext); + + expect(mockOctokit.request).toHaveBeenCalledWith( + 'POST /repos/{owner}/{repo}/pages', + { + owner: 'owner', + repo: 'repo', + build_type: 'workflow', + source: { + branch: 'main', + path: '/', + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }, + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts new file mode 100644 index 0000000000..044005961e --- /dev/null +++ b/plugins/scaffolder-backend-module-github/src/actions/githubPagesEnable.ts @@ -0,0 +1,130 @@ +/* + * Copyright 2024 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 { InputError } from '@backstage/errors'; +import { + GithubCredentialsProvider, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { Octokit } from 'octokit'; +import { + createTemplateAction, + parseRepoUrl, +} from '@backstage/plugin-scaffolder-node'; +import { examples } from './githubPagesEnable.examples'; +import { getOctokitOptions } from '@backstage/plugin-scaffolder-backend-module-github'; + +/** + * Creates a new action that enables GitHub Pages for a repository. + * + * @public + */ +export function createGithubPagesEnableAction(options: { + integrations: ScmIntegrationRegistry; + githubCredentialsProvider?: GithubCredentialsProvider; +}) { + const { integrations, githubCredentialsProvider } = options; + + return createTemplateAction<{ + repoUrl: string; + buildType?: 'legacy' | 'workflow'; + sourceBranch?: string; + sourcePath?: '/' | '/docs'; + token?: string; + }>({ + id: 'github:pages:enable', + examples, + description: 'Enables GitHub Pages for a repository.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + buildType: { + title: 'Build Type', + type: 'string', + description: + 'The GitHub Pages build type - "legacy" or "workflow". Default is "workflow', + }, + sourceBranch: { + title: 'Source Branch', + type: 'string', + description: + 'The the GitHub Pages source branch. Default is "main"', + }, + sourcePath: { + title: 'Source Path', + type: 'string', + description: + 'The the GitHub Pages source path - "/" or "/docs". Default is "/"', + }, + token: { + title: 'Authorization Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + buildType = 'workflow', + sourceBranch = 'main', + sourcePath = '/', + token: providedToken, + } = ctx.input; + + const octokitOptions = await getOctokitOptions({ + integrations, + credentialsProvider: githubCredentialsProvider, + token: providedToken, + repoUrl: repoUrl, + }); + const client = new Octokit(octokitOptions); + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError('Invalid repository owner provided in repoUrl'); + } + + ctx.logger.info( + `Attempting to enable GitHub Pages for ${owner}/${repo} with "${buildType}" build type, on source branch "${sourceBranch}" and source path "${sourcePath}"`, + ); + + await client.request('POST /repos/{owner}/{repo}/pages', { + owner: owner, + repo: repo, + build_type: buildType, + source: { + branch: sourceBranch, + path: sourcePath, + }, + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + + ctx.logger.info('Completed enabling GitHub Pages'); + }, + }); +} diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts index 843a32f758..1a81a54c64 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.test.ts @@ -58,6 +58,7 @@ const mockOctokit = { getRepoPublicKey: jest.fn(), }, }, + request: jest.fn().mockResolvedValue({}), }; jest.mock('octokit', () => ({ Octokit: class { @@ -188,4 +189,2218 @@ describe('github:repo:create examples', () => { has_wiki: false, // disable wiki }); }); + + it(`Should ${examples[3].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://example.com', + }); + }); + + it(`Should ${examples[4].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[5].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[6].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[7].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: false, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[8].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'pull_request_title', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[9].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'blank', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[10].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: true, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[11].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[12].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[12].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[13].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[13].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[14].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[14].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[15].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[15].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[16].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[16].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[17].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[17].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: true, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[18].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[18].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[19].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[19].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[20].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[20].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: 'A repository for project XYZ', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://project-xyz.com', + }); + }); + + it(`Should ${examples[21].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[21].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[22].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[22].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[23].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[23].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: true, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[24].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[24].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[25].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[25].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: false, + description: undefined, + has_projects: false, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[26].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[26].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[27].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[27].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[28].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[28].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for project ABC', + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[29].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[29].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'public', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[30].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[30].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[31].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[31].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[32].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[32].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[33].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[33].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[34].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[34].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: 'Internal repository for team collaboration', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://internal.example.com', + }); + }); + + it(`Should ${examples[35].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[35].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[36].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[36].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: true, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[37].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[37].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[38].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[38].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[39].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[39].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'internal', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[40].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[40].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for web application project', + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://webapp.example.com', + }); + }); + + it(`Should ${examples[41].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[41].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'pull_request_description', + allow_merge_commit: false, + allow_rebase_merge: false, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[42].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[42].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: false, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[43].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[43].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[44].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[44].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[45].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[45].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: false, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'public', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[46].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[46].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: false, + description: 'Repository for backend service', + has_projects: undefined, + has_wiki: false, + homepage: undefined, + }); + }); + + it(`Should ${examples[47].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[47].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[48].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[48].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[49].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[49].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[50].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[50].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: false, + allow_rebase_merge: true, + allow_auto_merge: true, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[51].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[51].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: 'https://example.com', + }); + }); + + it(`Should ${examples[52].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[52].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: 'Repository for microservice development', + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[53].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[53].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[54].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[54].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: true, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[55].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[55].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); + + it(`Should ${examples[56].description}`, async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); + + let input; + try { + input = yaml.parse(examples[56].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockOctokit.rest.repos.createInOrg).toHaveBeenCalledWith({ + name: 'repo', + org: 'owner', + private: true, + delete_branch_on_merge: false, + allow_squash_merge: true, + squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', + squash_merge_commit_message: 'COMMIT_MESSAGES', + allow_merge_commit: true, + allow_rebase_merge: true, + allow_auto_merge: false, + visibility: 'private', + has_issues: undefined, + description: undefined, + has_projects: undefined, + has_wiki: undefined, + homepage: undefined, + }); + }); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts index a58da1ee29..1c1f1ca5d2 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoCreate.examples.ts @@ -63,4 +63,932 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Set repository homepage.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with homepage', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + homepage: 'https://example.com', + }, + }, + ], + }), + }, + { + description: 'Create a private repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + }, + }, + ], + }), + }, + { + description: 'Enable required code owner reviews.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required code owner reviews', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + }, + }, + ], + }), + }, + { + description: 'Set required approving review count to 2.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required approving review count', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: 'Allow squash merge only.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only squash merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowSquashMerge: true, + allowRebaseMerge: false, + }, + }, + ], + }), + }, + { + description: 'Set squash merge commit title to pull request title.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with squash merge commit title set to pull request title', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + squashMergeCommitTitle: 'pull_request_title', + }, + }, + ], + }), + }, + { + description: 'Set squash merge commit message to blank.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with squash merge commit message set to blank', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + squashMergeCommitMessage: 'blank', + }, + }, + ], + }), + }, + { + description: 'Allow auto-merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing auto-merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowAutoMerge: true, + }, + }, + ], + }), + }, + { + description: 'Set collaborators with push access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with collaborators having push access', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + collaborators: [ + { username: 'user1', permission: 'push' }, + { username: 'user2', permission: 'push' }, + ], + }, + }, + ], + }), + }, + { + description: 'Add topics to repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['devops', 'kubernetes', 'ci-cd'], + }, + }, + ], + }), + }, + { + description: 'Add secret variables to repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'SECRET_KEY', value: 'supersecretkey' }, + { name: 'API_TOKEN', value: 'tokenvalue' }, + ], + }, + }, + ], + }), + }, + { + description: 'Enable branch protection requiring status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/circleci: build'], + }, + }, + ], + }), + }, + { + description: 'Require branches to be up-to-date before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring branches to be up-to-date before merging', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: 'Require conversation resolution before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring conversation resolution before merging', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + }, + }, + ], + }), + }, + { + description: 'Delete branch on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch deletion on merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + deleteBranchOnMerge: true, + }, + }, + ], + }), + }, + { + description: 'Customize OIDC token.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC token customization', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://github.com', + }, + }, + }, + ], + }), + }, + { + description: 'Require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Set multiple properties including description, homepage, and visibility.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple properties', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'A repository for project XYZ', + homepage: 'https://project-xyz.com', + repoVisibility: 'internal', + }, + }, + ], + }), + }, + { + description: 'Configure branch protection with multiple settings.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection settings', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: [ + 'ci/circleci: build', + 'ci/circleci: test', + ], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: true, + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: + 'Set repository access to private and add collaborators with admin access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository with collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + collaborators: [ + { username: 'admin1', permission: 'admin' }, + { username: 'admin2', permission: 'admin' }, + ], + }, + }, + ], + }), + }, + { + description: 'Enable GitHub Projects for the repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with GitHub Projects enabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + hasProjects: true, + }, + }, + ], + }), + }, + { + description: + 'Disable merge commits and allow only rebase and squash merges.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only rebase and squash merges', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowRebaseMerge: true, + allowSquashMerge: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository access to internal with no projects and issues.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository without projects and issues', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + hasProjects: false, + hasIssues: false, + }, + }, + ], + }), + }, + { + description: + 'Create repository with OIDC customization for specific audience.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization for specific audience', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://specific-audience.com', + }, + }, + }, + ], + }), + }, + { + description: 'Require all branches to be up-to-date before merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository requiring all branches to be up-to-date', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: 'Set description and topics for the repository.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for project ABC', + topics: ['python', 'machine-learning', 'data-science'], + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to public and enable commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new public GitHub repository with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'public', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with collaborators and default branch protection.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with collaborators and branch protection', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + collaborators: [ + { username: 'contributor1', permission: 'write' }, + { username: 'contributor2', permission: 'write' }, + ], + requiredStatusCheckContexts: ['ci/travis: build'], + }, + }, + ], + }), + }, + { + description: 'Add multiple secret variables.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'SECRET_KEY_1', value: 'value1' }, + { name: 'SECRET_KEY_2', value: 'value2' }, + ], + }, + }, + ], + }), + }, + { + description: 'Require a minimum of 2 approving reviews for merging.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with 2 required approving reviews', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection with conversation resolution required.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection and conversation resolution required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to internal with description and homepage.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository with description and homepage', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + description: 'Internal repository for team collaboration', + homepage: 'https://internal.example.com', + }, + }, + ], + }), + }, + { + description: 'Disable auto-merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with auto-merge disabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowAutoMerge: false, + }, + }, + ], + }), + }, + { + description: 'Set repository topics and enable GitHub Projects.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with topics and GitHub Projects enabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['opensource', 'nodejs', 'api'], + hasProjects: true, + }, + }, + ], + }), + }, + { + description: + 'Create a private repository with collaborators having admin and write access.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository with multiple collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + collaborators: [ + { username: 'admin1', permission: 'admin' }, + { username: 'writer1', permission: 'write' }, + ], + }, + }, + ], + }), + }, + { + description: 'Disable branch deletion on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch deletion on merge disabled', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + deleteBranchOnMerge: false, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to internal and enable commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new internal GitHub repository with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'internal', + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create repository with description, homepage, and required status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description, homepage, and status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for web application project', + homepage: 'https://webapp.example.com', + requiredStatusCheckContexts: [ + 'ci/travis: build', + 'ci/travis: lint', + ], + }, + }, + ], + }), + }, + { + description: + 'Enable squash merges only and set commit message to pull request description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only squash merges with commit message set to pull request description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowSquashMerge: true, + allowRebaseMerge: false, + squashMergeCommitMessage: 'pull_request_description', + }, + }, + ], + }), + }, + { + description: 'Enable rebase merges only and require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository allowing only rebase merges with commit signing required', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowRebaseMerge: true, + allowSquashMerge: false, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create repository with OIDC customization for multiple audiences.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization for multiple audiences', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: ['https://audience1.com', 'https://audience2.com'], + }, + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection with required approving reviews and status checks.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring approving reviews and status checks', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredApprovingReviewCount: 2, + requiredStatusCheckContexts: [ + 'ci/circleci: build', + 'ci/circleci: test', + ], + }, + }, + ], + }), + }, + { + description: 'Create a public repository with topics and secret variables.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new public GitHub repository with topics and secret variables', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'public', + topics: ['javascript', 'react', 'frontend'], + secrets: [ + { name: 'API_KEY', value: 'apikeyvalue' }, + { name: 'DB_PASSWORD', value: 'dbpasswordvalue' }, + ], + }, + }, + ], + }), + }, + { + description: 'Set repository description and disable issues and wiki.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with description, and disable issues and wiki', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + description: 'Repository for backend service', + hasIssues: false, + hasWiki: false, + }, + }, + ], + }), + }, + { + description: 'Enable required conversation resolution and commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required conversation resolution and commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredConversationResolution: true, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Set repository visibility to private and require branches to be up-to-date.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new private GitHub repository requiring branches to be up-to-date', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + repoVisibility: 'private', + requireBranchesToBeUpToDate: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with default settings and add multiple topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with default settings and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + topics: ['devops', 'ci-cd', 'automation'], + }, + }, + ], + }), + }, + { + description: + 'Disable merge commits, enable auto-merge, and require commit signing.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository disabling merge commits, enabling auto-merge, and requiring commit signing', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + allowMergeCommit: false, + allowAutoMerge: true, + requiredCommitSigning: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with homepage, collaborators, and topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with homepage, collaborators, and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + homepage: 'https://example.com', + collaborators: [ + { username: 'user1', permission: 'push' }, + { username: 'user2', permission: 'admin' }, + ], + topics: ['opensource', 'contribution'], + }, + }, + ], + }), + }, + { + description: 'Create a repository with branch protection and description.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection and description', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/travis: build'], + requiredApprovingReviewCount: 1, + description: 'Repository for microservice development', + }, + }, + ], + }), + }, + { + description: 'Create a repository with OIDC customization and topics.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with OIDC customization and topics', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + oidcCustomization: { + sub: 'repo:owner/repo', + aud: 'https://api.example.com', + }, + topics: ['api', 'security'], + }, + }, + ], + }), + }, + { + description: + 'Enable required code owner reviews and branch deletion on merge.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with required code owner reviews and branch deletion on merge', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requireCodeOwnerReviews: true, + deleteBranchOnMerge: true, + }, + }, + ], + }), + }, + { + description: + 'Create a repository with multiple secret variables and collaborators.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with multiple secret variables and collaborators', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + secrets: [ + { name: 'API_SECRET', value: 'secretvalue' }, + { name: 'DB_USER', value: 'dbuser' }, + ], + collaborators: [ + { username: 'dev1', permission: 'write' }, + { username: 'dev2', permission: 'push' }, + ], + }, + }, + ], + }), + }, + { + description: + 'Enable branch protection requiring status checks and conversation resolution.', + example: yaml.stringify({ + steps: [ + { + action: 'github:repo:create', + name: 'Create a new GitHub repository with branch protection requiring status checks and conversation resolution', + input: { + repoUrl: 'github.com?repo=repo&owner=owner', + requiredStatusCheckContexts: ['ci/build'], + requiredConversationResolution: true, + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts index 4d16a25774..d0ee4bfa12 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.test.ts @@ -305,168 +305,6 @@ describe('github:repo:push', () => { ); }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { - mockOctokit.rest.repos.get.mockResolvedValue({ - data: { - clone_url: 'https://github.com/clone/url.git', - html_url: 'https://github.com/html/url', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requireCodeOwnerReviews: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: true, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requireCodeOwnerReviews: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - }); - - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of enforceAdmins', async () => { - mockOctokit.rest.repos.get.mockResolvedValue({ - data: { - clone_url: 'https://github.com/clone/url.git', - html_url: 'https://github.com/html/url', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - protectEnforceAdmins: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - protectEnforceAdmins: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: false, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredStatusCheckContexts and requireBranchesToBeUpToDate', async () => { mockOctokit.rest.repos.get.mockResolvedValue({ data: { @@ -487,6 +325,7 @@ describe('github:repo:push', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, bypassPullRequestAllowances: undefined, @@ -501,7 +340,6 @@ describe('github:repo:push', () => { ...mockContext.input, requiredStatusCheckContexts: ['statusCheck'], requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, }, }); @@ -515,6 +353,7 @@ describe('github:repo:push', () => { requiredStatusCheckContexts: ['statusCheck'], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, bypassPullRequestAllowances: undefined, @@ -542,6 +381,7 @@ describe('github:repo:push', () => { requiredStatusCheckContexts: ['statusCheck'], requireBranchesToBeUpToDate: false, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, bypassPullRequestAllowances: undefined, @@ -556,7 +396,6 @@ describe('github:repo:push', () => { ...mockContext.input, requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, }, }); @@ -570,6 +409,7 @@ describe('github:repo:push', () => { requiredStatusCheckContexts: [], requireBranchesToBeUpToDate: true, requiredConversationResolution: false, + requireLastPushApproval: false, enforceAdmins: true, dismissStaleReviews: false, bypassPullRequestAllowances: undefined, @@ -598,165 +438,135 @@ describe('github:repo:push', () => { expect(enableBranchProtectionOnDefaultRepoBranch).not.toHaveBeenCalled(); }); - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of dismissStaleReviews', async () => { - mockOctokit.rest.repos.get.mockResolvedValue({ - data: { - clone_url: 'https://github.com/clone/url.git', - html_url: 'https://github.com/html/url', - }, - }); + it.each([ + { + inputProperty: 'dismissStaleReviews', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'requiredConversationResolution', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'requireLastPushApproval', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'requiredApprovingReviewCount', + defaultValue: 1, + overrideValue: 2, + }, + { + inputProperty: 'requiredCommitSigning', + defaultValue: false, + overrideValue: true, + }, + { + inputProperty: 'protectEnforceAdmins', + defaultValue: true, + overrideValue: false, + octokitParameter: 'enforceAdmins', + }, + { + inputProperty: 'requireCodeOwnerReviews', + defaultValue: false, + overrideValue: true, + }, + ])( + 'should call enableBranchProtectionOnDefaultRepoBranch with the correct values of $inputProperty', + async ({ + inputProperty, + defaultValue, + overrideValue, + octokitParameter, + }) => { + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + clone_url: 'https://github.com/clone/url.git', + html_url: 'https://github.com/html/url', + }, + }); - await action.handler(mockContext); + await action.handler(mockContext); - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - dismissStaleReviews: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: true, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - dismissStaleReviews: false, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - }); - - it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requiredConversationResolution', async () => { - mockOctokit.rest.repos.get.mockResolvedValue({ - data: { - clone_url: 'https://github.com/clone/url.git', - html_url: 'https://github.com/html/url', - }, - }); - - await action.handler(mockContext); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, - requiredConversationResolution: true, - }, - }); - - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: true, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - - await action.handler({ - ...mockContext, - input: { - ...mockContext.input, + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, requiredConversationResolution: false, - }, - }); + requireLastPushApproval: false, + enforceAdmins: true, + dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, + [octokitParameter || inputProperty]: defaultValue, + }); - expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ - owner: 'owner', - client: mockOctokit, - repoName: 'repository', - logger: mockContext.logger, - defaultBranch: 'master', - requireCodeOwnerReviews: false, - requiredStatusCheckContexts: [], - requireBranchesToBeUpToDate: true, - requiredConversationResolution: false, - enforceAdmins: true, - dismissStaleReviews: false, - bypassPullRequestAllowances: undefined, - requiredApprovingReviewCount: 1, - requiredCommitSigning: false, - restrictions: undefined, - }); - }); + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + [inputProperty]: overrideValue, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + requireLastPushApproval: false, + enforceAdmins: true, + dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, + [octokitParameter || inputProperty]: overrideValue, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + [inputProperty]: defaultValue, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockOctokit, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + requiredStatusCheckContexts: [], + requireBranchesToBeUpToDate: true, + requiredConversationResolution: false, + requireLastPushApproval: false, + enforceAdmins: true, + dismissStaleReviews: false, + bypassPullRequestAllowances: undefined, + requiredApprovingReviewCount: 1, + requiredCommitSigning: false, + restrictions: undefined, + [octokitParameter || inputProperty]: defaultValue, + }); + }, + ); }); diff --git a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts index 5b9db78f48..a4fa7673c6 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/githubRepoPush.ts @@ -75,6 +75,7 @@ export function createGithubRepoPushAction(options: { sourcePath?: string; token?: string; requiredCommitSigning?: boolean; + requireLastPushApproval?: boolean; }>({ id: 'github:repo:push', description: @@ -95,6 +96,7 @@ export function createGithubRepoPushAction(options: { requireBranchesToBeUpToDate: inputProps.requireBranchesToBeUpToDate, requiredConversationResolution: inputProps.requiredConversationResolution, + requireLastPushApproval: inputProps.requireLastPushApproval, defaultBranch: inputProps.defaultBranch, protectDefaultBranch: inputProps.protectDefaultBranch, protectEnforceAdmins: inputProps.protectEnforceAdmins, @@ -132,6 +134,7 @@ export function createGithubRepoPushAction(options: { requiredStatusCheckContexts = [], requireBranchesToBeUpToDate = true, requiredConversationResolution = false, + requireLastPushApproval = false, token: providedToken, requiredCommitSigning = false, } = ctx.input; @@ -174,6 +177,7 @@ export function createGithubRepoPushAction(options: { requiredStatusCheckContexts, requireBranchesToBeUpToDate, requiredConversationResolution, + requireLastPushApproval, config, ctx.logger, gitCommitMessage, diff --git a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts index dd2f4a9f76..eec6f73f92 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/helpers.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/helpers.ts @@ -360,6 +360,7 @@ export async function initRepoPushAndProtect( requiredStatusCheckContexts: string[], requireBranchesToBeUpToDate: boolean, requiredConversationResolution: boolean, + requireLastPushApproval: boolean, config: Config, logger: any, gitCommitMessage?: string, @@ -408,6 +409,7 @@ export async function initRepoPushAndProtect( requiredStatusCheckContexts, requireBranchesToBeUpToDate, requiredConversationResolution, + requireLastPushApproval, enforceAdmins: protectEnforceAdmins, dismissStaleReviews: dismissStaleReviews, requiredCommitSigning: requiredCommitSigning, diff --git a/plugins/scaffolder-backend-module-github/src/actions/index.ts b/plugins/scaffolder-backend-module-github/src/actions/index.ts index 40a0da5455..6f37678099 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/index.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/index.ts @@ -21,12 +21,12 @@ export { createGithubRepoPushAction } from './githubRepoPush'; export { createGithubWebhookAction } from './githubWebhook'; export { createGithubDeployKeyAction } from './githubDeployKey'; export { createGithubEnvironmentAction } from './githubEnvironment'; - export { createPublishGithubPullRequestAction, type CreateGithubPullRequestActionOptions, } from './githubPullRequest'; export { createPublishGithubAction } from './github'; export { createGithubAutolinksAction } from './githubAutolinks'; +export { createGithubPagesEnableAction } from './githubPagesEnable'; export { getOctokitOptions } from './helpers'; diff --git a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts index 585526fe94..f6fc592b72 100644 --- a/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts +++ b/plugins/scaffolder-backend-module-github/src/actions/inputProperties.ts @@ -64,6 +64,11 @@ const requiredConversationResolution = { 'Requires all conversations on code to be resolved before a pull request can be merged into this branch', type: 'boolean', }; +const requireLastPushApproval = { + title: 'Require last push approval', + type: 'boolean', + description: `Whether the most recent push to a PR must be approved by someone other than the person who pushed it. The default value is 'false'`, +}; const repoVisibility = { title: 'Repository Visibility', type: 'string', @@ -326,6 +331,7 @@ export { dismissStaleReviews }; export { requiredStatusCheckContexts }; export { requireBranchesToBeUpToDate }; export { requiredConversationResolution }; +export { requireLastPushApproval }; export { hasProjects }; export { hasIssues }; export { hasWiki }; diff --git a/plugins/scaffolder-backend-module-github/src/module.ts b/plugins/scaffolder-backend-module-github/src/module.ts index 1817a68bfb..a5676ab8c9 100644 --- a/plugins/scaffolder-backend-module-github/src/module.ts +++ b/plugins/scaffolder-backend-module-github/src/module.ts @@ -29,6 +29,7 @@ import { createGithubWebhookAction, createPublishGithubAction, createPublishGithubPullRequestAction, + createGithubPagesEnableAction, } from './actions'; import { DefaultGithubCredentialsProvider, @@ -91,6 +92,10 @@ export const githubModule = createBackendModule({ githubCredentialsProvider, config, }), + createGithubPagesEnableAction({ + integrations, + githubCredentialsProvider, + }), ); }, }); diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md index eb1386679c..f62e5b4db8 100644 --- a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,92 @@ # @backstage/plugin-scaffolder-backend-module-gitlab +## 0.4.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.4.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.4.5-next.0 + +### Patch Changes + +- fad1b90: Allow the `createGitlabProjectVariableAction` to use oauth tokens +- aab708e: Added test cases for gitlab:issue:edit examples +- ef742dc: Added test cases for gitlab:projectAccessToken:create example +- 1ba4c2f: Added test cases for gitlab:pipeline:trigger examples +- a6603e4: Add custom action for merge request: **auto** + + The **Auto** action selects the committed action between _create_ and _update_. + + The **Auto** action fetches files using the **/projects/repository/tree endpoint**. + After fetching, it checks if the file exists locally and in the repository. If it does, it chooses **update**; otherwise, it chooses **create**. + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.4.4 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- 2fb0eb8: Added support for passing `variables` to `gitlab:pipeline:trigger` +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.4.4-next.1 + +### Patch Changes + +- 2fb0eb8: Added support for passing `variables` to `gitlab:pipeline:trigger` +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.4.3-next.0 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.4.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md index 4f9239e9b2..ab89669ede 100644 --- a/plugins/scaffolder-backend-module-gitlab/api-report.md +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -193,7 +193,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'update' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; @@ -211,6 +211,7 @@ export const createTriggerGitlabPipelineAction: (options: { projectId: number; tokenDescription: string; token?: string | undefined; + variables?: Record | undefined; }, { pipelineUrl: string; diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json index 096cf9d077..dde6e48a6e 100644 --- a/plugins/scaffolder-backend-module-gitlab/package.json +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-gitlab", - "version": "0.4.1", + "version": "0.4.5-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts new file mode 100644 index 0000000000..917ba61d91 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.test.ts @@ -0,0 +1,711 @@ +/* + * Copyright 2024 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 { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabIssueAction } from './gitlabIssueCreate'; +import { examples } from './gitlabIssueCreate.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Issues: { + create: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issues:create', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(2005, 6, 4, 11, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'sample-token', + apiBaseUrl: 'https://gitlab.com/api/v1', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createGitlabIssueAction({ integrations }); + + it(`should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [18], + confidential: false, + epicId: undefined, + labels: '', + createdAt: input.createdAt, + dueDate: input.dueDate, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: input.confidential, + epicId: undefined, + labels: input.labels, + createdAt: input.createdAt, + dueDate: input.dueDate, + discussionToResolve: input.discussionToResolve, + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: undefined, + weight: undefined, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [18, 22], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: '', + mergeRequestToResolveDiscussionsOf: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`should ${examples[10].description}`, async () => { + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 234, + title: 'Computer banks to rule the world', + token: 'myAwesomeToken', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.create.mockResolvedValue({ + id: 12, + iid: 4, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.create).toHaveBeenCalledWith( + 12, + input.title, + { + issueType: input.issueType, + description: input.description, + assigneeIds: [], + confidential: false, + epicId: undefined, + labels: '', + createdAt: new Date().toISOString(), + dueDate: undefined, + discussionToResolve: 'abc123', + mergeRequestToResolveDiscussionsOf: + input.mergeRequestToResolveDiscussionsOf, + milestoneId: input.milestoneId, + weight: input.weight, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 12); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 4); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts index 760caf0f9f..04fdeba46d 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueCreate.examples.ts @@ -50,8 +50,8 @@ export const examples: TemplateExample[] = [ title: 'Test Issue', assignees: [18], description: 'This is the description of the issue', - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', + createdAt: '2022-09-27T18:00:00.000Z', + dueDate: '2022-09-28T12:00:00.000Z', }, }, ], @@ -72,9 +72,9 @@ export const examples: TemplateExample[] = [ assignees: [18, 15], description: 'This is the description of the issue', confidential: false, - createdAt: '2022-09-27 18:00:00.000', - dueDate: '2022-09-28 12:00:00.000', - discussionToResolve: 1, + createdAt: '2022-09-27T18:00:00.000Z', + dueDate: '2022-09-28T12:00:00.000Z', + discussionToResolve: '1', epicId: 1, labels: 'phase1:label1,phase2:label2', }, @@ -82,4 +82,162 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab issue with token', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue', + description: 'This is the description of the issue', + token: 'sample token', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue with a specific milestone and weight', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue with Milestone', + description: 'This is the description of the issue', + milestoneId: 5, + weight: 3, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type INCIDENT', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'incident', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type TEST', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'test_case', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type TASK with assignees', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'task', + assignees: [18, 22], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type ISSUE and close it', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'issue', + stateEvent: 'close', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab issue of type INCIDENT and reopen it', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Test Issue', + description: 'This is the description of the issue', + issueType: 'incident', + stateEvent: 'reopen', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab issue to resolve a discussion in a merge request', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'Issues', + action: 'gitlab:issues:create', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test Issue for MR Discussion', + description: 'This is the description of the issue', + mergeRequestToResolveDiscussionsOf: 42, + discussionToResolve: 'abc123', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts new file mode 100644 index 0000000000..7e50eac66c --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.test.ts @@ -0,0 +1,817 @@ +/* + * Copyright 2024 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 { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { editGitlabIssueAction } from './gitlabIssueEdit'; +import { examples } from './gitlabIssueEdit.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + Issues: { + edit: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:issue:edit', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(1999, 6, 14, 12, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'myIntegrationsToken', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = editGitlabIssueAction({ integrations }); + + it(`Should ${examples[0].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[1].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: false, + discussionLocked: true, + epicId: undefined, + labels: 'phase1:label1,phase2:label2', + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + epicId: undefined, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 15], + confidential: false, + discussionLocked: true, + epicId: undefined, + labels: 'phase1:label1,phase2:label2', + updatedAt: '2024-05-10T18:00:00.000Z', + dueDate: '2024-09-28', + milestoneId: undefined, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[3].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: 'Computer banks to rule the world', + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: 'close', + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[4].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: 'Computer banks to rule the world', + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: undefined, + weight: undefined, + stateEvent: 'reopen', + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[5].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [18, 20], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: undefined, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: undefined, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[6].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[7].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: input.confidential, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[8].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: undefined, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: input.discussionLocked, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + + it(`Should ${examples[9].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + it(`Should ${examples[10].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[10].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: undefined, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); + it(`Should ${examples[11].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + issueIid: 42, + title: 'Computer banks to rule the world', + }, + workspacePath: 'seen2much', + }); + + mockGitlabClient.Issues.edit.mockResolvedValue({ + id: 123, + iid: 42, + web_url: 'https://gitlab.com/hangar18-/issues/42', + }); + + let input; + try { + input = yaml.parse(examples[11].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.Issues.edit).toHaveBeenCalledWith(12, 42, { + title: input.title, + issueType: input.issueType, + addLabels: undefined, + removeLabels: input.removeLabels, + description: input.description, + assigneeIds: [], + confidential: false, + discussionLocked: false, + epicId: undefined, + labels: input.labels, + updatedAt: new Date().toISOString(), + dueDate: undefined, + milestoneId: input.milestoneId, + weight: input.weight, + stateEvent: undefined, + }); + + expect(mockContext.output).toHaveBeenCalledWith('issueId', 123); + expect(mockContext.output).toHaveBeenCalledWith('issueIid', 42); + expect(mockContext.output).toHaveBeenCalledWith( + 'issueUrl', + 'https://gitlab.com/hangar18-/issues/42', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts index 861c0736f0..8686a66250 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabIssueEdit.examples.ts @@ -50,7 +50,7 @@ export const examples: TemplateExample[] = [ title: 'Test Issue', assignees: [18], description: 'This is the edited description of the issue', - updatedAt: '2024-05-10 18:00:00.000', + updatedAt: '2024-05-10T18:00:00.000Z', dueDate: '2024-09-28', }, }, @@ -72,7 +72,7 @@ export const examples: TemplateExample[] = [ assignees: [18, 15], description: 'This is the description of the issue', confidential: false, - updatedAt: '2024-05-10 18:00:00.000', + updatedAt: '2024-05-10T18:00:00.000Z', dueDate: '2024-09-28', discussionLocked: true, epicId: 1, @@ -82,4 +82,177 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Edit a gitlab issue to change its state to close', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + stateEvent: 'close', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to change its state to reopened', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + stateEvent: 'reopen', + }, + }, + ], + }), + }, + { + description: + 'Edit a gitlab issue to assign it to multiple users and set milestone', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Test issue with milestone', + assignees: [18, 20], + description: 'This issue has milestone set', + milestoneId: 5, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to add weight and update labels', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with weight and labels', + description: 'This issue has weight and new labels', + weight: 3, + labels: 'bug,urgent', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to make it confidential', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Confidential Issue', + description: 'This issue is confidential', + confidential: true, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to lock the discussion', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Locked Discussion Issue', + description: 'This discussion on this issue is locked', + discussionLocked: true, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to remove labels and update milestone', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with labels removed and milestone updated', + description: 'This issue has labels removed and milestone updated', + removeLabels: 'phase1:label1', + milestoneId: 6, + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to remove some labels and new ones', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with labels updated', + description: 'This issue has labels removed and new ones added', + removeLabels: 'bug,urgent', + labels: 'enhancement:documentation', + }, + }, + ], + }), + }, + { + description: 'Edit a gitlab issue to change issue type and add labels', + example: yaml.stringify({ + steps: [ + { + id: 'gitlabIssue', + name: 'EditIssues', + action: 'gitlab:issue:edit', + input: { + ...commonGitlabConfigExample, + projectId: 12, + title: 'Issue with type and labels', + description: 'This issue has been changes and new labels added', + labels: 'task,high-priority', + issueType: 'task', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts index befc2c01c9..3eda50a924 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.test.ts @@ -61,6 +61,28 @@ const mockGitlabClient = { ]; }), }, + Repositories: { + tree: jest.fn( + async ( + repoID: string | number, + options: { ref: string; recursive: boolean; path: string | undefined }, + ) => { + if (repoID !== 'owner/repo') throw new Error('repo does not exist'); + if (options.recursive === false) throw new Error('malformed options'); + else { + return [ + { + id: 'a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba', + name: 'auto.txt', + type: 'blob', + path: 'source/auto.txt', + mode: '040000', + }, + ]; + } + }, + ), + }, }; jest.mock('@gitbeaker/node', () => ({ @@ -407,7 +429,7 @@ describe('createGitLabMergeRequest', () => { }); describe('createGitLabMergeRequestWithoutCommitAction', () => { - it('default commitAction is create', async () => { + it('default commitAction is auto', async () => { const input = { repoUrl: 'gitlab.com?repo=repo&owner=owner', title: 'Create my new MR', @@ -417,7 +439,7 @@ describe('createGitLabMergeRequest', () => { }; mockDir.setContent({ [workspacePath]: { - source: { 'foo.txt': 'Hello there!' }, + source: { 'foo.txt': 'Hello there!', 'auto.txt': 'File exist' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, }, }); @@ -429,6 +451,13 @@ describe('createGitLabMergeRequest', () => { 'new-mr', 'Create my new MR', [ + { + action: 'update', + filePath: 'source/auto.txt', + content: 'RmlsZSBleGlzdA==', + encoding: 'base64', + execute_filemode: false, + }, { action: 'create', filePath: 'source/foo.txt', @@ -512,6 +541,88 @@ describe('createGitLabMergeRequest', () => { ); }); + it('commitAction is auto when auto is passed in options', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'MR description', + commitAction: 'auto', + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!', 'auto.txt': 'File exist' }, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'Create my new MR', + [ + { + action: 'update', + filePath: 'source/auto.txt', + content: 'RmlsZSBleGlzdA==', + encoding: 'base64', + execute_filemode: false, + }, + { + action: 'create', + filePath: 'source/foo.txt', + content: 'SGVsbG8gdGhlcmUh', + encoding: 'base64', + execute_filemode: false, + }, + ], + ); + }); + + it('commitAction is auto when auto is passed in options with targetPath', async () => { + const input = { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + title: 'Create my new MR', + branchName: 'new-mr', + description: 'MR description', + commitAction: 'auto', + targetPath: 'source', + }; + mockDir.setContent({ + [workspacePath]: { + source: { 'foo.txt': 'Hello there!', 'auto.txt': 'File exist' }, + irrevelant: {}, + }, + }); + + const ctx = createMockActionContext({ input, workspacePath }); + await instance.handler(ctx); + + expect(mockGitlabClient.Commits.create).toHaveBeenCalledWith( + 'owner/repo', + 'new-mr', + 'Create my new MR', + [ + { + action: 'update', + filePath: 'source/auto.txt', + content: 'RmlsZSBleGlzdA==', + encoding: 'base64', + execute_filemode: false, + }, + { + action: 'create', + filePath: 'source/foo.txt', + content: 'SGVsbG8gdGhlcmUh', + encoding: 'base64', + execute_filemode: false, + }, + ], + ); + }); + it('commitAction is delete when delete is passed in options', async () => { const input = { repoUrl: 'gitlab.com?repo=repo&owner=owner', diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts index a549585426..733365c6df 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabMergeRequest.ts @@ -17,6 +17,7 @@ import { createTemplateAction, parseRepoUrl, + SerializedFile, serializeDirectoryContents, } from '@backstage/plugin-scaffolder-node'; import { Types } from '@gitbeaker/core'; @@ -27,6 +28,21 @@ import { resolveSafeChildPath } from '@backstage/backend-plugin-api'; import { createGitlabApi } from './helpers'; import { examples } from './gitlabMergeRequest.examples'; +function getFileAction( + fileInfo: { file: SerializedFile; targetPath: string | undefined }, + remoteFiles: Types.RepositoryTreeSchema[], + defaultCommitAction: 'create' | 'delete' | 'update' | 'auto' | undefined, +): 'create' | 'delete' | 'update' { + if (!defaultCommitAction || defaultCommitAction === 'auto') { + const filePath = path.join(fileInfo.targetPath ?? '', fileInfo.file.path); + return remoteFiles && + remoteFiles.some(remoteFile => remoteFile.path === filePath) + ? 'update' + : 'create'; + } + return defaultCommitAction; +} + /** * Create a new action that creates a gitlab merge request. * @@ -46,7 +62,7 @@ export const createPublishGitlabMergeRequestAction = (options: { sourcePath?: string; targetPath?: string; token?: string; - commitAction?: 'create' | 'delete' | 'update'; + commitAction?: 'create' | 'delete' | 'update' | 'auto'; /** @deprecated projectID passed as query parameters in the repoUrl */ projectid?: string; removeSourceBranch?: boolean; @@ -109,9 +125,9 @@ export const createPublishGitlabMergeRequestAction = (options: { commitAction: { title: 'Commit action', type: 'string', - enum: ['create', 'update', 'delete'], + enum: ['create', 'update', 'delete', 'auto'], description: - 'The action to be used for git commit. Defaults to create.', + 'The action to be used for git commit. Defaults to auto. "auto" is custom action provide by backstage, (automatic assign create or update action) /!\\ Use more api calls /!\\ *', }, removeSourceBranch: { title: 'Delete source branch', @@ -199,16 +215,6 @@ export const createPublishGitlabMergeRequestAction = (options: { gitignore: true, }); - const actions: Types.CommitAction[] = fileContents.map(file => ({ - action: ctx.input.commitAction ?? 'create', - filePath: targetPath - ? path.posix.join(targetPath, file.path) - : file.path, - encoding: 'base64', - content: file.content.toString('base64'), - execute_filemode: file.executable, - })); - let targetBranch = targetBranchName; if (!targetBranch) { const projects = await api.Projects.show(repoID); @@ -217,6 +223,35 @@ export const createPublishGitlabMergeRequestAction = (options: { targetBranch = defaultBranch!; } + let remoteFiles: Types.RepositoryTreeSchema[] = []; + if (!ctx.input.commitAction || ctx.input.commitAction === 'auto') { + try { + remoteFiles = await api.Repositories.tree(repoID, { + ref: targetBranch, + recursive: true, + path: targetPath ?? undefined, + }); + } catch (e) { + ctx.logger.warn( + `Could not retrieve the list of files for ${repoID} (branch: ${targetBranch}) : ${e}`, + ); + } + } + + const actions: Types.CommitAction[] = fileContents.map(file => ({ + action: getFileAction( + { file, targetPath }, + remoteFiles, + ctx.input.commitAction, + ), + filePath: targetPath + ? path.posix.join(targetPath, file.path) + : file.path, + encoding: 'base64', + content: file.content.toString('base64'), + execute_filemode: file.executable, + })); + try { await api.Branches.create(repoID, branchName, String(targetBranch)); } catch (e) { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts new file mode 100644 index 0000000000..53cbfb0b87 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.test.ts @@ -0,0 +1,320 @@ +/* + * Copyright 2024 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 { ConfigReader } from '@backstage/core-app-api'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createTriggerGitlabPipelineAction } from './gitlabPipelineTrigger'; +import { examples } from './gitlabPipelineTrigger.examples'; +import yaml from 'yaml'; + +const mockGitlabClient = { + PipelineTriggerTokens: { + create: jest.fn(), + trigger: jest.fn(), + remove: jest.fn(), + }, +}; +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:pipeline:trigger', () => { + beforeEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + jest.useFakeTimers({ + now: new Date(1988, 5, 3, 12, 0, 0), + }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'glpat-abcdef', + apiBaseUrl: 'https://gitlab.com/api/v4', + }, + ], + }, + }); + const integrations = ScmIntegrations.fromConfig(config); + + const action = createTriggerGitlabPipelineAction({ integrations }); + + it(`should ${examples[0].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + + it(`should ${examples[1].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + it(`should ${examples[2].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); + it(`should ${examples[3].description}`, async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'Gitlab sample pipeline', + branch: 'main', + }, + workspacePath: 'repository', + }); + + mockGitlabClient.PipelineTriggerTokens.create.mockResolvedValue({ + id: 42, + description: 'Gitlab sample pipeline', + createdAt: new Date().toISOString(), + last_used: null, + token: 'abcdd-ghijkl', + updated_at: new Date().toISOString(), + owner: null, + }); + + mockGitlabClient.PipelineTriggerTokens.trigger.mockResolvedValue({ + id: 99, + web_url: 'https://gitlab.com/hangar18-/pipelines/99', + }); + + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(mockGitlabClient.PipelineTriggerTokens.create).toHaveBeenCalledWith( + input.projectId, + input.tokenDescription, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + input.projectId, + 'main', + 'abcdd-ghijkl', + { variables: input.variables }, + ); + + expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( + input.projectId, + 42, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'pipelineUrl', + 'https://gitlab.com/hangar18-/pipelines/99', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts index 4948db908b..f213fcd83f 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.examples.ts @@ -33,6 +33,70 @@ export const examples: TemplateExample[] = [ 'This is the text that will appear in the pipeline token', token: 'glpt-xxxxxxxxxxxx', branch: 'main', + variables: { var_one: 'one', var_two: 'two' }, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with No Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: {}, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with Single Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: { var_one: 'one' }, + }, + }, + ], + }), + }, + { + description: 'Trigger a GitLab Project Pipeline with Multiple Variables', + example: yaml.stringify({ + steps: [ + { + id: 'triggerPipeline', + name: 'Trigger Project Pipeline', + action: 'gitlab:pipeline:trigger', + input: { + ...commonGitlabConfigExample, + projectId: 12, + tokenDescription: + 'This is the text that will appear in the pipeline token', + token: 'glpt-xxxxxxxxxxxx', + branch: 'main', + variables: { var_one: 'one', var_two: 'two', var_three: 'three' }, }, }, ], diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.test.ts index a7412032a4..3803198ec8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.test.ts @@ -101,6 +101,7 @@ describe('gitlab:pipeline:trigger', () => { 123, 'main', 'glptt-abcdef', + { variables: undefined }, ); expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( @@ -189,6 +190,7 @@ describe('gitlab:pipeline:trigger', () => { 123, 'main', 'glptt-abcdef', + { variables: undefined }, ); expect(mockGitlabClient.PipelineTriggerTokens.remove).toHaveBeenCalledWith( @@ -196,6 +198,7 @@ describe('gitlab:pipeline:trigger', () => { 42, ); }); + it('should clean up pipeline token on failure', async () => { const mockContext = createMockActionContext({ input: { @@ -232,4 +235,30 @@ describe('gitlab:pipeline:trigger', () => { 42, ); }); + + it('should succeed trigger and pass variables', async () => { + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 123, + tokenDescription: 'My cool pipeline token', + branch: 'main', + variables: { var_one: 'val1', var_two: 'val2' }, + }, + workspacePath: 'seen2much', + }); + + await expect( + action.handler({ + ...mockContext, + }), + ).rejects.toThrow('Failed to trigger pipeline'); + + expect(mockGitlabClient.PipelineTriggerTokens.trigger).toHaveBeenCalledWith( + 123, + 'main', + 'glptt-abcdef', + { variables: { var_one: 'val1', var_two: 'val2' } }, + ); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.ts index 6937fe44aa..b9f629edfb 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabPipelineTrigger.ts @@ -30,6 +30,12 @@ const pipelineInputProperties = z.object({ projectId: z.number().describe('Project Id'), tokenDescription: z.string().describe('Pipeline token description'), branch: z.string().describe('Project branch'), + variables: z + .record(z.string(), z.string()) + .optional() + .describe( + 'A object/record of key-valued strings containing the pipeline variables.', + ), }); const pipelineOutputProperties = z.object({ @@ -57,7 +63,7 @@ export const createTriggerGitlabPipelineAction = (options: { async handler(ctx) { let pipelineTokenResponse: PipelineTriggerTokenSchema | null = null; - const { repoUrl, projectId, tokenDescription, token, branch } = + const { repoUrl, projectId, tokenDescription, token, branch, variables } = commonGitlabConfig.merge(pipelineInputProperties).parse(ctx.input); const { host } = parseRepoUrl(repoUrl, integrations); @@ -84,6 +90,7 @@ export const createTriggerGitlabPipelineAction = (options: { projectId, branch, pipelineTokenResponse.token, + { variables }, )) as ExpandedPipelineSchema; if (!pipelineTriggerResponse.id) { diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts index 811926cc82..bdf14ce1c8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.test.ts @@ -51,6 +51,10 @@ describe('gitlab:projectAccessToken:create examples', () => { host: 'hosted.gitlab.com', apiBaseUrl: 'https://api.hosted.gitlab.com', }, + { + host: 'gitlab.example.com', + apiBaseUrl: 'https://api.gitlab.example.com', + }, ], }, }); @@ -192,4 +196,256 @@ describe('gitlab:projectAccessToken:create examples', () => { expect(mockContext.output).toHaveBeenCalledWith('access_token', 'TOKEN'); }); + + it(`should ${examples[5].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[5].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '456', + 'tokenname', + ['read_repository'], + { + accessLevel: 30, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[6].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[6].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '456', + 'full-access-token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '2024-12-31', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[7].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[7].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[8].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[8].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository', 'read_api'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[9].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[9].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 10, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[10].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[10].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[11].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[11].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 50, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[12].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[12].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'no-expiry-token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); + + it(`should ${examples[13].description}`, async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'personal-access-token', + username: 'gitlab-user', + }); + + const input = yaml.parse(examples[13].example).steps[0].input; + await action.handler({ + ...mockContext, + input, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '101112', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'personal-access-token', + ); + }); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts index 8d4f335a46..89f8ae6269 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.examples.ts @@ -102,4 +102,162 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Create a GitLab project access token with an access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + accessLevel: 30, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with multiple options', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '456', + accessLevel: 40, + name: 'full-access-token', + expiresAt: '2024-12-31', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a token for authorization', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + token: 'personal-access-token', + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with read-only scopes', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + scopes: ['read_repository', 'read_api'], + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with guest access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 10, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with maintainer access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 40, + }, + }, + ], + }), + }, + { + description: 'Create a GitLab project access token with owner access level', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + accessLevel: 50, + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token with a specified name and no expiration date', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '101112', + name: 'no-expiry-token', + }, + }, + ], + }), + }, + { + description: + 'Create a GitLab project access token for a specific gitlab instance', + example: yaml.stringify({ + steps: [ + { + id: 'createAccessToken', + action: 'gitlab:projectAccessToken:create', + name: 'Create GitLab Project Access Token', + input: { + repoUrl: 'gitlab.example.com?repo=repo&owner=owner', + projectId: '101112', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts new file mode 100644 index 0000000000..34ce8878f2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectAccessTokenCreate.test.ts @@ -0,0 +1,221 @@ +/* + * Copyright 2024 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 { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { createGitlabProjectAccessTokenAction } from './gitlabProjectAccessTokenCreate'; // Adjust the import based on your project structure + +import { DateTime } from 'luxon'; + +jest.mock('node-fetch'); + +const mockGitlabClient = { + ProjectAccessTokens: { + create: jest.fn(), + }, +}; + +jest.mock('@gitbeaker/rest', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:projectAccessToken:create examples', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'gitlab-token', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGitlabProjectAccessTokenAction({ integrations }); + + const mockContext = createMockActionContext({ + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + }, + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should create a GitLab project access token with minimal options.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '987', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '987', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with custom scopes.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '987', + scopes: ['read_registry', 'write_repository'], + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '987', + 'tokenname', + ['read_registry', 'write_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a specified name.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '2110', + name: 'token', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '2110', + 'token', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a numeric project ID.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: 23, + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + 23, + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: DateTime.now().plus({ days: 365 }).toISODate()!, + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); + + it('should create a GitLab project access token with a specified expired Date.', async () => { + mockGitlabClient.ProjectAccessTokens.create.mockResolvedValue({ + token: 'gitlab-token', + username: 'gitlab-user', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '123', + expiresAt: '1999-07-14', + }, + }); + + expect(mockGitlabClient.ProjectAccessTokens.create).toHaveBeenCalledWith( + '123', + 'tokenname', + ['read_repository'], + { + accessLevel: 40, + expiresAt: '1999-07-14', + }, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'access_token', + 'gitlab-token', + ); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts index 1eeb01ca91..99b26682d8 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.examples.test.ts @@ -26,7 +26,7 @@ const mockGitlabClient = { create: jest.fn(), }, }; -jest.mock('@gitbeaker/node', () => ({ +jest.mock('@gitbeaker/rest', () => ({ Gitlab: class { constructor() { return mockGitlabClient; @@ -41,7 +41,7 @@ describe('gitlab:projectVariableAction: create examples', () => { { host: 'gitlab.com', token: 'tokenlols', - apiBaseUrl: 'https://api.gitlab.com', + apiBaseUrl: 'https://api.gitlab.com/api/v4', }, { host: 'hosted.gitlab.com', @@ -79,17 +79,18 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', - variable_type: 'env_var', - environment_scope: '*', + variableType: 'env_var', // Correctly using variableType + environmentScope: '*', masked: false, protected: false, raw: false, }, ); }); + it(`Should ${examples[1].description}`, async () => { mockGitlabClient.ProjectVariables.create.mockResolvedValue({ token: 'TOKEN', @@ -102,14 +103,14 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my-file-content', { - key: 'MY_VARIABLE', - value: 'my-file-content', + variableType: 'file', // Correctly using variableType protected: false, masked: false, raw: false, - environment_scope: '*', - variable_type: 'file', + environmentScope: '*', }, ); }); @@ -126,13 +127,13 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '456', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', masked: false, raw: false, - environment_scope: '*', - variable_type: 'env_var', + environmentScope: '*', + variableType: 'env_var', // Correctly using variableType protected: true, }, ); @@ -150,13 +151,13 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '789', + 'DB_PASSWORD', + 'password123', { - key: 'DB_PASSWORD', - value: 'password123', protected: false, raw: false, - environment_scope: '*', - variable_type: 'env_var', + environmentScope: '*', + variableType: 'env_var', // Correctly using variableType masked: true, }, ); @@ -174,12 +175,12 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', protected: false, - environment_scope: '*', - variable_type: 'env_var', + environmentScope: '*', + variableType: 'env_var', // Correctly using variableType masked: false, raw: true, }, @@ -198,14 +199,14 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', protected: false, - variable_type: 'env_var', + variableType: 'env_var', // Correctly using variableType masked: false, raw: false, - environment_scope: 'production', + environmentScope: 'production', }, ); }); @@ -222,14 +223,14 @@ describe('gitlab:projectVariableAction: create examples', () => { expect(mockGitlabClient.ProjectVariables.create).toHaveBeenCalledWith( '123', + 'MY_VARIABLE', + 'my_value', { - key: 'MY_VARIABLE', - value: 'my_value', protected: false, - variable_type: 'env_var', + variableType: 'env_var', // Correctly using variableType masked: false, raw: false, - environment_scope: '*', + environmentScope: '*', }, ); }); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts index a07ec747fa..3a9eeec0a1 100644 --- a/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/gitlabProjectVariableCreate.ts @@ -16,10 +16,10 @@ import { ScmIntegrationRegistry } from '@backstage/integration'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import { Gitlab } from '@gitbeaker/node'; +import { VariableType } from '@gitbeaker/rest'; import { z } from 'zod'; import commonGitlabConfig from '../commonGitlabConfig'; -import { getToken } from '../util'; +import { getClient, parseRepoUrl } from '../util'; import { examples } from './gitlabProjectVariableCreate.examples'; /** @@ -72,6 +72,7 @@ export const createGitlabProjectVariableAction = (options: { }, async handler(ctx) { const { + repoUrl, projectId, key, value, @@ -80,21 +81,19 @@ export const createGitlabProjectVariableAction = (options: { masked = false, raw = false, environmentScope = '*', + token, } = ctx.input; - const { token, integrationConfig } = getToken(ctx.input, integrations); - const api = new Gitlab({ - host: integrationConfig.config.baseUrl, - token: token, - }); - await api.ProjectVariables.create(projectId, { - key: key, - value: value, - variable_type: variableType, + const { host } = parseRepoUrl(repoUrl, integrations); + + const api = getClient({ host, integrations, token }); + + await api.ProjectVariables.create(projectId, key, value, { + variableType: variableType as VariableType, protected: variableProtected, - masked: masked, - raw: raw, - environment_scope: environmentScope, + masked, + raw, + environmentScope, }); }, }); diff --git a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md index a1828b9546..85da251d02 100644 --- a/plugins/scaffolder-backend-module-notifications/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-notifications/CHANGELOG.md @@ -1,5 +1,72 @@ # @backstage/plugin-scaffolder-backend-module-notifications +## 0.0.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-notifications-node@0.2.4-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-notifications-common@0.0.5 + +## 0.0.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.0.6-next.0 + +### Patch Changes + +- 6fc03c7: Add examples for notification:send scaffolder action & improve related tests +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.4-next.0 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.0.5 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-notifications-common@0.0.5 + - @backstage/plugin-notifications-node@0.2.3 + +## 0.0.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/plugin-notifications-common@0.0.4 + - @backstage/plugin-notifications-node@0.2.3-next.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.0.4-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-notifications-node@0.2.2-next.0 + - @backstage/plugin-notifications-common@0.0.4 + ## 0.0.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-notifications/package.json b/plugins/scaffolder-backend-module-notifications/package.json index 8f9c150d1b..204bf597e9 100644 --- a/plugins/scaffolder-backend-module-notifications/package.json +++ b/plugins/scaffolder-backend-module-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-notifications", - "version": "0.0.2", + "version": "0.0.6-next.2", "description": "The notifications backend module for the scaffolder plugin.", "backstage": { "role": "backend-plugin-module", @@ -38,7 +38,8 @@ "@backstage/plugin-notifications-common": "workspace:^", "@backstage/plugin-notifications-node": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "octokit": "^3.0.0" + "octokit": "^3.0.0", + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts new file mode 100644 index 0000000000..84473335e0 --- /dev/null +++ b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.test.ts @@ -0,0 +1,272 @@ +/* + * Copyright 2024 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 { createSendNotificationAction } from './sendNotification'; +import { NotificationService } from '@backstage/plugin-notifications-node'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { examples } from './sendNotification.examples'; +import yaml from 'yaml'; + +describe('notification:send', () => { + const notificationService: jest.Mocked = { + send: jest.fn(), + }; + + let action: TemplateAction; + + beforeEach(() => { + jest.resetAllMocks(); + action = createSendNotificationAction({ + notifications: notificationService, + }); + }); + + const mockContext = createMockActionContext({ + input: { + recipients: 'broadcast', + title: 'Test notification', + }, + }); + + it(`should ${examples[0].description}`, async () => { + const input = yaml.parse(examples[0].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + title: 'Test notification', + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + const input = yaml.parse(examples[1].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + entityRef: ['entity:component:1'], + type: 'entity', + }, + payload: { + description: 'A security update has been applied. Please review.', + title: 'Security Update', + link: 'https://example.com/security/update', + severity: 'high', + scope: 'internal', + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + const input = yaml.parse(examples[2].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'entity', + entityRef: ['entity:component:1'], + }, + payload: { + description: 'Here is your weekly update.', + link: undefined, + severity: 'low', + scope: undefined, + title: 'Weekly Update', + }, + }); + }); + + it(`should ${examples[3].description}`, async () => { + const input = yaml.parse(examples[3].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: 'Version 2.0.0 is now available. Upgrade now!', + link: undefined, + severity: 'normal', + scope: 'public', + title: 'New Release Available', + }, + }); + }); + + it(`should ${examples[4].description}`, async () => { + const input = yaml.parse(examples[4].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'entity', + entityRef: ['entity:component:1'], + }, + payload: { + description: + 'A critical bug has been identified. Immediate action required.', + link: undefined, + scope: 'internal', + severity: 'critical', + title: 'Critical Bug Found', + }, + }); + }); + + it(`should ${examples[5].description}`, async () => { + const input = yaml.parse(examples[5].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'broadcast', + }, + payload: { + description: 'Server maintenance will occur tonight at 11 PM.', + link: undefined, + scope: 'internal', + severity: 'normal', + title: 'Server Maintenance Scheduled', + }, + }); + }); + + it(`should ${examples[6].description}`, async () => { + const input = yaml.parse(examples[6].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: 'New features have been deployed. Explore them now!', + link: undefined, + scope: undefined, + severity: 'normal', + title: 'New Feature Deployment', + }, + }); + }); + + it(`should ${examples[7].description}`, async () => { + const input = yaml.parse(examples[7].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: undefined, + link: undefined, + scope: 'internal', + severity: 'low', + title: 'Holiday Office Closure', + }, + }); + }); + + it(`should ${examples[8].description}`, async () => { + const input = yaml.parse(examples[8].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + description: + "Don't forget, the weekly meeting is scheduled for tomorrow.", + link: undefined, + scope: 'internal', + severity: undefined, + title: 'Reminder: Weekly Meeting Tomorrow', + }, + }); + }); + + it(`should ${examples[9].description}`, async () => { + const input = yaml.parse(examples[9].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + title: 'Important Announcement', + description: + 'Please read the latest announcement regarding the upcoming changes.', + link: undefined, + scope: undefined, + severity: 'high', + }, + }); + }); + + it(`should ${examples[10].description}`, async () => { + const input = yaml.parse(examples[10].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { type: 'broadcast' }, + payload: { + title: 'Broadcast Notification', + description: 'This is a broadcast notification', + link: undefined, + scope: undefined, + severity: 'low', + }, + }); + }); + + it(`should ${examples[11].description}`, async () => { + const input = yaml.parse(examples[11].example).steps[0].input; + const ctx = Object.assign({}, mockContext, { + input: input, + }); + await action.handler(ctx); + expect(notificationService.send).toHaveBeenCalledWith({ + recipients: { + type: 'entity', + entityRef: ['entity:service1'], + }, + payload: { + title: 'Entity Notification', + description: 'This is a notification for entity service1', + link: undefined, + scope: undefined, + severity: 'normal', + }, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts new file mode 100644 index 0000000000..05bbd368f8 --- /dev/null +++ b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.examples.ts @@ -0,0 +1,255 @@ +/* + * Copyright 2023 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Sends a notification with minimal options', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Test notification', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with entity recipients and link', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'entity', + entityRefs: ['entity:component:1'], + title: 'Security Update', + info: 'A security update has been applied. Please review.', + link: 'https://example.com/security/update', + severity: 'high', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with entity recipients and optional flag', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'entity', + entityRefs: ['entity:component:1'], + title: 'Weekly Update', + info: 'Here is your weekly update.', + severity: 'low', + optional: true, + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and custom scope', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'New Release Available', + info: 'Version 2.0.0 is now available. Upgrade now!', + severity: 'normal', + scope: 'public', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with entity recipients and custom severity', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'entity', + entityRefs: ['entity:component:1'], + title: 'Critical Bug Found', + info: 'A critical bug has been identified. Immediate action required.', + severity: 'critical', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with broadcast recipients and no link', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Server Maintenance Scheduled', + info: 'Server maintenance will occur tonight at 11 PM.', + severity: 'normal', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and optional flag', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'New Feature Deployment', + info: 'New features have been deployed. Explore them now!', + severity: 'normal', + optional: true, + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and no description', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Holiday Office Closure', + severity: 'low', + scope: 'internal', + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with broadcast recipients and no severity', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Reminder: Weekly Meeting Tomorrow', + info: "Don't forget, the weekly meeting is scheduled for tomorrow.", + scope: 'internal', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with broadcast recipients and no scope', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Important Announcement', + info: 'Please read the latest announcement regarding the upcoming changes.', + severity: 'high', + }, + }, + ], + }), + }, + { + description: 'Sends a notification with optional parameters', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification', + input: { + recipients: 'broadcast', + title: 'Broadcast Notification', + info: 'This is a broadcast notification', + severity: 'low', + optional: true, + }, + }, + ], + }), + }, + { + description: + 'Sends a notification with entity recipients and optional set to false', + example: yaml.stringify({ + steps: [ + { + id: 'sendNotification', + action: 'notification:send', + name: 'Send Notification to Entity', + input: { + recipients: 'entity', + entityRefs: ['entity:service1'], + title: 'Entity Notification', + info: 'This is a notification for entity service1', + severity: 'normal', + optional: false, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts index f50e178174..9646de0001 100644 --- a/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts +++ b/plugins/scaffolder-backend-module-notifications/src/actions/sendNotification.ts @@ -22,6 +22,7 @@ import { NotificationSeverity, } from '@backstage/plugin-notifications-common'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { examples } from './sendNotification.examples'; /** * @public @@ -42,6 +43,7 @@ export function createSendNotificationAction(options: { }>({ id: 'notification:send', description: 'Sends a notification using NotificationService', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 5bf7ac953b..ed34f47a58 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,86 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.40-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.40-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.4.40-next.0 + +### Patch Changes + +- 449def7: Add examples for fetch:rails scaffolder action & improve related tests +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.4.39 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.39-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.4.38-next.0 + +### Patch Changes + +- 0ac124b: Updated configuration instructions +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + ## 0.4.36 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 9da87efa1d..8cbd5e1067 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", - "version": "0.4.36", + "version": "0.4.40-next.2", "description": "A module for the scaffolder backend that lets you template projects using Rails", "backstage": { "role": "backend-plugin-module", @@ -51,7 +51,8 @@ "@backstage/plugin-scaffolder-node": "workspace:^", "@backstage/types": "workspace:^", "command-exists": "^1.2.9", - "fs-extra": "^11.0.0" + "fs-extra": "^11.0.0", + "yaml": "^2.0.0" }, "devDependencies": { "@backstage/backend-test-utils": "workspace:^", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts new file mode 100644 index 0000000000..37e678ab6a --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.test.ts @@ -0,0 +1,341 @@ +/* + * 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. + */ + +const mockRailsTemplater = { run: jest.fn() }; +jest.mock('@backstage/plugin-scaffolder-node', () => ({ + ...jest.requireActual('@backstage/plugin-scaffolder-node'), + fetchContents: jest.fn(), +})); +jest.mock('./railsNewRunner', () => { + return { + RailsNewRunner: jest.fn().mockImplementation(() => { + return mockRailsTemplater; + }), + }; +}); + +import { ContainerRunner, UrlReader } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { resolve as resolvePath } from 'path'; +import { createFetchRailsAction } from './index'; +import { fetchContents } from '@backstage/plugin-scaffolder-node'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { examples } from './index.examples'; +import yaml from 'yaml'; + +describe('fetch:rails', () => { + const mockDir = createMockDirectory(); + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [ + { host: 'dev.azure.com', token: 'tokenlols' }, + { host: 'myazurehostnotoken.com' }, + ], + }, + }), + ); + + const mockContext = createMockActionContext({ + input: { + url: 'https://rubyonrails.org/generator', + targetPath: 'something', + values: { + help: 'me', + }, + }, + templateInfo: { + baseUrl: 'somebase', + entityRef: 'template:default/myTemplate', + }, + workspacePath: mockDir.path, + }); + + const mockReader: UrlReader = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + const containerRunner: ContainerRunner = { + runContainer: jest.fn(), + }; + + const action = createFetchRailsAction({ + integrations, + reader: mockReader, + containerRunner, + allowedImageNames: ['foo/rails-custom-image'], + }); + + beforeEach(() => { + mockDir.setContent({ + result: '{}', + }); + jest.clearAllMocks(); + }); + + it(`should ${examples[0].description}`, async () => { + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[1].description}`, async () => { + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[2].description}`, async () => { + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[3].description}`, async () => { + let input; + try { + input = yaml.parse(examples[3].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[4].description}`, async () => { + let input; + try { + input = yaml.parse(examples[4].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[5].description}`, async () => { + let input; + try { + input = yaml.parse(examples[5].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[6].description}`, async () => { + let input; + try { + input = yaml.parse(examples[6].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[7].description}`, async () => { + let input; + try { + input = yaml.parse(examples[7].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[8].description}`, async () => { + let input; + try { + input = yaml.parse(examples[8].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); + + it(`should ${examples[9].description}`, async () => { + let input; + try { + input = yaml.parse(examples[9].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + ...input, + }, + }); + + expect(fetchContents).toHaveBeenCalledWith({ + reader: mockReader, + integrations, + baseUrl: mockContext.templateInfo?.baseUrl, + fetchUrl: input.url, + outputPath: resolvePath(mockContext.workspacePath), + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts new file mode 100644 index 0000000000..4c77eb3c1b --- /dev/null +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.examples.ts @@ -0,0 +1,370 @@ +/* + * 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 { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Fetch and template a minimal Rails app', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: true, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: true, + api: true, + webpacker: 'react', + database: 'postgresql', + railsVersion: 'dev', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Fetch and template a Rails app with custom options', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + targetPath: 'custom-path', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: false, + api: false, + webpacker: 'vue', + database: 'mysql', + railsVersion: 'edge', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Fetch and template a Rails app with Docker image', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: true, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: true, + api: true, + webpacker: 'react', + database: 'postgresql', + railsVersion: 'dev', + }, + imageName: 'custom/rails-image', + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with different database and action mailer settings', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: true, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: true, + api: true, + webpacker: 'angular', + database: 'sqlite3', + railsVersion: 'master', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with full options including action mailer and active storage', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: false, + api: false, + webpacker: 'stimulus', + database: 'postgresql', + railsVersion: 'fromImage', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with API-only stack and custom template path', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: true, + api: true, + webpacker: 'elm', + database: 'mysql', + railsVersion: 'dev', + template: 'path/to/custom/template', + }, + }, + }, + }, + ], + }), + }, + { + description: 'Fetch and template a Rails app with custom Webpack setup', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: false, + api: false, + webpacker: 'angular', + database: 'postgresql', + railsVersion: 'dev', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with specific Rails version and Action Mailer enabled', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: true, + api: true, + webpacker: 'react', + database: 'sqlite3', + railsVersion: 'master', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with specific database and Active Storage enabled', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: false, + skipWebpackInstall: false, + skipTest: false, + skipActionCable: false, + skipActionMailer: false, + skipActionMailbox: false, + skipActiveStorage: false, + skipActionText: false, + skipActiveRecord: false, + force: true, + api: true, + webpacker: 'vue', + database: 'postgresql', + railsVersion: 'fromImage', + }, + }, + }, + }, + ], + }), + }, + { + description: + 'Fetch and template a Rails app with full options including database and skip options', + example: yaml.stringify({ + steps: [ + { + id: 'fetchTemplate', + action: 'fetch:rails', + name: 'Fetch and Template Rails App', + input: { + url: 'https://github.com/backstage/backstage', + values: { + railsArguments: { + minimal: false, + skipBundle: true, + skipWebpackInstall: true, + skipTest: true, + skipActionCable: true, + skipActionMailer: true, + skipActionMailbox: true, + skipActiveStorage: true, + skipActionText: true, + skipActiveRecord: true, + force: false, + api: false, + webpacker: 'stimulus', + database: 'sqlite3', + railsVersion: 'dev', + }, + }, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 394d0fb02c..94a2a3539b 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -27,6 +27,7 @@ import { import { resolve as resolvePath } from 'path'; import { RailsNewRunner } from './railsNewRunner'; import { PassThrough } from 'stream'; +import { examples } from './index.examples'; /** * Creates the `fetch:rails` Scaffolder action. @@ -56,6 +57,7 @@ export function createFetchRailsAction(options: { id: 'fetch:rails', description: 'Downloads a template from the given `url` into the workspace, and runs a rails new generator on it.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 922b8fd274..c4421d314b 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.31-next.2 + +### Patch Changes + +- 93095ee: Make sure node-fetch is version 2.7.0 or greater +- 382e868: Added test cases for sentry:project:create examples +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.31-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.31-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.30 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + +## 0.1.30-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.29-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + ## 0.1.27 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index efde9aa2fe..f683e23d09 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.27", + "version": "0.1.31-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", @@ -46,7 +46,7 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-scaffolder-node": "workspace:^", - "node-fetch": "^2.6.7", + "node-fetch": "^2.7.0", "yaml": "^2.3.3" }, "devDependencies": { diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts new file mode 100644 index 0000000000..3e31aef3bc --- /dev/null +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2024 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 { registerMswTestHooks } from '@backstage/backend-test-utils'; +import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; +import { ConfigReader } from '@backstage/config'; +import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { JsonObject } from '@backstage/types'; +import { randomBytes } from 'crypto'; +import { setupServer } from 'msw/node'; +import { HttpResponse, http } from 'msw'; +import { createSentryCreateProjectAction } from './createProject'; +import yaml from 'yaml'; +import { examples } from './createProject.examples'; + +describe('sentry:project:create action', () => { + const worker = setupServer(); + registerMswTestHooks(worker); + + const createScaffolderConfig = (configData: JsonObject = {}) => ({ + config: new ConfigReader({ + scaffolder: { + ...configData, + }, + }), + }); + + const getActionContext = (): ActionContext<{ + organizationSlug: string; + teamSlug: string; + name: string; + slug?: string; + authToken?: string; + }> => + createMockActionContext({ + workspacePath: './dev/proj', + logger: jest.createMockFromModule('winston'), + input: { + organizationSlug: 'org', + teamSlug: 'team', + name: 'test project', + authToken: randomBytes(5).toString('hex'), + }, + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[0].example).steps[1].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer b15711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d4gf93`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project B', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[0].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[0].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + actionContext.input = { ...actionContext.input, slug: 'project-slug' }; + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer a14711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d5df96`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project A', + slug: 'scaff-proj-a', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[1].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[1].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const sentryScaffolderConfigToken = randomBytes(5).toString('hex'); + const action = createSentryCreateProjectAction( + createScaffolderConfig({ + sentry: { + token: sentryScaffolderConfigToken, + }, + }), + ); + const actionContext = getActionContext(); + actionContext.input.authToken = undefined; + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer ${sentryScaffolderConfigToken}`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'Scaffolded project A', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); + + it(`should ${examples[2].description}`, async () => { + expect.assertions(3); + + let input; + try { + input = yaml.parse(examples[2].example).steps[0].input; + } catch (error) { + console.error('Failed to parse YAML:', error); + } + + const action = createSentryCreateProjectAction(createScaffolderConfig()); + const actionContext = getActionContext(); + + worker.use( + http.post( + `https://sentry.io/api/0/teams/${input.organizationSlug}/${input.teamSlug}/projects/`, + async ({ request }) => { + expect(request.headers.get('Authorization')).toBe( + `Bearer c16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf94`, + ); + expect(request.headers.get('Content-Type')).toBe(`application/json`); + await expect(request.json()).resolves.toEqual({ + name: 'A very long name for the scaffolded project C that will generate a slug', + }); + return HttpResponse.json( + { detail: 'project creation mocked result' }, + { status: 201 }, + ); + }, + ), + ); + + await action.handler({ + ...actionContext, + input: { + ...actionContext.input, + ...input, + }, + }); + }); +}); diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts index 9d4551c1b5..3af88a4663 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.examples.ts @@ -50,4 +50,41 @@ export const examples: TemplateExample[] = [ ], }), }, + { + description: 'Creates a Sentry project when no auth token is passed', + example: yaml.stringify({ + steps: [ + { + id: 'create-sentry-project', + action: 'sentry:project:create', + name: 'Create a Sentry project with provided project slug.', + input: { + organizationSlug: 'my-org', + teamSlug: 'team-a', + name: 'Scaffolded project A', + }, + }, + ], + }), + }, + { + description: + 'Creates a Sentry project with a long project name and automatically generated slug.', + example: yaml.stringify({ + steps: [ + { + id: 'create-sentry-project', + action: 'sentry:project:create', + name: 'Create a Sentry project with a long name and no slug provided.', + input: { + organizationSlug: 'my-org', + teamSlug: 'team-c', + name: 'A very long name for the scaffolded project C that will generate a slug', + authToken: + 'c16711beb516e1e910d2ede554dc1bf725654ef3c75e5a9106de9aec13d6gf94', + }, + }, + ], + }), + }, ]; diff --git a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts index 158b7db4ae..48c437d2a4 100644 --- a/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts +++ b/plugins/scaffolder-backend-module-sentry/src/actions/createProject.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { registerMswTestHooks } from '@backstage/backend-test-utils'; import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils'; import { ConfigReader } from '@backstage/config'; import { InputError } from '@backstage/errors'; @@ -27,7 +27,7 @@ import { createSentryCreateProjectAction } from './createProject'; describe('sentry:project:create action', () => { const worker = setupServer(); - setupRequestMockHandlers(worker); + registerMswTestHooks(worker); const createScaffolderConfig = (configData: JsonObject = {}) => ({ config: new ConfigReader({ diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index 7be229cdea..4833302170 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,65 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.2 + - @backstage/types@1.1.1 + +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.1 + +## 0.3.7-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.1.10-next.0 + +## 0.3.6 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-node-test-utils@0.1.9 + - @backstage/types@1.1.1 + +## 0.3.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node-test-utils@0.1.9-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.3.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-scaffolder-node-test-utils@0.1.8-next.0 + - @backstage/types@1.1.1 + ## 0.3.2 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index be380ac160..36aed409cc 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.3.2", + "version": "0.3.7-next.2", "backstage": { "role": "backend-plugin-module", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index fa81406ba0..0ed359768e 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,215 @@ # @backstage/plugin-scaffolder-backend +## 1.23.1-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.2 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.2 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-permission-node@0.8.1-next.2 + - @backstage/backend-tasks@0.5.28-next.2 + - @backstage/plugin-catalog-node@1.12.5-next.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.1 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.2 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.2 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.2 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.23.1-next.1 + +### Patch Changes + +- ef87e06: Fix scaffolder action `catalog:write` to write to directories that don't already exist +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.1 + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/plugin-permission-node@0.8.1-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.1 + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.1 + - @backstage/backend-tasks@0.5.28-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 1.23.1-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.5-next.0 + - @backstage/plugin-catalog-node@1.12.5-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/backend-tasks@0.5.28-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.22-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.21-next.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.1-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.15-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.15-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.1-next.0 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 1.23.0 + +### Minor Changes + +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- b9451dd: Updated `catalog:write` scaffolder action to show correct file path location in log message +- ff1bb4c: Added a documentation how to use checkpoints +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- 62d1fe3: Fix user entity not being fetched for scaffolder dry runner +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/backend-tasks@0.5.27 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0 + - @backstage/plugin-permission-common@0.8.0 + - @backstage/plugin-permission-node@0.8.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.14 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12 + - @backstage/plugin-bitbucket-cloud-common@0.2.21 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20 + - @backstage/plugin-catalog-node@1.12.4 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.12 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 1.23.0-next.2 + +### Patch Changes + +- ff1bb4c: Added a documentation how to use checkpoints + +## 1.23.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.1 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.4-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/backend-tasks@0.5.27-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.20-next.1 + - @backstage/plugin-catalog-node@1.12.4-next.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-permission-node@0.7.33-next.1 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.14-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.12-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.12-next.1 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.12-next.1 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.14-next.1 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.12-next.1 + - @backstage/plugin-scaffolder-common@1.5.3 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 1.23.0-next.0 + +### Minor Changes + +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket + +### Patch Changes + +- da90cce: Updated dependency `esbuild` to `^0.21.0`. +- 62d1fe3: Fix user entity not being fetched for scaffolder dry runner +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/backend-tasks@0.5.26-next.0 + - @backstage/plugin-scaffolder-backend-module-gitlab@0.4.3-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-server@0.1.11-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/plugin-scaffolder-backend-module-azure@0.1.13-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket-cloud@0.1.11-next.0 + - @backstage/plugin-bitbucket-cloud-common@0.2.21-next.0 + - @backstage/plugin-scaffolder-backend-module-github@0.4.0-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-catalog-backend-module-scaffolder-entity-model@0.1.19-next.0 + - @backstage/plugin-catalog-node@1.12.3-next.0 + - @backstage/plugin-permission-node@0.7.32-next.0 + - @backstage/plugin-scaffolder-backend-module-bitbucket@0.2.11-next.0 + - @backstage/plugin-scaffolder-backend-module-gerrit@0.1.13-next.0 + - @backstage/plugin-scaffolder-backend-module-gitea@0.1.11-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-permission-common@0.7.14 + - @backstage/plugin-scaffolder-common@1.5.3 + ## 1.22.9 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 4c7b11ea43..1e60d5b026 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -7,6 +7,7 @@ import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-node'; import { AuthService } from '@backstage/backend-plugin-api'; +import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; import * as azure from '@backstage/plugin-scaffolder-backend-module-azure'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; import * as bitbucket from '@backstage/plugin-scaffolder-backend-module-bitbucket'; @@ -63,6 +64,7 @@ import { TemplateFilter as TemplateFilter_2 } from '@backstage/plugin-scaffolder import { TemplateGlobal as TemplateGlobal_2 } from '@backstage/plugin-scaffolder-node'; import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; import { UrlReader } from '@backstage/backend-common'; +import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; import { ZodType } from 'zod'; import { ZodTypeDef } from 'zod'; @@ -310,7 +312,7 @@ export const createPublishGitlabMergeRequestAction: (options: { sourcePath?: string | undefined; targetPath?: string | undefined; token?: string | undefined; - commitAction?: 'update' | 'delete' | 'create' | undefined; + commitAction?: 'auto' | 'update' | 'delete' | 'create' | undefined; projectid?: string | undefined; removeSourceBranch?: boolean | undefined; assignee?: string | undefined; @@ -425,7 +427,7 @@ export class DatabaseTaskStore implements TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list(options: { createdBy?: string }): Promise<{ + list(options: { createdBy?: string; status?: TaskStatus_2 }): Promise<{ tasks: SerializedTask_2[]; }>; // (undocumented) @@ -476,8 +478,12 @@ export interface RouterOptions { // (undocumented) additionalTemplateGlobals?: Record; // (undocumented) + additionalWorkspaceProviders?: Record; + // (undocumented) auth?: AuthService; // (undocumented) + autocompleteHandlers?: Record; + // (undocumented) catalogClient: CatalogApi; concurrentTasksLimit?: number; // (undocumented) @@ -556,6 +562,7 @@ export class TaskManager implements TaskContext_2 { logger: Logger, auth?: AuthService, config?: Config, + additionalWorkspaceProviders?: Record, ): TaskManager; // (undocumented) get createdBy(): string | undefined; @@ -639,7 +646,7 @@ export interface TaskStore { // (undocumented) heartbeatTask(taskId: string): Promise; // (undocumented) - list?(options: { createdBy?: string }): Promise<{ + list?(options: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-backend/config.d.ts b/plugins/scaffolder-backend/config.d.ts index 754eedb55b..f6b9522575 100644 --- a/plugins/scaffolder-backend/config.d.ts +++ b/plugins/scaffolder-backend/config.d.ts @@ -52,6 +52,13 @@ export interface Config { */ EXPERIMENTAL_workspaceSerialization?: boolean; + /** + * Sets the provider for workspace serialization. + * + * By default, it is your database. + */ + EXPERIMENTAL_workspaceSerializationProvider?: string; + /** * Every task which is in progress state and having a last heartbeat longer than a specified timeout is going to * be attempted to recover. diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 0ca98ed300..974125ad94 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "1.22.9", + "version": "1.23.1-next.2", "description": "The Backstage backend plugin that helps you create new things", "backstage": { "role": "backend-plugin", @@ -70,6 +70,7 @@ "@backstage/errors": "workspace:^", "@backstage/integration": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-bitbucket-cloud-common": "workspace:^", "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", @@ -113,6 +114,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@backstage/backend-app-api": "workspace:^", "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-scaffolder-node-test-utils": "workspace:^", @@ -120,7 +122,7 @@ "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", "@types/zen-observable": "^0.8.0", - "esbuild": "^0.20.0", + "esbuild": "^0.21.0", "strip-ansi": "^7.1.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2" diff --git a/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile b/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile index ecaefee183..0c8b305de5 100644 --- a/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile +++ b/plugins/scaffolder-backend/scripts/Cookiecutter.dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.19 +FROM alpine:3.20 RUN apk add --update \ git \ diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index a8213cf585..27b4f6b196 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -28,9 +28,13 @@ import { TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; import { + AutocompleteHandler, scaffolderActionsExtensionPoint, + scaffolderAutocompleteExtensionPoint, scaffolderTaskBrokerExtensionPoint, scaffolderTemplatingExtensionPoint, + scaffolderWorkspaceProviderExtensionPoint, + WorkspaceProvider, } from '@backstage/plugin-scaffolder-node/alpha'; import { createCatalogRegisterAction, @@ -82,6 +86,20 @@ export const scaffolderPlugin = createBackendPlugin({ }, }); + const autocompleteHandlers: Record = {}; + env.registerExtensionPoint(scaffolderAutocompleteExtensionPoint, { + addAutocompleteProvider(provider) { + autocompleteHandlers[provider.id] = provider.handler; + }, + }); + + const additionalWorkspaceProviders: Record = {}; + env.registerExtensionPoint(scaffolderWorkspaceProviderExtensionPoint, { + addProviders(provider) { + Object.assign(additionalWorkspaceProviders, provider); + }, + }); + env.registerInit({ deps: { logger: coreServices.logger, @@ -162,6 +180,8 @@ export const scaffolderPlugin = createBackendPlugin({ httpAuth, discovery, permissions, + autocompleteHandlers, + additionalWorkspaceProviders, }); httpRouter.use(router); }, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts index e6b4bcde47..f900e87a1d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts @@ -55,8 +55,8 @@ describe('catalog:write', () => { input: yaml.parse(examples[0].example).steps[0].input, }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( + expect(fsMock.outputFile).toHaveBeenCalledTimes(1); + expect(fsMock.outputFile).toHaveBeenCalledWith( resolvePath(mockContext.workspacePath, 'catalog-info.yaml'), yaml.stringify(entity), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts index 658e4365bf..469b4d5c6d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.test.ts @@ -59,8 +59,8 @@ describe('catalog:write', () => { }, }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( + expect(fsMock.outputFile).toHaveBeenCalledTimes(1); + expect(fsMock.outputFile).toHaveBeenCalledWith( resolvePath(mockContext.workspacePath, 'catalog-info.yaml'), yaml.stringify(entity), ); @@ -88,8 +88,8 @@ describe('catalog:write', () => { }, }); - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( + expect(fsMock.outputFile).toHaveBeenCalledTimes(1); + expect(fsMock.outputFile).toHaveBeenCalledWith( resolvePath(mockContext.workspacePath, 'some-dir/entity-info.yaml'), yaml.stringify(entity), ); @@ -118,8 +118,8 @@ describe('catalog:write', () => { 'backstage.io/source-template': 'template:default/test-skeleton', }; - expect(fsMock.writeFile).toHaveBeenCalledTimes(1); - expect(fsMock.writeFile).toHaveBeenCalledWith( + expect(fsMock.outputFile).toHaveBeenCalledTimes(1); + expect(fsMock.outputFile).toHaveBeenCalledWith( resolvePath(mockContext.workspacePath, 'catalog-info.yaml'), yaml.stringify(expectedEntity), ); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index 9feaa14c07..29ce6c986d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -49,12 +49,12 @@ export function createCatalogWriteAction() { examples, supportsDryRun: true, async handler(ctx) { - ctx.logger.info(`Writing catalog-info.yaml`); const { filePath, entity } = ctx.input; const entityRef = ctx.templateInfo?.entityRef; const path = filePath ?? 'catalog-info.yaml'; + ctx.logger.info(`Writing ${path}`); - await fs.writeFile( + await fs.outputFile( resolveSafeChildPath(ctx.workspacePath, path), yaml.stringify({ ...entity, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts index fd5fdd49e8..5d037b44b6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/deprecated.ts @@ -22,56 +22,66 @@ import * as bitbucketServer from '@backstage/plugin-scaffolder-backend-module-bi import * as gerrit from '@backstage/plugin-scaffolder-backend-module-gerrit'; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubActionsDispatchAction = github.createGithubActionsDispatchAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubDeployKeyAction = github.createGithubDeployKeyAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubEnvironmentAction = github.createGithubEnvironmentAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubIssuesLabelAction = github.createGithubIssuesLabelAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export type CreateGithubPullRequestActionOptions = github.CreateGithubPullRequestActionOptions; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubRepoCreateAction = github.createGithubRepoCreateAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubRepoPushAction = github.createGithubRepoPushAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createGithubWebhookAction = github.createGithubWebhookAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createPublishGithubAction = github.createPublishGithubAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-github instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-github` instead */ export const createPublishGithubPullRequestAction = github.createPublishGithubPullRequestAction; @@ -83,46 +93,54 @@ export const createPublishBitbucketAction = bitbucket.createPublishBitbucketAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-cloud instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-cloud` instead */ export const createPublishBitbucketCloudAction = bitbucketCloud.createPublishBitbucketCloudAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerAction = bitbucketServer.createPublishBitbucketServerAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-bitbucket-server instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-bitbucket-server` instead */ export const createPublishBitbucketServerPullRequestAction = bitbucketServer.createPublishBitbucketServerPullRequestAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-azure instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-azure` instead */ export const createPublishAzureAction = azure.createPublishAzureAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gerrit instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ export const createPublishGerritAction = gerrit.createPublishGerritAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gerrit instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gerrit` instead */ export const createPublishGerritReviewAction = gerrit.createPublishGerritReviewAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gitlab instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ export const createPublishGitlabAction = gitlab.createPublishGitlabAction; /** - * @public @deprecated use import from \@backstage/plugin-scaffolder-backend-module-gitlab instead + * @public + * @deprecated use import from `@backstage/plugin-scaffolder-backend-module-gitlab` instead */ export const createPublishGitlabMergeRequestAction = gitlab.createPublishGitlabMergeRequestAction; diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts index 6b8caf5a3d..c993e0d46d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts @@ -38,12 +38,17 @@ import { BackstageCredentials, resolveSafeChildPath, } from '@backstage/backend-plugin-api'; +import type { UserEntity } from '@backstage/catalog-model'; interface DryRunInput { spec: TaskSpec; secrets?: TaskSecrets; directoryContents: SerializedFile[]; credentials: BackstageCredentials; + user?: { + entity?: UserEntity; + ref?: string; + }; } interface DryRunResult { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts index f4fae0715b..78a362b5c7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts @@ -95,6 +95,33 @@ describe('DatabaseTaskStore', () => { expect(tasks[0].id).toBeDefined(); }); + it('should list filtered created tasks by status', async () => { + const { store } = await createStore(); + + const { taskId } = await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'me', + }); + + await store.createTask({ + spec: {} as TaskSpec, + createdBy: 'him', + }); + + const message = `This task was marked as stale as it exceeded its timeout`; + await store.completeTask({ + taskId, + status: 'cancelled', + eventBody: { message }, + }); + + const { tasks } = await store.list({ status: 'open' }); + expect(tasks.length).toBe(1); + expect(tasks[0].createdBy).toBe('him'); + expect(tasks[0].status).toBe('open'); + expect(tasks[0].id).toBeDefined(); + }); + it('should sent an event to start cancelling the task', async () => { const { store } = await createStore(); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 9c95c11017..ba675b2cc7 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -22,25 +22,28 @@ import { Knex } from 'knex'; import { v4 as uuid } from 'uuid'; import { TaskStore, - TaskStoreEmitOptions, - TaskStoreListEventsOptions, TaskStoreCreateTaskOptions, TaskStoreCreateTaskResult, - TaskStoreShutDownTaskOptions, + TaskStoreEmitOptions, + TaskStoreListEventsOptions, TaskStoreRecoverTaskOptions, + TaskStoreShutDownTaskOptions, } from './types'; import { - SerializedTaskEvent, SerializedTask, - TaskStatus, + SerializedTaskEvent, TaskEventType, TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { DateTime, Duration } from 'luxon'; import { TaskRecovery, TaskSpec } from '@backstage/plugin-scaffolder-common'; import { trimEventsTillLastRecovery } from './taskRecoveryHelper'; import { intervalFromNowTill } from './dbUtil'; -import { restoreWorkspace, serializeWorkspace } from './serializer'; +import { + restoreWorkspace, + serializeWorkspace, +} from '@backstage/plugin-scaffolder-node/alpha'; const migrationsDir = resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -179,6 +182,7 @@ export class DatabaseTaskStore implements TaskStore { async list(options: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { const queryBuilder = this.db('tasks'); @@ -188,6 +192,10 @@ export class DatabaseTaskStore implements TaskStore { }); } + if (options.status) { + queryBuilder.where({ status: options.status }); + } + const results = await queryBuilder.orderBy('created_at', 'desc').select(); const tasks = results.map(result => ({ @@ -517,7 +525,10 @@ export class DatabaseTaskStore implements TaskStore { .where({ id: options.taskId }) .select('workspace'); - await restoreWorkspace(options.targetPath, result.workspace); + await restoreWorkspace({ + path: options.targetPath, + buffer: result.workspace, + }); } async cleanWorkspace({ taskId }: { taskId: string }): Promise { @@ -534,7 +545,7 @@ export class DatabaseTaskStore implements TaskStore { await this.db('tasks') .where({ id: options.taskId }) .update({ - workspace: await serializeWorkspace(options.path), + workspace: (await serializeWorkspace(options)).contents, }); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseWorkspaceProvider.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseWorkspaceProvider.ts new file mode 100644 index 0000000000..69f90b3d7e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseWorkspaceProvider.ts @@ -0,0 +1,45 @@ +/* + * 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 { TaskStore } from './types'; + +import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; + +export class DatabaseWorkspaceProvider implements WorkspaceProvider { + static create(storage: TaskStore) { + return new DatabaseWorkspaceProvider(storage); + } + + private constructor(private readonly storage: TaskStore) {} + + public async serializeWorkspace(options: { + path: string; + taskId: string; + }): Promise { + this.storage.serializeWorkspace?.(options); + } + + public async rehydrateWorkspace(options: { + taskId: string; + targetPath: string; + }): Promise { + return this.storage.rehydrateWorkspace?.(options); + } + + public async cleanWorkspace(options: { taskId: string }): Promise { + return this.storage.cleanWorkspace?.(options); + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index e9b1d78e36..3b88fd0f10 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -443,7 +443,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { throw new Error(`Step ${step.name} has been cancelled.`); } - await task.cleanWorkspace?.(); await stepTrack.markSuccessful(); } catch (err) { await taskTrack.markFailed(step, err); @@ -513,6 +512,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { return { output }; } finally { if (workspacePath) { + await task.cleanWorkspace?.(); await fs.remove(workspacePath); } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 03a2833938..d17fb33b3b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -20,13 +20,14 @@ import { JsonObject, JsonValue, Observable } from '@backstage/types'; import { Logger } from 'winston'; import ObservableImpl from 'zen-observable'; import { - TaskSecrets, SerializedTask, SerializedTaskEvent, TaskBroker, TaskBrokerDispatchOptions, TaskCompletionState, TaskContext, + TaskSecrets, + TaskStatus, } from '@backstage/plugin-scaffolder-node'; import { InternalTaskSecrets, TaskStore } from './types'; import { readDuration } from './helper'; @@ -34,6 +35,8 @@ import { AuthService, BackstageCredentials, } from '@backstage/backend-plugin-api'; +import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService'; +import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; type TaskState = { checkpoints: { @@ -65,14 +68,22 @@ export class TaskManager implements TaskContext { logger: Logger, auth?: AuthService, config?: Config, + additionalWorkspaceProviders?: Record, ) { + const workspaceService = DefaultWorkspaceService.create( + task, + storage, + additionalWorkspaceProviders, + config, + ); + const agent = new TaskManager( task, storage, abortSignal, logger, + workspaceService, auth, - config, ); agent.startTimeout(); return agent; @@ -84,8 +95,8 @@ export class TaskManager implements TaskContext { private readonly storage: TaskStore, private readonly signal: AbortSignal, private readonly logger: Logger, + private readonly workspaceService: WorkspaceService, private readonly auth?: AuthService, - private readonly config?: Config, ) {} get spec() { @@ -112,9 +123,7 @@ export class TaskManager implements TaskContext { taskId: string; targetPath: string; }): Promise { - if (this.isWorkspaceSerializationEnabled()) { - this.storage.rehydrateWorkspace?.(options); - } + await this.workspaceService.rehydrateWorkspace(options); } get done() { @@ -163,18 +172,11 @@ export class TaskManager implements TaskContext { } async serializeWorkspace?(options: { path: string }): Promise { - if (this.isWorkspaceSerializationEnabled()) { - await this.storage.serializeWorkspace?.({ - path: options.path, - taskId: this.task.taskId, - }); - } + await this.workspaceService.serializeWorkspace(options); } async cleanWorkspace?(): Promise { - if (this.isWorkspaceSerializationEnabled()) { - await this.storage.cleanWorkspace?.({ taskId: this.task.taskId }); - } + await this.workspaceService.cleanWorkspace(); } async complete( @@ -211,14 +213,6 @@ export class TaskManager implements TaskContext { }, 1000); } - private isWorkspaceSerializationEnabled(): boolean { - return ( - this.config?.getOptionalBoolean( - 'scaffolder.EXPERIMENTAL_workspaceSerialization', - ) ?? false - ); - } - async getInitiatorCredentials(): Promise { const secrets = this.task.secrets as InternalTaskSecrets; @@ -278,17 +272,25 @@ export class StorageTaskBroker implements TaskBroker { private readonly logger: Logger, private readonly config?: Config, private readonly auth?: AuthService, + private readonly additionalWorkspaceProviders?: Record< + string, + WorkspaceProvider + >, ) {} async list(options?: { createdBy?: string; + status?: TaskStatus; }): Promise<{ tasks: SerializedTask[] }> { if (!this.storage.list) { throw new Error( 'TaskStore does not implement the list method. Please implement the list method to be able to list tasks', ); } - return await this.storage.list({ createdBy: options?.createdBy }); + return await this.storage.list({ + createdBy: options?.createdBy, + status: options?.status, + }); } private deferredDispatch = defer(); @@ -366,6 +368,7 @@ export class StorageTaskBroker implements TaskBroker { this.logger, this.auth, this.config, + this.additionalWorkspaceProviders, ); } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/WorkspaceService.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/WorkspaceService.ts new file mode 100644 index 0000000000..ab424806bf --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/WorkspaceService.ts @@ -0,0 +1,88 @@ +/* + * Copyright 2024 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 { Config } from '@backstage/config'; +import { CurrentClaimedTask } from './StorageTaskBroker'; +import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; +import { DatabaseWorkspaceProvider } from './DatabaseWorkspaceProvider'; +import { TaskStore } from './types'; + +export interface WorkspaceService { + serializeWorkspace(options: { path: string }): Promise; + + cleanWorkspace(): Promise; + + rehydrateWorkspace(options: { + taskId: string; + targetPath: string; + }): Promise; +} + +export class DefaultWorkspaceService implements WorkspaceService { + static create( + task: CurrentClaimedTask, + storage: TaskStore, + additionalWorkspaceProviders?: Record, + config?: Config, + ) { + const workspaceProviderName = + config?.getOptionalString( + 'scaffolder.EXPERIMENTAL_workspaceSerializationProvider', + ) ?? 'database'; + const workspaceProvider = + additionalWorkspaceProviders?.[workspaceProviderName] ?? + DatabaseWorkspaceProvider.create(storage); + return new DefaultWorkspaceService(task, workspaceProvider, config); + } + + private constructor( + private readonly task: CurrentClaimedTask, + private readonly workspaceProvider: WorkspaceProvider, + private readonly config?: Config, + ) {} + + public async serializeWorkspace(options: { path: string }): Promise { + if (this.isWorkspaceSerializationEnabled()) { + await this.workspaceProvider.serializeWorkspace({ + path: options.path, + taskId: this.task.taskId, + }); + } + } + + public async cleanWorkspace(): Promise { + if (this.isWorkspaceSerializationEnabled()) { + await this.workspaceProvider.cleanWorkspace({ taskId: this.task.taskId }); + } + } + + public async rehydrateWorkspace(options: { + taskId: string; + targetPath: string; + }): Promise { + if (this.isWorkspaceSerializationEnabled()) { + await this.workspaceProvider.rehydrateWorkspace(options); + } + } + + private isWorkspaceSerializationEnabled(): boolean { + return ( + this.config?.getOptionalBoolean( + 'scaffolder.EXPERIMENTAL_workspaceSerialization', + ) ?? false + ); + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 0e0ebd706b..89bcce3cc9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -14,20 +14,20 @@ * limitations under the License. */ -import { JsonValue, JsonObject, HumanDuration } from '@backstage/types'; +import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common'; -import { TaskSecrets } from '@backstage/plugin-scaffolder-node'; import { - TemplateAction, - TaskStatus as _TaskStatus, - TaskCompletionState as _TaskCompletionState, SerializedTask as _SerializedTask, - TaskEventType as _TaskEventType, SerializedTaskEvent as _SerializedTaskEvent, - TaskBrokerDispatchResult as _TaskBrokerDispatchResult, - TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, - TaskContext as _TaskContext, TaskBroker as _TaskBroker, + TaskBrokerDispatchOptions as _TaskBrokerDispatchOptions, + TaskBrokerDispatchResult as _TaskBrokerDispatchResult, + TaskCompletionState as _TaskCompletionState, + TaskContext as _TaskContext, + TaskEventType as _TaskEventType, + TaskSecrets, + TaskStatus as _TaskStatus, + TemplateAction, } from '@backstage/plugin-scaffolder-node'; /** @@ -190,7 +190,10 @@ export interface TaskStore { tasks: { taskId: string }[]; }>; - list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; emitLogEvent(options: TaskStoreEmitOptions): Promise; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 8ba26c68a4..01a2fba6e8 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -46,6 +46,8 @@ import { PermissionEvaluator, } from '@backstage/plugin-permission-common'; import { mockCredentials, mockServices } from '@backstage/backend-test-utils'; +import { AutocompleteHandler } from '@backstage/plugin-scaffolder-node/alpha'; +import { MiddlewareFactory } from '@backstage/backend-app-api'; const mockAccess = jest.fn(); @@ -430,10 +432,11 @@ describe('createRouter', () => { }); const response = await request(app).get( - `/v2/tasks?createdBy=user:default/foo`, + `/v2/tasks?createdBy=user:default/foo&status=completed`, ); expect(taskBroker.list).toHaveBeenCalledWith({ createdBy: 'user:default/foo', + status: 'completed', }); expect(response.status).toEqual(200); @@ -691,6 +694,34 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(subscriber!.closed).toBe(true); }); }); + + describe('POST /v2/dry-run', () => { + it('should get user entity', async () => { + const mockToken = mockCredentials.user.token(); + const mockTemplate = getMockTemplate(); + + const catalogSpy = jest.spyOn(catalogClient, 'getEntityByRef'); + + await request(app) + .post('/v2/dry-run') + .set('Authorization', `Bearer ${mockToken}`) + .send({ + template: mockTemplate, + values: { + requiredParameter1: 'required-value-1', + requiredParameter2: 'required-value-2', + }, + directoryContents: [], + }); + + expect(catalogSpy).toHaveBeenCalledTimes(1); + + expect(catalogSpy).toHaveBeenCalledWith( + 'user:default/mock', + expect.anything(), + ); + }); + }); }); describe('providing an identity api', () => { @@ -1461,5 +1492,75 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{ expect(subscriber!.closed).toBe(true); }); }); + + describe('GET /v2/autocomplete/:provider/:resource', () => { + let handleAutocompleteRequest: AutocompleteHandler; + + beforeEach(async () => { + handleAutocompleteRequest = jest.fn().mockResolvedValue({ + results: [{ title: 'blob' }], + }); + + const logger = mockServices.logger.mock(); + const middleware = MiddlewareFactory.create({ config, logger }); + const router = await createRouter({ + logger: loggerToWinstonLogger(mockServices.logger.mock()), + config: new ConfigReader({}), + database: createDatabase(), + catalogClient, + reader: mockUrlReader, + taskBroker, + permissions: permissionApi, + auth, + httpAuth, + discovery, + autocompleteHandlers: { + 'test-provider': handleAutocompleteRequest, + }, + }); + + app = express().use(router).use(middleware.error()); + }); + + it('should throw an error when the provider is not registered', async () => { + const response = await request(app) + .post('/v2/autocomplete/unknown-provider/resource') + .send({ + token: 'token', + context: {}, + }); + + expect(response.status).toEqual(400); + expect(response.body).toEqual( + expect.objectContaining({ + error: { + message: 'Unsupported provider: unknown-provider', + name: 'InputError', + }, + }), + ); + }); + + it('should call the autocomplete handler', async () => { + const context = { mock: 'context' }; + const mockToken = 'mocktoken'; + + const response = await request(app) + .post('/v2/autocomplete/test-provider/resource') + .send({ + token: mockToken, + context, + }); + + expect(response.status).toEqual(200); + + expect(response.body).toEqual({ results: [{ title: 'blob' }] }); + expect(handleAutocompleteRequest).toHaveBeenCalledWith({ + token: mockToken, + context, + resource: 'resource', + }); + }); + }); }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 384b9f5340..a25731ce45 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -15,10 +15,10 @@ */ import { + createLegacyAuthAdapters, HostDiscovery, PluginDatabaseManager, UrlReader, - createLegacyAuthAdapters, } from '@backstage/backend-common'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { CatalogApi } from '@backstage/catalog-client'; @@ -35,22 +35,22 @@ import { ScmIntegrations } from '@backstage/integration'; import { HumanDuration, JsonObject, JsonValue } from '@backstage/types'; import { TaskSpec, + TemplateEntityStepV1beta3, TemplateEntityV1beta3, templateEntityV1beta3Validator, TemplateParametersV1beta3, - TemplateEntityStepV1beta3, } from '@backstage/plugin-scaffolder-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE, scaffolderActionPermissions, + scaffolderTaskPermissions, scaffolderTemplatePermissions, taskCancelPermission, taskCreatePermission, taskReadPermission, templateParameterReadPermission, templateStepReadPermission, - scaffolderTaskPermissions, } from '@backstage/plugin-scaffolder-common/alpha'; import express from 'express'; import Router from 'express-promise-router'; @@ -58,8 +58,9 @@ import { validate } from 'jsonschema'; import { Logger } from 'winston'; import { z } from 'zod'; import { - TemplateAction, TaskBroker, + TaskStatus, + TemplateAction, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; @@ -94,6 +95,10 @@ import { } from '@backstage/plugin-auth-node'; import { InternalTaskSecrets } from '../scaffolder/tasks/types'; import { checkPermission } from '../util/checkPermissions'; +import { + AutocompleteHandler, + WorkspaceProvider, +} from '@backstage/plugin-scaffolder-node/alpha'; /** * @@ -158,6 +163,7 @@ export interface RouterOptions { taskBroker?: TaskBroker; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; + additionalWorkspaceProviders?: Record; permissions?: PermissionsService; permissionRules?: Array< TemplatePermissionRuleInput | ActionPermissionRuleInput @@ -166,6 +172,8 @@ export interface RouterOptions { httpAuth?: HttpAuthService; identity?: IdentityApi; discovery?: DiscoveryService; + + autocompleteHandlers?: Record; } function isSupportedTemplate(entity: TemplateEntityV1beta3) { @@ -269,10 +277,12 @@ export async function createRouter( scheduler, additionalTemplateFilters, additionalTemplateGlobals, + additionalWorkspaceProviders, permissions, permissionRules, discovery = HostDiscovery.fromConfig(config), identity = buildDefaultIdentityClient(options), + autocompleteHandlers = {}, } = options; const { auth, httpAuth } = createLegacyAuthAdapters({ @@ -293,7 +303,13 @@ export async function createRouter( let taskBroker: TaskBroker; if (!options.taskBroker) { const databaseTaskStore = await DatabaseTaskStore.create({ database }); - taskBroker = new StorageTaskBroker(databaseTaskStore, logger, config, auth); + taskBroker = new StorageTaskBroker( + databaseTaskStore, + logger, + config, + auth, + additionalWorkspaceProviders, + ); if (scheduler && databaseTaskStore.listStaleTasks) { await scheduler.scheduleTask({ @@ -572,8 +588,17 @@ export async function createRouter( ); } + const [statusQuery] = [req.query.status].flat(); + if ( + typeof statusQuery !== 'string' && + typeof statusQuery !== 'undefined' + ) { + throw new InputError('status query parameter must be a string'); + } + const tasks = await taskBroker.list({ createdBy: userEntityRef, + status: statusQuery ? (statusQuery as TaskStatus) : undefined, }); res.status(200).json(tasks); @@ -730,6 +755,14 @@ export async function createRouter( targetPluginId: 'catalog', }); + const userEntityRef = auth.isPrincipal(credentials, 'user') + ? credentials.principal.userEntityRef + : undefined; + + const userEntity = userEntityRef + ? await catalogClient.getEntityByRef(userEntityRef, { token }) + : undefined; + for (const parameters of [template.spec.parameters ?? []].flat()) { const result = validate(body.values, parameters); if (!result.valid) { @@ -750,6 +783,10 @@ export async function createRouter( steps, output: template.spec.output ?? {}, parameters: body.values as JsonObject, + user: { + entity: userEntity as UserEntity, + ref: userEntityRef, + }, }, directoryContents: (body.directoryContents ?? []).map(file => ({ path: file.path, @@ -771,6 +808,24 @@ export async function createRouter( base64Content: file.content.toString('base64'), })), }); + }) + .post('/v2/autocomplete/:provider/:resource', async (req, res) => { + const { token, context } = req.body; + const { provider, resource } = req.params; + + if (!token) throw new InputError('Missing token query parameter'); + + if (!autocompleteHandlers[provider]) { + throw new InputError(`Unsupported provider: ${provider}`); + } + + const { results } = await autocompleteHandlers[provider]({ + resource, + token, + context, + }); + + res.status(200).json({ results }); }); const app = express(); diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 12f3f9e235..4810656ef4 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,32 @@ # @backstage/plugin-scaffolder-common +## 1.5.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 1.5.5-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + +## 1.5.4 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-permission-common@0.8.0 + - @backstage/catalog-model@1.5.0 + - @backstage/types@1.1.1 + ## 1.5.3 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index 3e56510daf..949a831906 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-common", - "version": "1.5.3", + "version": "1.5.5-next.1", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", "backstage": { "role": "common-library", diff --git a/plugins/scaffolder-node-test-utils/CHANGELOG.md b/plugins/scaffolder-node-test-utils/CHANGELOG.md index ebdf3092b7..c7d37fb0c8 100644 --- a/plugins/scaffolder-node-test-utils/CHANGELOG.md +++ b/plugins/scaffolder-node-test-utils/CHANGELOG.md @@ -1,5 +1,67 @@ # @backstage/plugin-scaffolder-node-test-utils +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.4.5-next.2 + - @backstage/plugin-scaffolder-node@0.4.9-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/types@1.1.1 + +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.1 + - @backstage/backend-test-utils@0.4.5-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.1 + +## 0.1.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-test-utils@0.4.5-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.9-next.0 + +## 0.1.9 + +### Patch Changes + +- 54429b5: Filename and imports correction for mockActionContext.ts +- Updated dependencies + - @backstage/backend-test-utils@0.4.4 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-scaffolder-node@0.4.8 + - @backstage/types@1.1.1 + +## 0.1.9-next.1 + +### Patch Changes + +- 54429b5: Filename and imports correction for mockActionContext.ts +- Updated dependencies + - @backstage/backend-test-utils@0.4.4-next.1 + - @backstage/backend-common@0.23.3-next.1 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-node@0.4.8-next.1 + +## 0.1.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-scaffolder-node@0.4.7-next.0 + - @backstage/backend-test-utils@0.4.3-next.0 + - @backstage/types@1.1.1 + ## 0.1.5 ### Patch Changes diff --git a/plugins/scaffolder-node-test-utils/package.json b/plugins/scaffolder-node-test-utils/package.json index e15b33dad3..707ffa1388 100644 --- a/plugins/scaffolder-node-test-utils/package.json +++ b/plugins/scaffolder-node-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node-test-utils", - "version": "0.1.5", + "version": "0.1.10-next.2", "backstage": { "role": "node-library", "pluginId": "scaffolder", diff --git a/plugins/scaffolder-node-test-utils/src/actions/index.ts b/plugins/scaffolder-node-test-utils/src/actions/index.ts index 161bae8521..aee195cd34 100644 --- a/plugins/scaffolder-node-test-utils/src/actions/index.ts +++ b/plugins/scaffolder-node-test-utils/src/actions/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { createMockActionContext } from './mockActionConext'; +export { createMockActionContext } from './mockActionContext'; diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts similarity index 100% rename from plugins/scaffolder-node-test-utils/src/actions/mockActionConext.ts rename to plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index f5da44ae1e..42fc13a644 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,90 @@ # @backstage/plugin-scaffolder-node +## 0.4.9-next.2 + +### Patch Changes + +- c544f81: Add support for status filtering in scaffolder tasks endpoint +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/integration@1.14.0-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.9-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/integration@1.14.0-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.4 + +## 0.4.8 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/integration@1.13.0 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + +## 0.4.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/integration@1.13.0-next.0 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.3 + +## 0.4.7-next.0 + +### Patch Changes + +- 661b354: Fixed a bug where the `RepoUrlPicker` would still require the `owner` field for `azure` +- b5deed0: Add support for `autocomplete` extension point to provide additional `autocomplete` handlers +- 0b52438: Serialization of the scaffolder workspace into GCP bucket +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/integration@1.13.0-next.0 + - @backstage/catalog-model@1.5.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-scaffolder-common@1.5.3 + ## 0.4.5 ### Patch Changes diff --git a/plugins/scaffolder-node/api-report-alpha.md b/plugins/scaffolder-node/api-report-alpha.md index c2a2a1597e..c9b8130f0e 100644 --- a/plugins/scaffolder-node/api-report-alpha.md +++ b/plugins/scaffolder-node/api-report-alpha.md @@ -3,12 +3,35 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { TaskBroker } from '@backstage/plugin-scaffolder-node'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { TemplateFilter } from '@backstage/plugin-scaffolder-node'; import { TemplateGlobal } from '@backstage/plugin-scaffolder-node'; +// @alpha +export type AutocompleteHandler = ({ + resource, + token, + context, +}: { + resource: string; + token: string; + context: Record; +}) => Promise<{ + results: { + title: string; + }[]; +}>; + +// @alpha +export const restoreWorkspace: (opts: { + path: string; + buffer?: Buffer; +}) => Promise; + // @alpha export interface ScaffolderActionsExtensionPoint { // (undocumented) @@ -18,6 +41,21 @@ export interface ScaffolderActionsExtensionPoint { // @alpha export const scaffolderActionsExtensionPoint: ExtensionPoint; +// @alpha +export interface ScaffolderAutocompleteExtensionPoint { + // (undocumented) + addAutocompleteProvider({ + id, + handler, + }: { + id: string; + handler: AutocompleteHandler; + }): void; +} + +// @alpha +export const scaffolderAutocompleteExtensionPoint: ExtensionPoint; + // @alpha export interface ScaffolderTaskBrokerExtensionPoint { // (undocumented) @@ -38,5 +76,38 @@ export interface ScaffolderTemplatingExtensionPoint { // @alpha export const scaffolderTemplatingExtensionPoint: ExtensionPoint; +// @alpha +export interface ScaffolderWorkspaceProviderExtensionPoint { + // (undocumented) + addProviders(providers: Record): void; +} + +// @alpha +export const scaffolderWorkspaceProviderExtensionPoint: ExtensionPoint; + +// @alpha +export const serializeWorkspace: (opts: { path: string }) => Promise<{ + contents: Buffer; +}>; + +// @alpha +export interface WorkspaceProvider { + // (undocumented) + cleanWorkspace(options: { taskId: string }): Promise; + // (undocumented) + rehydrateWorkspace(options: { + taskId: string; + targetPath: string; + }): Promise; + // (undocumented) + serializeWorkspace({ + path, + taskId, + }: { + path: string; + taskId: string; + }): Promise; +} + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md index 66e8ccd678..515a169cb2 100644 --- a/plugins/scaffolder-node/api-report.md +++ b/plugins/scaffolder-node/api-report.md @@ -315,7 +315,7 @@ export interface TaskBroker { // (undocumented) get(taskId: string): Promise; // (undocumented) - list?(options?: { createdBy?: string }): Promise<{ + list?(options?: { createdBy?: string; status?: TaskStatus }): Promise<{ tasks: SerializedTask[]; }>; // (undocumented) diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index ed3c016257..6e84b278c2 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-node", - "version": "0.4.5", + "version": "0.4.9-next.2", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", "backstage": { "role": "node-library", @@ -61,11 +61,13 @@ "@backstage/integration": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/types": "workspace:^", + "concat-stream": "^2.0.0", "fs-extra": "^11.2.0", "globby": "^11.0.0", "isomorphic-git": "^1.23.0", "jsonschema": "^1.2.6", "p-limit": "^3.1.0", + "tar": "^6.1.12", "winston": "^3.2.1", "zod": "^3.22.4", "zod-to-json-schema": "^3.20.4" diff --git a/plugins/scaffolder-node/src/actions/util.ts b/plugins/scaffolder-node/src/actions/util.ts index 2b3a1296bf..5e4ce60a1a 100644 --- a/plugins/scaffolder-node/src/actions/util.ts +++ b/plugins/scaffolder-node/src/actions/util.ts @@ -85,6 +85,10 @@ export const parseRepoUrl = ( checkRequiredParams(parsed, 'project', 'repo'); break; } + case 'azure': { + checkRequiredParams(parsed, 'project', 'repo'); + break; + } case 'gitlab': { // project is the projectID, and if defined, owner and repo won't be needed. if (!project) { diff --git a/plugins/scaffolder-node/src/alpha.ts b/plugins/scaffolder-node/src/alpha.ts index b85da5b590..6a0e34faf7 100644 --- a/plugins/scaffolder-node/src/alpha.ts +++ b/plugins/scaffolder-node/src/alpha.ts @@ -22,6 +22,8 @@ import { TaskBroker, } from '@backstage/plugin-scaffolder-node'; +export * from './tasks/alpha'; + /** * Extension point for managing scaffolder actions. * @@ -67,6 +69,7 @@ export const scaffolderTaskBrokerExtensionPoint = */ export interface ScaffolderTemplatingExtensionPoint { addTemplateFilters(filters: Record): void; + addTemplateGlobals(filters: Record): void; } @@ -79,3 +82,82 @@ export const scaffolderTemplatingExtensionPoint = createExtensionPoint({ id: 'scaffolder.templating', }); + +/** + * Autocomplete handler for the scaffolder. + * @alpha + */ +export type AutocompleteHandler = ({ + resource, + token, + context, +}: { + resource: string; + token: string; + context: Record; +}) => Promise<{ results: { title: string }[] }>; + +/** + * Extension point for adding autocomplete handler providers + * @alpha + */ +export interface ScaffolderAutocompleteExtensionPoint { + addAutocompleteProvider({ + id, + handler, + }: { + id: string; + handler: AutocompleteHandler; + }): void; +} + +/** + * Extension point for adding autocomplete handlers. + * + * @alpha + */ +export const scaffolderAutocompleteExtensionPoint = + createExtensionPoint({ + id: 'scaffolder.autocomplete', + }); + +/** + * This provider has to be implemented to make it possible to serialize/deserialize scaffolder workspace. + * + * @alpha + */ +export interface WorkspaceProvider { + serializeWorkspace({ + path, + taskId, + }: { + path: string; + taskId: string; + }): Promise; + + cleanWorkspace(options: { taskId: string }): Promise; + + rehydrateWorkspace(options: { + taskId: string; + targetPath: string; + }): Promise; +} + +/** + * Extension point for adding workspace providers. + * + * @alpha + */ +export interface ScaffolderWorkspaceProviderExtensionPoint { + addProviders(providers: Record): void; +} + +/** + * Extension point for adding workspace providers. + * + * @alpha + */ +export const scaffolderWorkspaceProviderExtensionPoint = + createExtensionPoint({ + id: 'scaffolder.workspace.provider', + }); diff --git a/packages/backend-common/src/service/index.ts b/plugins/scaffolder-node/src/tasks/alpha.ts similarity index 83% rename from packages/backend-common/src/service/index.ts rename to plugins/scaffolder-node/src/tasks/alpha.ts index c2ae6ba87b..fb6b09026c 100644 --- a/packages/backend-common/src/service/index.ts +++ b/plugins/scaffolder-node/src/tasks/alpha.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2024 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. @@ -13,5 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -export { createStatusCheckRouter } from './createStatusCheckRouter'; +export * from './serializer'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/serializer.test.ts b/plugins/scaffolder-node/src/tasks/serializer.test.ts similarity index 96% rename from plugins/scaffolder-backend/src/scaffolder/tasks/serializer.test.ts rename to plugins/scaffolder-node/src/tasks/serializer.test.ts index bec5398e28..a0ddfa624d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/serializer.test.ts +++ b/plugins/scaffolder-node/src/tasks/serializer.test.ts @@ -72,8 +72,11 @@ describe('serializer', () => { const restoredWorkspaceDir = createMockDirectory(); it('should be able to archive and restore the workspace', async () => { - const workspaceBuffer = await serializeWorkspace(workspaceDir.path); - await restoreWorkspace(restoredWorkspaceDir.path, workspaceBuffer); + const workspaceBuffer = await serializeWorkspace(workspaceDir); + await restoreWorkspace({ + path: restoredWorkspaceDir.path, + buffer: workspaceBuffer.contents, + }); expect( fs.existsSync(`${restoredWorkspaceDir.path}/\$\{ESCAPE_ME\}.txt`), diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/serializer.ts b/plugins/scaffolder-node/src/tasks/serializer.ts similarity index 59% rename from plugins/scaffolder-backend/src/scaffolder/tasks/serializer.ts rename to plugins/scaffolder-node/src/tasks/serializer.ts index 69c48fb769..baf1b42157 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/serializer.ts +++ b/plugins/scaffolder-node/src/tasks/serializer.ts @@ -20,14 +20,34 @@ import { promisify } from 'util'; import { pipeline as pipelineCb, Readable } from 'stream'; const pipeline = promisify(pipelineCb); - -export const serializeWorkspace = async (path: string): Promise => { - return await new Promise(async resolve => { - await pipeline(tar.create({ cwd: path }, ['']), concatStream(resolve)); +/** + * Serializes provided path into tar archive + * + * @alpha + */ +export const serializeWorkspace = async (opts: { + path: string; +}): Promise<{ contents: Buffer }> => { + return new Promise<{ contents: Buffer }>(async resolve => { + await pipeline( + tar.create({ cwd: opts.path }, ['']), + concatStream(buffer => { + return resolve({ contents: buffer }); + }), + ); }); }; -export const restoreWorkspace = async (path: string, buffer?: Buffer) => { +/** + * Rehydrates the provided buffer of tar archive into the provide destination path + * + * @alpha + */ +export const restoreWorkspace = async (opts: { + path: string; + buffer?: Buffer; +}): Promise => { + const { buffer, path } = opts; if (buffer) { await pipeline( Readable.from(buffer), diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index a2adb97b9c..8324e352f2 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -180,5 +180,8 @@ export interface TaskBroker { get(taskId: string): Promise; - list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>; + list?(options?: { + createdBy?: string; + status?: TaskStatus; + }): Promise<{ tasks: SerializedTask[] }>; } diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 666fe36e24..1661f3d1c7 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,139 @@ # @backstage/plugin-scaffolder-react +## 1.11.0-next.2 + +### Patch Changes + +- 072c00c: Fixed a bug in `DefaultTableOutputs` where output elements overlapped on smaller screen sizes +- 04759f2: Fix null check in `isJsonObject` utility function for scaffolder review state component +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/plugin-permission-react@0.4.25-next.1 + - @backstage/plugin-scaffolder-common@1.5.5-next.1 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.11.0-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/plugin-permission-react@0.4.25-next.0 + - @backstage/plugin-scaffolder-common@1.5.5-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.11.0-next.0 + +### Minor Changes + +- 8839381: Add scaffolder option to display object items in separate rows on review page + +### Patch Changes + +- d0e95a7: Add ability to customise form fields in the UI by exposing `uiSchema` and `formContext` in `FormProps` +- 4670f06: support `ajv-errors` for scaffolder validation to allow for customizing the error messages +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + +## 1.10.0 + +### Minor Changes + +- 354e68c: Improve validation error display text in scaffolder +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- 4d7e11f: disables rendering of output box if no output is returned +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/plugin-permission-react@0.4.24 + - @backstage/plugin-scaffolder-common@1.5.4 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + +## 1.10.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + +## 1.10.0-next.1 + +### Minor Changes + +- 354e68c: Improve validation error display text in scaffolder + +### Patch Changes + +- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`. + Updated dependency `@rjsf/core` to `5.18.5`. + Updated dependency `@rjsf/material-ui` to `5.18.5`. + Updated dependency `@rjsf/validator-ajv8` to `5.18.5`. +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + +## 1.10.0-next.0 + +### Minor Changes + +- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker` + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/catalog-client@1.6.5 + - @backstage/catalog-model@1.5.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/version-bridge@1.0.8 + - @backstage/plugin-permission-react@0.4.23 + - @backstage/plugin-scaffolder-common@1.5.3 + ## 1.9.0 ### Minor Changes diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index 850c30ced8..a19a656d17 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -128,7 +128,7 @@ export interface FieldExtensionUiSchema // @public export type FormProps = Pick< FormProps_2, - 'transformErrors' | 'noHtml5Validate' + 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' >; // @public @@ -179,6 +179,17 @@ export type ReviewStepProps = { // @public export interface ScaffolderApi { + // (undocumented) + autocomplete?(options: { + token: string; + provider: string; + resource: string; + context?: Record; + }): Promise<{ + results: { + title: string; + }[]; + }>; cancelTask(taskId: string): Promise; // (undocumented) dryRun?(options: ScaffolderDryRunOptions): Promise; diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index 4bb86ca98d..b47bbd731f 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-react", - "version": "1.9.0", + "version": "1.11.0-next.2", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", "backstage": { "role": "web-library", @@ -72,12 +72,13 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", "@react-hookz/web": "^24.0.0", - "@rjsf/core": "5.18.4", - "@rjsf/material-ui": "5.18.4", - "@rjsf/utils": "5.18.4", - "@rjsf/validator-ajv8": "5.18.4", + "@rjsf/core": "5.18.5", + "@rjsf/material-ui": "5.18.5", + "@rjsf/utils": "5.18.5", + "@rjsf/validator-ajv8": "5.18.5", "@types/json-schema": "^7.0.9", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "ajv-errors": "^3.0.0", "classnames": "^2.2.6", "flatted": "3.3.1", "humanize-duration": "^3.25.1", diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts index b4a3deea54..eaef54ff1b 100644 --- a/plugins/scaffolder-react/src/api/types.ts +++ b/plugins/scaffolder-react/src/api/types.ts @@ -229,4 +229,11 @@ export interface ScaffolderApi { streamLogs(options: ScaffolderStreamLogsOptions): Observable; dryRun?(options: ScaffolderDryRunOptions): Promise; + + autocomplete?(options: { + token: string; + provider: string; + resource: string; + context?: Record; + }): Promise<{ results: { title: string }[] }>; } diff --git a/plugins/scaffolder-react/src/components/types.ts b/plugins/scaffolder-react/src/components/types.ts index 597998bac4..d5d317015d 100644 --- a/plugins/scaffolder-react/src/components/types.ts +++ b/plugins/scaffolder-react/src/components/types.ts @@ -32,7 +32,7 @@ export type TemplateGroupFilter = { */ export type FormProps = Pick< SchemaFormProps, - 'transformErrors' | 'noHtml5Validate' + 'transformErrors' | 'noHtml5Validate' | 'uiSchema' | 'formContext' >; /** diff --git a/plugins/scaffolder-react/src/next/components/Form/Form.tsx b/plugins/scaffolder-react/src/next/components/Form/Form.tsx index 8717c8b55a..05c8a4934f 100644 --- a/plugins/scaffolder-react/src/next/components/Form/Form.tsx +++ b/plugins/scaffolder-react/src/next/components/Form/Form.tsx @@ -44,6 +44,8 @@ export const Form = (props: PropsWithChildren) => { uiSchema={wrapperProps.uiSchema ?? {}} formData={wrapperProps.formData} rawErrors={wrapperProps.rawErrors ?? []} + disabled={wrapperProps.disabled ?? false} + readonly={wrapperProps.readonly ?? false} /> ); }, diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx index e9277b2230..194c5f3e77 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.test.tsx @@ -69,6 +69,9 @@ describe('ReviewState', () => { const formState = { name: 'John Doe', test: 'bob', + nest: { + foo: 'bar', + }, }; const schemas: ParsedTemplateSchema[] = [ @@ -85,6 +88,20 @@ describe('ReviewState', () => { }, }, }, + nest: { + type: 'object', + properties: { + foo: { + type: 'string', + 'ui:widget': 'password', + 'ui:backstage': { + review: { + show: false, + }, + }, + }, + }, + }, }, }, schema: {}, @@ -99,12 +116,16 @@ describe('ReviewState', () => { ); expect(getAllByRole('row').length).toEqual(1); expect(queryByRole('row', { name: 'Name ******' })).not.toBeInTheDocument(); + expect(queryByRole('row', { name: 'Foo ******' })).not.toBeInTheDocument(); }); it('should allow for masking an option with a set text', () => { const formState = { name: 'John Doe', test: 'bob', + nest: { + foo: 'bar', + }, }; const schemas: ParsedTemplateSchema[] = [ @@ -121,6 +142,20 @@ describe('ReviewState', () => { }, }, }, + nest: { + type: 'object', + properties: { + foo: { + type: 'string', + 'ui:widget': 'password', + 'ui:backstage': { + review: { + mask: 'lols', + }, + }, + }, + }, + }, }, }, schema: {}, @@ -135,11 +170,15 @@ describe('ReviewState', () => { ); expect(getByRole('row', { name: 'Name lols' })).toBeInTheDocument(); + expect(getByRole('row', { name: 'Foo lols' })).toBeInTheDocument(); }); it('should display enum label from enumNames', async () => { const formState = { name: 'type2', + nest: { + foo: 'type2', + }, }; const schemas: ParsedTemplateSchema[] = [ @@ -153,6 +192,17 @@ describe('ReviewState', () => { enum: ['type1', 'type2', 'type3'], enumNames: ['Label-type1', 'Label-type2', 'Label-type3'], }, + nest: { + type: 'object', + properties: { + foo: { + type: 'string', + default: 'type1', + enum: ['type1', 'type2', 'type3'], + enumNames: ['Label-type1', 'Label-type2', 'Label-type3'], + }, + }, + }, }, }, schema: {}, @@ -169,6 +219,7 @@ describe('ReviewState', () => { expect( queryByRole('row', { name: 'Name Label-type2' }), ).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Foo Label-type2' })).toBeInTheDocument(); }); it('should display enum value if no corresponding enumNames', async () => { @@ -202,4 +253,159 @@ describe('ReviewState', () => { expect(queryByRole('row', { name: 'Name type4' })).toBeInTheDocument(); }); + + it('should display object in separate rows', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + }); + + it('should display nested objects in separate rows', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + example: { + test: 'type6', + }, + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + example: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Test type6' })).toBeInTheDocument(); + }); + + it('should display partially nested objects', async () => { + const formState = { + name: { + foo: 'type3', + bar: 'type4', + example: { + test: 'type6', + }, + }, + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'object', + properties: { + foo: { + type: 'string', + default: 'type1', + }, + bar: { + type: 'string', + default: 'type2', + }, + example: { + type: 'object', + 'ui:backstage': { + review: { + explode: false, + }, + }, + properties: { + test: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + }, + ]; + + const { queryByRole } = render( + , + ); + + expect(queryByRole('row', { name: 'Foo type3' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Bar type4' })).toBeInTheDocument(); + expect(queryByRole('row', { name: 'Test type6' })).not.toBeInTheDocument(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx index e3c7bc303e..d87584afff 100644 --- a/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx +++ b/plugins/scaffolder-react/src/next/components/ReviewState/ReviewState.tsx @@ -15,9 +15,10 @@ */ import React from 'react'; import { StructuredMetadataTable } from '@backstage/core-components'; -import { JsonObject } from '@backstage/types'; +import { JsonObject, JsonValue } from '@backstage/types'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { ParsedTemplateSchema } from '../../hooks/useTemplateSchema'; +import { isJsonObject, getLastKey } from './util'; /** * The props for the {@link ReviewState} component. @@ -28,6 +29,55 @@ export type ReviewStateProps = { formState: JsonObject; }; +function processSchema( + key: string, + value: JsonValue | undefined, + schema: ParsedTemplateSchema, + formState: JsonObject, +): [string, JsonValue | undefined][] { + const parsedSchema = new JSONSchema(schema.mergedSchema); + const definitionInSchema = parsedSchema.getSchema({ + pointer: `#/${key}`, + data: formState, + }); + + if (definitionInSchema) { + const backstageReviewOptions = definitionInSchema['ui:backstage']?.review; + if (backstageReviewOptions) { + if (backstageReviewOptions.mask) { + return [[getLastKey(key), backstageReviewOptions.mask]]; + } + if (backstageReviewOptions.show === false) { + return []; + } + } + + if (definitionInSchema['ui:widget'] === 'password') { + return [[getLastKey(key), '******']]; + } + + if (definitionInSchema.enum && definitionInSchema.enumNames) { + return [ + [ + getLastKey(key), + definitionInSchema.enumNames[ + definitionInSchema.enum.indexOf(value) + ] || value, + ], + ]; + } + + if (backstageReviewOptions?.explode !== false && isJsonObject(value)) { + // Recurse nested objects + return Object.entries(value).flatMap(([nestedKey, nestedValue]) => + processSchema(`${key}/${nestedKey}`, nestedValue, schema, formState), + ); + } + } + + return [[getLastKey(key), value]]; +} + /** * The component used by the {@link Stepper} to render the review step. * @alpha @@ -35,42 +85,11 @@ export type ReviewStateProps = { export const ReviewState = (props: ReviewStateProps) => { const reviewData = Object.fromEntries( Object.entries(props.formState) - .map(([key, value]) => { + .flatMap(([key, value]) => { for (const step of props.schemas) { - const parsedSchema = new JSONSchema(step.mergedSchema); - const definitionInSchema = parsedSchema.getSchema({ - pointer: `#/${key}`, - data: props.formState, - }); - - if (definitionInSchema) { - const backstageReviewOptions = - definitionInSchema['ui:backstage']?.review; - - if (backstageReviewOptions) { - if (backstageReviewOptions.mask) { - return [key, backstageReviewOptions.mask]; - } - if (backstageReviewOptions.show === false) { - return []; - } - } - - if (definitionInSchema['ui:widget'] === 'password') { - return [key, '******']; - } - - if (definitionInSchema.enum && definitionInSchema.enumNames) { - return [ - key, - definitionInSchema.enumNames[ - definitionInSchema.enum.indexOf(value) - ] || value, - ]; - } - } + return processSchema(key, value, step, props.formState); } - return [key, value]; + return [[key, value]]; }) .filter(prop => prop.length > 0), ); diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts new file mode 100644 index 0000000000..649d0c3160 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2024 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 { isJsonObject, getLastKey } from './util'; + +describe('isJsonObject', () => { + it('should return true for non-null objects', () => { + expect(isJsonObject({})).toBe(true); + expect(isJsonObject({ key: 'value' })).toBe(true); + }); + + it('should return false for arrays', () => { + expect(isJsonObject([])).toBe(false); + expect(isJsonObject([1, 2, 3])).toBe(false); + }); + + it('should return false for non-objects', () => { + expect(isJsonObject('string')).toBe(false); + expect(isJsonObject(123)).toBe(false); + expect(isJsonObject(true)).toBe(false); + expect(isJsonObject(undefined)).toBe(false); + }); + + it('should return false for null values', () => { + expect(isJsonObject(null)).toBe(false); + }); +}); + +describe('getLastKey', () => { + it('should return the last part of a simple key', () => { + expect(getLastKey('simple')).toBe('simple'); + }); + + it('should return the last part of a nested key', () => { + expect(getLastKey('parent/child')).toBe('child'); + }); + + it('should return the last part of a deeply nested key', () => { + expect(getLastKey('grandparent/parent/child')).toBe('child'); + }); + + it('should handle keys with trailing slash', () => { + expect(getLastKey('parent/child/')).toBe(''); + }); + + it('should handle empty string', () => { + expect(getLastKey('')).toBe(''); + }); + + it('should handle keys with multiple consecutive slashes', () => { + expect(getLastKey('parent//child')).toBe('child'); + }); + + it('should handle keys with only slashes', () => { + expect(getLastKey('////')).toBe(''); + }); + + it('should handle keys with spaces', () => { + expect(getLastKey('parent/child with spaces')).toBe('child with spaces'); + }); + + it('should handle keys with special characters', () => { + expect(getLastKey('parent/child@!#$%^&*()')).toBe('child@!#$%^&*()'); + }); +}); diff --git a/plugins/scaffolder-react/src/next/components/ReviewState/util.ts b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts new file mode 100644 index 0000000000..d2caf71964 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/ReviewState/util.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2024 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/types'; + +export function isJsonObject(value?: JsonValue): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +// Helper function to get the last part of the key +export function getLastKey(key: string): string { + const parts = key.split('/'); + return parts[parts.length - 1]; +} diff --git a/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.test.tsx index c5ddf6854f..d632340031 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.test.tsx @@ -19,25 +19,88 @@ import { renderInTestApp } from '@backstage/test-utils'; import { ErrorListProps } from '@rjsf/utils'; describe('Error List Template', () => { - const errorList = { - errors: [ - { - stack: 'Test error 1', - }, - { - stack: 'Test error 2', - }, - ], - errorSchema: {}, - } as Partial as ErrorListProps; + describe('should render the error messages', () => { + it('no properties', async () => { + const errorList = { + errors: [ + { + stack: 'Test error 1', + }, + { + stack: 'Test error 2', + }, + ], + errorSchema: {}, + } as Partial as ErrorListProps; - it('should render the error messages', async () => { - const { getByText } = await renderInTestApp( - , - ); + const { getByText } = await renderInTestApp( + , + ); - for (const error of errorList.errors) { - expect(getByText(error.stack)).toBeInTheDocument(); - } + for (const error of errorList.errors) { + expect(getByText(error.stack)).toBeInTheDocument(); + } + }); + + it('properties no title', async () => { + const errorList = { + errors: [ + { + property: '.foo', + stack: 'Test error 1', + message: 'Test error 1', + }, + { + property: '.anExampleTitle', + stack: 'Test error 2', + message: 'Test error 2', + }, + ], + errorSchema: {}, + schema: {}, + } as Partial as ErrorListProps; + + const { getByText } = await renderInTestApp( + , + ); + + expect(getByText("'Foo' Test error 1")).toBeInTheDocument(); + expect(getByText("'An Example Title' Test error 2")).toBeInTheDocument(); + }); + + it('properties with title', async () => { + const errorList = { + errors: [ + { + property: '.foo', + stack: 'Test error 1', + message: 'Test error 1', + }, + { + property: '.bar', + stack: 'Test error 2', + message: 'Test error 2', + }, + ], + errorSchema: {}, + schema: { + properties: { + foo: { + title: 'Hello', + }, + bar: { + title: 'Example Title', + }, + }, + }, + } as Partial as ErrorListProps; + + const { getByText } = await renderInTestApp( + , + ); + + expect(getByText("'Hello' Test error 1")).toBeInTheDocument(); + expect(getByText("'Example Title' Test error 2")).toBeInTheDocument(); + }); }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.tsx b/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.tsx index a5310a5806..6b664fc235 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/ErrorListTemplate/errorListTemplate.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { ErrorListProps } from '@rjsf/utils'; +import { ErrorListProps, RJSFValidationError } from '@rjsf/utils'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; @@ -22,6 +22,7 @@ import ListItemText from '@material-ui/core/ListItemText'; import Paper from '@material-ui/core/Paper'; import { Theme, createStyles, makeStyles } from '@material-ui/core/styles'; import ErrorIcon from '@material-ui/icons/Error'; +import startCase from 'lodash/startCase'; const useStyles = makeStyles((_theme: Theme) => createStyles({ @@ -39,9 +40,28 @@ const useStyles = makeStyles((_theme: Theme) => * * @public */ -export const ErrorListTemplate = ({ errors }: ErrorListProps) => { +export const ErrorListTemplate = ({ errors, schema }: ErrorListProps) => { const classes = useStyles(); + function formatErrorMessage(error: RJSFValidationError) { + if (error.property && error.message) { + const propertyName = error.property.startsWith('.') + ? error.property.substring(1) + : error.property; + if (schema.properties && propertyName in schema.properties) { + const property = schema.properties[propertyName]; + + if (typeof property === 'object' && 'title' in property) { + return `'${property.title}' ${error.message}`; + } + } + // fall back to property name + return `'${startCase(propertyName)}' ${error.message}`; + } + // fall back if property does not exist + return error.stack; + } + return ( @@ -52,7 +72,7 @@ export const ErrorListTemplate = ({ errors }: ErrorListProps) => { ))} diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index d7e271f8b3..78aef3428f 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -392,6 +392,46 @@ describe('Stepper', () => { expect(getByText('invalid postcode')).toBeInTheDocument(); }); + it('should render ajv-errors message', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { + title: 'Step 1', + schema: { + properties: { + postcode: { + type: 'string', + pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]', + }, + }, + errorMessage: { + properties: { + postcode: 'invalid postcode', + }, + }, + }, + }, + ], + title: 'transformErrors Form Test', + }; + + const { getByText, getByRole } = await renderInTestApp( + + + , + ); + + await fireEvent.change(getByRole('textbox', { name: 'postcode' }), { + target: { value: 'invalid' }, + }); + + await act(async () => { + await fireEvent.click(getByRole('button', { name: 'Review' })); + }); + + expect(getByText('invalid postcode')).toBeInTheDocument(); + }); + it('should grab the initial formData from the query', async () => { const manifest: TemplateParameterSchema = { steps: [ @@ -509,6 +549,66 @@ describe('Stepper', () => { }); }); + it('should allow overrides to the uiSchema and formContext correctly', async () => { + const manifest: TemplateParameterSchema = { + title: 'Custom Fields', + steps: [ + { + title: 'Test', + schema: { + properties: { + name: { + type: 'string', + 'ui:placeholder': 'Enter your name', + }, + age: { + type: 'number', + 'ui:placeholder': 'Enter your age', + }, + }, + }, + }, + ], + }; + + const uiSchema = { + name: { + 'ui:readonly': true, + 'ui:placeholder': 'Should be overwritten', + }, + }; + + const formContext = { + readOnlyAsDisabled: true, + }; + + const { getByRole } = await renderInTestApp( + + + , + ); + + expect(getByRole('textbox', { name: 'name' })).toHaveValue('Some Name'); + expect(getByRole('textbox', { name: 'name' })).toBeDisabled(); + expect(getByRole('textbox', { name: 'name' })).toHaveAttribute( + 'placeholder', + 'Enter your name', + ); + + expect(getByRole('spinbutton', { name: 'age' })).toHaveValue(40); + expect(getByRole('spinbutton', { name: 'age' })).toBeEnabled(); + expect(getByRole('spinbutton', { name: 'age' })).toHaveAttribute( + 'placeholder', + 'Enter your age', + ); + }); + describe('Scaffolder Layouts', () => { it('should render the step in the scaffolder layout', async () => { const ScaffolderLayout: LayoutTemplate = ({ properties }) => ( diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 269b6a11d5..3a8858144b 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -35,7 +35,7 @@ import { } from './createAsyncValidators'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; import { useTemplateSchema, useFormDataFromQuery } from '../../hooks'; -import validator from '@rjsf/validator-ajv8'; +import { customizeValidator } from '@rjsf/validator-ajv8'; import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; import { hasErrors } from './utils'; import * as FieldOverrides from './FieldOverrides'; @@ -50,6 +50,11 @@ import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { ErrorListTemplate } from './ErrorListTemplate'; import { makeStyles } from '@material-ui/core/styles'; import { PasswordWidget } from '../PasswordWidget/PasswordWidget'; +import ajvErrors from 'ajv-errors'; +import { merge } from 'lodash'; + +const validator = customizeValidator(); +ajvErrors(validator.ajv); const useStyles = makeStyles(theme => ({ backButton: { @@ -189,6 +194,14 @@ export const Stepper = (stepperProps: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; + const { + formContext: propFormContext, + uiSchema: propUiSchema, + ...restFormProps + } = props.formProps ?? {}; + + const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); + return ( <> {isValidating && } @@ -225,9 +238,9 @@ export const Stepper = (stepperProps: StepperProps) => { validator={validator} extraErrors={errors as unknown as ErrorSchema} formData={formState} - formContext={{ formData: formState }} + formContext={{ ...propFormContext, formData: formState }} schema={currentStep.schema} - uiSchema={currentStep.uiSchema} + uiSchema={mergedUiSchema} onSubmit={handleNext} fields={fields} showErrorList="top" @@ -237,7 +250,7 @@ export const Stepper = (stepperProps: StepperProps) => { experimental_defaultFormStateBehavior={{ allOf: 'populateDefaults', }} - {...(props.formProps ?? {})} + {...restFormProps} >
    + } + /> + ); +}; + +export const handleMetaRedirects = ( + navigate: (to: string) => void, + entityName: string, +): Transformer => { + const redirectAfterMs = 4000; + const determineRedirectURL = (metaUrl: string) => { + const normalizedCurrentUrl = normalizeUrl(window.location.href); + // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. + const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); + const isExternalRedirect = + absoluteRedirectObj.hostname !== window.location.hostname; + + if (isExternalRedirect) { + const currentTechDocPath = window.location.pathname; + const indexOfSiteHome = currentTechDocPath.indexOf(entityName); + const siteHomePath = currentTechDocPath.slice( + 0, + indexOfSiteHome + entityName.length, + ); + return siteHomePath; + } + // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. + return absoluteRedirectObj.href; + }; + + return dom => { + for (const elem of Array.from(dom.querySelectorAll('meta'))) { + if (elem.getAttribute('http-equiv') === 'refresh') { + const metaContentParameters = elem + .getAttribute('content') + ?.split('url='); + if (!metaContentParameters || metaContentParameters.length < 2) { + continue; + } + const metaUrl = metaContentParameters[1]; + const redirectURL = determineRedirectURL(metaUrl); + const container = document.createElement('div'); + + renderReactElement( + navigate(redirectURL)} + autoHideDuration={redirectAfterMs} + />, + container, + ); + document.body.appendChild(container); + + setTimeout(() => { + navigate(redirectURL); + }, redirectAfterMs); + + return dom; + } + } + return dom; + }; +}; diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx index ed5fbba085..e45d66c676 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx +++ b/plugins/techdocs/src/reader/transformers/html/transformer.test.tsx @@ -81,4 +81,44 @@ describe('Transformers > Html', () => { expect(iframes).toHaveLength(1); expect(iframes[0].src).toMatch('docs.google.com'); }); + + it('should return a function that allows refresh meta tags', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const cleanDom = await result.current(dirtyDom); // calling html transformer + + const metaTags = Array.from( + cleanDom.querySelectorAll('meta'), + ); + + expect(metaTags).toHaveLength(1); + expect(metaTags[0].getAttribute('http-equiv')).toEqual('refresh'); + expect(metaTags[0].getAttribute('content')).toEqual( + '0;url=https://test.com', + ); + }); + + it('should return a function that does not allow non-refresh meta tags', async () => { + const { result } = renderHook(() => useSanitizerTransformer(), { wrapper }); + + const dirtyDom = document.createElement('html'); + dirtyDom.innerHTML = ` + + + + `; + const cleanDom = await result.current(dirtyDom); // calling html transformer + + const metaTags = Array.from( + cleanDom.querySelectorAll('meta'), + ); + + expect(metaTags).toHaveLength(0); + }); }); diff --git a/plugins/techdocs/src/reader/transformers/html/transformer.ts b/plugins/techdocs/src/reader/transformers/html/transformer.ts index 61498a37b0..4938a36648 100644 --- a/plugins/techdocs/src/reader/transformers/html/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/html/transformer.ts @@ -44,17 +44,39 @@ export const useSanitizerTransformer = (): Transformer => { const hosts = config?.getOptionalStringArray('allowedIframeHosts'); DOMPurify.addHook('beforeSanitizeElements', removeUnsafeLinks); - const tags = ['link']; + const tags = ['link', 'meta']; if (hosts) { tags.push('iframe'); DOMPurify.addHook('beforeSanitizeElements', removeUnsafeIframes(hosts)); } + // Only allow meta tags if they are used for refreshing the page. They are required for the redirect feature. + DOMPurify.addHook('uponSanitizeElement', (currNode, data) => { + if (data.tagName === 'meta') { + const isMetaRefreshTag = + currNode.getAttribute('http-equiv') === 'refresh' && + currNode.getAttribute('content')?.includes('url='); + if (!isMetaRefreshTag) { + currNode.parentNode?.removeChild(currNode); + } + } + }); + + // Only allow http-equiv and content attributes on meta tags. They are required for the redirect feature. + DOMPurify.addHook('uponSanitizeAttribute', (currNode, data) => { + if (currNode.tagName !== 'meta') { + if (data.attrName === 'http-equiv' || data.attrName === 'content') { + currNode.removeAttribute(data.attrName); + } + } + }); + // using outerHTML as we want to preserve the html tag attributes (lang) return DOMPurify.sanitize(dom.outerHTML, { ADD_TAGS: tags, FORBID_TAGS: ['style'], + ADD_ATTR: ['http-equiv', 'content'], WHOLE_DOCUMENT: true, RETURN_DOM: true, }); diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts index d78f8222f8..78b73cb52a 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/layout.ts @@ -74,6 +74,7 @@ export default ({ theme, sidebar }: RuleOptions) => ` } .md-sidebar .md-sidebar__scrollwrap { width: calc(12.1rem); + overflow-y: hidden; } .md-sidebar--secondary { right: ${theme.spacing(3)}px; @@ -234,7 +235,7 @@ export default ({ theme, sidebar }: RuleOptions) => ` #toggle-sidebar { display: none; } - + .md-content { margin: 0; width: 100%; diff --git a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts index cc7ba6bace..c682008a4d 100644 --- a/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts +++ b/plugins/techdocs/src/reader/transformers/styles/rules/typeset.ts @@ -30,6 +30,8 @@ type TypographyHeadings = Pick< type TypographyHeadingsKeys = keyof TypographyHeadings; const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; +const relativeLengthUnit: RegExp = /(em)|(rem)/gi; +const cssVariable: RegExp = /var\(|\)/gi; export default ({ theme }: RuleOptions) => ` /*================== Typeset ==================*/ @@ -43,17 +45,27 @@ ${headings.reduce((style, heading) => { (theme.typography as BackstageTypography).htmlFontSize ?? 16; const styles = theme.typography[heading]; const { lineHeight, fontFamily, fontWeight, fontSize } = styles; - const calculate = (value: typeof fontSize) => { - let factor: number | string = 1; + const calculate = (value: typeof fontSize): string | undefined => { if (typeof value === 'number') { - // convert px to rem - // 60% of the size defined because it is too big - factor = (value / htmlFontSize) * 0.6; + // Convert px to rem and apply 60% factor + return calculate(`${(value / htmlFontSize) * 0.6}rem`); + } else if (typeof value === 'string') { + if (value.match(cssVariable)) { + // Resolve css variable and calculate recursively + const resolvedValue = window + .getComputedStyle(document.body) + .getPropertyValue(value.replaceAll(cssVariable, '')); + if (resolvedValue !== '') { + return calculate(resolvedValue); + } + } else if (value.match(relativeLengthUnit)) { + // Use relative size as factor + const factor = value.replace(relativeLengthUnit, ''); + return `calc(${factor} * var(--md-typeset-font-size))`; + } } - if (typeof value === 'string') { - factor = value.replace('rem', ''); - } - return `calc(${factor} * var(--md-typeset-font-size))`; + // Value is not a number, relative length unit, or CSS variable, return as is + return value; }; return style.concat(` .md-typeset ${heading} { diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts b/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts deleted file mode 100644 index 90244e96ad..0000000000 --- a/plugins/techdocs/src/reader/transformers/styles/transformer.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2022 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 { renderHook } from '@testing-library/react'; -import { useStylesTransformer } from './transformer'; - -describe('Transformers > Styles', () => { - it('should return a function that injects all styles into a given dom element', () => { - const { result } = renderHook(() => useStylesTransformer()); - - const dom = document.createElement('html'); - dom.innerHTML = ''; - result.current(dom); // calling styles transformer - - const style = dom.querySelector('head > style'); - expect(style).toHaveTextContent( - '/*================== Variables ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Reset ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Layout ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Typeset ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Animations ==================*/', - ); - expect(style).toHaveTextContent( - '/*================== Extensions ==================*/', - ); - }); -}); diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx new file mode 100644 index 0000000000..b55e372212 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/styles/transformer.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright 2022 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 { renderHook } from '@testing-library/react'; +import { useStylesTransformer } from './transformer'; +import { createTheme, ThemeProvider } from '@material-ui/core/styles'; +import React from 'react'; + +describe('Transformers > Styles', () => { + it('should return a function that injects all styles into a given dom element', () => { + const { result } = renderHook(() => useStylesTransformer()); + + const dom = document.createElement('html'); + dom.innerHTML = ''; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + + expect(style).toHaveTextContent( + '/*================== Variables ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Reset ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Layout ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Typeset ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Animations ==================*/', + ); + expect(style).toHaveTextContent( + '/*================== Extensions ==================*/', + ); + }); + + it('should use headers relative font-size value as the factor for the md-typeset variable', () => { + const theme = createTheme({ + typography: { + h1: { + fontSize: '20rem', + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + 'font-size: calc(20 * var(--md-typeset-font-size));', + ); + }); + + it('should resolve header sizes that are variables', () => { + document.body.style.setProperty('--font-size-h1', '20rem'); + const theme = createTheme({ + typography: { + h1: { + fontSize: 'var(--font-size-h1)', + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + 'font-size: calc(20 * var(--md-typeset-font-size));', + ); + }); + + it('should convert pixel header sizes to REM and reduce by 60%', () => { + const theme = createTheme({ + typography: { + htmlFontSize: 16, + h1: { + fontSize: 100, + }, + }, + }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useStylesTransformer(), { wrapper }); + + const dom = document.createElement('html'); + dom.innerHTML = ``; + result.current(dom); // calling styles transformer + + const style = dom.querySelector('head > style'); + expect(style).not.toBeNull(); + + const h1 = style!.textContent?.match(/\.md-typeset h1 {.*?}/s); + expect(h1).toHaveLength(1); + expect(h1![0]).toContain( + // 100px / 16px * 0.6 = 3.75rem + 'font-size: calc(3.75 * var(--md-typeset-font-size));', + ); + }); +}); diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 489315b458..144d27d627 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,89 @@ # @backstage/plugin-user-settings-backend +## 0.2.22-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.8.0-next.2 + - @backstage/backend-common@0.23.4-next.2 + - @backstage/plugin-auth-node@0.5.0-next.2 + - @backstage/plugin-signals-node@0.1.9-next.2 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.2.22-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.1-next.1 + - @backstage/backend-common@0.23.4-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.1 + - @backstage/plugin-signals-node@0.1.9-next.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.2.22-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.4-next.0 + - @backstage/backend-plugin-api@0.7.1-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.18-next.0 + - @backstage/plugin-signals-node@0.1.9-next.0 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.2.21 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.7.0 + - @backstage/backend-common@0.23.3 + - @backstage/plugin-auth-node@0.4.17 + - @backstage/plugin-signals-node@0.1.8 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.2.21-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.23.3-next.1 + - @backstage/backend-plugin-api@0.6.22-next.1 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-auth-node@0.4.17-next.1 + - @backstage/plugin-signals-node@0.1.8-next.1 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.2.20-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.6.21-next.0 + - @backstage/backend-common@0.23.2-next.0 + - @backstage/plugin-auth-node@0.4.16-next.0 + - @backstage/plugin-signals-node@0.1.7-next.0 + - @backstage/config@1.2.0 + - @backstage/errors@1.2.4 + - @backstage/types@1.1.1 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.2.18 ### Patch Changes diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 556b0d3e4a..808f59fec4 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings-backend", - "version": "0.2.18", + "version": "0.2.22-next.2", "description": "The Backstage backend plugin to manage user settings", "backstage": { "role": "backend-plugin", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index bde8fa4408..a1aaa74828 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,118 @@ # @backstage/plugin-user-settings +## 0.8.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.7.0-next.2 + - @backstage/core-compat-api@0.2.8-next.2 + - @backstage/plugin-catalog-react@1.12.3-next.2 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.8.11-next.1 + +### Patch Changes + +- 6349099: Added config input type to the extensions +- Updated dependencies + - @backstage/plugin-catalog-react@1.12.3-next.1 + - @backstage/frontend-plugin-api@0.6.8-next.1 + - @backstage/core-compat-api@0.2.8-next.1 + - @backstage/core-app-api@1.14.2-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.8.10-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/frontend-plugin-api@0.6.8-next.0 + - @backstage/plugin-catalog-react@1.12.3-next.0 + - @backstage/core-components@0.14.10-next.0 + - @backstage/core-app-api@1.14.1-next.0 + - @backstage/core-compat-api@0.2.8-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.8.9 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9 + - @backstage/core-app-api@1.14.0 + - @backstage/plugin-catalog-react@1.12.2 + - @backstage/frontend-plugin-api@0.6.7 + - @backstage/core-compat-api@0.2.7 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.8.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.1 + - @backstage/frontend-plugin-api@0.6.7-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.2 + - @backstage/core-compat-api@0.2.7-next.1 + +## 0.8.9-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@1.13.1-next.1 + - @backstage/plugin-catalog-react@1.12.2-next.1 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + +## 0.8.8-next.0 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.14.9-next.0 + - @backstage/core-app-api@1.13.0-next.0 + - @backstage/plugin-catalog-react@1.12.2-next.0 + - @backstage/frontend-plugin-api@0.6.7-next.0 + - @backstage/core-compat-api@0.2.7-next.0 + - @backstage/core-plugin-api@1.9.3 + - @backstage/errors@1.2.4 + - @backstage/theme@0.5.6 + - @backstage/types@1.1.1 + - @backstage/plugin-signals-react@0.0.4 + - @backstage/plugin-user-settings-common@0.0.1 + ## 0.8.7 ### Patch Changes diff --git a/plugins/user-settings/api-report-alpha.md b/plugins/user-settings/api-report-alpha.md index b06fd1e583..700e06a7ad 100644 --- a/plugins/user-settings/api-report-alpha.md +++ b/plugins/user-settings/api-report-alpha.md @@ -13,14 +13,25 @@ const _default: BackstagePlugin< { root: RouteRef; }, + {}, {} >; export default _default; // @alpha (undocumented) -export const settingsNavItem: ExtensionDefinition<{ - title: string; -}>; +export const settingsNavItem: ExtensionDefinition< + { + title: string; + }, + { + title?: string | undefined; + }, + never, + never, + string | undefined, + string | undefined, + string | undefined +>; // @alpha (undocumented) export const userSettingsTranslationRef: TranslationRef< diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 05d594d85e..4217cf3329 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.8.7", + "version": "0.8.11-next.2", "description": "A Backstage plugin that provides a settings page", "backstage": { "role": "frontend-plugin", diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index a421c413dc..6bb0035b83 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -21,14 +21,14 @@ import { IdentityApi, StorageApi, } from '@backstage/core-plugin-api'; -import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserSettingsStorage } from './UserSettingsStorage'; describe('Persistent Storage API', () => { const server = setupServer(); - setupRequestMockHandlers(server); + registerMswTestHooks(server); const mockBaseUrl = 'http://backstage:9191/api'; const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 0493be1bca..018c4668a2 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -30,6 +30,7 @@ const IGNORED_WHEN_LISTING = [ /^.*[/\\]knip-report\.md$/, /^docs[/\\]releases[/\\].*-changelog\.md$/, /^docs[/\\]reference[/\\]/, + /^README-[a-z]{2}_[A-Z]{2}\.md$/, ]; const IGNORED_WHEN_EXPLICIT = [ diff --git a/storybook/.storybook/main.js b/storybook/.storybook/main.js index cb5f364a32..799c6c7c85 100644 --- a/storybook/.storybook/main.js +++ b/storybook/.storybook/main.js @@ -69,7 +69,7 @@ module.exports = Object.assign(({ args }) => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'ES2022', parser: { syntax: 'typescript', tsx: true, @@ -89,7 +89,7 @@ module.exports = Object.assign(({ args }) => { loader: require.resolve('swc-loader'), options: { jsc: { - target: 'es2019', + target: 'ES2022', parser: { syntax: 'ecmascript', jsx: true, diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 1702d6f4cc..28bbe4b144 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -11912,17 +11912,17 @@ __metadata: linkType: hard "ws@npm:^8.2.3": - version: 8.8.1 - resolution: "ws@npm:8.8.1" + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 2152cf862cae0693f3775bc688a6afb2e989d19d626d215e70f5fcd8eb55b1c3b0d3a6a4052905ec320e2d7734e20aeedbf9744496d62f15a26ad79cf4cf7dae + checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 7f0bd34e43..c14251269c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -385,69 +385,58 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/crc32c@npm:3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/crc32c@npm:3.0.0" +"@aws-crypto/crc32@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32@npm:5.2.0" dependencies: - "@aws-crypto/util": ^3.0.0 + "@aws-crypto/util": ^5.2.0 "@aws-sdk/types": ^3.222.0 - tslib: ^1.11.1 - checksum: 0a116b5d1c5b09a3dde65aab04a07b32f543e87b68f2d175081e3f4a1a17502343f223d691dd883ace1ddce65cd40093673e7c7415dcd99062202ba87ffb4038 + tslib: ^2.6.2 + checksum: 1ddf7ec3fccf106205ff2476d90ae1d6625eabd47752f689c761b71e41fe451962b7a1c9ed25fe54e17dd747a62fbf4de06030fe56fe625f95285f6f70b96c57 languageName: node linkType: hard -"@aws-crypto/ie11-detection@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/ie11-detection@npm:3.0.0" +"@aws-crypto/crc32c@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/crc32c@npm:5.2.0" dependencies: - tslib: ^1.11.1 - checksum: 299b2ddd46eddac1f2d54d91386ceb37af81aef8a800669281c73d634ed17fd855dcfb8b3157f2879344b93a2666a6d602550eb84b71e4d7868100ad6da8f803 + "@aws-crypto/util": ^5.2.0 + "@aws-sdk/types": ^3.222.0 + tslib: ^2.6.2 + checksum: 0b399de8607c59e1e46c05d2b24a16b56d507944fdac925c611f0ba7302f5555c098139806d7da1ebef1f89bf4e4b5d4dec74d4809ce0f18238b72072065effe languageName: node linkType: hard -"@aws-crypto/sha1-browser@npm:3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha1-browser@npm:3.0.0" +"@aws-crypto/sha1-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha1-browser@npm:5.2.0" dependencies: - "@aws-crypto/ie11-detection": ^3.0.0 - "@aws-crypto/supports-web-crypto": ^3.0.0 - "@aws-crypto/util": ^3.0.0 + "@aws-crypto/supports-web-crypto": ^5.2.0 + "@aws-crypto/util": ^5.2.0 "@aws-sdk/types": ^3.222.0 "@aws-sdk/util-locate-window": ^3.0.0 - "@aws-sdk/util-utf8-browser": ^3.0.0 - tslib: ^1.11.1 - checksum: 78c379e105a0c4e7b2ed745dffd8f55054d7dde8b350b61de682bbc3cd081a50e2f87861954fa9cd53c7ea711ebca1ca0137b14cb36483efc971f60f573cf129 + "@smithy/util-utf8": ^2.0.0 + tslib: ^2.6.2 + checksum: 8b04af601d945c5ef0f5f733b55681edc95b81c02ce5067b57f1eb4ee718e45485cf9aeeb7a84da9131656d09e1c4bc78040ec759f557a46703422d8df098d59 languageName: node linkType: hard -"@aws-crypto/sha256-browser@npm:3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha256-browser@npm:3.0.0" +"@aws-crypto/sha256-browser@npm:5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/sha256-browser@npm:5.2.0" dependencies: - "@aws-crypto/ie11-detection": ^3.0.0 - "@aws-crypto/sha256-js": ^3.0.0 - "@aws-crypto/supports-web-crypto": ^3.0.0 - "@aws-crypto/util": ^3.0.0 + "@aws-crypto/sha256-js": ^5.2.0 + "@aws-crypto/supports-web-crypto": ^5.2.0 + "@aws-crypto/util": ^5.2.0 "@aws-sdk/types": ^3.222.0 "@aws-sdk/util-locate-window": ^3.0.0 - "@aws-sdk/util-utf8-browser": ^3.0.0 - tslib: ^1.11.1 - checksum: ca89456bf508db2e08060a7f656460db97ac9a15b11e39d6fa7665e2b156508a1758695bff8e82d0a00178d6ac5c36f35eb4bcfac2e48621265224ca14a19bd2 + "@smithy/util-utf8": ^2.0.0 + tslib: ^2.6.2 + checksum: 773f12f2026d82a6bb4a23a8f491894a6d32525bd9b8bfbc12896526cf11882a7607a671c478c45f9cd7d6ba1caaed48a62b67c6f725244bd83a1275108f46c7 languageName: node linkType: hard -"@aws-crypto/sha256-js@npm:3.0.0, @aws-crypto/sha256-js@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/sha256-js@npm:3.0.0" - dependencies: - "@aws-crypto/util": ^3.0.0 - "@aws-sdk/types": ^3.222.0 - tslib: ^1.11.1 - checksum: 644ded32ea310237811afae873d3c7320739cb6f6cc39dced9c94801379e68e5ee2cca0c34f0384793fa9e750a7e0a5e2468f95754bd08e6fd72ab833c8fe23c - languageName: node - linkType: hard - -"@aws-crypto/sha256-js@npm:^5.0.0": +"@aws-crypto/sha256-js@npm:5.2.0, @aws-crypto/sha256-js@npm:^5.0.0, @aws-crypto/sha256-js@npm:^5.2.0": version: 5.2.0 resolution: "@aws-crypto/sha256-js@npm:5.2.0" dependencies: @@ -458,12 +447,12 @@ __metadata: languageName: node linkType: hard -"@aws-crypto/supports-web-crypto@npm:^3.0.0": - version: 3.0.0 - resolution: "@aws-crypto/supports-web-crypto@npm:3.0.0" +"@aws-crypto/supports-web-crypto@npm:^5.2.0": + version: 5.2.0 + resolution: "@aws-crypto/supports-web-crypto@npm:5.2.0" dependencies: - tslib: ^1.11.1 - checksum: 35479a1558db9e9a521df6877a99f95670e972c602f2a0349303477e5d638a5baf569fb037c853710e382086e6fd77e8ed58d3fb9b49f6e1186a9d26ce7be006 + tslib: ^2.6.2 + checksum: 6ffc21de48b2b2c3e918193101d7e8fe949d47b37688892e1c39eaedaa938be80c0f404fe1c874c30cce16781026777a53bf47d5d90143ca91d0feb7c4a6f830 languageName: node linkType: hard @@ -500,665 +489,677 @@ __metadata: linkType: hard "@aws-sdk/client-codecommit@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/client-codecommit@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/client-codecommit@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: 09a7393ba36cf0eaf527041580e468b62346634768ee1ced243e5e894d6a532f96332642ddc1965c9e2078cbe4183c84bc6003a427a05e6ea781ceff5c143e0d + checksum: be8a5ebbe178f7d81c44a34f7e1ee54b8f7d3912c92de570855be4612cf60a02444957373eb9d9abfbf606f9998fc34acf020a2fb1d7be1e0e77be6c30388408 languageName: node linkType: hard -"@aws-sdk/client-cognito-identity@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/client-cognito-identity@npm:3.556.0" +"@aws-sdk/client-cognito-identity@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/client-cognito-identity@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 964ee562d41df0fc3d71a739ef86846dddbfa8742df74831926dd629de0111ace69faed4869d9b7a50cd5eb8d2c1ac09da2126b7071e9d69f8ba8e8d85d24323 + checksum: 227fb03620396c3d5f9ca36fd463fc2e3e2e04909f5dea59b90e25a89d110d3a869e89e870237b808b494156bba06fb966406d2dc192a8ad6a072b7abdd4d81c languageName: node linkType: hard "@aws-sdk/client-eks@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/client-eks@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/client-eks@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 - "@smithy/util-waiter": ^2.2.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 + "@smithy/util-waiter": ^3.1.2 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: c36a0f23d3f01ed9fcbeec286b5e919cacc2ecb0bb3e880844e9b84018d81935c4b8f3dc33a3392c808a6e001659f5ca0c99889d830491c48040b9df3d474cc8 + checksum: a69d67d1d6c447e541baebc06f2e5e4ac6532933422a0af2850c916075986c555d1e3ba9f397c1c35e95efd770642b5985d6f2c73d38cd566c4425fecc0dc2fa languageName: node linkType: hard "@aws-sdk/client-organizations@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/client-organizations@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/client-organizations@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: e747d1f2f75a9985944765ac920408bffe5fc79dfb700c56b1633fec4e6f480b9568e30f72a6c80f09dcb7998480f158fa5c40646e8af4fed4a68e694d78c4f8 + checksum: 28db7d0490cc93a512cc2299afa67ce01d60f2f07ec4a59f9e826c86ef0769164ac103128c0c8617ea86f0a4d2a7be2468803aace84e054e1db7a00c871f7783 languageName: node linkType: hard "@aws-sdk/client-s3@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/client-s3@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/client-s3@npm:3.621.0" dependencies: - "@aws-crypto/sha1-browser": 3.0.0 - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-bucket-endpoint": 3.535.0 - "@aws-sdk/middleware-expect-continue": 3.535.0 - "@aws-sdk/middleware-flexible-checksums": 3.535.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-location-constraint": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-sdk-s3": 3.556.0 - "@aws-sdk/middleware-signing": 3.556.0 - "@aws-sdk/middleware-ssec": 3.537.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/signature-v4-multi-region": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@aws-sdk/xml-builder": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/eventstream-serde-browser": ^2.2.0 - "@smithy/eventstream-serde-config-resolver": ^2.2.0 - "@smithy/eventstream-serde-node": ^2.2.0 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-blob-browser": ^2.2.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/hash-stream-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/md5-js": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-stream": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 - "@smithy/util-waiter": ^2.2.0 + "@aws-crypto/sha1-browser": 5.2.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-bucket-endpoint": 3.620.0 + "@aws-sdk/middleware-expect-continue": 3.620.0 + "@aws-sdk/middleware-flexible-checksums": 3.620.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-location-constraint": 3.609.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-sdk-s3": 3.621.0 + "@aws-sdk/middleware-signing": 3.620.0 + "@aws-sdk/middleware-ssec": 3.609.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/signature-v4-multi-region": 3.621.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@aws-sdk/xml-builder": 3.609.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/eventstream-serde-browser": ^3.0.5 + "@smithy/eventstream-serde-config-resolver": ^3.0.3 + "@smithy/eventstream-serde-node": ^3.0.4 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-blob-browser": ^3.1.2 + "@smithy/hash-node": ^3.0.3 + "@smithy/hash-stream-node": ^3.1.2 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/md5-js": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-stream": ^3.1.3 + "@smithy/util-utf8": ^3.0.0 + "@smithy/util-waiter": ^3.1.2 tslib: ^2.6.2 - checksum: e5b04013a9270b438b196ccf60bedba14187ee059fbd3fd42a8bac927c7935226ba031c1a5312befa6e37e4ea8cd075fe28b8b524fe51b5b372bf5543dd46216 + checksum: 120bfaa142a61a8dbc2f82d6fe5e36cb2ec0acdb12390795d15326dfad4f67b0d2e95515afb04da34b3f686fef0368f7a79a20c85735b28df654cbc5bdb1f04c languageName: node linkType: hard "@aws-sdk/client-ses@npm:^3.550.0": - version: 3.556.0 - resolution: "@aws-sdk/client-ses@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/client-ses@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 - "@smithy/util-waiter": ^2.2.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 + "@smithy/util-waiter": ^3.1.2 tslib: ^2.6.2 - checksum: 913108e79061185faae51711b121df99da624f988f530af23c3e4270299be60771be83f507dc8ed4af4051765077ea038edab19e4d0d32e159c4e67faf5fc9f8 + checksum: 40de6b66fc6ac93eae26906cfb77b9a9d4bfd1a990e68f5bdc1a7024a624397e470149c1054e904c181b87b09e1a9d1ec4308dc206ffdfa5740113c0a199b5eb languageName: node linkType: hard "@aws-sdk/client-sqs@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/client-sqs@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/client-sqs@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-sdk-sqs": 3.552.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/md5-js": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-sdk-sqs": 3.621.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/md5-js": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: fe14caaffe9115b430d7c89895b193b2415532862b6cb70918ea0790f01017ca4bbf998ed221bf7fb4285b7eb6090c156bca4c8ccf49f61f2c34f96fd57e7ed4 + checksum: 293d3164570623e6228fc9e14c803b20e7ca5e562ce470f6f8ee3adf2b9a4cffd886ab69859d60afa1cf46f2df8cd9e0190a081335c1c2be21fee0739f21ce9f languageName: node linkType: hard -"@aws-sdk/client-sso-oidc@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/client-sso-oidc@npm:3.556.0" +"@aws-sdk/client-sso-oidc@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/client-sso-oidc@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.556.0 - checksum: d932569016b811e14bd977e2e198556e3e4bf156108b098eb8850f83486cf361e23e17cc2fbae2ae7ebb1c2268f312a64c90dafb9f19e8dd6e9a817397e79552 + "@aws-sdk/client-sts": ^3.621.0 + checksum: 12aa50a56f2498a9202b877b0b789fa95c038707b648dc3c9e93976c209ed3d4518868279707ee8fe12f42858eba7181098bdbf68556341c9017bd5e31f89135 languageName: node linkType: hard -"@aws-sdk/client-sso@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/client-sso@npm:3.556.0" +"@aws-sdk/client-sso@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/client-sso@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: bf0f2d449983c650e25befbee80dd48508464c1814e5b412a8f5e725e88e0764ac75a1b959df509c746b638dbbb571f51617be4c02f7b0ef8185c045255f6c52 + checksum: fe72875685c07b9cf42b7fcde06502d42a5398161b37de87c5360f10a0d28cf7b32fc95f5eecdf782cff04c8109bd7a20d9644bfacc78e3521b6a7670ed76b40 languageName: node linkType: hard -"@aws-sdk/client-sts@npm:3.556.0, @aws-sdk/client-sts@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/client-sts@npm:3.556.0" +"@aws-sdk/client-sts@npm:3.621.0, @aws-sdk/client-sts@npm:^3.350.0": + version: 3.621.0 + resolution: "@aws-sdk/client-sts@npm:3.621.0" dependencies: - "@aws-crypto/sha256-browser": 3.0.0 - "@aws-crypto/sha256-js": 3.0.0 - "@aws-sdk/core": 3.556.0 - "@aws-sdk/middleware-host-header": 3.535.0 - "@aws-sdk/middleware-logger": 3.535.0 - "@aws-sdk/middleware-recursion-detection": 3.535.0 - "@aws-sdk/middleware-user-agent": 3.540.0 - "@aws-sdk/region-config-resolver": 3.535.0 - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@aws-sdk/util-user-agent-browser": 3.535.0 - "@aws-sdk/util-user-agent-node": 3.535.0 - "@smithy/config-resolver": ^2.2.0 - "@smithy/core": ^1.4.2 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/hash-node": ^2.2.0 - "@smithy/invalid-dependency": ^2.2.0 - "@smithy/middleware-content-length": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-body-length-browser": ^2.2.0 - "@smithy/util-body-length-node": ^2.3.0 - "@smithy/util-defaults-mode-browser": ^2.2.1 - "@smithy/util-defaults-mode-node": ^2.3.1 - "@smithy/util-endpoints": ^1.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/sha256-browser": 5.2.0 + "@aws-crypto/sha256-js": 5.2.0 + "@aws-sdk/client-sso-oidc": 3.621.0 + "@aws-sdk/core": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/middleware-host-header": 3.620.0 + "@aws-sdk/middleware-logger": 3.609.0 + "@aws-sdk/middleware-recursion-detection": 3.620.0 + "@aws-sdk/middleware-user-agent": 3.620.0 + "@aws-sdk/region-config-resolver": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@aws-sdk/util-user-agent-browser": 3.609.0 + "@aws-sdk/util-user-agent-node": 3.614.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/core": ^2.3.1 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/hash-node": ^3.0.3 + "@smithy/invalid-dependency": ^3.0.3 + "@smithy/middleware-content-length": ^3.0.5 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-body-length-browser": ^3.0.0 + "@smithy/util-body-length-node": ^3.0.0 + "@smithy/util-defaults-mode-browser": ^3.0.13 + "@smithy/util-defaults-mode-node": ^3.0.13 + "@smithy/util-endpoints": ^2.0.5 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 + "@smithy/util-utf8": ^3.0.0 + tslib: ^2.6.2 + checksum: 35f543e1c4b419dc598489b4ce4dfbce22ad726ffe8f807876de13c0e8b5db10684835f7b95841e988d4f16aeb921935133e5775d35b47186c18cced6715a82a + languageName: node + linkType: hard + +"@aws-sdk/core@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/core@npm:3.621.0" + dependencies: + "@smithy/core": ^2.3.1 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/signature-v4": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/util-middleware": ^3.0.3 + fast-xml-parser: 4.4.1 + tslib: ^2.6.2 + checksum: 789409227f2aa0f3c735e0f1bebf1d900b359e589bad50324e2fbec20ddb30435e1dcf9132501dad7aaf793829530980d1670257208c5e407c78be5f831e796d + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-cognito-identity@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.621.0" + dependencies: + "@aws-sdk/client-cognito-identity": 3.621.0 + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: e8a913d61ac4e772e07cd1a6144ef8680e630894c2192aed81d06f36fb9607c4de3b95386155d52f050b99d8b4d78ceb69eb7fa0987d339631765cc1dc8c9588 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-env@npm:3.620.1": + version: 3.620.1 + resolution: "@aws-sdk/credential-provider-env@npm:3.620.1" + dependencies: + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: 3e05eeb6c6490e7759d03b7b688ef321d5e6c97929f21226e912b01aa926815312043febd505e8b43b54010b5ae2717559aba6deeea93bda7d6cf0d8a0b9fc76 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-http@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/credential-provider-http@npm:3.621.0" + dependencies: + "@aws-sdk/types": 3.609.0 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/property-provider": ^3.1.3 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/util-stream": ^3.1.3 + tslib: ^2.6.2 + checksum: bc45dfe7d0a6868978d71fa73731784af1839f822c09ef13b5a08ad74fa57c008dd874dd1765317df61ab022f9470bf509df38a70dd0aa5262d1d2234cc5eda2 + languageName: node + linkType: hard + +"@aws-sdk/credential-provider-ini@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/credential-provider-ini@npm:3.621.0" + dependencies: + "@aws-sdk/credential-provider-env": 3.620.1 + "@aws-sdk/credential-provider-http": 3.621.0 + "@aws-sdk/credential-provider-process": 3.620.1 + "@aws-sdk/credential-provider-sso": 3.621.0 + "@aws-sdk/credential-provider-web-identity": 3.621.0 + "@aws-sdk/types": 3.609.0 + "@smithy/credential-provider-imds": ^3.2.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/credential-provider-node": ^3.556.0 - checksum: 81ce40440de0c3012b6e21d7f9b748bc46982e72ffaccc763efeed34d79eee519ed9a95e2cb37b2e411460cced2897f409d90e42a5b012344358839c717fa435 + "@aws-sdk/client-sts": ^3.621.0 + checksum: a3db43adfbdb2ea1d6d80b1ca6789e48b729fce2499039f99721f504322cba325e4b48ed842bd26ae6f49ca771e5817016b75cda31cf247fd3e2bd0ac235b332 languageName: node linkType: hard -"@aws-sdk/core@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/core@npm:3.556.0" +"@aws-sdk/credential-provider-node@npm:3.621.0, @aws-sdk/credential-provider-node@npm:^3.350.0": + version: 3.621.0 + resolution: "@aws-sdk/credential-provider-node@npm:3.621.0" dependencies: - "@smithy/core": ^1.4.2 - "@smithy/protocol-http": ^3.3.0 - "@smithy/signature-v4": ^2.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - fast-xml-parser: 4.2.5 + "@aws-sdk/credential-provider-env": 3.620.1 + "@aws-sdk/credential-provider-http": 3.621.0 + "@aws-sdk/credential-provider-ini": 3.621.0 + "@aws-sdk/credential-provider-process": 3.620.1 + "@aws-sdk/credential-provider-sso": 3.621.0 + "@aws-sdk/credential-provider-web-identity": 3.621.0 + "@aws-sdk/types": 3.609.0 + "@smithy/credential-provider-imds": ^3.2.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 9b446f0939f8573289b598282194aeff81b3e8c9aa76b29d6e865fe40f38b301f041fab39d565f975ed6e8a3ba4d4b9b43444aaab7dbf2c7ea84e67d370a2695 + checksum: aada1f0624caee0e00b9ba287e66120031c8f5095acbcd80723b2ab6b5e4bffcd662665fffe62b7f2091bfb89d33ea13212a82893318005699914f13fbed70c3 languageName: node linkType: hard -"@aws-sdk/credential-provider-cognito-identity@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/credential-provider-cognito-identity@npm:3.556.0" +"@aws-sdk/credential-provider-process@npm:3.620.1": + version: 3.620.1 + resolution: "@aws-sdk/credential-provider-process@npm:3.620.1" dependencies: - "@aws-sdk/client-cognito-identity": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 4c66b780f9f8c3e5df9d5e397069e0bbd31596bfb93b5ac76baa39cb0ef7bebc848a879b940dbcdda7c40cb78beeb46e0b10fab94e71ea317e0512e49c9b6f80 + checksum: 1fd6d3abf35ccfaed0882ee74e4d876e8c6cfe8d8b23c574dfe98364b2a2f3226d97fe35e2d48b88dd514906595cbb5fa1e0066d989a9582f73f92117796168f languageName: node linkType: hard -"@aws-sdk/credential-provider-env@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/credential-provider-env@npm:3.535.0" +"@aws-sdk/credential-provider-sso@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/credential-provider-sso@npm:3.621.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/client-sso": 3.621.0 + "@aws-sdk/token-providers": 3.614.0 + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: ec7d011051d69643efd26fe7f3767b1301c6e6bdba12640dad542b60efb2b5fe31df01f8137703d814cf857a69485cc928d0de49ab56c5d05f79815dce19bdbf + checksum: 4602308a0bf74bda9dd7b92caf02868b855935deb703a8894d052c81864ef9c9359408ec84569f71527c1b204577940ebe1a9f6cbba8de677471eb590d91c3ba languageName: node linkType: hard -"@aws-sdk/credential-provider-http@npm:3.552.0": - version: 3.552.0 - resolution: "@aws-sdk/credential-provider-http@npm:3.552.0" +"@aws-sdk/credential-provider-web-identity@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/credential-provider-web-identity@npm:3.621.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/util-stream": ^2.2.0 + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 86be07b2a0c73b5c1140557e00b08c091303da1908ff40a29bcc69b73bf4ad175a5ad945e0d45d5c76b02bee3f15a947abb18838b2771ad71feb795a5309a446 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-ini@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/credential-provider-ini@npm:3.556.0" - dependencies: - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/credential-provider-env": 3.535.0 - "@aws-sdk/credential-provider-process": 3.535.0 - "@aws-sdk/credential-provider-sso": 3.556.0 - "@aws-sdk/credential-provider-web-identity": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/credential-provider-imds": ^2.3.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 - tslib: ^2.6.2 - checksum: 24b7dc1349f80090455bde95856955aef824752bff8b91dbad1dea784dc03b48b9671650aa2002de8427bd1df832c33be52b5bf157ad2759ad65a6d37247e339 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-node@npm:3.556.0, @aws-sdk/credential-provider-node@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/credential-provider-node@npm:3.556.0" - dependencies: - "@aws-sdk/credential-provider-env": 3.535.0 - "@aws-sdk/credential-provider-http": 3.552.0 - "@aws-sdk/credential-provider-ini": 3.556.0 - "@aws-sdk/credential-provider-process": 3.535.0 - "@aws-sdk/credential-provider-sso": 3.556.0 - "@aws-sdk/credential-provider-web-identity": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/credential-provider-imds": ^2.3.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 - tslib: ^2.6.2 - checksum: c4b83c36426941caff81a25393eec101cf2d21264c29c69e5a7a940ae8af1f4b2a8f0a26f47c2294a32f381d53eed9ba2dc2cbbcd053585aa9016acb79e98ad8 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-process@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/credential-provider-process@npm:3.535.0" - dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 - tslib: ^2.6.2 - checksum: af5fad70513e8d0bc10013f220588e45e83d488e0780873d7e76bf5b7099e70c9b4e49b6409d16add64ae212c53a472b12a2178e8bafd7878fdf121d6e2fd112 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-sso@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/credential-provider-sso@npm:3.556.0" - dependencies: - "@aws-sdk/client-sso": 3.556.0 - "@aws-sdk/token-providers": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 - tslib: ^2.6.2 - checksum: ea6c9bd7f183abedf79ff7959fdc31c16cb250afece2304d3005343aae4a203d04f2421ac1dd0a815a6d1823c721b277449d0ab27dc17fe8c75b211e44a31667 - languageName: node - linkType: hard - -"@aws-sdk/credential-provider-web-identity@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/credential-provider-web-identity@npm:3.556.0" - dependencies: - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/types": ^2.12.0 - tslib: ^2.6.2 - checksum: 79d3277db07130b201b5788a76e8a71c2816be1e190ce7f96d174c2c8153fdcfb6adbf17575d8fd5c928452327a37872f18a48e563805997247bdc3cda2b9cac + peerDependencies: + "@aws-sdk/client-sts": ^3.621.0 + checksum: b1157e83b81c21f384ad95d29c646149124136c38e6395265ff8d9f5f6afec1e3e2b70c1f0be165c440058802c3871f3766c5e7ba44705ced447de221f4d04df languageName: node linkType: hard "@aws-sdk/credential-providers@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/credential-providers@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/credential-providers@npm:3.621.0" dependencies: - "@aws-sdk/client-cognito-identity": 3.556.0 - "@aws-sdk/client-sso": 3.556.0 - "@aws-sdk/client-sts": 3.556.0 - "@aws-sdk/credential-provider-cognito-identity": 3.556.0 - "@aws-sdk/credential-provider-env": 3.535.0 - "@aws-sdk/credential-provider-http": 3.552.0 - "@aws-sdk/credential-provider-ini": 3.556.0 - "@aws-sdk/credential-provider-node": 3.556.0 - "@aws-sdk/credential-provider-process": 3.535.0 - "@aws-sdk/credential-provider-sso": 3.556.0 - "@aws-sdk/credential-provider-web-identity": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/credential-provider-imds": ^2.3.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/client-cognito-identity": 3.621.0 + "@aws-sdk/client-sso": 3.621.0 + "@aws-sdk/client-sts": 3.621.0 + "@aws-sdk/credential-provider-cognito-identity": 3.621.0 + "@aws-sdk/credential-provider-env": 3.620.1 + "@aws-sdk/credential-provider-http": 3.621.0 + "@aws-sdk/credential-provider-ini": 3.621.0 + "@aws-sdk/credential-provider-node": 3.621.0 + "@aws-sdk/credential-provider-process": 3.620.1 + "@aws-sdk/credential-provider-sso": 3.621.0 + "@aws-sdk/credential-provider-web-identity": 3.621.0 + "@aws-sdk/types": 3.609.0 + "@smithy/credential-provider-imds": ^3.2.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 6d00b6ab7fbd0e0ca83ecd2823b446290be20d68f7c2b26128b8f678ab9d6e0f7fb423adf94bb12903abdeb324b4aa7ce8dd81e0b17f5b3322f6b14e634552e0 + checksum: 152a8911ca52a0663e4552e720f73bef2d93088d7b9fc58da88cf2aac91ebc3b720ea8abc5d28228d1658888903bd737dbba88e7d5cc97e2a2c7fdc0e5243c6f languageName: node linkType: hard @@ -1184,34 +1185,34 @@ __metadata: linkType: hard "@aws-sdk/lib-storage@npm:^3.350.0": - version: 3.556.0 - resolution: "@aws-sdk/lib-storage@npm:3.556.0" + version: 3.621.0 + resolution: "@aws-sdk/lib-storage@npm:3.621.0" dependencies: - "@smithy/abort-controller": ^2.2.0 - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/smithy-client": ^2.5.1 + "@smithy/abort-controller": ^3.1.1 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/smithy-client": ^3.1.11 buffer: 5.6.0 events: 3.3.0 stream-browserify: 3.0.0 tslib: ^2.6.2 peerDependencies: - "@aws-sdk/client-s3": ^3.0.0 - checksum: 361e87ce38f566e7ffe0da2bd436bfda5dfce1c9e4f86f4fbab9e157ba24a47e169824dd9723f02eab8cd682b381a765453bd8d4c5fa0ae3c5273ff6d3a166e5 + "@aws-sdk/client-s3": ^3.621.0 + checksum: 6c84a1601bcbea53aabad52c2453afd3c577e3935eda4bb13a7d9d4b1405289a3f2f4747327fc30c5eeaf482a9a78d56dfb4453c4c892f876f491e677d0f6c95 languageName: node linkType: hard -"@aws-sdk/middleware-bucket-endpoint@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.535.0" +"@aws-sdk/middleware-bucket-endpoint@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-bucket-endpoint@npm:3.620.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-arn-parser": 3.535.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 - "@smithy/util-config-provider": ^2.3.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-arn-parser": 3.568.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 + "@smithy/util-config-provider": ^3.0.0 tslib: ^2.6.2 - checksum: d72dfd197dc9aa1ed8ea3de6fb0a18363688fa8e18f6a9d01cb2f425087c5b72aa4bfd0955715c84da86adf9a46029755cff55a02a6ffbf4459009982a5a5415 + checksum: a5539ad611d3a2f7709952bf735a075a9d18ed5e9c0abe4e268bca32775a2100f3e956a264f6afb0ad0b48600f5b1d3389ec0566cd2795e6ae2dbe5af322b56b languageName: node linkType: hard @@ -1228,108 +1229,110 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-expect-continue@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-expect-continue@npm:3.535.0" +"@aws-sdk/middleware-expect-continue@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-expect-continue@npm:3.620.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: fdac9bc59833ca9688155f115f85c31f690df5ef690e9a8921b94b87c4faac3eb3ff10641699822faaaf9a6cc4ce4c67e693a7b4befe8baf8138a9d276d6714d + checksum: 739af454854562342984ac5e3c1c3dca39205344f52eda5961dae80020595062eaae6da2f76bf48ad486d92041c2a2bd30f1dd67b8b20ae043897d1d27b1a6bb languageName: node linkType: hard -"@aws-sdk/middleware-flexible-checksums@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.535.0" +"@aws-sdk/middleware-flexible-checksums@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-flexible-checksums@npm:3.620.0" dependencies: - "@aws-crypto/crc32": 3.0.0 - "@aws-crypto/crc32c": 3.0.0 - "@aws-sdk/types": 3.535.0 - "@smithy/is-array-buffer": ^2.2.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-crypto/crc32": 5.2.0 + "@aws-crypto/crc32c": 5.2.0 + "@aws-sdk/types": 3.609.0 + "@smithy/is-array-buffer": ^3.0.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 14c04cd932bdb9029466079b248c1fa8222183de4c288bf12ad661cdc7717938485fdb973b7948da729e5be218573cfd2cbe74b51a1bc2bd27131ca7d67909c1 + checksum: 2fc70ffa826d94924c0dae92ed5250c85aade93cbc15a488df81683d8f32eb7660f546569b1c2d2e878db5c8da02a83a3a98c331c5bb3adca46362e667abcbed languageName: node linkType: hard -"@aws-sdk/middleware-host-header@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-host-header@npm:3.535.0" +"@aws-sdk/middleware-host-header@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-host-header@npm:3.620.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 13a22a5b19912a86fe872c9c0f6ba9ab758ba9483ce9a2e73fa68acae498b68404e38e7fc1e3962639a6c3f80ebe19ea5c5340e1024e11ab417998696c8a5dca + checksum: 829c2d230e5051704f45c5283c42bec657607f4e4b2dd251fda8a716be90f8c9dfd6e7d45892a9a558c35cb64711628b3d0f88b90fe8cd061382c70b473991ef languageName: node linkType: hard -"@aws-sdk/middleware-location-constraint@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-location-constraint@npm:3.535.0" +"@aws-sdk/middleware-location-constraint@npm:3.609.0": + version: 3.609.0 + resolution: "@aws-sdk/middleware-location-constraint@npm:3.609.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 4e89b6c5d4253568e599f0e8be37e80cf12a755ba89b89d9c9ae2ced75213850c41922fa1871a9c92345057e15d5db956a353841eb63353d8f2bb69a61d4a10d + checksum: f7962cf0b382efdf56cd07f8c0279efead02365edd7a2c124be39551b51a8359ee0d6f0399fcbf679ead3d235e24d1765f79712cf88e06c0a5432bf2d0c317d8 languageName: node linkType: hard -"@aws-sdk/middleware-logger@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-logger@npm:3.535.0" +"@aws-sdk/middleware-logger@npm:3.609.0": + version: 3.609.0 + resolution: "@aws-sdk/middleware-logger@npm:3.609.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: e264d05aa88fadd94d17000b1df79bf1931c5fbc2c871616de9b836bab7254d8c21e0fc2ac509a07af578bd823b795efafaf48daca805bc4493986288ed0c4ab + checksum: b6f67a2e9ba082c8aec9d45905ae45ea5a95896f1beecb0c2d7fecfe17dd8fad99513f43b11ed7fd6ca9ff7764a0fc1ce63af91b1baed92b36f7b4b5390be5c6 languageName: node linkType: hard -"@aws-sdk/middleware-recursion-detection@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/middleware-recursion-detection@npm:3.535.0" +"@aws-sdk/middleware-recursion-detection@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-recursion-detection@npm:3.620.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: b6b518243a10dcdcea7c1fd4790cf2e1e0e3d4363ebce51ac364c9b15631a6f3d1db44d00e7c75cbdf1d558a0d868583d6e1853e571e15e23d0d2901f168e2e4 + checksum: 1dedbbab2f79d4c0b214ca183a45daf89e98f9a33b2a08dcd7d30e059c1bcc04a74499b49870300d5f0b827d660512582f96f857624d3feaaf194a7a92703cea languageName: node linkType: hard -"@aws-sdk/middleware-sdk-s3@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/middleware-sdk-s3@npm:3.556.0" +"@aws-sdk/middleware-sdk-s3@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/middleware-sdk-s3@npm:3.621.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-arn-parser": 3.535.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/signature-v4": ^2.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/util-config-provider": ^2.3.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-arn-parser": 3.568.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/signature-v4": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/util-config-provider": ^3.0.0 + "@smithy/util-stream": ^3.1.3 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: a50d96f898a8e6f7cfa0bc61c888240e1fd7aa55cb9fd9376d720f43d30a83d7ad4bd9d828224c125f62b362d77ccfc7fbd64c5c4563f97790f9683ea87abe93 + checksum: 2db38ad1575e9209f053474ff69731d0c0910304fc14dcdf22a09c06a849cbada0b18f36879d96e3020c0e050f80b02e47c45137da561a3f243cc080f6df4f3a languageName: node linkType: hard -"@aws-sdk/middleware-sdk-sqs@npm:3.552.0": - version: 3.552.0 - resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.552.0" +"@aws-sdk/middleware-sdk-sqs@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/middleware-sdk-sqs@npm:3.621.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/util-hex-encoding": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@aws-sdk/types": 3.609.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/util-hex-encoding": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: a2dfeab25b89b8c0f3204195df58bf5deefb261883db936c6701e70d5a1f6e2a0e8e3a756fea6a4f0e3a1dc4023d820c6b8b3717f759622ffa7cd8851cd9ed59 + checksum: d0cad52b1ffbfc6078fd62865bb88b4ac3b3a06d883ad5ef0a419dfaefe11d33314892f862150801d57e8c277e3a1465f8e798a1176c1ad6090765faf325c1d0 languageName: node linkType: hard @@ -1343,42 +1346,42 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/middleware-signing@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/middleware-signing@npm:3.556.0" +"@aws-sdk/middleware-signing@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-signing@npm:3.620.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/signature-v4": ^2.3.0 - "@smithy/types": ^2.12.0 - "@smithy/util-middleware": ^2.2.0 + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/protocol-http": ^4.1.0 + "@smithy/signature-v4": ^4.1.0 + "@smithy/types": ^3.3.0 + "@smithy/util-middleware": ^3.0.3 tslib: ^2.6.2 - checksum: b72c0223cc531d9eaac41124c3dcf60d974dbe0a5698fa6cf2ec877d73d83ff02ae310636478054ba5f7d1c3240ff277831c5144955ddb59e513b3de7cb4f272 + checksum: ef2365b282ccfe8dc29983b620ca6cd5f2e41f5f8d12b84d2e3cb73866535ba3f7383579e561ec5a3c422d3fefdfbb2f3461ae7806a9f87e4fe92fd10a929386 languageName: node linkType: hard -"@aws-sdk/middleware-ssec@npm:3.537.0": - version: 3.537.0 - resolution: "@aws-sdk/middleware-ssec@npm:3.537.0" +"@aws-sdk/middleware-ssec@npm:3.609.0": + version: 3.609.0 + resolution: "@aws-sdk/middleware-ssec@npm:3.609.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 7a963134cab4c9056d249113f6d5e2847be28847fc8c39b943c487ff5d22b9b1d91092e801694e2da95929a4820c45f6b5afc22c655a6dcfb5eb82e44952de14 + checksum: 4b40627ed103159ef0db4cc6bdc2148d1a65b786f3d1c643d34bccc79b9d265495613dc9bb34d18d5ab9b21b5d31110e495ec2b077e6e2f7603a0493254180a2 languageName: node linkType: hard -"@aws-sdk/middleware-user-agent@npm:3.540.0": - version: 3.540.0 - resolution: "@aws-sdk/middleware-user-agent@npm:3.540.0" +"@aws-sdk/middleware-user-agent@npm:3.620.0": + version: 3.620.0 + resolution: "@aws-sdk/middleware-user-agent@npm:3.620.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@aws-sdk/util-endpoints": 3.540.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@aws-sdk/util-endpoints": 3.614.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 7ee609fb2b669f93657a87c291dff17f8315973ddbbeee36c54a0233b4911091916880f6e27da93fef01e5fd53654b3834a68cf74256b88b1c4dbe5880a4c14f + checksum: 137a969b5c17ebf172aa1f5a393812c443425dd9833fdf786a3039af5b3197e7d17139659891858b268ff723d9f2c5fbc840e8ac6a7af4b1cb566a3d54746a51 languageName: node linkType: hard @@ -1426,31 +1429,31 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/region-config-resolver@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/region-config-resolver@npm:3.535.0" +"@aws-sdk/region-config-resolver@npm:3.614.0": + version: 3.614.0 + resolution: "@aws-sdk/region-config-resolver@npm:3.614.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/types": ^2.12.0 - "@smithy/util-config-provider": ^2.3.0 - "@smithy/util-middleware": ^2.2.0 + "@aws-sdk/types": 3.609.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/types": ^3.3.0 + "@smithy/util-config-provider": ^3.0.0 + "@smithy/util-middleware": ^3.0.3 tslib: ^2.6.2 - checksum: 7b556e47c721c0829ae635203a1dbcab7040e2885d2bdc37898eb3139dabb5f6da580b860202e3b7737330bfc02de5084a98ff8f8b5c36adcfe8e076b82d3487 + checksum: dbaca50792c99685845b21dd4a53228613e0458ee517a21db941890ee521d91eff80704f08e9ee71b6f04e70fb86362c4823750bb0b3727240af68d78d8fa4be languageName: node linkType: hard -"@aws-sdk/signature-v4-multi-region@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/signature-v4-multi-region@npm:3.556.0" +"@aws-sdk/signature-v4-multi-region@npm:3.621.0": + version: 3.621.0 + resolution: "@aws-sdk/signature-v4-multi-region@npm:3.621.0" dependencies: - "@aws-sdk/middleware-sdk-s3": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/signature-v4": ^2.3.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/middleware-sdk-s3": 3.621.0 + "@aws-sdk/types": 3.609.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/signature-v4": ^4.1.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 0a23fbb8f99e1b8ab6c7523d5584864749a2ad5b244b78667d88d4a0eb0f74127e22fff4b44a9b694c235e70a66520ee72e0e57b129847d9405c6d907abf5e81 + checksum: 741c07e65341da4c807d55b0bc6f1ec0064190c6fceb7571b0796f6ec94cc156a6214c0f308a93ad364884adae48502d248d30bd0ef43e7a4542586f264982c4 languageName: node linkType: hard @@ -1470,17 +1473,18 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/token-providers@npm:3.556.0": - version: 3.556.0 - resolution: "@aws-sdk/token-providers@npm:3.556.0" +"@aws-sdk/token-providers@npm:3.614.0": + version: 3.614.0 + resolution: "@aws-sdk/token-providers@npm:3.614.0" dependencies: - "@aws-sdk/client-sso-oidc": 3.556.0 - "@aws-sdk/types": 3.535.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 15726af45c8a52837eb3291391c23e5b66a153218275c4fcc228a80f63169b401270ff736271401b6d4c301944bc20620d63dfb14a87bbe743a50e3157e68c7e + peerDependencies: + "@aws-sdk/client-sso-oidc": ^3.614.0 + checksum: 2901b8428afc3b76ff1df9ac29a2698db6bf65d1d2afcd8424b9bf187313d2a3ca747c3b205afeb5c132068b5a5a94d84ce82710f775fa0cbb79499d7fea2d64 languageName: node linkType: hard @@ -1494,13 +1498,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.535.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": - version: 3.535.0 - resolution: "@aws-sdk/types@npm:3.535.0" +"@aws-sdk/types@npm:3.609.0, @aws-sdk/types@npm:^3.222.0, @aws-sdk/types@npm:^3.347.0": + version: 3.609.0 + resolution: "@aws-sdk/types@npm:3.609.0" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 3f735c78ea3a6f37d05387286f6d18b0e5deb41442095bd2f7c27b000659962872d1c801fa8484a6ac4b7d2725b2e176dc628c3fa2a903a3141d4be76488d48f + checksum: 522768d08f104065b0ff6a37eddaa7803186014acee1c0011b3dbd3ef841e47ae694e58f608aeec8a39d22d644d759ade996fe51d18b880617778dc2dbbe1ede languageName: node linkType: hard @@ -1515,12 +1519,12 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-arn-parser@npm:3.535.0, @aws-sdk/util-arn-parser@npm:^3.310.0": - version: 3.535.0 - resolution: "@aws-sdk/util-arn-parser@npm:3.535.0" +"@aws-sdk/util-arn-parser@npm:3.568.0, @aws-sdk/util-arn-parser@npm:^3.310.0": + version: 3.568.0 + resolution: "@aws-sdk/util-arn-parser@npm:3.568.0" dependencies: tslib: ^2.6.2 - checksum: 0e4c4ea080c7c6da00d359c337a1f9d961eac20c35cebad9cb6bb9b618f981955643990de7bfd6b53c29367a6d33550fe27b2a7539efdff08e01bdd303f0c5c2 + checksum: e3c45e5d524a772954d0a33614d397414185b9eb635423d01253cad1c1b9add625798ed9cf23343d156fae89c701f484bc062ab673f67e2e2edfe362fde6d170 languageName: node linkType: hard @@ -1534,15 +1538,15 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-endpoints@npm:3.540.0": - version: 3.540.0 - resolution: "@aws-sdk/util-endpoints@npm:3.540.0" +"@aws-sdk/util-endpoints@npm:3.614.0": + version: 3.614.0 + resolution: "@aws-sdk/util-endpoints@npm:3.614.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/types": ^2.12.0 - "@smithy/util-endpoints": ^1.2.0 + "@aws-sdk/types": 3.609.0 + "@smithy/types": ^3.3.0 + "@smithy/util-endpoints": ^2.0.5 tslib: ^2.6.2 - checksum: 62312773965480f8df5edb62ecf82ab3e54d5aa1b1f7eebdad78a2925eab47218b0268bc25a5d03598ac4d0c08935733031a2e3e1ce72531535ed1ea8a9b56c3 + checksum: 9d9973ceee59bf30af85c7f4328083daea033a987ec396dcb89eb7649f470ceb19c6b96635e121f3557e726f7ec7453236c956cf43f22128883c277f17d2a13f languageName: node linkType: hard @@ -1594,32 +1598,32 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/util-user-agent-browser@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/util-user-agent-browser@npm:3.535.0" +"@aws-sdk/util-user-agent-browser@npm:3.609.0": + version: 3.609.0 + resolution: "@aws-sdk/util-user-agent-browser@npm:3.609.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/types": ^3.3.0 bowser: ^2.11.0 tslib: ^2.6.2 - checksum: 148b82900e4b9efd24f5fc4152a8d6010c90eb019517fa3c00c3ac42205055f7611ef3834fb63397def40e600ea20f901177a7dd5b6aefa2aef4208e994f7d90 + checksum: 75ba1ae74dd1001f47870766d92b66ac02a0a488efcf42c1a368962a7978a778d99536e880f07f7db1c2ca66cc9b1863fd3342957a22dcf78bf2f4398265a7a5 languageName: node linkType: hard -"@aws-sdk/util-user-agent-node@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/util-user-agent-node@npm:3.535.0" +"@aws-sdk/util-user-agent-node@npm:3.614.0": + version: 3.614.0 + resolution: "@aws-sdk/util-user-agent-node@npm:3.614.0" dependencies: - "@aws-sdk/types": 3.535.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/types": ^2.12.0 + "@aws-sdk/types": 3.609.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 peerDependencies: aws-crt: ">=1.0.0" peerDependenciesMeta: aws-crt: optional: true - checksum: f214c406fccde14591d1df25fed662e573510cdf1d5829d22e7a93e517ea4a3905acd123eaaeafdb8b217400e8c8e159231d37be02b67307922023996b398f42 + checksum: 1f010080c2301fd836908963a235ef39e597d959e27461d15d4958fa582ab20795022f8cb7429c183c386f558a5c125cb254a0c4e844dbc6422169f4884be34a languageName: node linkType: hard @@ -1642,13 +1646,13 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/xml-builder@npm:3.535.0": - version: 3.535.0 - resolution: "@aws-sdk/xml-builder@npm:3.535.0" +"@aws-sdk/xml-builder@npm:3.609.0": + version: 3.609.0 + resolution: "@aws-sdk/xml-builder@npm:3.609.0" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 9320f6600c7852b681ed4171f1521eda166321ef7ffea5a6b99a1d328835bb12f7423431ea23a696c678ec4fc8e6dbb2f979d941289f6e0cb55c3da7d5eee140 + checksum: 0e9c8b7786737ff50a6cf39f7ca9a758897c2db364718364b5dad45f50a33e65bd7801348fd033af60768a5be64b454c3a7e65222e13c70d145e8df6211ca33c languageName: node linkType: hard @@ -1661,7 +1665,16 @@ __metadata: languageName: node linkType: hard -"@azure/core-auth@npm:^1.3.0, @azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.5.0": +"@azure/abort-controller@npm:^2.0.0": + version: 2.1.2 + resolution: "@azure/abort-controller@npm:2.1.2" + dependencies: + tslib: ^2.6.2 + checksum: 22176c04ea01498311c6bbd336669f6e3faffad1cbb0c9ebc6ee9c1ff2cf958fd17ce73c7354b99d8bda9fcd311325ece7bee248875279174e3fc460e8b1a63d + languageName: node + linkType: hard + +"@azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.5.0": version: 1.5.0 resolution: "@azure/core-auth@npm:1.5.0" dependencies: @@ -1672,40 +1685,29 @@ __metadata: languageName: node linkType: hard -"@azure/core-client@npm:^1.4.0": - version: 1.7.3 - resolution: "@azure/core-client@npm:1.7.3" +"@azure/core-client@npm:^1.3.0, @azure/core-client@npm:^1.6.2, @azure/core-client@npm:^1.9.2": + version: 1.9.2 + resolution: "@azure/core-client@npm:1.9.2" dependencies: - "@azure/abort-controller": ^1.0.0 + "@azure/abort-controller": ^2.0.0 "@azure/core-auth": ^1.4.0 "@azure/core-rest-pipeline": ^1.9.1 "@azure/core-tracing": ^1.0.0 - "@azure/core-util": ^1.0.0 + "@azure/core-util": ^1.6.1 "@azure/logger": ^1.0.0 - tslib: ^2.2.0 - checksum: 155a188b75b2d5ea783d5fde50479337c41796736f0fced1576466c8251e429195c229f2aff0bf897761f15c19d8fd0deea9a54aab514bd3584e37140e3f0cdc + tslib: ^2.6.2 + checksum: 961b829dfda4f734a763e9480a2ea622a7031ba2da4126d0add6e351a9f73ddc5782bf2b766735d976b61da3857014e0a90223d1f85d1c68468747a7a56851c3 languageName: node linkType: hard -"@azure/core-http@npm:^3.0.0": - version: 3.0.2 - resolution: "@azure/core-http@npm:3.0.2" +"@azure/core-http-compat@npm:^2.0.0": + version: 2.1.2 + resolution: "@azure/core-http-compat@npm:2.1.2" dependencies: - "@azure/abort-controller": ^1.0.0 - "@azure/core-auth": ^1.3.0 - "@azure/core-tracing": 1.0.0-preview.13 - "@azure/core-util": ^1.1.1 - "@azure/logger": ^1.0.0 - "@types/node-fetch": ^2.5.0 - "@types/tunnel": ^0.0.3 - form-data: ^4.0.0 - node-fetch: ^2.6.7 - process: ^0.11.10 - tslib: ^2.2.0 - tunnel: ^0.0.6 - uuid: ^8.3.0 - xml2js: ^0.5.0 - checksum: 01b5a75e09533476dbb69c672dcd00d48298cf81db5015cd261c510c5be377176db8e3dc4809e6952459bfbe214f52f8a1ed84a116ac31b8a7388b2025098f66 + "@azure/abort-controller": ^2.0.0 + "@azure/core-client": ^1.3.0 + "@azure/core-rest-pipeline": ^1.3.0 + checksum: 387d0187607d95a6876f63d4b689210bce6ad243f48e56413136ba3875a8a9c4e238813307fb0cf0c53298f4b9d0893d04321c9331812bc74cf0f4e3e6872069 languageName: node linkType: hard @@ -1730,30 +1732,19 @@ __metadata: languageName: node linkType: hard -"@azure/core-rest-pipeline@npm:^1.1.0, @azure/core-rest-pipeline@npm:^1.9.1": - version: 1.12.2 - resolution: "@azure/core-rest-pipeline@npm:1.12.2" +"@azure/core-rest-pipeline@npm:^1.1.0, @azure/core-rest-pipeline@npm:^1.10.1, @azure/core-rest-pipeline@npm:^1.3.0, @azure/core-rest-pipeline@npm:^1.9.1": + version: 1.16.1 + resolution: "@azure/core-rest-pipeline@npm:1.16.1" dependencies: - "@azure/abort-controller": ^1.0.0 + "@azure/abort-controller": ^2.0.0 "@azure/core-auth": ^1.4.0 "@azure/core-tracing": ^1.0.1 - "@azure/core-util": ^1.3.0 + "@azure/core-util": ^1.9.0 "@azure/logger": ^1.0.0 - form-data: ^4.0.0 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 - tslib: ^2.2.0 - checksum: a774ab2e87ff56d7f3ef1844a814bbd76264ea1c33534081d38ce6df1098d803d0b13b15912f9053d0695ca2669b0dd104c8feec8dbf24df80a3608ea784fc9e - languageName: node - linkType: hard - -"@azure/core-tracing@npm:1.0.0-preview.13": - version: 1.0.0-preview.13 - resolution: "@azure/core-tracing@npm:1.0.0-preview.13" - dependencies: - "@opentelemetry/api": ^1.0.1 - tslib: ^2.2.0 - checksum: bc3ea8dce1fc6bb6e4cb2e82ec0c361b3e6f6e18e162f352eb347e6991c6461ebc249f5d1b36402cc0d295e2a6bcbaa68014445d7f4293c0792a698c430f145e + http-proxy-agent: ^7.0.0 + https-proxy-agent: ^7.0.0 + tslib: ^2.6.2 + checksum: ab2c5b715c4ebb9f6554f12d8e804833be29db0f23868adc092f4841aa8c7f140a6821dbcd9fa046bfb31e435085f171a87dab3f97d851cf9a62127d042fa75e languageName: node linkType: hard @@ -1766,23 +1757,33 @@ __metadata: languageName: node linkType: hard -"@azure/core-util@npm:^1.0.0, @azure/core-util@npm:^1.1.0, @azure/core-util@npm:^1.1.1, @azure/core-util@npm:^1.2.0, @azure/core-util@npm:^1.3.0": - version: 1.6.1 - resolution: "@azure/core-util@npm:1.6.1" +"@azure/core-util@npm:^1.1.0, @azure/core-util@npm:^1.2.0, @azure/core-util@npm:^1.3.0, @azure/core-util@npm:^1.6.1, @azure/core-util@npm:^1.9.0": + version: 1.9.0 + resolution: "@azure/core-util@npm:1.9.0" dependencies: - "@azure/abort-controller": ^1.0.0 - tslib: ^2.2.0 - checksum: 1f8cd130993f161c98925070af863510cbcc79e0471864e4b16852afc2ee7413c9c7fabe72f20f3e521ee75c3cd7e3085661fdc8d5d0a643a6e1b1b7bf691ddd + "@azure/abort-controller": ^2.0.0 + tslib: ^2.6.2 + checksum: 9246dc5bd246e7b94883ea8130fce04e2f22abd1e94afcff7a3e92a4c2da5e9b382dbf89a606b21d70bc8b01c7c89c84e803ca9da27f78d87f72bdff91ec7380 + languageName: node + linkType: hard + +"@azure/core-xml@npm:^1.3.2": + version: 1.4.2 + resolution: "@azure/core-xml@npm:1.4.2" + dependencies: + fast-xml-parser: ^4.3.2 + tslib: ^2.6.2 + checksum: f3815db55f0280db6080a3fc5b6187ad45cacf5c3842ea806a5565886edf2ebf2fa9fc1175407cf58d462341962a8396513943543b332b37b313ac3a79dc28fa languageName: node linkType: hard "@azure/identity@npm:^4.0.0": - version: 4.2.1 - resolution: "@azure/identity@npm:4.2.1" + version: 4.3.0 + resolution: "@azure/identity@npm:4.3.0" dependencies: "@azure/abort-controller": ^1.0.0 "@azure/core-auth": ^1.5.0 - "@azure/core-client": ^1.4.0 + "@azure/core-client": ^1.9.2 "@azure/core-rest-pipeline": ^1.1.0 "@azure/core-tracing": ^1.0.0 "@azure/core-util": ^1.3.0 @@ -1794,7 +1795,7 @@ __metadata: open: ^8.0.0 stoppable: ^1.1.0 tslib: ^2.2.0 - checksum: 2dbd7b0fa6b92904b7b8f828374d7316384cc4e00c71cdac3a2f2544b9e02c7287fe7ad58149bb8794c57913d77a927f5a7b5d1367e05abe82b0f2e1b162415c + checksum: 7f4256b39c4e27bf072c33f02dd63bd50dcf54ad578e3e483dcf89283546a6086b1640adfe4c93a537cbe483dd2e3a29f6d7b837e51f333229f7bbbc448966b3 languageName: node linkType: hard @@ -1842,18 +1843,23 @@ __metadata: linkType: hard "@azure/storage-blob@npm:^12.5.0": - version: 12.17.0 - resolution: "@azure/storage-blob@npm:12.17.0" + version: 12.23.0 + resolution: "@azure/storage-blob@npm:12.23.0" dependencies: "@azure/abort-controller": ^1.0.0 - "@azure/core-http": ^3.0.0 + "@azure/core-auth": ^1.4.0 + "@azure/core-client": ^1.6.2 + "@azure/core-http-compat": ^2.0.0 "@azure/core-lro": ^2.2.0 "@azure/core-paging": ^1.1.1 - "@azure/core-tracing": 1.0.0-preview.13 + "@azure/core-rest-pipeline": ^1.10.1 + "@azure/core-tracing": ^1.0.0 + "@azure/core-util": ^1.6.1 + "@azure/core-xml": ^1.3.2 "@azure/logger": ^1.0.0 events: ^3.0.0 tslib: ^2.2.0 - checksum: 2e5d3f26577f698498bba7ff7b7cfeb942709a713d9c72864b80633bee28e2b369370afc55f18e49ccc02b4dad9d1de3a4602caa99dce1f8d8196c8472a8add1 + checksum: 7f09b4bfae0d73f7cf8f383038edeaf8bc8610960bc24a9b8c140f4084bb3afd62971fb56e2ff5e013168165dcfff7b9a545f6cc837fe2669618184e4b77c107 languageName: node linkType: hard @@ -3383,22 +3389,22 @@ __metadata: languageName: node linkType: hard -"@backstage-community/plugin-explore-common@npm:^0.0.3": - version: 0.0.3 - resolution: "@backstage-community/plugin-explore-common@npm:0.0.3" - checksum: 54b81496122c11bd16cbb71c4fcc92e6bf16b5e4f580fc8043dbea35955447d67b463a40065e629ed81ef3aeeb36d1f41d445b7fa4d8ab7e0036a296b361e398 +"@backstage-community/plugin-explore-common@npm:^0.0.4": + version: 0.0.4 + resolution: "@backstage-community/plugin-explore-common@npm:0.0.4" + checksum: 0c27253dd79dcb56236d382f7591e20eb87f374878b091a690b3fede23ff652aefa3b074a58bd5b51462cc44faab12a97ee82443dcc6106f121212417b9c4cf9 languageName: node linkType: hard "@backstage-community/plugin-puppetdb@npm:^0.1.18": - version: 0.1.18 - resolution: "@backstage-community/plugin-puppetdb@npm:0.1.18" + version: 0.1.19 + resolution: "@backstage-community/plugin-puppetdb@npm:0.1.19" dependencies: - "@backstage/catalog-model": ^1.4.5 - "@backstage/core-components": ^0.14.4 - "@backstage/core-plugin-api": ^1.9.2 + "@backstage/catalog-model": ^1.5.0 + "@backstage/core-components": ^0.14.9 + "@backstage/core-plugin-api": ^1.9.3 "@backstage/errors": ^1.2.4 - "@backstage/plugin-catalog-react": ^1.11.3 + "@backstage/plugin-catalog-react": ^1.12.2 "@material-ui/core": ^4.12.2 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 react-use: ^17.2.4 @@ -3406,16 +3412,16 @@ __metadata: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: d0e4fe3f818aa05c527d773d558291b97f3a4fee1b1a765e0e6f5a2f36ca1897e3933e92abdeffeb8b33701c21314eb3565900ff78624fe1d5eff3ee2781cfff + checksum: abe5e3b5b9772b5cdc17074f58f38bdbf81516afe7b9241e06844729608ec2a56899437a7cf82d4c8f3608038c15217bd6ea971f41dbce2934e7b05986bfcc10 languageName: node linkType: hard "@backstage-community/plugin-stackstorm@npm:^0.1.16": - version: 0.1.16 - resolution: "@backstage-community/plugin-stackstorm@npm:0.1.16" + version: 0.1.17 + resolution: "@backstage-community/plugin-stackstorm@npm:0.1.17" dependencies: - "@backstage/core-components": ^0.14.4 - "@backstage/core-plugin-api": ^1.9.2 + "@backstage/core-components": ^0.14.9 + "@backstage/core-plugin-api": ^1.9.3 "@backstage/errors": ^1.2.4 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 @@ -3425,7 +3431,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: 2fd7d290cd98ad09b16862a5dee3302796942e85818abf9c4a9102038a4fb83e86ceee39d901e59bb7fb7c60df7797524f8c6a5cfee1a4a0ea59edcbb4de5a0a + checksum: 3fa2b45da036e4001a9e034a8841b58877d0b4189d244806b3b1808d3c2a34bb4ab4acbcff629edda47d03b2941c51743dd84f7352ed0d700988aaee946b65f3 languageName: node linkType: hard @@ -3497,7 +3503,7 @@ __metadata: minimist: ^1.2.5 morgan: ^1.10.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 path-to-regexp: ^6.2.1 selfsigned: ^2.0.0 @@ -3559,7 +3565,7 @@ __metadata: archiver: ^6.0.0 aws-sdk-client-mock: ^4.0.0 base64-stream: ^1.0.0 - better-sqlite3: ^9.0.0 + better-sqlite3: ^11.0.0 compression: ^1.7.4 concat-stream: ^2.0.0 cors: ^2.8.5 @@ -3582,7 +3588,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 mysql2: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 p-limit: ^3.1.0 path-to-regexp: ^6.2.1 @@ -3647,7 +3653,7 @@ __metadata: archiver: ^6.0.0 aws-sdk-client-mock: ^4.0.0 base64-stream: ^1.0.0 - better-sqlite3: ^9.0.0 + better-sqlite3: ^11.0.0 compression: ^1.7.4 concat-stream: ^2.0.0 cookie: ^0.6.0 @@ -3671,7 +3677,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 mysql2: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-forge: ^1.3.1 p-limit: ^3.1.0 path-to-regexp: ^6.2.1 @@ -3818,7 +3824,7 @@ __metadata: "@keyv/redis": ^2.5.3 "@types/keyv": ^4.2.0 "@types/supertest": ^2.0.8 - better-sqlite3: ^9.0.0 + better-sqlite3: ^11.0.0 cookie: ^0.6.0 express: ^4.17.1 fs-extra: ^11.0.0 @@ -3838,7 +3844,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": +"@backstage/catalog-client@^1.6.5, @backstage/catalog-client@workspace:^, @backstage/catalog-client@workspace:packages/catalog-client": version: 0.0.0-use.local resolution: "@backstage/catalog-client@workspace:packages/catalog-client" dependencies: @@ -3851,7 +3857,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.4.5, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": +"@backstage/catalog-model@^1.4.3, @backstage/catalog-model@^1.5.0, @backstage/catalog-model@workspace:^, @backstage/catalog-model@workspace:packages/catalog-model": version: 0.0.0-use.local resolution: "@backstage/catalog-model@workspace:packages/catalog-model" dependencies: @@ -3916,6 +3922,7 @@ __metadata: "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" "@manypkg/get-packages": ^1.1.3 + "@module-federation/enhanced": ^0.1.19 "@octokit/graphql": ^5.0.0 "@octokit/graphql-schema": ^13.7.0 "@octokit/oauth-app": ^4.2.0 @@ -3971,7 +3978,7 @@ __metadata: ctrlc-windows: ^2.1.0 del: ^7.0.0 diff: ^5.0.0 - esbuild: ^0.20.0 + esbuild: ^0.21.0 esbuild-loader: ^4.0.0 eslint: ^8.6.0 eslint-config-prettier: ^9.0.0 @@ -4002,7 +4009,7 @@ __metadata: mini-css-extract-plugin: ^2.4.2 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-libs-browser: ^2.2.1 nodemon: ^3.0.1 npm-packlist: ^5.0.0 @@ -4096,14 +4103,14 @@ __metadata: lodash: ^4.17.21 minimist: ^1.2.5 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 typescript-json-schema: ^0.63.0 yaml: ^2.0.0 zen-observable: ^0.10.0 languageName: unknown linkType: soft -"@backstage/config@^1.1.1, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": +"@backstage/config@^1.1.1, @backstage/config@^1.2.0, @backstage/config@workspace:^, @backstage/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@backstage/config@workspace:packages/config" dependencies: @@ -4164,6 +4171,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-catalog": "workspace:^" + "@backstage/test-utils": "workspace:^" "@backstage/version-bridge": "workspace:^" "@oriflame/backstage-plugin-score-card": ^0.8.0 "@testing-library/jest-dom": ^6.0.0 @@ -4175,7 +4183,107 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-components@^0.14.4, @backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": +"@backstage/core-components@npm:^0.13.10": + version: 0.13.10 + resolution: "@backstage/core-components@npm:0.13.10" + dependencies: + "@backstage/config": ^1.1.1 + "@backstage/core-plugin-api": ^1.8.2 + "@backstage/errors": ^1.2.3 + "@backstage/theme": ^0.5.0 + "@backstage/version-bridge": ^1.0.7 + "@date-io/core": ^1.3.13 + "@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.61 + "@react-hookz/web": ^23.0.0 + "@types/react": ^16.13.1 || ^17.0.0 + "@types/react-sparklines": ^1.7.0 + "@types/react-text-truncate": ^0.14.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.6.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-text-truncate: ^0.19.0 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: ec2a0d0a27bc4d6b9d4da97f0b4df600148529ee51bf2c8e7d3adc45c3950adac662535d3beabc451db346f2c7e3c412ceaa756fc774012b43dff5e2695f2271 + languageName: node + linkType: hard + +"@backstage/core-components@npm:^0.14.9": + version: 0.14.9 + resolution: "@backstage/core-components@npm:0.14.9" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/errors": ^1.2.4 + "@backstage/theme": ^0.5.6 + "@backstage/version-bridge": ^1.0.8 + "@date-io/core": ^1.3.13 + "@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.61 + "@react-hookz/web": ^24.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + "@types/react-sparklines": ^1.7.0 + ansi-regex: ^6.0.1 + classnames: ^2.2.6 + d3-selection: ^3.0.0 + d3-shape: ^3.0.0 + d3-zoom: ^3.0.0 + dagre: ^0.8.5 + linkify-react: 4.1.3 + linkifyjs: 4.1.3 + lodash: ^4.17.21 + pluralize: ^8.0.0 + qs: ^6.9.4 + rc-progress: 3.5.1 + react-helmet: 6.1.0 + react-hook-form: ^7.12.2 + react-idle-timer: 5.7.2 + react-markdown: ^8.0.0 + react-sparklines: ^1.7.0 + react-syntax-highlighter: ^15.4.5 + react-use: ^17.3.2 + react-virtualized-auto-sizer: ^1.0.11 + react-window: ^1.8.6 + remark-gfm: ^3.0.1 + zen-observable: ^0.10.0 + zod: ^3.22.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: cf89c9e19dd33270bdc2bb9e7d89487c71b78e6645dcef9a6ac0d1df67b52e4eee61f2873abe093ed921d756a75f97e870c294aa5c2a730598e520b06e38b41f + languageName: node + linkType: hard + +"@backstage/core-components@workspace:^, @backstage/core-components@workspace:packages/core-components": version: 0.0.0-use.local resolution: "@backstage/core-components@workspace:packages/core-components" dependencies: @@ -4246,58 +4354,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/core-components@npm:^0.13.10": - version: 0.13.10 - resolution: "@backstage/core-components@npm:0.13.10" - dependencies: - "@backstage/config": ^1.1.1 - "@backstage/core-plugin-api": ^1.8.2 - "@backstage/errors": ^1.2.3 - "@backstage/theme": ^0.5.0 - "@backstage/version-bridge": ^1.0.7 - "@date-io/core": ^1.3.13 - "@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.61 - "@react-hookz/web": ^23.0.0 - "@types/react": ^16.13.1 || ^17.0.0 - "@types/react-sparklines": ^1.7.0 - "@types/react-text-truncate": ^0.14.0 - ansi-regex: ^6.0.1 - classnames: ^2.2.6 - d3-selection: ^3.0.0 - d3-shape: ^3.0.0 - d3-zoom: ^3.0.0 - dagre: ^0.8.5 - linkify-react: 4.1.3 - linkifyjs: 4.1.3 - lodash: ^4.17.21 - pluralize: ^8.0.0 - qs: ^6.9.4 - rc-progress: 3.5.1 - react-helmet: 6.1.0 - react-hook-form: ^7.12.2 - react-idle-timer: 5.6.2 - react-markdown: ^8.0.0 - react-sparklines: ^1.7.0 - react-syntax-highlighter: ^15.4.5 - react-text-truncate: ^0.19.0 - react-use: ^17.3.2 - react-virtualized-auto-sizer: ^1.0.11 - react-window: ^1.8.6 - remark-gfm: ^3.0.1 - zen-observable: ^0.10.0 - zod: ^3.22.4 - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 - react-router-dom: 6.0.0-beta.0 || ^6.3.0 - checksum: ec2a0d0a27bc4d6b9d4da97f0b4df600148529ee51bf2c8e7d3adc45c3950adac662535d3beabc451db346f2c7e3c412ceaa756fc774012b43dff5e2695f2271 - languageName: node - linkType: hard - -"@backstage/core-plugin-api@^1.8.2, @backstage/core-plugin-api@^1.9.2, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": +"@backstage/core-plugin-api@^1.8.2, @backstage/core-plugin-api@^1.9.3, @backstage/core-plugin-api@workspace:^, @backstage/core-plugin-api@workspace:packages/core-plugin-api": version: 0.0.0-use.local resolution: "@backstage/core-plugin-api@workspace:packages/core-plugin-api" dependencies: @@ -4339,7 +4396,7 @@ __metadata: handlebars: ^4.7.3 inquirer: ^8.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 nodemon: ^3.0.1 ora: ^5.3.0 recursive-readdir: ^2.2.2 @@ -4444,6 +4501,26 @@ __metadata: languageName: unknown linkType: soft +"@backstage/frontend-plugin-api@npm:^0.6.7": + version: 0.6.7 + resolution: "@backstage/frontend-plugin-api@npm:0.6.7" + dependencies: + "@backstage/core-components": ^0.14.9 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.8 + "@material-ui/core": ^4.12.4 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + lodash: ^4.17.21 + zod: ^3.22.4 + zod-to-json-schema: ^3.21.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: cf7485aedcae02c5855a62adea6fffcfad256d614731ade38d2bebd5f98e91aa5d1020cd9ac25b74d67d2681c010addff4a89d5be1e2deb2e688bf1915134c32 + languageName: node + linkType: hard + "@backstage/frontend-plugin-api@workspace:^, @backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/frontend-plugin-api@workspace:packages/frontend-plugin-api" @@ -4475,6 +4552,7 @@ __metadata: resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils" dependencies: "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -4507,6 +4585,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/integration-react@npm:^1.1.29": + version: 1.1.29 + resolution: "@backstage/integration-react@npm:1.1.29" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/integration": ^1.13.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@types/react": ^16.13.1 || ^17.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: add10ad8c108933354037f9d74ec554174d2af53d74a031e12ed01e98bd51481413b93c694460cf045b3bae818dd18b5efa061fc60c210e145e4629e0cb227b7 + languageName: node + linkType: hard + "@backstage/integration-react@workspace:^, @backstage/integration-react@workspace:packages/integration-react": version: 0.0.0-use.local resolution: "@backstage/integration-react@workspace:packages/integration-react" @@ -4531,6 +4627,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/integration@npm:^1.13.0": + version: 1.13.0 + resolution: "@backstage/integration@npm:1.13.0" + dependencies: + "@azure/identity": ^4.0.0 + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@octokit/auth-app": ^4.0.0 + "@octokit/rest": ^19.0.3 + cross-fetch: ^4.0.0 + git-url-parse: ^14.0.0 + lodash: ^4.17.21 + luxon: ^3.0.0 + checksum: 2b824261b0d30771e54515f8571c2af0b4c64f5cd3d1f990c234152b19e81d1e4ddbbbfae2a0b74f44abbfb67f52609c7eee9dc9fbb61d5f0076acaa80b3e670 + languageName: node + linkType: hard + "@backstage/integration@workspace:^, @backstage/integration@workspace:packages/integration": version: 0.0.0-use.local resolution: "@backstage/integration@workspace:packages/integration" @@ -4584,7 +4697,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" - "@graphiql/react": ^0.20.0 + "@graphiql/react": ^0.23.0 "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 @@ -4634,7 +4747,7 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.3 yn: ^4.0.0 languageName: unknown @@ -4704,7 +4817,7 @@ __metadata: jose: ^5.0.0 msw: ^2.0.8 node-cache: ^5.1.2 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 languageName: unknown linkType: soft @@ -4759,7 +4872,7 @@ __metadata: express: ^4.18.2 jose: ^5.0.0 msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-mocks-http: ^1.0.0 uuid: ^9.0.0 languageName: unknown @@ -4859,10 +4972,8 @@ __metadata: "@types/passport-microsoft": ^1.0.0 express: ^4.18.2 jose: ^5.0.0 - lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 - passport: ^0.7.0 + node-fetch: ^2.7.0 passport-microsoft: ^1.0.0 supertest: ^6.3.3 languageName: unknown @@ -5061,7 +5172,7 @@ __metadata: morgan: ^1.10.0 msw: ^1.0.0 node-cache: ^5.1.2 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 openid-client: ^5.2.1 passport: ^0.7.0 passport-auth0: ^1.4.3 @@ -5098,7 +5209,7 @@ __metadata: jose: ^5.0.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 passport: ^0.7.0 supertest: ^6.1.3 uuid: ^9.0.0 @@ -5191,7 +5302,7 @@ __metadata: "@backstage/plugin-catalog-node": "workspace:^" luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5257,7 +5368,7 @@ __metadata: "@types/node-fetch": ^2.5.12 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5296,7 +5407,7 @@ __metadata: fs-extra: ^11.2.0 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5343,7 +5454,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5387,7 +5498,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5440,6 +5551,19 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-catalog-backend-module-logs@workspace:plugins/catalog-backend-module-logs": + version: 0.0.0-use.local + resolution: "@backstage/plugin-catalog-backend-module-logs@workspace:plugins/catalog-backend-module-logs" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-catalog-backend": "workspace:^" + "@backstage/plugin-events-backend-test-utils": "workspace:^" + "@backstage/plugin-events-node": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-catalog-backend-module-msgraph@workspace:plugins/catalog-backend-module-msgraph": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-backend-module-msgraph@workspace:plugins/catalog-backend-module-msgraph" @@ -5460,7 +5584,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.0.2 qs: ^6.9.4 uuid: ^9.0.0 @@ -5507,7 +5631,7 @@ __metadata: lodash: ^4.17.21 luxon: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -5577,7 +5701,7 @@ __metadata: "@types/lodash": ^4.14.151 "@types/supertest": ^2.0.8 "@types/uuid": ^9.0.0 - better-sqlite3: ^9.0.0 + better-sqlite3: ^11.0.0 codeowners-utils: ^1.0.2 core-js: ^3.6.5 express: ^4.17.1 @@ -5590,7 +5714,7 @@ __metadata: luxon: ^3.0.0 minimatch: ^9.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.0.2 prom-client: ^15.0.0 supertest: ^6.1.3 @@ -5603,7 +5727,18 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-common@^1.0.20, @backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": +"@backstage/plugin-catalog-common@npm:^1.0.20, @backstage/plugin-catalog-common@npm:^1.0.25": + version: 1.0.25 + resolution: "@backstage/plugin-catalog-common@npm:1.0.25" + dependencies: + "@backstage/catalog-model": ^1.5.0 + "@backstage/plugin-permission-common": ^0.8.0 + "@backstage/plugin-search-common": ^1.2.13 + checksum: 37078811a78423accc078ae43d1572de6e74e0daff6b03a3d6f30d3ce37e71d30f60ac90aea5cf442ca0d2123259b267b37b966e29aa4f2dba08845d562d0e68 + languageName: node + linkType: hard + +"@backstage/plugin-catalog-common@workspace:^, @backstage/plugin-catalog-common@workspace:plugins/catalog-common": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-common@workspace:plugins/catalog-common" dependencies: @@ -5712,7 +5847,43 @@ __metadata: languageName: unknown linkType: soft -"@backstage/plugin-catalog-react@^1.11.3, @backstage/plugin-catalog-react@^1.9.3, @backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": +"@backstage/plugin-catalog-react@npm:^1.12.2, @backstage/plugin-catalog-react@npm:^1.9.3": + version: 1.12.2 + resolution: "@backstage/plugin-catalog-react@npm:1.12.2" + dependencies: + "@backstage/catalog-client": ^1.6.5 + "@backstage/catalog-model": ^1.5.0 + "@backstage/core-components": ^0.14.9 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/errors": ^1.2.4 + "@backstage/frontend-plugin-api": ^0.6.7 + "@backstage/integration-react": ^1.1.29 + "@backstage/plugin-catalog-common": ^1.0.25 + "@backstage/plugin-permission-common": ^0.8.0 + "@backstage/plugin-permission-react": ^0.4.24 + "@backstage/types": ^1.1.1 + "@backstage/version-bridge": ^1.0.8 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@react-hookz/web": ^24.0.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + classnames: ^2.2.6 + lodash: ^4.17.21 + material-ui-popup-state: ^1.9.3 + qs: ^6.9.4 + react-use: ^17.2.4 + yaml: ^2.0.0 + zen-observable: ^0.10.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 8570e71e56e89cc8ba890ad9cd1f2ce22147635e9a326703291ee8a75d704086c4a31cd11b2b2c3eb54f3e1dc82822b1bc7b9317e8c6d81f6ab9758c2c01ec5a + languageName: node + linkType: hard + +"@backstage/plugin-catalog-react@workspace:^, @backstage/plugin-catalog-react@workspace:plugins/catalog-react": version: 0.0.0-use.local resolution: "@backstage/plugin-catalog-react@workspace:plugins/catalog-react" dependencies: @@ -5894,7 +6065,7 @@ __metadata: express-promise-router: ^4.1.0 fs-extra: ^11.0.0 lodash: ^4.17.21 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 ping: ^0.4.1 semver: ^7.5.3 supertest: ^6.2.4 @@ -6070,7 +6241,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 - "@rjsf/utils": 5.18.4 + "@rjsf/utils": 5.18.5 "@types/react": ^16.13.1 || ^17.0.0 "@types/react-grid-layout": ^1.3.2 peerDependencies: @@ -6101,10 +6272,10 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 - "@rjsf/core": 5.18.4 - "@rjsf/material-ui": 5.18.4 - "@rjsf/utils": 5.18.4 - "@rjsf/validator-ajv8": 5.18.4 + "@rjsf/core": 5.18.5 + "@rjsf/material-ui": 5.18.5 + "@rjsf/utils": 5.18.5 + "@rjsf/validator-ajv8": 5.18.5 "@testing-library/dom": ^10.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 @@ -6169,7 +6340,7 @@ __metadata: luxon: ^3.0.0 morgan: ^1.10.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 stream-buffers: ^3.0.2 supertest: ^6.1.3 winston: ^3.2.1 @@ -6242,7 +6413,7 @@ __metadata: "@backstage/types": "workspace:^" "@kubernetes/client-node": ^0.20.0 msw: ^1.3.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.3 winston: ^3.2.1 languageName: unknown @@ -6292,9 +6463,11 @@ __metadata: dependencies: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-kubernetes-common": "workspace:^" "@backstage/plugin-kubernetes-react": "workspace:^" @@ -6374,7 +6547,7 @@ __metadata: express-promise-router: ^4.1.0 knex: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.2.4 uuid: ^9.0.0 winston: ^3.2.1 @@ -6387,6 +6560,7 @@ __metadata: resolution: "@backstage/plugin-notifications-common@workspace:plugins/notifications-common" dependencies: "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" "@material-ui/icons": ^4.9.1 languageName: unknown linkType: soft @@ -6406,7 +6580,7 @@ __metadata: "@backstage/test-utils": "workspace:^" knex: ^3.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 languageName: unknown linkType: soft @@ -6548,13 +6722,27 @@ __metadata: express-promise-router: ^4.1.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.1.6 yn: ^4.0.0 zod: ^3.22.4 languageName: unknown linkType: soft +"@backstage/plugin-permission-common@npm:^0.8.0": + version: 0.8.0 + resolution: "@backstage/plugin-permission-common@npm:0.8.0" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/errors": ^1.2.4 + "@backstage/types": ^1.1.1 + cross-fetch: ^4.0.0 + uuid: ^9.0.0 + zod: ^3.22.4 + checksum: 03d706cf764d65319fd32156438b6f826568c7d51e79fc6e7cd951b023017a1e07c7380f2babec7efd2ed96f441485ee68072d06fb5e42a1bf7aba7cd1f6ce6f + languageName: node + linkType: hard + "@backstage/plugin-permission-common@workspace:^, @backstage/plugin-permission-common@workspace:plugins/permission-common": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-common@workspace:plugins/permission-common" @@ -6567,6 +6755,7 @@ __metadata: msw: ^1.0.0 uuid: ^9.0.0 zod: ^3.22.4 + zod-to-json-schema: ^3.20.4 languageName: unknown linkType: soft @@ -6593,6 +6782,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-permission-react@npm:^0.4.24": + version: 0.4.24 + resolution: "@backstage/plugin-permission-react@npm:0.4.24" + dependencies: + "@backstage/config": ^1.2.0 + "@backstage/core-plugin-api": ^1.9.3 + "@backstage/plugin-permission-common": ^0.8.0 + "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + swr: ^2.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + checksum: 6fa895f4a25f7f08642f64a9b4b41e5f3adfbf3c3f1ede9e1abc7bb08f532d9d0c9baf9f08b472fff54803a2c2aaeff2306b5e6d1514193a2407af4b12433d3a + languageName: node + linkType: hard + "@backstage/plugin-permission-react@workspace:^, @backstage/plugin-permission-react@workspace:plugins/permission-react": version: 0.0.0-use.local resolution: "@backstage/plugin-permission-react@workspace:plugins/permission-react" @@ -6636,7 +6842,7 @@ __metadata: http-proxy-middleware: ^2.0.0 morgan: ^1.10.0 msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 uuid: ^9.0.0 winston: ^3.2.1 yaml: ^2.0.0 @@ -6671,11 +6877,12 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" + "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6694,7 +6901,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6715,7 +6922,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" fs-extra: ^11.2.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6736,7 +6943,7 @@ __metadata: fs-extra: ^11.2.0 git-url-parse: ^14.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 node-html-markdown: ^1.3.0 yaml: ^2.0.0 languageName: unknown @@ -6761,10 +6968,27 @@ __metadata: command-exists: ^1.2.9 fs-extra: ^11.2.0 winston: ^3.2.1 + yaml: ^2.0.0 yn: ^4.0.0 languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-backend-module-gcp@workspace:plugins/scaffolder-backend-module-gcp": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-backend-module-gcp@workspace:plugins/scaffolder-backend-module-gcp" + dependencies: + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/integration": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" + "@google-cloud/storage": ^7.0.0 + raw-body: ^2.4.1 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-backend-module-gerrit@workspace:^, @backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-gerrit@workspace:plugins/scaffolder-backend-module-gerrit" @@ -6778,7 +7002,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6796,7 +7020,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6860,6 +7084,7 @@ __metadata: "@backstage/plugin-scaffolder-node": "workspace:^" "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" octokit: ^3.0.0 + yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6883,6 +7108,7 @@ __metadata: command-exists: ^1.2.9 fs-extra: ^11.0.0 jest-when: ^3.1.0 + yaml: ^2.0.0 languageName: unknown linkType: soft @@ -6899,7 +7125,7 @@ __metadata: "@backstage/plugin-scaffolder-node-test-utils": "workspace:^" "@backstage/types": "workspace:^" msw: ^2.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 yaml: ^2.3.3 languageName: unknown linkType: soft @@ -6924,6 +7150,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend@workspace:plugins/scaffolder-backend" dependencies: + "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" @@ -6935,6 +7162,7 @@ __metadata: "@backstage/errors": "workspace:^" "@backstage/integration": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-bitbucket-cloud-common": "workspace:^" "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" @@ -6958,7 +7186,7 @@ __metadata: "@types/supertest": ^2.0.8 "@types/zen-observable": ^0.8.0 concat-stream: ^2.0.0 - esbuild: ^0.20.0 + esbuild: ^0.21.0 express: ^4.17.1 express-promise-router: ^4.1.0 fs-extra: ^11.2.0 @@ -7026,11 +7254,13 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/plugin-scaffolder-common": "workspace:^" "@backstage/types": "workspace:^" + concat-stream: ^2.0.0 fs-extra: ^11.2.0 globby: ^11.0.0 isomorphic-git: ^1.23.0 jsonschema: ^1.2.6 p-limit: ^3.1.0 + tar: ^6.1.12 winston: ^3.2.1 zod: ^3.22.4 zod-to-json-schema: ^3.20.4 @@ -7061,10 +7291,10 @@ __metadata: "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.61 "@react-hookz/web": ^24.0.0 - "@rjsf/core": 5.18.4 - "@rjsf/material-ui": 5.18.4 - "@rjsf/utils": 5.18.4 - "@rjsf/validator-ajv8": 5.18.4 + "@rjsf/core": 5.18.5 + "@rjsf/material-ui": 5.18.5 + "@rjsf/utils": 5.18.5 + "@rjsf/validator-ajv8": 5.18.5 "@testing-library/dom": ^10.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 @@ -7073,6 +7303,7 @@ __metadata: "@types/json-schema": ^7.0.9 "@types/luxon": ^3.0.0 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + ajv-errors: ^3.0.0 classnames: ^2.2.6 flatted: 3.3.1 humanize-duration: ^3.25.1 @@ -7127,10 +7358,10 @@ __metadata: "@material-ui/lab": 4.0.0-alpha.61 "@microsoft/fetch-event-source": ^2.0.1 "@react-hookz/web": ^24.0.0 - "@rjsf/core": 5.18.4 - "@rjsf/material-ui": 5.18.4 - "@rjsf/utils": 5.18.4 - "@rjsf/validator-ajv8": 5.18.4 + "@rjsf/core": 5.18.5 + "@rjsf/material-ui": 5.18.5 + "@rjsf/utils": 5.18.5 + "@rjsf/validator-ajv8": 5.18.5 "@testing-library/dom": ^10.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 @@ -7138,6 +7369,8 @@ __metadata: "@types/humanize-duration": ^3.18.1 "@types/json-schema": ^7.0.9 "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 + "@types/react-resizable": ^3.0.8 + "@types/react-window": ^1.8.8 "@uiw/react-codemirror": ^4.9.3 classnames: ^2.2.6 git-url-parse: ^14.0.0 @@ -7149,7 +7382,9 @@ __metadata: luxon: ^3.0.0 msw: ^1.0.0 qs: ^6.9.4 + react-resizable: ^3.0.5 react-use: ^17.2.4 + react-window: ^1.8.10 swr: ^2.0.0 yaml: ^2.0.0 zen-observable: ^0.10.0 @@ -7212,7 +7447,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-search-backend-module-explore@workspace:plugins/search-backend-module-explore" dependencies: - "@backstage-community/plugin-explore-common": ^0.0.3 + "@backstage-community/plugin-explore-common": ^0.0.4 "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-tasks": "workspace:^" @@ -7222,7 +7457,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.2.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 languageName: unknown linkType: soft @@ -7230,7 +7465,6 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-search-backend-module-pg@workspace:plugins/search-backend-module-pg" dependencies: - "@backstage/backend-app-api": "workspace:^" "@backstage/backend-common": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" @@ -7258,7 +7492,7 @@ __metadata: "@backstage/plugin-search-backend-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" msw: ^1.2.1 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 qs: ^6.9.4 languageName: unknown linkType: soft @@ -7283,7 +7517,7 @@ __metadata: "@backstage/plugin-techdocs-node": "workspace:^" lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.1.0 languageName: unknown linkType: soft @@ -7341,6 +7575,16 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-search-common@npm:^1.2.13": + version: 1.2.13 + resolution: "@backstage/plugin-search-common@npm:1.2.13" + dependencies: + "@backstage/plugin-permission-common": ^0.8.0 + "@backstage/types": ^1.1.1 + checksum: 0c7b7b791b648e874081b845488f636791754bc4da1a4780b49e7281cc6294bcad0a9feab9fd7dd5f6a583b5fce372e704f440ad7bc33ab828ebd2afc5344504 + languageName: node + linkType: hard + "@backstage/plugin-search-common@workspace:^, @backstage/plugin-search-common@workspace:plugins/search-common": version: 0.0.0-use.local resolution: "@backstage/plugin-search-common@workspace:plugins/search-common" @@ -7444,7 +7688,7 @@ __metadata: express-promise-router: ^4.1.0 http-proxy-middleware: ^2.0.0 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 supertest: ^6.2.4 uuid: ^9.0.0 winston: ^3.2.1 @@ -7558,6 +7802,7 @@ __metadata: "@backstage/plugin-catalog-common": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-search-backend-module-techdocs": "workspace:^" + "@backstage/plugin-techdocs-common": "workspace:^" "@backstage/plugin-techdocs-node": "workspace:^" "@types/express": ^4.17.6 express: ^4.17.1 @@ -7566,13 +7811,21 @@ __metadata: knex: ^3.0.0 lodash: ^4.17.21 msw: ^1.0.0 - node-fetch: ^2.6.7 + node-fetch: ^2.7.0 p-limit: ^3.1.0 supertest: ^6.1.3 winston: ^3.2.1 languageName: unknown linkType: soft +"@backstage/plugin-techdocs-common@workspace:^, @backstage/plugin-techdocs-common@workspace:plugins/techdocs-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-techdocs-common@workspace:plugins/techdocs-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-techdocs-module-addons-contrib@workspace:^, @backstage/plugin-techdocs-module-addons-contrib@workspace:plugins/techdocs-module-addons-contrib": version: 0.0.0-use.local resolution: "@backstage/plugin-techdocs-module-addons-contrib@workspace:plugins/techdocs-module-addons-contrib" @@ -7621,6 +7874,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/integration-aws-node": "workspace:^" "@backstage/plugin-search-common": "workspace:^" + "@backstage/plugin-techdocs-common": "workspace:^" "@google-cloud/storage": ^7.0.0 "@smithy/node-http-handler": ^2.1.7 "@trendyol-js/openstack-swift-sdk": ^0.0.7 @@ -7694,6 +7948,7 @@ __metadata: "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^" + "@backstage/plugin-techdocs-common": "workspace:^" "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^" "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/test-utils": "workspace:^" @@ -7891,7 +8146,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/theme@^0.5.0, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": +"@backstage/theme@^0.5.0, @backstage/theme@^0.5.6, @backstage/theme@workspace:^, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" dependencies: @@ -7922,7 +8177,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/version-bridge@^1.0.7, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": +"@backstage/version-bridge@^1.0.7, @backstage/version-bridge@^1.0.8, @backstage/version-bridge@workspace:^, @backstage/version-bridge@workspace:packages/version-bridge": version: 0.0.0-use.local resolution: "@backstage/version-bridge@workspace:packages/version-bridge" dependencies: @@ -7976,12 +8231,12 @@ __metadata: languageName: node linkType: hard -"@changesets/apply-release-plan@npm:^7.0.3": - version: 7.0.3 - resolution: "@changesets/apply-release-plan@npm:7.0.3" +"@changesets/apply-release-plan@npm:^7.0.4": + version: 7.0.4 + resolution: "@changesets/apply-release-plan@npm:7.0.4" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/config": ^3.0.1 + "@changesets/config": ^3.0.2 "@changesets/get-version-range-type": ^0.4.0 "@changesets/git": ^3.0.0 "@changesets/should-skip-package": ^0.1.0 @@ -7994,22 +8249,22 @@ __metadata: prettier: ^2.7.1 resolve-from: ^5.0.0 semver: ^7.5.3 - checksum: 622b1ed7652b072b28bee431d5407ce686cff058caa3db9353848cd1177dc535d352711f6e5f9f819a7331235e72279f7e0f34955da67681c50d051ec30f476a + checksum: 5a61a8794fd4762e9270ffec474dc7c5d420c1d0479c5316a12e5264589882ccfb3ec08af6c0ebd07de2bf26ae880ad2f65e8f16d5886745aecf48cb11f625f5 languageName: node linkType: hard -"@changesets/assemble-release-plan@npm:^6.0.2": - version: 6.0.2 - resolution: "@changesets/assemble-release-plan@npm:6.0.2" +"@changesets/assemble-release-plan@npm:^6.0.3": + version: 6.0.3 + resolution: "@changesets/assemble-release-plan@npm:6.0.3" dependencies: "@babel/runtime": ^7.20.1 "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.1.0 + "@changesets/get-dependents-graph": ^2.1.1 "@changesets/should-skip-package": ^0.1.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 semver: ^7.5.3 - checksum: 90019a9bd22ad1368f23f1335d1b7eea5a55b1ed8b9141c9cc2fdab99bda85f9cb3886062a33c078cfb6f08f1a6f165aedbb1d1ce2a63c15ce0bfa7a7c365854 + checksum: ae0ac29a44640fc6c0d3782c134adad3f0af80903ecb8bf663f0bf5f569586c9620980100737a05639f1a7252f145942ae62fa25d0757e89b5d6311abf899cd5 languageName: node linkType: hard @@ -8023,17 +8278,17 @@ __metadata: linkType: hard "@changesets/cli@npm:^2.14.0": - version: 2.27.5 - resolution: "@changesets/cli@npm:2.27.5" + version: 2.27.7 + resolution: "@changesets/cli@npm:2.27.7" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/apply-release-plan": ^7.0.3 - "@changesets/assemble-release-plan": ^6.0.2 + "@changesets/apply-release-plan": ^7.0.4 + "@changesets/assemble-release-plan": ^6.0.3 "@changesets/changelog-git": ^0.2.0 - "@changesets/config": ^3.0.1 + "@changesets/config": ^3.0.2 "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.1.0 - "@changesets/get-release-plan": ^4.0.2 + "@changesets/get-dependents-graph": ^2.1.1 + "@changesets/get-release-plan": ^4.0.3 "@changesets/git": ^3.0.0 "@changesets/logger": ^0.1.0 "@changesets/pre": ^2.0.0 @@ -8050,7 +8305,7 @@ __metadata: external-editor: ^3.1.0 fs-extra: ^7.0.1 human-id: ^1.0.2 - meow: ^6.0.0 + mri: ^1.2.0 outdent: ^0.5.0 p-limit: ^2.2.0 preferred-pm: ^3.0.0 @@ -8058,25 +8313,24 @@ __metadata: semver: ^7.5.3 spawndamnit: ^2.0.0 term-size: ^2.1.0 - tty-table: ^4.1.5 bin: changeset: bin.js - checksum: 7e0d0aadabcf3028abbb2e079b58135b8e39f78bc174f4fa669cf94482ca3a4ec873e4b4ff9a7e071d83aee592780f0c47bd36a3a12282ab5f13a493c796663f + checksum: f518121a516ce8186dfc71dcca2f4be5fea28fe29a54b038aa787964bb4d4f1e866840d6d765776db33f7723d1a850ee4a38ed726f302a26e2e56a5e2b9422e8 languageName: node linkType: hard -"@changesets/config@npm:^3.0.1": - version: 3.0.1 - resolution: "@changesets/config@npm:3.0.1" +"@changesets/config@npm:^3.0.2": + version: 3.0.2 + resolution: "@changesets/config@npm:3.0.2" dependencies: "@changesets/errors": ^0.2.0 - "@changesets/get-dependents-graph": ^2.1.0 + "@changesets/get-dependents-graph": ^2.1.1 "@changesets/logger": ^0.1.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 fs-extra: ^7.0.1 micromatch: ^4.0.2 - checksum: 588a8139f721e2917c9bad842e3a933a8a5216b6a73c6a203ddcb834c274f873eaa780b32b42f89a477159e898ad3c1c93ca0ac28a07401b7b3f56185672a6a8 + checksum: 4160e59f74c1f36a2d3d011868f83c46410553f9fc164a83dde46ec7e37f13ba983526b9c299de3cd3f317e5e42808a991959931c9d34f2913f3e8d4c515cd12 languageName: node linkType: hard @@ -8089,31 +8343,31 @@ __metadata: languageName: node linkType: hard -"@changesets/get-dependents-graph@npm:^2.1.0": - version: 2.1.0 - resolution: "@changesets/get-dependents-graph@npm:2.1.0" +"@changesets/get-dependents-graph@npm:^2.1.1": + version: 2.1.1 + resolution: "@changesets/get-dependents-graph@npm:2.1.1" dependencies: "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 chalk: ^2.1.0 fs-extra: ^7.0.1 semver: ^7.5.3 - checksum: 1cd9c0f145a20d735ab9e1d615c5bdf52179cd2fc9b8555649d74979d8dbc915f9e85af4e20f7b71da76d3d860cd2090cee4c6ecccac4f5bf42ae11bb245471d + checksum: e427603a3cad3690deec86a5831f15766dc32dc0819532e3ad70fbda027317bb46bb207c6a3bc947daf00e5e8229a01dd67aa3ec7248aa11f87611a7ecd74097 languageName: node linkType: hard -"@changesets/get-release-plan@npm:^4.0.2": - version: 4.0.2 - resolution: "@changesets/get-release-plan@npm:4.0.2" +"@changesets/get-release-plan@npm:^4.0.3": + version: 4.0.3 + resolution: "@changesets/get-release-plan@npm:4.0.3" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/assemble-release-plan": ^6.0.2 - "@changesets/config": ^3.0.1 + "@changesets/assemble-release-plan": ^6.0.3 + "@changesets/config": ^3.0.2 "@changesets/pre": ^2.0.0 "@changesets/read": ^0.6.0 "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 - checksum: 651814230890bf7605f4094332e4842505eb060092fbe65d9c8fa1a44f724ce0086890b8437f2fa96aedfa11c88e622e133d700847dea4c92141c2fbb486460f + checksum: baf01eb124f95df729e1798c23e4ac1996acd74e0d22336492e4a0fd68f3a19c97197337fdcb58d47d0b580ca38e0b0888cbacaa1c99d7c173c3cecb859ab4cc languageName: node linkType: hard @@ -8319,13 +8573,13 @@ __metadata: linkType: hard "@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.23.0": - version: 6.26.3 - resolution: "@codemirror/view@npm:6.26.3" + version: 6.28.2 + resolution: "@codemirror/view@npm:6.28.2" dependencies: "@codemirror/state": ^6.4.0 style-mod: ^4.1.0 w3c-keyname: ^2.2.4 - checksum: fdee35fb5e0bbba7b6f1a9b43a865880911bbfafd30360da5dda21b35f81ba2d080ff66b6c3d94dbe946b6b7ec98a76208786360b8f030ef10bcb054b816de05 + checksum: d2d36d7bbb1fc094fb421251b96144b67a932f43d12092bd2318ced283db0081e6e78cc99c5cecf9eedd24278d39fe41deaa490fcc03637d85c5cf332af58fec languageName: node linkType: hard @@ -8624,6 +8878,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm64@npm:0.18.20" @@ -8638,6 +8899,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm@npm:0.18.20" @@ -8652,6 +8920,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-x64@npm:0.18.20" @@ -8666,6 +8941,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-arm64@npm:0.18.20" @@ -8680,6 +8962,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/darwin-x64@npm:0.18.20" @@ -8694,6 +8983,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-arm64@npm:0.18.20" @@ -8708,6 +9004,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/freebsd-x64@npm:0.18.20" @@ -8722,6 +9025,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm64@npm:0.18.20" @@ -8736,6 +9046,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-arm@npm:0.18.20" @@ -8750,6 +9067,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ia32@npm:0.18.20" @@ -8764,6 +9088,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-loong64@npm:0.18.20" @@ -8778,6 +9109,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-mips64el@npm:0.18.20" @@ -8792,6 +9130,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-ppc64@npm:0.18.20" @@ -8806,6 +9151,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-riscv64@npm:0.18.20" @@ -8820,6 +9172,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-s390x@npm:0.18.20" @@ -8834,6 +9193,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/linux-x64@npm:0.18.20" @@ -8848,6 +9214,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/netbsd-x64@npm:0.18.20" @@ -8862,6 +9235,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/openbsd-x64@npm:0.18.20" @@ -8876,6 +9256,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/sunos-x64@npm:0.18.20" @@ -8890,6 +9277,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-arm64@npm:0.18.20" @@ -8904,6 +9298,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-ia32@npm:0.18.20" @@ -8918,6 +9319,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/win32-x64@npm:0.18.20" @@ -8932,6 +9340,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -9131,23 +9546,23 @@ __metadata: linkType: hard "@google-cloud/container@npm:^5.0.0": - version: 5.12.0 - resolution: "@google-cloud/container@npm:5.12.0" + version: 5.15.0 + resolution: "@google-cloud/container@npm:5.15.0" dependencies: google-gax: ^4.0.3 - checksum: bf22c6754d67b133f272522b41f9f383e960a3288847d345407a171cce593856527cd3ee3df13bb4b09091bfce03f3404d24559ce2c28733ce71866068fb5277 + checksum: e31332b17816688f0cf831c66ef1ccc24c7600e4ec798fdeed33b66ed934372b1a962e92c665903bae661db5aaf872d79becc7d67b3f9c6a55b403acdfd534ec languageName: node linkType: hard "@google-cloud/firestore@npm:^7.0.0": - version: 7.4.0 - resolution: "@google-cloud/firestore@npm:7.4.0" + version: 7.9.0 + resolution: "@google-cloud/firestore@npm:7.9.0" dependencies: fast-deep-equal: ^3.1.1 functional-red-black-tree: ^1.0.1 - google-gax: ^4.0.4 - protobufjs: ^7.2.5 - checksum: df0094c3be5473a96f925d7bcd4234f376e4928065bdd96bdc2debc73b4c976df160c2553dfc8ed3d73f859778996159e69770983fa0224a53afbe4e77754db3 + google-gax: ^4.3.3 + protobufjs: ^7.2.6 + checksum: 5d0657736f1e364cd110ea6083eb8b6aeb1605ea409ea5478d5c8430bfa568c62d472b41e892d0849dfb405b06d8aba412fd57a5d1e807429ba5264c48eb2673 languageName: node linkType: hard @@ -9176,8 +9591,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^7.0.0": - version: 7.10.1 - resolution: "@google-cloud/storage@npm:7.10.1" + version: 7.11.2 + resolution: "@google-cloud/storage@npm:7.11.2" dependencies: "@google-cloud/paginator": ^5.0.0 "@google-cloud/projectify": ^4.0.0 @@ -9185,20 +9600,20 @@ __metadata: abort-controller: ^3.0.0 async-retry: ^1.3.3 duplexify: ^4.1.3 - ent: ^2.2.0 fast-xml-parser: ^4.3.0 gaxios: ^6.0.2 google-auth-library: ^9.6.3 + html-entities: ^2.5.2 mime: ^3.0.0 p-limit: ^3.0.1 retry-request: ^7.0.0 teeny-request: ^9.0.0 uuid: ^8.0.0 - checksum: 0605f8abf67cde98bc48b05c749b569416af821fcc3eb8ed3fac6dc5ade9c1f2f3210404c85e30525260ea4f34785d7436f7684e6eacd083613237669badff25 + checksum: 763cd560245006e11a060fdbd5edff5d6bfe22b0c0b06dfe88af43b7b14458598c00a71c4e1b079e9ab09096d7d262bdc27fa77d76a8969af0765f23c93f8405 languageName: node linkType: hard -"@graphiql/react@npm:^0.20.0, @graphiql/react@npm:^0.20.3": +"@graphiql/react@npm:^0.20.3": version: 0.20.3 resolution: "@graphiql/react@npm:0.20.3" dependencies: @@ -9225,9 +9640,36 @@ __metadata: languageName: node linkType: hard -"@graphiql/toolkit@npm:^0.9.1": - version: 0.9.1 - resolution: "@graphiql/toolkit@npm:0.9.1" +"@graphiql/react@npm:^0.23.0": + version: 0.23.0 + resolution: "@graphiql/react@npm:0.23.0" + dependencies: + "@graphiql/toolkit": ^0.9.2 + "@headlessui/react": ^1.7.15 + "@radix-ui/react-dialog": ^1.0.4 + "@radix-ui/react-dropdown-menu": ^2.0.5 + "@radix-ui/react-tooltip": ^1.0.6 + "@radix-ui/react-visually-hidden": ^1.0.3 + "@types/codemirror": ^5.60.8 + clsx: ^1.2.1 + codemirror: ^5.65.3 + codemirror-graphql: ^2.0.13 + copy-to-clipboard: ^3.2.0 + framer-motion: ^6.5.1 + graphql-language-service: ^5.2.2 + markdown-it: ^14.1.0 + set-value: ^4.1.0 + peerDependencies: + graphql: ^15.5.0 || ^16.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + checksum: a62ee7d763d410f5ff8c5670596194069e97f11b906762c4e38c74797e4e34c33cd91e06aae24d5118e0fa05dc6c1ffa6c3d3d8ff5bc2b6af0b689088236f613 + languageName: node + linkType: hard + +"@graphiql/toolkit@npm:^0.9.1, @graphiql/toolkit@npm:^0.9.2": + version: 0.9.2 + resolution: "@graphiql/toolkit@npm:0.9.2" dependencies: "@n1ru4l/push-pull-async-iterable-iterator": ^3.1.0 meros: ^1.1.4 @@ -9237,7 +9679,7 @@ __metadata: peerDependenciesMeta: graphql-ws: optional: true - checksum: 5328426051b7f9a9ffbd569c950d1a103ce0e2ee7b5d7a57f3d899488ad43d1a5101e8aeced7416e106c7687d67bb7981aa7e87dea5b0f17b77569aa738bf3b5 + checksum: 4474a998a948d0df5cef8371245b0e895be40803a07e49231ab91798aa761880e6e4a684478a44b723c013f0e8206dab3d86e5b0fc9b688df035fa9f1fa29734 languageName: node linkType: hard @@ -9525,27 +9967,17 @@ __metadata: languageName: node linkType: hard -"@grpc/grpc-js@npm:^1.7.1": - version: 1.10.7 - resolution: "@grpc/grpc-js@npm:1.10.7" +"@grpc/grpc-js@npm:^1.10.9, @grpc/grpc-js@npm:^1.7.1": + version: 1.10.10 + resolution: "@grpc/grpc-js@npm:1.10.10" dependencies: "@grpc/proto-loader": ^0.7.13 "@js-sdsl/ordered-map": ^4.4.2 - checksum: 69e88768e59b53ca020e2cfa9474fbd645f4ee7dd3269559c9fb91970273da6e8db480c0c439bdd73b49f1831d8f47c9bc5305dc5f9ed4db8873d53572e4f019 + checksum: e48274ab6e377031a2314197d33416c3261abf9601638ba05d8ae523e6e28338f97f852efdf9f9f763e49056812c83d3719cdf5913881d46cfca837dea2ab23c languageName: node linkType: hard -"@grpc/grpc-js@npm:~1.9.6": - version: 1.9.11 - resolution: "@grpc/grpc-js@npm:1.9.11" - dependencies: - "@grpc/proto-loader": ^0.7.8 - "@types/node": ">=12.12.47" - checksum: ed11ebcb0f8c34e2ada1d16ac2eef2465e1812871dd42bf40a405dfa06e2bfe895e680aff10644f9d51c33ce1ee1882ee466237ee478365c5ed265c0d7c5c015 - languageName: node - linkType: hard - -"@grpc/proto-loader@npm:^0.7.0, @grpc/proto-loader@npm:^0.7.13, @grpc/proto-loader@npm:^0.7.8": +"@grpc/proto-loader@npm:^0.7.13": version: 0.7.13 resolution: "@grpc/proto-loader@npm:0.7.13" dependencies: @@ -9888,11 +10320,11 @@ __metadata: linkType: hard "@jest-mock/express@npm:^2.0.1": - version: 2.0.2 - resolution: "@jest-mock/express@npm:2.0.2" + version: 2.1.0 + resolution: "@jest-mock/express@npm:2.1.0" dependencies: - "@types/express": ^4.17.17 - checksum: 558feb47e7e9620a5e53b83c51b3802782ecf7ab32b8d81077b16f641b7e1a6dc6fffa652d2ab48b28ba0f0634fa6387bd21b08f62b92bd730dfca167a4c2795 + "@types/express": ^4.17.21 + checksum: 305b72c214c1e80ed16271aa48e4d6d906346f5221a4e873d1e8ae48c32c2316cb88bed819b195c934c339b9ab41d4b34c4e41ac66feefe980d2cd16bc0b6d20 languageName: node linkType: hard @@ -10700,6 +11132,145 @@ __metadata: languageName: node linkType: hard +"@module-federation/dts-plugin@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/dts-plugin@npm:0.1.21" + dependencies: + "@module-federation/managers": 0.1.21 + "@module-federation/sdk": 0.1.21 + "@module-federation/third-party-dts-extractor": 0.1.21 + adm-zip: ^0.5.10 + ansi-colors: ^4.1.3 + axios: ^1.6.7 + chalk: 3.0.0 + fs-extra: 9.1.0 + isomorphic-ws: 5.0.0 + koa: 2.11.0 + lodash.clonedeepwith: 4.5.0 + log4js: 6.9.1 + node-schedule: 2.1.1 + rambda: ^9.1.0 + ws: 8.17.0 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ^1.0.24 + peerDependenciesMeta: + vue-tsc: + optional: true + checksum: ec4cd030a25617698754cbac2da5463f8942cdd0a64bdc95f6ff5fd29fff6b88cf3db90e53e6b260cd3593893fe6ee2d6e149d0a46698eb7f9cc19a9df26193d + languageName: node + linkType: hard + +"@module-federation/enhanced@npm:^0.1.19": + version: 0.1.21 + resolution: "@module-federation/enhanced@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/manifest": 0.1.21 + "@module-federation/rspack": 0.1.21 + "@module-federation/runtime-tools": 0.1.21 + "@module-federation/sdk": 0.1.21 + upath: 2.0.1 + peerDependencies: + typescript: ^4.9.0 || ^5.0.0 + vue-tsc: ^1.0.24 + webpack: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + vue-tsc: + optional: true + webpack: + optional: true + checksum: bc0ff541db1066b290b3ad9ab868437dc3d0754b9d06ab263f8fb7f54e08238eae9232a482a681dd6152e4a175578d65df9f27fe181fc8fb602d7cc1ae34807d + languageName: node + linkType: hard + +"@module-federation/managers@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/managers@npm:0.1.21" + dependencies: + "@module-federation/sdk": 0.1.21 + find-pkg: 2.0.0 + fs-extra: 9.1.0 + checksum: 5f230d5795d86dfd68c404ee2b7a1264950c283a4b1c6f4ee9cc9579fabb413718dfbc1ff726b9c213f9d3223d944dd38dd9d04b700962e6398c3c3728d6323e + languageName: node + linkType: hard + +"@module-federation/manifest@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/manifest@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/sdk": 0.1.21 + chalk: 3.0.0 + find-pkg: 2.0.0 + checksum: cef2011875f14e853a355626ae1dbc8ae3b0714d31140e329b5dd71525782b08c2e1d6ca45276a563bb3c3b7f7c4e64a31f0698ef12606f05aa6da46e759f345 + languageName: node + linkType: hard + +"@module-federation/rspack@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/rspack@npm:0.1.21" + dependencies: + "@module-federation/dts-plugin": 0.1.21 + "@module-federation/managers": 0.1.21 + "@module-federation/manifest": 0.1.21 + "@module-federation/runtime-tools": 0.1.21 + "@module-federation/sdk": 0.1.21 + checksum: 55516285e23f4ca7127afafb14af667defbe46dc3224f85d7e07edbc8937d7fac909dfebc2f9dd73120b99bbe5135372cf0fbbe282990d80e6953a60dfa4c93e + languageName: node + linkType: hard + +"@module-federation/runtime-tools@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/runtime-tools@npm:0.1.21" + dependencies: + "@module-federation/runtime": 0.1.21 + "@module-federation/webpack-bundler-runtime": 0.1.21 + checksum: 628c0c4834093520f9c71481d587c9e18163f82e481b05b1900f04e2d5da4abb69af6d814ac5cd1951057b28d73f3adeb1cee7cd83628305b10cc7988405fbc5 + languageName: node + linkType: hard + +"@module-federation/runtime@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/runtime@npm:0.1.21" + dependencies: + "@module-federation/sdk": 0.1.21 + checksum: ce4de8515b54f1cd07a3c7c4cbd35fea163294b9fb24be10827872f3ebb62cd5c289f3602efe4149d963282739f79b51947afa039ee6f36be7f66dea83d590fc + languageName: node + linkType: hard + +"@module-federation/sdk@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/sdk@npm:0.1.21" + checksum: 6856dcfe2ef5ae939890b82010aaad911fa6c4330a05f290ae054c316c9b532d3691456a1f9e176fe05f1df2d6f2d8c7e0c842ca5648a0fd7abf270e44ed9ecb + languageName: node + linkType: hard + +"@module-federation/third-party-dts-extractor@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/third-party-dts-extractor@npm:0.1.21" + dependencies: + find-pkg: 2.0.0 + fs-extra: 9.1.0 + resolve: 1.22.8 + checksum: e394fd7c2e6dbdf8df6937628680e7356ac897ee6f1309d7fbc38c00bcf4be9c0363f8bc1a75c29f7987a5a2f11f7855481813889b18e8b444ee9006aeb4a299 + languageName: node + linkType: hard + +"@module-federation/webpack-bundler-runtime@npm:0.1.21": + version: 0.1.21 + resolution: "@module-federation/webpack-bundler-runtime@npm:0.1.21" + dependencies: + "@module-federation/runtime": 0.1.21 + "@module-federation/sdk": 0.1.21 + checksum: 7d96002066e63bdb503964fd5fb2798be25f4135a599d87721f4d26ebe1de1affbf447c56b082f7ee850ae7798d0ac637f6a486f58591269065e114051b466e5 + languageName: node + linkType: hard + "@motionone/animation@npm:^10.12.0": version: 10.16.3 resolution: "@motionone/animation@npm:10.16.3" @@ -10834,23 +11405,23 @@ __metadata: languageName: node linkType: hard -"@mui/core-downloads-tracker@npm:^5.15.15": - version: 5.15.15 - resolution: "@mui/core-downloads-tracker@npm:5.15.15" - checksum: 3e99a04e03f66d5fa5f0c23cdce0f9fa2331ba08c99a75dc2347ccaa1c6ed520153e04aaeb0d613c9dca099a3e6242558a6284c33d93f95cc65e3243b17860bc +"@mui/core-downloads-tracker@npm:^5.15.20": + version: 5.15.20 + resolution: "@mui/core-downloads-tracker@npm:5.15.20" + checksum: 8cc71169ce1201c5a37fad6c5c58739850694e90c7bda636c692d47a79fd4c7884eab2c24b0084d6732a3d28c9a4a6d222dd5747f9d91f5075614700a3a3282a languageName: node linkType: hard "@mui/material@npm:^5.12.2": - version: 5.15.15 - resolution: "@mui/material@npm:5.15.15" + version: 5.15.20 + resolution: "@mui/material@npm:5.15.20" dependencies: "@babel/runtime": ^7.23.9 "@mui/base": 5.0.0-beta.40 - "@mui/core-downloads-tracker": ^5.15.15 - "@mui/system": ^5.15.15 + "@mui/core-downloads-tracker": ^5.15.20 + "@mui/system": ^5.15.20 "@mui/types": ^7.2.14 - "@mui/utils": ^5.15.14 + "@mui/utils": ^5.15.20 "@types/react-transition-group": ^4.4.10 clsx: ^2.1.0 csstype: ^3.1.3 @@ -10870,16 +11441,16 @@ __metadata: optional: true "@types/react": optional: true - checksum: ee0dc22fc4d617f7cf69f2451b6d5139978e6c5319e3056e7719159aff786ee3b80abd07691e230371811d9b5b574aef4559d7855bfe2f8493d596d960a91ab7 + checksum: 0d0e29682fa8f4ff03746efabb4b480da9dfe004e68982a36662ff0d40d76f5840ebf8ef3b0f9629056afb3030145dd5eb3bd2b0ab96d201fac7a300fef155b0 languageName: node linkType: hard -"@mui/private-theming@npm:^5.15.14": - version: 5.15.14 - resolution: "@mui/private-theming@npm:5.15.14" +"@mui/private-theming@npm:^5.15.20": + version: 5.15.20 + resolution: "@mui/private-theming@npm:5.15.20" dependencies: "@babel/runtime": ^7.23.9 - "@mui/utils": ^5.15.14 + "@mui/utils": ^5.15.20 prop-types: ^15.8.1 peerDependencies: "@types/react": ^17.0.0 || ^18.0.0 @@ -10887,7 +11458,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 1b1ef54e8281c9b13fcc58f4c39682efc610946a68402283c19fcfbce8a7d7a231d61b536d6df9bf7a59a1426591bd403a453a59eb8efb9689437fb58554dc8c + checksum: 5c70a9ebca3827cea11d67b3f8ed24d18a10c44f30f14d8d1c37b3f1e74d7a5e91cec5ffb7f24e8b2d974b38e4d046a93473c20a0cd7c7a880580e5df3d1afb7 languageName: node linkType: hard @@ -10913,14 +11484,14 @@ __metadata: linkType: hard "@mui/styles@npm:^5.14.18": - version: 5.15.15 - resolution: "@mui/styles@npm:5.15.15" + version: 5.15.20 + resolution: "@mui/styles@npm:5.15.20" dependencies: "@babel/runtime": ^7.23.9 "@emotion/hash": ^0.9.1 - "@mui/private-theming": ^5.15.14 + "@mui/private-theming": ^5.15.20 "@mui/types": ^7.2.14 - "@mui/utils": ^5.15.14 + "@mui/utils": ^5.15.20 clsx: ^2.1.0 csstype: ^3.1.3 hoist-non-react-statics: ^3.3.2 @@ -10939,19 +11510,19 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 5e436a8a22ae9a2e6fcb63ead3a6eff2092cc9731fce0b22d04ab9c67639a173a0e27fb617628e7720592862118a32248be458bedfabaf1de5448543a97b0307 + checksum: bdc19d66caf3fb3272dd1c67d6a40b0e77bca2c3c7a5b9112ebed2cf3d6795932401bcb9ddfb9daba9c3e293455cf9cb2c24975eff15c75a2ce3d40ed30d59d7 languageName: node linkType: hard -"@mui/system@npm:^5.15.15": - version: 5.15.15 - resolution: "@mui/system@npm:5.15.15" +"@mui/system@npm:^5.15.20": + version: 5.15.20 + resolution: "@mui/system@npm:5.15.20" dependencies: "@babel/runtime": ^7.23.9 - "@mui/private-theming": ^5.15.14 + "@mui/private-theming": ^5.15.20 "@mui/styled-engine": ^5.15.14 "@mui/types": ^7.2.14 - "@mui/utils": ^5.15.14 + "@mui/utils": ^5.15.20 clsx: ^2.1.0 csstype: ^3.1.3 prop-types: ^15.8.1 @@ -10967,7 +11538,7 @@ __metadata: optional: true "@types/react": optional: true - checksum: 9ca96d5f66b2a9d6471909cc98c671eea5ec0a6d58a7ec071073b9e5200b95c3f017f0ca5cc946abc7f83074bd11830ca18f5e30bc98e25cd6ca217bd1b3a26f + checksum: e9e79b6a905845aef87939de8ac7990d73a2f92bba02e41905dcb9f44e53727bdfa52a978cf59a2a2cfa73d2512df78c4351eb130d7c25af1880bb8b7ebfc686 languageName: node linkType: hard @@ -10983,9 +11554,9 @@ __metadata: languageName: node linkType: hard -"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.15.14": - version: 5.15.14 - resolution: "@mui/utils@npm:5.15.14" +"@mui/utils@npm:^5.14.15, @mui/utils@npm:^5.15.14, @mui/utils@npm:^5.15.20": + version: 5.15.20 + resolution: "@mui/utils@npm:5.15.20" dependencies: "@babel/runtime": ^7.23.9 "@types/prop-types": ^15.7.11 @@ -10997,7 +11568,7 @@ __metadata: peerDependenciesMeta: "@types/react": optional: true - checksum: 36543ba7e3b65fb3219ed27e8f1455aff15b47a74c9b642c63e60774e22baa6492a196079e72bcfa5a570421dab32160398f892110bd444428bcf8b266b11893 + checksum: 8f8c7f9129e0eb1bf55c1d675c647da1fa4acf1c7f50e0ce2ce750cc0603e62fdc21b76e8ef8ff9bf06bce054e9cf97210ed737f55887b0ef939bd6605b4e537 languageName: node linkType: hard @@ -11769,6 +12340,13 @@ __metadata: languageName: node linkType: hard +"@octokit/openapi-types@npm:^22.2.0": + version: 22.2.0 + resolution: "@octokit/openapi-types@npm:22.2.0" + checksum: eca41feac2b83298e0d95e253ac1c5b6d65155ac57f65c5fd8d4a485d9728922d85ff4bee0e815a1f3a5421311db092bdb6da9d6104a1b1843d8b274bcad9630 + languageName: node + linkType: hard + "@octokit/plugin-paginate-graphql@npm:^4.0.0": version: 4.0.0 resolution: "@octokit/plugin-paginate-graphql@npm:4.0.0" @@ -11778,6 +12356,17 @@ __metadata: languageName: node linkType: hard +"@octokit/plugin-paginate-rest@npm:11.3.1": + version: 11.3.1 + resolution: "@octokit/plugin-paginate-rest@npm:11.3.1" + dependencies: + "@octokit/types": ^13.5.0 + peerDependencies: + "@octokit/core": 5 + checksum: 42c7c08e7287b4b85d2ae47852d2ffeb238c134ad6bcff18bddc154b15f6bec31778816c0763181401c370198390db7f6b0c3c44750fdfeec459594f7f4b5933 + languageName: node + linkType: hard + "@octokit/plugin-paginate-rest@npm:^6.1.2": version: 6.1.2 resolution: "@octokit/plugin-paginate-rest@npm:6.1.2" @@ -11810,14 +12399,14 @@ __metadata: languageName: node linkType: hard -"@octokit/plugin-rest-endpoint-methods@npm:^10.0.0": - version: 10.2.0 - resolution: "@octokit/plugin-rest-endpoint-methods@npm:10.2.0" +"@octokit/plugin-rest-endpoint-methods@npm:13.2.2": + version: 13.2.2 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:13.2.2" dependencies: - "@octokit/types": ^12.3.0 + "@octokit/types": ^13.5.0 peerDependencies: - "@octokit/core": ">=5" - checksum: 3209688bf508d22a525fe32d632ff928b048688c1859c7e4bbb08bd181aa07f580b375a502e34368628103e5d5cccf7f9fb0ff0c8fd4262470ac8eeffb80ac6b + "@octokit/core": ^5 + checksum: 347b3a891a561ed1dcc307a2dce42ca48c318c465ad91a26225d3d6493aef1b7ff868e6c56a0d7aa4170d028c7429ca1ec52aed6be34615a6ed701c3bcafdb17 languageName: node linkType: hard @@ -11950,7 +12539,7 @@ __metadata: languageName: node linkType: hard -"@octokit/types@npm:^12.0.0, @octokit/types@npm:^12.2.0, @octokit/types@npm:^12.3.0, @octokit/types@npm:^12.4.0": +"@octokit/types@npm:^12.0.0, @octokit/types@npm:^12.2.0, @octokit/types@npm:^12.4.0": version: 12.4.0 resolution: "@octokit/types@npm:12.4.0" dependencies: @@ -11959,6 +12548,15 @@ __metadata: languageName: node linkType: hard +"@octokit/types@npm:^13.0.0, @octokit/types@npm:^13.5.0": + version: 13.5.0 + resolution: "@octokit/types@npm:13.5.0" + dependencies: + "@octokit/openapi-types": ^22.2.0 + checksum: 8e92f2b145b3c28a35312f93714245824a7b6b7353caa88edfdc85fc2ed4108321ed0c3988001ea53449fbb212febe0e8e9582744e85c3574dabe9d0441af5a0 + languageName: node + linkType: hard + "@octokit/types@npm:^6.0.3, @octokit/types@npm:^6.10.0, @octokit/types@npm:^6.12.2, @octokit/types@npm:^6.16.1": version: 6.41.0 resolution: "@octokit/types@npm:6.41.0" @@ -12146,7 +12744,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.0.1, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0": +"@opentelemetry/api@npm:^1.0.0, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0": version: 1.8.0 resolution: "@opentelemetry/api@npm:1.8.0" checksum: 0e32079975f05bee6de2ad8ade097f0afdc63f462c76550150fce2444c73ab92aaf851ac85e638b6e3b269da6640ac7e63f33913a0fd7df9f9beec2e100759df @@ -13399,26 +13997,26 @@ __metadata: linkType: hard "@playwright/test@npm:^1.32.3": - version: 1.44.1 - resolution: "@playwright/test@npm:1.44.1" + version: 1.45.3 + resolution: "@playwright/test@npm:1.45.3" dependencies: - playwright: 1.44.1 + playwright: 1.45.3 bin: playwright: cli.js - checksum: 90507b77e388aa984deb92db56f7bb3b305c6be441e1d0087ca046989cfdda068bbc26d75bd29c20ad3dbb2434e69a8ff0d6b30003b88c9234cd3aa6a9f7deb9 + checksum: 3e2c88d40f98cf94ab7947263804d1ee78c4bb21a35c8dbb64855eed5565ffc688509c5f07bda5438cba6c354374981448dcba3dbe326d1699b4fef75c9ce43d languageName: node linkType: hard "@pmmmwh/react-refresh-webpack-plugin@npm:^0.5.7": - version: 0.5.13 - resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.13" + version: 0.5.15 + resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.5.15" dependencies: - ansi-html-community: ^0.0.8 + ansi-html: ^0.0.9 core-js-pure: ^3.23.3 error-stack-parser: ^2.0.6 html-entities: ^2.1.0 loader-utils: ^2.0.4 - schema-utils: ^3.0.0 + schema-utils: ^4.2.0 source-map: ^0.7.3 peerDependencies: "@types/webpack": 4.x || 5.x @@ -13442,7 +14040,7 @@ __metadata: optional: true webpack-plugin-serve: optional: true - checksum: 9f931cf79945f58ee31569b83f4b294ae0849ea8232b6c79e690b46a3d7f2b981aa72718a4bd7517ab82657dddfed2a691c9d9e37295a87dfd0b18b2693d4aa6 + checksum: 82df6244146209d63a12f0ca2e70b05274ee058c7e6d6eb4ced1228afde3b039a7f3f3cc0c76f1bb4b28deadbcf08bc2821c814f0bfee06979128578300fff3d languageName: node linkType: hard @@ -14144,9 +14742,9 @@ __metadata: languageName: node linkType: hard -"@rjsf/core@npm:5.18.4": - version: 5.18.4 - resolution: "@rjsf/core@npm:5.18.4" +"@rjsf/core@npm:5.18.5": + version: 5.18.5 + resolution: "@rjsf/core@npm:5.18.5" dependencies: lodash: ^4.17.21 lodash-es: ^4.17.21 @@ -14156,26 +14754,26 @@ __metadata: peerDependencies: "@rjsf/utils": ^5.18.x react: ^16.14.0 || >=17 - checksum: 8c3f49914be396595ce67dc4c36ac25c5cb6673917ec82c47f79321f5bb78d02741e8dca39287d0435270e7c9ccb06f7d40e396bdf71a3e9eb1371ef16954817 + checksum: 7b986ec075d5b2de503d01172abdb3e3d33d04e5298fbf80842400e682f0aecdabfe4bb8482dde8a6aed90a35f4f5574cb7b24a11d689566c0cc0599485b334a languageName: node linkType: hard -"@rjsf/material-ui@npm:5.18.4": - version: 5.18.4 - resolution: "@rjsf/material-ui@npm:5.18.4" +"@rjsf/material-ui@npm:5.18.5": + version: 5.18.5 + resolution: "@rjsf/material-ui@npm:5.18.5" peerDependencies: "@material-ui/core": ^4.12.3 "@material-ui/icons": ^4.11.2 "@rjsf/core": ^5.18.x "@rjsf/utils": ^5.18.x react: ^16.14.0 || >=17 - checksum: 2bb0eeb0603bd11b034785ac1902cfcf3630cb4d04161728adff86b2629ca4d21bbb42beb9e6acdd4c9a6a4d84cce1ca325413a9bd1fd94eda47f58d62eb3164 + checksum: 61337141bcd7bcb46013d81ae6b618919eff3f55319800511443e524876bf5c84089970e14148716de203d5def341aa15c4d102243f0b694f120f05189a3adb7 languageName: node linkType: hard -"@rjsf/utils@npm:5.18.4": - version: 5.18.4 - resolution: "@rjsf/utils@npm:5.18.4" +"@rjsf/utils@npm:5.18.5": + version: 5.18.5 + resolution: "@rjsf/utils@npm:5.18.5" dependencies: json-schema-merge-allof: ^0.8.1 jsonpointer: ^5.0.1 @@ -14184,13 +14782,13 @@ __metadata: react-is: ^18.2.0 peerDependencies: react: ^16.14.0 || >=17 - checksum: d7cf514527ec50a94751c5ec1f9e5eafd89d0c56441a22ae28a4e667aaa7c60447e1e1ccf8355c5be5b97e9a1163853c116816b13307e3463433d50f6b89bb3e + checksum: 8da3ce82a0ec83e78806461db7226fd20489f6e8c41906ccde9d55c554aec3f50b8b8bac8c35e3cdf12d788c18fdad71c992f0d8b44f6161a1bf9e13598ded99 languageName: node linkType: hard -"@rjsf/validator-ajv8@npm:5.18.4": - version: 5.18.4 - resolution: "@rjsf/validator-ajv8@npm:5.18.4" +"@rjsf/validator-ajv8@npm:5.18.5": + version: 5.18.5 + resolution: "@rjsf/validator-ajv8@npm:5.18.5" dependencies: ajv: ^8.12.0 ajv-formats: ^2.1.1 @@ -14198,7 +14796,7 @@ __metadata: lodash-es: ^4.17.21 peerDependencies: "@rjsf/utils": ^5.18.x - checksum: edf3b24e07babe972e57645d396467c6404ff3f95a79118667f2d62082aa28e4ee7df5fad81e28071cd24b5455fe687a9f9bcf714718d4fd6036912b04c69a84 + checksum: 350d1f8043b7bbd88cf5d7fa28560062f4141e1ef9ef004c2997b63d0281d4a1af04be4b951c2c9c4d1c56d10f0e2fbab665c7176b38e87dd6da6d5303e1c84e languageName: node linkType: hard @@ -14666,177 +15264,187 @@ __metadata: languageName: node linkType: hard -"@smithy/chunked-blob-reader-native@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/chunked-blob-reader-native@npm:2.2.0" +"@smithy/abort-controller@npm:^3.1.1": + version: 3.1.1 + resolution: "@smithy/abort-controller@npm:3.1.1" dependencies: - "@smithy/util-base64": ^2.3.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: ac619f18844e8a8288672c40b8967a82b78f5398119638b3e4fcadf451a3356139307c2d9f24c8c041530238f1ce6e0f90ce82adfcb050d08afefa2f0541c2d0 + checksum: 7b7497f49d58787cad858f8c5ea9931ccd44d39536db4abdd531a5abf37784469522e41d9ad1d541892caa0ed3bea750447809a0a18f4689a9543d672aa61d48 languageName: node linkType: hard -"@smithy/chunked-blob-reader@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/chunked-blob-reader@npm:2.2.0" +"@smithy/chunked-blob-reader-native@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/chunked-blob-reader-native@npm:3.0.0" dependencies: + "@smithy/util-base64": ^3.0.0 tslib: ^2.6.2 - checksum: f5acb1e812f97d7c233ccf955557ac10c7e94c8c9610d2fad715d1010fe30ee686a93a5d6e589ce8ae4eb7cf201d5eab61cee5e8646bbebdfa8a5f23693d7a5a + checksum: f97c0c0ce5e9bd2350883df3c232311aa82eb87eb387125f685900326f86fc3aca208e9004291f742f6978abf91a0c1112cc9a803cd0caf0dffbcfa9b6d0239e languageName: node linkType: hard -"@smithy/config-resolver@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/config-resolver@npm:2.2.0" +"@smithy/chunked-blob-reader@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/chunked-blob-reader@npm:3.0.0" dependencies: - "@smithy/node-config-provider": ^2.3.0 - "@smithy/types": ^2.12.0 - "@smithy/util-config-provider": ^2.3.0 - "@smithy/util-middleware": ^2.2.0 tslib: ^2.6.2 - checksum: dcb15d40faf46c370cd83dfbf1e632fae29c64c500b33b53850a520cfb02c9fa6f7e239c07824793b47645462567d51cb1554c02f9ec4531bd51bc759aede2ed + checksum: 6f520884ade14f1073adb640db2f03eb22a9920f342f37958df3e98327890b741cd909b16cbbc6f70c6c8dd250d6b3a8d76841b685d4871b0403f309267def4f languageName: node linkType: hard -"@smithy/core@npm:^1.4.2": - version: 1.4.2 - resolution: "@smithy/core@npm:1.4.2" +"@smithy/config-resolver@npm:^3.0.5": + version: 3.0.5 + resolution: "@smithy/config-resolver@npm:3.0.5" dependencies: - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-retry": ^2.3.1 - "@smithy/middleware-serde": ^2.3.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/util-middleware": ^2.2.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/types": ^3.3.0 + "@smithy/util-config-provider": ^3.0.0 + "@smithy/util-middleware": ^3.0.3 tslib: ^2.6.2 - checksum: 414ec1c392ab5346f2b833f310078d7e850df8b9e5db6fedbce65116146c2fda116d56db841401ba05b5e7399a5f5c426870d324bf6fd060143ce66e2f3eafbb + checksum: 96895ae0622a229655fa08f009d29a20157043020125014e84cb5ca33a10171c9724c309491214c2422d9c4c6681e7f5ec5f7faa8f45e11250449cf07f3552ec languageName: node linkType: hard -"@smithy/credential-provider-imds@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/credential-provider-imds@npm:2.3.0" +"@smithy/core@npm:^2.3.1": + version: 2.3.1 + resolution: "@smithy/core@npm:2.3.1" dependencies: - "@smithy/node-config-provider": ^2.3.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-retry": ^3.0.13 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/protocol-http": ^4.1.0 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/util-middleware": ^3.0.3 tslib: ^2.6.2 - checksum: dd57e09e60bd51ed103f7a5363a43e1373470ea3cee04ace66f5bbaafab005355ffbfa3e137e2ecac34aa28911fb5b6ecac60845846c6a4a5432f3e57a74b837 + checksum: f191a36742a08c6898f4aaa43335bfb96cdca9a9099c0608fb0ef151917816b03ee1b27ef3720d89fba90d2d24fedfb47478f837d47c1c2e5ff8ceeb844ec7d6 languageName: node linkType: hard -"@smithy/eventstream-codec@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/eventstream-codec@npm:2.2.0" +"@smithy/credential-provider-imds@npm:^3.2.0": + version: 3.2.0 + resolution: "@smithy/credential-provider-imds@npm:3.2.0" dependencies: - "@aws-crypto/crc32": 3.0.0 - "@smithy/types": ^2.12.0 - "@smithy/util-hex-encoding": ^2.2.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/property-provider": ^3.1.3 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 tslib: ^2.6.2 - checksum: ae59067964e19c6728b1be74a6e19793e4d3decdcbcea546bd40f77c3cc1eacc48c30272ef68927ba477c2b6450d023474f2dec516dfd93e204150ba18cab697 + checksum: fc79919133008db91a83f2caf6eba11d704f34af5fa3dd1f7b8cc048214e805d9f20a3f302f5ed4862ee6b6c3bb28ff3a5d8c77d2f497d10f3be14915e59debe languageName: node linkType: hard -"@smithy/eventstream-serde-browser@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/eventstream-serde-browser@npm:2.2.0" +"@smithy/eventstream-codec@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/eventstream-codec@npm:3.1.2" dependencies: - "@smithy/eventstream-serde-universal": ^2.2.0 - "@smithy/types": ^2.12.0 + "@aws-crypto/crc32": 5.2.0 + "@smithy/types": ^3.3.0 + "@smithy/util-hex-encoding": ^3.0.0 tslib: ^2.6.2 - checksum: c00bd592365f42ddafcad83f06d3c85ce8ee21bd806de903043ef132de9acca8bf1592ed811b11daba1742332928fc73a66c9032b06df2f6526da0339918f8d5 + checksum: b0c836acbf59b57a7e2ef948a54bd441d11b75d70f1c334723c27fce1ab0ff93ea9f936976b754272b5e90413b5a169c60b1df7ecfd7d061ebaae8d5cc067d94 languageName: node linkType: hard -"@smithy/eventstream-serde-config-resolver@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/eventstream-serde-config-resolver@npm:2.2.0" +"@smithy/eventstream-serde-browser@npm:^3.0.5": + version: 3.0.5 + resolution: "@smithy/eventstream-serde-browser@npm:3.0.5" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/eventstream-serde-universal": ^3.0.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: a35dbcbc14ad1825ce22a9e7daac93067d8ade6173a3ce33b819eed61390f8d93ea63b70945f6d1bced175fad58def3d09a14ee3043c63a798ecef407b2d1701 + checksum: 14e8a2027745e7a1ad261068e792e4a660043ce53fefc5f564b38b841ba02d40992b38fbd2357e762f0a1ecb658df3bbf23cf5ef33c3ec2488d316be95b61b9e languageName: node linkType: hard -"@smithy/eventstream-serde-node@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/eventstream-serde-node@npm:2.2.0" +"@smithy/eventstream-serde-config-resolver@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/eventstream-serde-config-resolver@npm:3.0.3" dependencies: - "@smithy/eventstream-serde-universal": ^2.2.0 - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 1d4971b99654c4672716608a63e668ccefd78cc1806c0ea4df5c3cc0ca0208b7647f7914d2c77a37d0a29b31b66cff660ce2ab2f46f56d997c9a58ea6b6241b2 + checksum: c61780aa0ad8c479618d0b3fcb2b42f1f9a74dcf814dba08305107ed1f088f56aa1c346db9c72439ff18617f31b9c59c6895060e4c9765c81d759150a22674af languageName: node linkType: hard -"@smithy/eventstream-serde-universal@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/eventstream-serde-universal@npm:2.2.0" +"@smithy/eventstream-serde-node@npm:^3.0.4": + version: 3.0.4 + resolution: "@smithy/eventstream-serde-node@npm:3.0.4" dependencies: - "@smithy/eventstream-codec": ^2.2.0 - "@smithy/types": ^2.12.0 + "@smithy/eventstream-serde-universal": ^3.0.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: c28038c2f57deed7b5e0e5f8ab8150d4a7947f2971241da96ef1d53b45d83dfa661717065f059099c420ee66ae2455818ae124bb8601b609558040d4a7509227 + checksum: 0a75b184d95ab8c08efd93bf32c5fd9d735b5879df556599bd2ab78f23e3f77452e597bbdd42586c9bbedcc2b0b7683de4c816db739c19a2ebd62a34096ca86d languageName: node linkType: hard -"@smithy/fetch-http-handler@npm:^2.5.0": - version: 2.5.0 - resolution: "@smithy/fetch-http-handler@npm:2.5.0" +"@smithy/eventstream-serde-universal@npm:^3.0.4": + version: 3.0.4 + resolution: "@smithy/eventstream-serde-universal@npm:3.0.4" dependencies: - "@smithy/protocol-http": ^3.3.0 - "@smithy/querystring-builder": ^2.2.0 - "@smithy/types": ^2.12.0 - "@smithy/util-base64": ^2.3.0 + "@smithy/eventstream-codec": ^3.1.2 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 91a58ac32c6b4afc6d7fb2b9ac3e3b817171f76e09b013a6506308b044455054444a92e1acbd8f98bdd159b15fdd44b1e3fb52c21cbb2e69be8e3698d2206021 + checksum: 8463403ca4caf4ad48dba89b126f394439a289c9095ce6361c1f186c6021c1cd8ea402d1ce06b7284069c3415091ae4d802f66ded1b89e9da9d4c255b8402668 languageName: node linkType: hard -"@smithy/hash-blob-browser@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/hash-blob-browser@npm:2.2.0" +"@smithy/fetch-http-handler@npm:^3.2.4": + version: 3.2.4 + resolution: "@smithy/fetch-http-handler@npm:3.2.4" dependencies: - "@smithy/chunked-blob-reader": ^2.2.0 - "@smithy/chunked-blob-reader-native": ^2.2.0 - "@smithy/types": ^2.12.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/querystring-builder": ^3.0.3 + "@smithy/types": ^3.3.0 + "@smithy/util-base64": ^3.0.0 tslib: ^2.6.2 - checksum: 1b748b4449ccee723c8b47a412491283fa7b5a2a6c27b0b73e03d905c2af70b56b74d63a658d8ef0bd330cc4617bc11431c86e24a4932b4722aad08e1b25e576 + checksum: 73df885c637c14353f449678a4e109aeb19945c5370a615793ca2a54a29746a78e725e324b01cfd86fc71f4afd6386da2758fccc49d247a623ecbe70f607cb74 languageName: node linkType: hard -"@smithy/hash-node@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/hash-node@npm:2.2.0" +"@smithy/hash-blob-browser@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/hash-blob-browser@npm:3.1.2" dependencies: - "@smithy/types": ^2.12.0 - "@smithy/util-buffer-from": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@smithy/chunked-blob-reader": ^3.0.0 + "@smithy/chunked-blob-reader-native": ^3.0.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 3305b5778fa99558375b16629ad98fd00a1fb33ea905037977b0a7c93d92c8de1481756ef7dbc004e45210b23f983dec04bcd13d43c98f36a5f47291cbed9d89 + checksum: 959ec975cd4b3d86e3d0288e24b460343795bc305ef38fc43f8485cd1440da4068d375c5d1dab73ae875f02e861f194512a7adf5afcd7395bbeb97897d8a809b languageName: node linkType: hard -"@smithy/hash-stream-node@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/hash-stream-node@npm:2.2.0" +"@smithy/hash-node@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/hash-node@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 - "@smithy/util-utf8": ^2.3.0 + "@smithy/types": ^3.3.0 + "@smithy/util-buffer-from": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 191d76fd1df705c32d24463794f8b8b391061c7ca7265591cd4f070259fa80395c2f115fd3d37f6bb3a4a2303b3811a31509ea767d0c3d0a9644789ae8283118 + checksum: 203a3581bec5373e63d42e03f62129022f03d17390e9358a4e25fc1d44c43962ea80ab5bcbb91605e3025e22136bed059665a3b16835f66316f43ed391df9548 languageName: node linkType: hard -"@smithy/invalid-dependency@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/invalid-dependency@npm:2.2.0" +"@smithy/hash-stream-node@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/hash-stream-node@npm:3.1.2" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: ed17980ccdf4c564cfcb517f3959dfeb7c7dbddd76eaf2c9e10031ebd19e78e56609df3377626215e51a6c4b98db03cfa88ad46f15ba26bb55c34351f3182a98 + checksum: e5284ef06548e301aa50bd06fe06bf3e2ed11ecd57f73d2d85c98cf26119c2cc0084b5b8be49d4127cb798c6011651d5361958eb6546c19b45fd6c94ea11ef47 + languageName: node + linkType: hard + +"@smithy/invalid-dependency@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/invalid-dependency@npm:3.0.3" + dependencies: + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: 459b4ae4e47595e8a675ff2e8bfea7f58a41f77138416ea310c89e29312e08963a701cdc354324da9dd578a7995158b4421695365070d74b0276ddff7f701bba languageName: node linkType: hard @@ -14849,93 +15457,102 @@ __metadata: languageName: node linkType: hard -"@smithy/md5-js@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/md5-js@npm:2.2.0" +"@smithy/is-array-buffer@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/is-array-buffer@npm:3.0.0" dependencies: - "@smithy/types": ^2.12.0 - "@smithy/util-utf8": ^2.3.0 tslib: ^2.6.2 - checksum: ae343c198a8d8c6689bcb1d7f766e29578d370e8d79180db9b6183b5c74ac091829e8abe3053df0589f53324c01a79c7f9e889e5cd92094e3b5c4be96fb7b970 + checksum: ce7440fcb1ce3c46722cff11c33e2f62a9df86d74fa2054a8e6b540302a91211cf6e4e3b1b7aac7030c6c8909158c1b6867c394201fa8afc6b631979956610e5 languageName: node linkType: hard -"@smithy/middleware-content-length@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/middleware-content-length@npm:2.2.0" +"@smithy/md5-js@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/md5-js@npm:3.0.3" dependencies: - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 1eae8d2b6f432ce9a849e741d4f2426baee8a51f22a5262c11802e125078ee33d9d8f4183fb142043ba9d1371adad9c835c784333a394d865fb248339f7482e6 + checksum: 52ef56439be4187cc65391f4252173ffad0ce5a2ce5f636d78e9cdfb517844889340156ddbdbbe86f63e7f7e0fc924fe6905749a1c833910784015133a467406 languageName: node linkType: hard -"@smithy/middleware-endpoint@npm:^2.5.1": - version: 2.5.1 - resolution: "@smithy/middleware-endpoint@npm:2.5.1" +"@smithy/middleware-content-length@npm:^3.0.5": + version: 3.0.5 + resolution: "@smithy/middleware-content-length@npm:3.0.5" dependencies: - "@smithy/middleware-serde": ^2.3.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 - "@smithy/url-parser": ^2.2.0 - "@smithy/util-middleware": ^2.2.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 7ac2a35a6f52c33d868fc4b73330ae34fecfc43c59b8d501ee9fb81924c6747494700b55b3025b83fe7bea3d4e323c8853ec5b117c17cccf06cc27bbc4f492b2 + checksum: 21c667530f5e64db300827dfe8aa5b18396d151ac489a3e634f882179fae5c0f84940e1f831f0bf7b4b6b9623283f4a516da92d89c13ba395aede8788b523cd3 languageName: node linkType: hard -"@smithy/middleware-retry@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/middleware-retry@npm:2.3.1" +"@smithy/middleware-endpoint@npm:^3.1.0": + version: 3.1.0 + resolution: "@smithy/middleware-endpoint@npm:3.1.0" dependencies: - "@smithy/node-config-provider": ^2.3.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/service-error-classification": ^2.1.5 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-retry": ^2.2.0 + "@smithy/middleware-serde": ^3.0.3 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 + "@smithy/url-parser": ^3.0.3 + "@smithy/util-middleware": ^3.0.3 + tslib: ^2.6.2 + checksum: 3271b7c1ec5d01db63439757d198e921618a2f1d4827c64fc425163e7d1bc969049d8c8ce74f3a8c0b0dac5ea3466c154f38805c8d62fe19fb9bd6af72ca2d3c + languageName: node + linkType: hard + +"@smithy/middleware-retry@npm:^3.0.13": + version: 3.0.13 + resolution: "@smithy/middleware-retry@npm:3.0.13" + dependencies: + "@smithy/node-config-provider": ^3.1.4 + "@smithy/protocol-http": ^4.1.0 + "@smithy/service-error-classification": ^3.0.3 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-retry": ^3.0.3 tslib: ^2.6.2 uuid: ^9.0.1 - checksum: 5eebf9d26fccc6c8c517924463e93d244edebe52d120b28d5d705904f83773da1734296b8d57b4f30a15cbf36690e508f5946dfd56749cbcda501d08cb778933 + checksum: 3aa98ae08633022dedc19a60b173289d4c8e04c94fe45a626499269f596de517a94b587272a578d554d1e91dc8cb19297e3d3c45da01829d520a6c730e77df36 languageName: node linkType: hard -"@smithy/middleware-serde@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/middleware-serde@npm:2.3.0" +"@smithy/middleware-serde@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/middleware-serde@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 5393370c0f8a820d8ca36eccecff5b6434c4f81fbaad8800088fb4c8dad5312bf3eb47f67533784de959807bbb3379c23d81a1bcbaf8824254034dd2b83fd76b + checksum: 6c633bb8957e078d480888bd33d5a8c269a483a1358c2b28c62daecfd442c711c509d9e69302e6b19fc298139ee67cdda63a604e7da0e4ef9005117d8e0897cc languageName: node linkType: hard -"@smithy/middleware-stack@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/middleware-stack@npm:2.2.0" +"@smithy/middleware-stack@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/middleware-stack@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 293d76764e327a5ada4ea7de268f451e62a6a56983ba7dcdbc63fdbb0427c01071a9a81d7807b16586977df829ce5d9587facbd9367b089841bbc9fc329ce6af + checksum: f4a450e2ebca0a8a3b4e1bbfad7d7e9c45edccbe1c984a22f2228092a526120748365e8964b478357249675d8bbc28fdaa8a4a19643a3c1d86bd74e1499327c5 languageName: node linkType: hard -"@smithy/node-config-provider@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/node-config-provider@npm:2.3.0" +"@smithy/node-config-provider@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/node-config-provider@npm:3.1.4" dependencies: - "@smithy/property-provider": ^2.2.0 - "@smithy/shared-ini-file-loader": ^2.4.0 - "@smithy/types": ^2.12.0 + "@smithy/property-provider": ^3.1.3 + "@smithy/shared-ini-file-loader": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 9c1dc6d97e0379d947498e7d64e593ea183d5f2c89dace4561c1c613850bf264581b597105c15d64ceabdea954e57ad8e6bf9e42642ddc3f737464f350ffbb5b + checksum: 7ea4e7cea93ab154ab89a9d6b2453c8f96b96db18883070d287bc5fa9cfd10091bb00006a15bb7e6ed25810fd1a133d458e45310a8eaa1727a55d4ce2be3ba09 languageName: node linkType: hard -"@smithy/node-http-handler@npm:^2.1.7, @smithy/node-http-handler@npm:^2.5.0": +"@smithy/node-http-handler@npm:^2.1.7": version: 2.5.0 resolution: "@smithy/node-http-handler@npm:2.5.0" dependencies: @@ -14948,13 +15565,26 @@ __metadata: languageName: node linkType: hard -"@smithy/property-provider@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/property-provider@npm:2.2.0" +"@smithy/node-http-handler@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/node-http-handler@npm:3.1.4" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/abort-controller": ^3.1.1 + "@smithy/protocol-http": ^4.1.0 + "@smithy/querystring-builder": ^3.0.3 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 8d257cbc5222baf6706e288c3b51196588f135878141f8af76fcb3f0abafc027ed46cf4bb938266d1906111175082ee85f73806d5a2b1c929aee16ec8b5283e6 + checksum: 8f2f611bef99800b122b852103c3d1ff391b91077df191ca06676bd8c4e50d7e335bf2a311f90ff6a9fa0639a812216492e5e0ecee703e89eb05a4b253c8273c + languageName: node + linkType: hard + +"@smithy/property-provider@npm:^3.1.3": + version: 3.1.3 + resolution: "@smithy/property-provider@npm:3.1.3" + dependencies: + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: 37a3d92267a2a32c2cc17fd1f0ab2b336f75fb7807db88f6194efede9d6a66068658a7effb7773451404fca990924393dbbf3d57e2aca67ef2e489a85666e225 languageName: node linkType: hard @@ -14968,6 +15598,16 @@ __metadata: languageName: node linkType: hard +"@smithy/protocol-http@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/protocol-http@npm:4.1.0" + dependencies: + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: fe4f97bc35075c6e6669c12ff90a4ab3dbe620b375298795d0c1104b30d04536cf002ea81f29983895c6042c7a30eecd1d2306d3ac90bf7910ec02929233f5ad + languageName: node + linkType: hard + "@smithy/querystring-builder@npm:^2.2.0": version: 2.2.0 resolution: "@smithy/querystring-builder@npm:2.2.0" @@ -14979,61 +15619,73 @@ __metadata: languageName: node linkType: hard -"@smithy/querystring-parser@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/querystring-parser@npm:2.2.0" +"@smithy/querystring-builder@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/querystring-builder@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 + "@smithy/util-uri-escape": ^3.0.0 tslib: ^2.6.2 - checksum: 9b27751c329fecc84bdfe7f128ab766c7e5f1d4bdda6184699a0df8999e95aef21fafc6179d6c693e519c78874e738fd9afb5ac4679901cb68d092a86a612419 + checksum: 5c46c620d87f9b4e67b8eb543667b0160fb05bbec01d62d45adb94305369dca9e82daba47d81e840fdc399fa47f9b5930ce668d65fe83ee278a1b27d59d0b5d3 languageName: node linkType: hard -"@smithy/service-error-classification@npm:^2.1.5": - version: 2.1.5 - resolution: "@smithy/service-error-classification@npm:2.1.5" +"@smithy/querystring-parser@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/querystring-parser@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 - checksum: 00ac54110a258c7a47c62d4f655d4998bd40e5adb47e10281b28df7a585f2f1e960dc35325eac006636280e7fb2b81dbeb32b89e08bac87acc136c4d29a4dc53 + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: 1de11cbc4325578b243a0e3e89b46371f4705d3df41ea51b37e8efa655d3b75253180b0fca9ceed8b3955a2d458689f551cd24fd904d0f65647c62c6b08795bf languageName: node linkType: hard -"@smithy/shared-ini-file-loader@npm:^2.4.0": - version: 2.4.0 - resolution: "@smithy/shared-ini-file-loader@npm:2.4.0" +"@smithy/service-error-classification@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/service-error-classification@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 - tslib: ^2.6.2 - checksum: b0c9e045bfe2150e07f4b31ae7d69d3646679337df9fec1e1201b845cc64ea2250c37db8e8d0e7573fc3c11188164adba43bbaf32275fa8a9f70e8bbc77146bf + "@smithy/types": ^3.3.0 + checksum: 5bef710f5698c929c97865cba41f36b0c59100b9a1c4478a2d47caeb5e3a1a18077b870b365efaa45c94666f2075bc8978f7a6e8b964afbba3a4e490eb6c13eb languageName: node linkType: hard -"@smithy/signature-v4@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/signature-v4@npm:2.3.0" +"@smithy/shared-ini-file-loader@npm:^3.1.4": + version: 3.1.4 + resolution: "@smithy/shared-ini-file-loader@npm:3.1.4" dependencies: - "@smithy/is-array-buffer": ^2.2.0 - "@smithy/types": ^2.12.0 - "@smithy/util-hex-encoding": ^2.2.0 - "@smithy/util-middleware": ^2.2.0 - "@smithy/util-uri-escape": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 96050956b86876d0137af9b003d0a30005766bffc730495d7c106bd2eb05c8ada2da23ceac51d56e04f98b304e0ea55d698e1a10c99cda3ade44b3ac30166a00 + checksum: c5321635f3be34e424009fc9045454a9ceec543ec20b3b9719bf3a48bbfc03b794f4545546e9c2dcb0a987de2ca5ff8999df9bf7c166c6fc7685c1fa1f068bc1 languageName: node linkType: hard -"@smithy/smithy-client@npm:^2.5.1": - version: 2.5.1 - resolution: "@smithy/smithy-client@npm:2.5.1" +"@smithy/signature-v4@npm:^4.1.0": + version: 4.1.0 + resolution: "@smithy/signature-v4@npm:4.1.0" dependencies: - "@smithy/middleware-endpoint": ^2.5.1 - "@smithy/middleware-stack": ^2.2.0 - "@smithy/protocol-http": ^3.3.0 - "@smithy/types": ^2.12.0 - "@smithy/util-stream": ^2.2.0 + "@smithy/is-array-buffer": ^3.0.0 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 + "@smithy/util-hex-encoding": ^3.0.0 + "@smithy/util-middleware": ^3.0.3 + "@smithy/util-uri-escape": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: 10d51793aab8f6e0ba0890a2a101216ffc3a1c43664f8ed9688dcbc174ca0f83f2d1c8d7af484c84491174067af3d6b235b765a58b12f2308a6bfe42b1d74f59 + checksum: 8c58bbc5b3f9eed092351f36dc0193ed2e43f916856dc95eadc65b42460a0c5f662016156034ef8a25419f95be8f6a15a7e85469358b77db236445919f66c77e + languageName: node + linkType: hard + +"@smithy/smithy-client@npm:^3.1.11": + version: 3.1.11 + resolution: "@smithy/smithy-client@npm:3.1.11" + dependencies: + "@smithy/middleware-endpoint": ^3.1.0 + "@smithy/middleware-stack": ^3.0.3 + "@smithy/protocol-http": ^4.1.0 + "@smithy/types": ^3.3.0 + "@smithy/util-stream": ^3.1.3 + tslib: ^2.6.2 + checksum: acbbd29f45d342845eebf9086fd37cbf5abc563c60f37f787be92e4922bf09594cd882a6f9dcec85073120580c030ad14a035fd10c120733ca402938d87c143b languageName: node linkType: hard @@ -15055,43 +15707,52 @@ __metadata: languageName: node linkType: hard -"@smithy/url-parser@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/url-parser@npm:2.2.0" +"@smithy/types@npm:^3.3.0": + version: 3.3.0 + resolution: "@smithy/types@npm:3.3.0" dependencies: - "@smithy/querystring-parser": ^2.2.0 - "@smithy/types": ^2.12.0 tslib: ^2.6.2 - checksum: f21f1e44bc2a4634220465990651f5ee0708cb6759b3685b8a8c00cc2cd64bbbc7807f66cd79ec6e654f7245867d4fb4ced406ad5c14612ebc47eae3f34e63c5 + checksum: 29bb5f83c41e32f8d4094a2aba2d3dfbd763ab5943784a700f3fa22df0dcf0ccac1b1907f7a87fbb9f6f2269fcd4750524bcb48f892249e200ffe397c0981309 languageName: node linkType: hard -"@smithy/util-base64@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/util-base64@npm:2.3.0" +"@smithy/url-parser@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/url-parser@npm:3.0.3" dependencies: - "@smithy/util-buffer-from": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@smithy/querystring-parser": ^3.0.3 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 2ce995c5d12037e9518bb2732f24090bc493d48118dfd6519faa41e19cd91863895bc0b5958b790d2cdeb919a8c410790dcffa3a452d560f0eeab73dc0c92cbd + checksum: 86b4bc8e6c176b56076c30233ca4cfeb98d162fe27a348ddfda5f163ce7d173b8e684aa26202bbf4e0b5695b0ad43c0cb40170ca6793652d0ea6edb00443c036 languageName: node linkType: hard -"@smithy/util-body-length-browser@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-body-length-browser@npm:2.2.0" +"@smithy/util-base64@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-base64@npm:3.0.0" dependencies: + "@smithy/util-buffer-from": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: e9c1d16b3b95d529011476e6154eaf282d3a983204b29dcf1e7ef04a9f5c2deae30167e06190f315771c813c768f19f486d3139fe9fcaf34d12c2333350f3412 + checksum: 413f26046a7e98b2661a078f218a8d040c820fc5a02f5e364aff58c3957e28fde1ac4048c2ebbad5d87b9da4b9aa98a8d4a7fb0d2ce97def33738bd7d8d79aa0 languageName: node linkType: hard -"@smithy/util-body-length-node@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/util-body-length-node@npm:2.3.0" +"@smithy/util-body-length-browser@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-body-length-browser@npm:3.0.0" dependencies: tslib: ^2.6.2 - checksum: 5d5c31b071e0b3222dcfe863ea2d179253f0dfaa30d03f40ebfa352ed292e00a451053cc523e27527e61094d5ed475069d2287ef19a857c6da0364ca71cfdf3c + checksum: b01d8258b9a25b262734fc49cefefe48583ba193c3eefd49a6f7fd5922c3015d23dda88b52f3dd9a16827cad16b5b9425eef01e91bd0c71bb5abc469d2952c07 + languageName: node + linkType: hard + +"@smithy/util-body-length-node@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-body-length-node@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: da1baf4790609d3dc28c88385c7274fdf9b91a641fe3c5af22b78e18156df17bd470181348f43b2c739680936b1dafb1526158dfd817c3d9ecb71e653b4cbe3f languageName: node linkType: hard @@ -15105,97 +15766,107 @@ __metadata: languageName: node linkType: hard -"@smithy/util-config-provider@npm:^2.3.0": - version: 2.3.0 - resolution: "@smithy/util-config-provider@npm:2.3.0" +"@smithy/util-buffer-from@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-buffer-from@npm:3.0.0" dependencies: + "@smithy/is-array-buffer": ^3.0.0 tslib: ^2.6.2 - checksum: 0f3f113c2658bd5a79f98dc28d53ca9c0adf8ec3c8c86c7dd91d2cd37149b4cf83d85cc89d5fe67ffe5cd319ec85f139ef229844eb039017193b307a4c315399 + checksum: 1bfc4ab093fe98132bbc1ccd36a0b9ad75a31ed26bac4b7e9350205513a2481eb190ae44679ab4fecc5e10d367b5e6592bbfbf792671579d17d17bd7f7f233f5 languageName: node linkType: hard -"@smithy/util-defaults-mode-browser@npm:^2.2.1": - version: 2.2.1 - resolution: "@smithy/util-defaults-mode-browser@npm:2.2.1" +"@smithy/util-config-provider@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-config-provider@npm:3.0.0" dependencies: - "@smithy/property-provider": ^2.2.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 + tslib: ^2.6.2 + checksum: fc0f5f57d30261cf3a6693d8e338b9d269332c478ee18d905309a769844188190caf0564855d7e84f6c61e56aa556195dda89f65e8c30791951cf4999e4a70e7 + languageName: node + linkType: hard + +"@smithy/util-defaults-mode-browser@npm:^3.0.13": + version: 3.0.13 + resolution: "@smithy/util-defaults-mode-browser@npm:3.0.13" + dependencies: + "@smithy/property-provider": ^3.1.3 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 bowser: ^2.11.0 tslib: ^2.6.2 - checksum: 286337d9165181e1df3d28d348210e88f20662ec63a9d6c1bcfce3342003de130a218dab42ce64aa4399ef345e2528414f73f399f8f4c8e9a6fcbc8e48250f98 + checksum: 087c583d0b276df7369053b068217821c4474bf3d9de3111110ef81e8d895539f80b53a11ecaaae105ce2d24a78a52d958840b8a6c6d3e18696c45f27015a3f3 languageName: node linkType: hard -"@smithy/util-defaults-mode-node@npm:^2.3.1": - version: 2.3.1 - resolution: "@smithy/util-defaults-mode-node@npm:2.3.1" +"@smithy/util-defaults-mode-node@npm:^3.0.13": + version: 3.0.13 + resolution: "@smithy/util-defaults-mode-node@npm:3.0.13" dependencies: - "@smithy/config-resolver": ^2.2.0 - "@smithy/credential-provider-imds": ^2.3.0 - "@smithy/node-config-provider": ^2.3.0 - "@smithy/property-provider": ^2.2.0 - "@smithy/smithy-client": ^2.5.1 - "@smithy/types": ^2.12.0 + "@smithy/config-resolver": ^3.0.5 + "@smithy/credential-provider-imds": ^3.2.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/property-provider": ^3.1.3 + "@smithy/smithy-client": ^3.1.11 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 7fb9d0ac8b5919955399284c1801f49a1f5275f6cf240894ba0a91a8825248bb167938647a983d89905d6bfa7b226789781e85d2ff7f27c58cdd32c2e68600ae + checksum: a56c9f06472bb42e4180fd424ec237a6ca170eef5d487d1a3806a8845cd9c0bef37b431b22a9340afee5ae26b6bca6c11647112ef73433ba949f8c671e2b2067 languageName: node linkType: hard -"@smithy/util-endpoints@npm:^1.2.0": - version: 1.2.0 - resolution: "@smithy/util-endpoints@npm:1.2.0" +"@smithy/util-endpoints@npm:^2.0.5": + version: 2.0.5 + resolution: "@smithy/util-endpoints@npm:2.0.5" dependencies: - "@smithy/node-config-provider": ^2.3.0 - "@smithy/types": ^2.12.0 + "@smithy/node-config-provider": ^3.1.4 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 19a59b1c9b214457371d4d7109b190c237de5ebd06f5b4f3665dddc5fe0879dbb19bcdc5dec23d1825cd04388b7f9bf7fddf354e1a23e84d9c690ad21e71cb86 + checksum: bb2a96323f52beaf2820f4e5764c865cff3ac5bca0c0df6923bb4582b0f87faf1606110cd4e36005ac43f41e9673ebdca4bbb8b913880fc2a4e0ff3301250da8 languageName: node linkType: hard -"@smithy/util-hex-encoding@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-hex-encoding@npm:2.2.0" +"@smithy/util-hex-encoding@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-hex-encoding@npm:3.0.0" dependencies: tslib: ^2.6.2 - checksum: 7d14589bc4a44eebf878595290c53ee4d90cc6b5445b5fe130608d6dea477c292730b85e4e08190a1555ef7664214f0f00dc478ba725516787a49fff658e725e + checksum: dd32fd71e915825987a18bf7c0f8f0c4956d0b17a0ee71592b5563bb20e04f24dbf81d36161aac07caab3bb5e535cc609fce20aa4a38f66b457c4c6f5c7748d9 languageName: node linkType: hard -"@smithy/util-middleware@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-middleware@npm:2.2.0" +"@smithy/util-middleware@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/util-middleware@npm:3.0.3" dependencies: - "@smithy/types": ^2.12.0 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 312dc86e5415a12e2580a02311750b350aec8fb9da5a60c3010c10694990ded869b7ca5b87aa20e5facbacdd233e928e418b7765d7797019cd48177052aedd03 + checksum: f37f25d65595af5ff4c3f69fa7e66545ac1651f77979e15ffbc9047e18fc668dae90458ee76add85a49ea3729c49d317e40542d5430e81e2eafe8dcae2ddb3bc languageName: node linkType: hard -"@smithy/util-retry@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-retry@npm:2.2.0" +"@smithy/util-retry@npm:^3.0.3": + version: 3.0.3 + resolution: "@smithy/util-retry@npm:3.0.3" dependencies: - "@smithy/service-error-classification": ^2.1.5 - "@smithy/types": ^2.12.0 + "@smithy/service-error-classification": ^3.0.3 + "@smithy/types": ^3.3.0 tslib: ^2.6.2 - checksum: 1a8071c8ac5a2646b3d3894e3bd9c36a9db045f52eadb194f32b02d2fdedd69fb267a2b02bcef9f91d0f8f3fe061754ac075d07ac166d90894acb27d68c62a41 + checksum: c760595376154be67414083aa6f76094022df72987521469b124ef3ef5848c0536757dcd2006520580380db6a4d7b597a05569470c3151f71d5e678df63f4c13 languageName: node linkType: hard -"@smithy/util-stream@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-stream@npm:2.2.0" +"@smithy/util-stream@npm:^3.1.3": + version: 3.1.3 + resolution: "@smithy/util-stream@npm:3.1.3" dependencies: - "@smithy/fetch-http-handler": ^2.5.0 - "@smithy/node-http-handler": ^2.5.0 - "@smithy/types": ^2.12.0 - "@smithy/util-base64": ^2.3.0 - "@smithy/util-buffer-from": ^2.2.0 - "@smithy/util-hex-encoding": ^2.2.0 - "@smithy/util-utf8": ^2.3.0 + "@smithy/fetch-http-handler": ^3.2.4 + "@smithy/node-http-handler": ^3.1.4 + "@smithy/types": ^3.3.0 + "@smithy/util-base64": ^3.0.0 + "@smithy/util-buffer-from": ^3.0.0 + "@smithy/util-hex-encoding": ^3.0.0 + "@smithy/util-utf8": ^3.0.0 tslib: ^2.6.2 - checksum: f0febd1a7558201d9178c0018478f89729800e9b8962dc735ec99f41ce01d1128373e3bd6008f0b4ff79b25ee4476db4fd5fa18d6feeb8b5b715d416da7027c3 + checksum: b663124c3b857b7744a0f2db47d091c570ff4fe4e8a8d254a7e1b8fed1d57fa4dc8fb12e496f7f607666ac5bc36d6f44a57ef3132bc882ff1d869b9bfdb5fc6e languageName: node linkType: hard @@ -15208,7 +15879,16 @@ __metadata: languageName: node linkType: hard -"@smithy/util-utf8@npm:^2.0.0, @smithy/util-utf8@npm:^2.3.0": +"@smithy/util-uri-escape@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-uri-escape@npm:3.0.0" + dependencies: + tslib: ^2.6.2 + checksum: d7ee01c978e2b08d0a89a3b678f5d5e5d5bb4ab4ab85567a238b1a6195dff1bdaf9ae62497e7f32ff5121b3dc007c370bcb6e8ef79b01fe5acdec5bbce8c7ce4 + languageName: node + linkType: hard + +"@smithy/util-utf8@npm:^2.0.0": version: 2.3.0 resolution: "@smithy/util-utf8@npm:2.3.0" dependencies: @@ -15218,14 +15898,24 @@ __metadata: languageName: node linkType: hard -"@smithy/util-waiter@npm:^2.2.0": - version: 2.2.0 - resolution: "@smithy/util-waiter@npm:2.2.0" +"@smithy/util-utf8@npm:^3.0.0": + version: 3.0.0 + resolution: "@smithy/util-utf8@npm:3.0.0" dependencies: - "@smithy/abort-controller": ^2.2.0 - "@smithy/types": ^2.12.0 + "@smithy/util-buffer-from": ^3.0.0 tslib: ^2.6.2 - checksum: 303f56beb9ba4afada862eff4950a17d904a4fdfc01bd8acb932b0457e457730981162777004414252e700014c554d894a1ce9d32e0bad75e1a4a2ca6492429e + checksum: d97be1748963263a1161ba80417d82318b977b38542f3fdf0379b0162461188be680e5bfb66a89d65652f0fad6ecf2ab23a43205979216e50602488f73434da3 + languageName: node + linkType: hard + +"@smithy/util-waiter@npm:^3.1.2": + version: 3.1.2 + resolution: "@smithy/util-waiter@npm:3.1.2" + dependencies: + "@smithy/abort-controller": ^3.1.1 + "@smithy/types": ^3.3.0 + tslib: ^2.6.2 + checksum: 35773b1bbbb215102555a55ce4de57cbd3e38f37546ca3e6748ce3856119019a613946b399c6d97981a0bad447ce9c41f87c276325ff4c0e5a2276ee4e9e384e languageName: node linkType: hard @@ -17114,14 +17804,14 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.33, @types/express-serve-static-core@npm:^4.17.5": - version: 4.19.3 - resolution: "@types/express-serve-static-core@npm:4.19.3" + version: 4.19.5 + resolution: "@types/express-serve-static-core@npm:4.19.5" dependencies: "@types/node": "*" "@types/qs": "*" "@types/range-parser": "*" "@types/send": "*" - checksum: fff38a7f43baeb6a62380682d39846c9d92047e0dce1737d76ebd944528619abc18addc4f0548bf43dbf4514090a1bd5140ba36695024656f941a87424b8ed7d + checksum: 72076c2f8df55e89136d4343fc874050d56c0f4afd885772a8aa506b98c3f4f3ddc7dcba42295a8b931c61000234fd679aec79ef50db15f376bf37d46234939a languageName: node linkType: hard @@ -17134,7 +17824,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.14, @types/express@npm:^4.17.17, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": +"@types/express@npm:*, @types/express@npm:^4.17.14, @types/express@npm:^4.17.21, @types/express@npm:^4.17.6": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -17717,7 +18407,7 @@ __metadata: languageName: node linkType: hard -"@types/node-fetch@npm:^2.5.0, @types/node-fetch@npm:^2.5.12": +"@types/node-fetch@npm:^2.5.12": version: 2.6.11 resolution: "@types/node-fetch@npm:2.6.11" dependencies: @@ -17736,7 +18426,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^20.1.1, @types/node@npm:^20.10.6, @types/node@npm:^20.11.16": +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^20.1.1, @types/node@npm:^20.10.6, @types/node@npm:^20.11.16": version: 20.12.7 resolution: "@types/node@npm:20.12.7" dependencies: @@ -17791,13 +18481,6 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.0": - version: 2.4.1 - resolution: "@types/normalize-package-data@npm:2.4.1" - checksum: e87bccbf11f95035c89a132b52b79ce69a1e3652fe55962363063c9c0dae0fe2477ebc585e03a9652adc6f381d24ba5589cc5e51849df4ced3d3e004a7d40ed5 - languageName: node - linkType: hard - "@types/npm-packlist@npm:^3.0.0": version: 3.0.0 resolution: "@types/npm-packlist@npm:3.0.0" @@ -17962,9 +18645,9 @@ __metadata: linkType: hard "@types/prop-types@npm:*, @types/prop-types@npm:^15.0.0, @types/prop-types@npm:^15.7.11, @types/prop-types@npm:^15.7.3": - version: 15.7.11 - resolution: "@types/prop-types@npm:15.7.11" - checksum: 7519ff11d06fbf6b275029fe03fff9ec377b4cb6e864cac34d87d7146c7f5a7560fd164bdc1d2dbe00b60c43713631251af1fd3d34d46c69cd354602bc0c7c54 + version: 15.7.12 + resolution: "@types/prop-types@npm:15.7.12" + checksum: ac16cc3d0a84431ffa5cfdf89579ad1e2269549f32ce0c769321fdd078f84db4fbe1b461ed5a1a496caf09e637c0e367d600c541435716a55b1d9713f5035dfe languageName: node linkType: hard @@ -18039,6 +18722,15 @@ __metadata: languageName: node linkType: hard +"@types/react-resizable@npm:^3.0.8": + version: 3.0.8 + resolution: "@types/react-resizable@npm:3.0.8" + dependencies: + "@types/react": "*" + checksum: aabdef8056bbab3065559bce7ce93232e645fb4f915fd55f0903a2f4df5d2395762dfd75bb0242d5a73d39461e8c062d81eaab87b91213dddfd973ff908f79e4 + languageName: node + linkType: hard + "@types/react-sparklines@npm:^1.7.0": version: 1.7.5 resolution: "@types/react-sparklines@npm:1.7.5" @@ -18084,7 +18776,7 @@ __metadata: languageName: node linkType: hard -"@types/react-window@npm:^1.8.5": +"@types/react-window@npm:^1.8.5, @types/react-window@npm:^1.8.8": version: 1.8.8 resolution: "@types/react-window@npm:1.8.8" dependencies: @@ -18445,15 +19137,6 @@ __metadata: languageName: node linkType: hard -"@types/tunnel@npm:^0.0.3": - version: 0.0.3 - resolution: "@types/tunnel@npm:0.0.3" - dependencies: - "@types/node": "*" - checksum: 53e23a1f9fb14a491c00425b2a4fc443817564d77be5e1b95fcbeb6d009551b62ea82ffc3e5ca0c6b9f6b186824ca6ec46e7450c1bcd6674a46d1325f0116e24 - languageName: node - linkType: hard - "@types/unist@npm:*, @types/unist@npm:^2.0.0": version: 2.0.6 resolution: "@types/unist@npm:2.0.6" @@ -19858,6 +20541,16 @@ __metadata: languageName: node linkType: hard +"GendocuPublicApis@https://git.gendocu.com/gendocu/GendocuPublicApis.git#master": + version: 1.0.0 + resolution: "GendocuPublicApis@https://git.gendocu.com/gendocu/GendocuPublicApis.git#commit=6e8d4c1d2342556a2e8e719f19385b266001ebae" + dependencies: + "@improbable-eng/grpc-web": ^0.14.0 + google-protobuf: ^3.15.8 + checksum: 2c8f28cd89e9a71e5f9df819115a89659d98f22d6e7a6b2f3fc0bc24a4ca954c0112070c00401749dcbcdeeb42ca070d6d8a36b3c205fbf13409fb1110b5966e + languageName: node + linkType: hard + "a-sync-waterfall@npm:^1.0.0": version: 1.0.1 resolution: "a-sync-waterfall@npm:1.0.1" @@ -19902,7 +20595,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -19979,6 +20672,13 @@ __metadata: languageName: node linkType: hard +"adm-zip@npm:^0.5.10": + version: 0.5.14 + resolution: "adm-zip@npm:0.5.14" + checksum: 83a4bc0bdff70f3276992810db96b10ffbf3c07667a9c3395e3dc60ca40ecbb920812dde212e1e21633b1508f1ea6fc427ac451aa68ee1a597b7251b2c1fc844 + languageName: node + linkType: hard + "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -20210,6 +20910,15 @@ __metadata: languageName: node linkType: hard +"ansi-html@npm:^0.0.9": + version: 0.0.9 + resolution: "ansi-html@npm:0.0.9" + bin: + ansi-html: bin/ansi-html + checksum: a03754d6f66bae33938ed8bb3dd98174b7f4895ebe45226185036ed4a1388a7aaf2f2b9581608f0626432ba7add92cfc590aa6475a78bbb90d9d1e1d1af8cbe6 + languageName: node + linkType: hard + "ansi-regex@npm:*, ansi-regex@npm:^6.0.1": version: 6.0.1 resolution: "ansi-regex@npm:6.0.1" @@ -20263,7 +20972,7 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.0.0": +"any-promise@npm:^1.0.0, any-promise@npm:^1.1.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" checksum: 0ee8a9bdbe882c90464d75d1f55cf027f5458650c4bd1f0467e65aec38ccccda07ca5844969ee77ed46d04e7dded3eaceb027e8d32f385688523fe305fa7e1de @@ -20548,7 +21257,7 @@ __metadata: languageName: node linkType: hard -"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": +"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -20584,16 +21293,16 @@ __metadata: languageName: node linkType: hard -"array.prototype.tosorted@npm:^1.1.3": - version: 1.1.3 - resolution: "array.prototype.tosorted@npm:1.1.3" +"array.prototype.tosorted@npm:^1.1.4": + version: 1.1.4 + resolution: "array.prototype.tosorted@npm:1.1.4" dependencies: - call-bind: ^1.0.5 + call-bind: ^1.0.7 define-properties: ^1.2.1 - es-abstract: ^1.22.3 - es-errors: ^1.1.0 + es-abstract: ^1.23.3 + es-errors: ^1.3.0 es-shim-unscopables: ^1.0.2 - checksum: 555e8808086bbde9e634c5dc5a8c0a2f1773075447b43b2fa76ab4f94f4e90f416d2a4f881024e1ce1a2931614caf76cd6b408af901c9d7cd13061d0d268f5af + checksum: e4142d6f556bcbb4f393c02e7dbaea9af8f620c040450c2be137c9cbbd1a17f216b9c688c5f2c08fbb038ab83f55993fa6efdd9a05881d84693c7bcb5422127a languageName: node linkType: hard @@ -20613,13 +21322,6 @@ __metadata: languageName: node linkType: hard -"arrify@npm:^1.0.1": - version: 1.0.1 - resolution: "arrify@npm:1.0.1" - checksum: 745075dd4a4624ff0225c331dacb99be501a515d39bcb7c84d24660314a6ec28e68131b137e6f7e16318170842ce97538cd298fc4cd6b2cc798e0b957f2747e7 - languageName: node - linkType: hard - "arrify@npm:^2.0.0, arrify@npm:^2.0.1": version: 2.0.1 resolution: "arrify@npm:2.0.1" @@ -20917,7 +21619,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.6.8, axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0": +"axios@npm:1.6.8": version: 1.6.8 resolution: "axios@npm:1.6.8" dependencies: @@ -20938,6 +21640,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.0.0, axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.6.7": + version: 1.7.2 + resolution: "axios@npm:1.7.2" + dependencies: + follow-redirects: ^1.15.6 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: e457e2b0ab748504621f6fa6609074ac08c824bf0881592209dfa15098ece7e88495300e02cd22ba50b3468fd712fe687e629dcb03d6a3f6a51989727405aedf + languageName: node + linkType: hard + "axobject-query@npm:^3.2.1": version: 3.2.1 resolution: "axobject-query@npm:3.2.1" @@ -21296,14 +22009,14 @@ __metadata: languageName: node linkType: hard -"better-sqlite3@npm:^9.0.0": - version: 9.6.0 - resolution: "better-sqlite3@npm:9.6.0" +"better-sqlite3@npm:^11.0.0": + version: 11.0.0 + resolution: "better-sqlite3@npm:11.0.0" dependencies: bindings: ^1.5.0 node-gyp: latest prebuild-install: ^7.1.1 - checksum: be3a1d2a3f6f9b5141be6607a38c0a51fa5849495b071955e507bc0c2a2fb08430852c1bf03796eec1a53344b25645807db48dcb51c71b0662b74c5a70420bb0 + checksum: 7bf02ea6ba3253f53cb36bd872f86379ebf1a66cb103d39b91c610995c4396dde2b976bdf866f07eb7d67eb6f5433aa5490a6a4f4264244f83a53562f68832dc languageName: node linkType: hard @@ -21519,15 +22232,6 @@ __metadata: languageName: node linkType: hard -"breakword@npm:^1.0.5": - version: 1.0.5 - resolution: "breakword@npm:1.0.5" - dependencies: - wcwidth: ^1.0.1 - checksum: 8ca7b10bbbbfe1c45c12c9119c4bc1e585452ddd58c5da93020a0c1deac3cf6bb335632675c9c705ba7b644065ae1d6623a25e79b7a48e0ee0ff42cb6e94b357 - languageName: node - linkType: hard - "brorand@npm:^1.0.1, brorand@npm:^1.1.0": version: 1.1.0 resolution: "brorand@npm:1.1.0" @@ -21876,6 +22580,16 @@ __metadata: languageName: node linkType: hard +"cache-content-type@npm:^1.0.0": + version: 1.0.1 + resolution: "cache-content-type@npm:1.0.1" + dependencies: + mime-types: ^2.1.18 + ylru: ^1.2.0 + checksum: 18db4d59452669ccbfd7146a1510a37eb28e9eccf18ca7a4eb603dff2edc5cccdca7498fc3042a2978f76f11151fba486eb9eb69d9afa3fb124957870aef4fd3 + languageName: node + linkType: hard + "cacheable-lookup@npm:^5.0.3": version: 5.0.3 resolution: "cacheable-lookup@npm:5.0.3" @@ -21957,17 +22671,6 @@ __metadata: languageName: node linkType: hard -"camelcase-keys@npm:^6.2.2": - version: 6.2.2 - resolution: "camelcase-keys@npm:6.2.2" - dependencies: - camelcase: ^5.3.1 - map-obj: ^4.0.0 - quick-lru: ^4.0.1 - checksum: 43c9af1adf840471e54c68ab3e5fe8a62719a6b7dbf4e2e86886b7b0ff96112c945736342b837bd2529ec9d1c7d1934e5653318478d98e0cf22c475c04658e2a - languageName: node - linkType: hard - "camelcase@npm:5.0.0": version: 5.0.0 resolution: "camelcase@npm:5.0.0" @@ -21975,7 +22678,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^5.0.0, camelcase@npm:^5.3.1": +"camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" checksum: e6effce26b9404e3c0f301498184f243811c30dfe6d0b9051863bd8e4034d09c8c2923794f280d6827e5aa055f6c434115ff97864a16a963366fb35fd673024b @@ -22052,6 +22755,16 @@ __metadata: languageName: node linkType: hard +"chalk@npm:3.0.0, chalk@npm:^3.0.0": + version: 3.0.0 + resolution: "chalk@npm:3.0.0" + dependencies: + ansi-styles: ^4.1.0 + supports-color: ^7.1.0 + checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 + languageName: node + linkType: hard + "chalk@npm:4.1.2, chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" @@ -22062,16 +22775,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^3.0.0": - version: 3.0.0 - resolution: "chalk@npm:3.0.0" - dependencies: - ansi-styles: ^4.1.0 - supports-color: ^7.1.0 - checksum: 8e3ddf3981c4da405ddbd7d9c8d91944ddf6e33d6837756979f7840a29272a69a5189ecae0ff84006750d6d1e92368d413335eab4db5476db6e6703a1d1e0505 - languageName: node - linkType: hard - "chalk@npm:~5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" @@ -22397,17 +23100,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^6.0.0": - version: 6.0.0 - resolution: "cliui@npm:6.0.0" - dependencies: - string-width: ^4.2.0 - strip-ansi: ^6.0.0 - wrap-ansi: ^6.2.0 - checksum: 4fcfd26d292c9f00238117f39fc797608292ae36bac2168cfee4c85923817d0607fe21b3329a8621e01aedf512c99b7eaa60e363a671ffd378df6649fb48ae42 - languageName: node - linkType: hard - "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -22538,17 +23230,17 @@ __metadata: languageName: node linkType: hard -"codemirror-graphql@npm:^2.0.10": - version: 2.0.11 - resolution: "codemirror-graphql@npm:2.0.11" +"codemirror-graphql@npm:^2.0.10, codemirror-graphql@npm:^2.0.13": + version: 2.0.13 + resolution: "codemirror-graphql@npm:2.0.13" dependencies: "@types/codemirror": ^0.0.90 - graphql-language-service: 5.2.0 + graphql-language-service: 5.2.2 peerDependencies: "@codemirror/language": 6.0.0 codemirror: ^5.65.3 graphql: ^15.5.0 || ^16.0.0 - checksum: cdbeb713af63a069c5526f864edf4f71dd811437e44dba7967164ea2d380c52dfe51d3ea3fde06c94ffeb596b93af737767923d6fc434c628fa4241621684950 + checksum: 7ecb2ba2716448b3c10ccc6168dbe4c636d2d82695beea40cda6f4938b4821e9abcf6446c99412bf59558340f10b45e5b74dce5f80ae4be4409b8a6b7bfcc5ec languageName: node linkType: hard @@ -23084,7 +23776,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.3": +"content-disposition@npm:0.5.4, content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -23093,7 +23785,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": +"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.4, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 @@ -23180,6 +23872,16 @@ __metadata: languageName: node linkType: hard +"cookies@npm:~0.8.0": + version: 0.8.0 + resolution: "cookies@npm:0.8.0" + dependencies: + depd: ~2.0.0 + keygrip: ~1.1.0 + checksum: 806055a44f128705265b1bc6a853058da18bf80dea3654ad99be20985b1fa1b14f86c1eef73644aab8071241f8a78acd57202b54c4c5c70769fc694fbb9c4edc + languageName: node + linkType: hard + "copy-to-clipboard@npm:^3.2.0, copy-to-clipboard@npm:^3.3.1": version: 3.3.1 resolution: "copy-to-clipboard@npm:3.3.1" @@ -23213,9 +23915,9 @@ __metadata: linkType: hard "core-js@npm:^3.6.5": - version: 3.37.0 - resolution: "core-js@npm:3.37.0" - checksum: 212c3e9b3fc277dbb63739ef58a61c5709ccd0b36f09c3ce6946aa91fa180c60f57f976d4a5fdb9cda0c6cb55417379ba5a008fc3a1384ec94ec8ec61826469d + version: 3.37.1 + resolution: "core-js@npm:3.37.1" + checksum: 2d58a5c599f05c3e04abc8bc5e64b88eb17d914c0f552f670fb800afa74ec54b4fcc7f231ad6bd45badaf62c0fb0ce30e6fe89cedb6bb6d54e6f19115c3c17ff languageName: node linkType: hard @@ -23397,6 +24099,15 @@ __metadata: languageName: node linkType: hard +"cron-parser@npm:^4.2.0": + version: 4.9.0 + resolution: "cron-parser@npm:4.9.0" + dependencies: + luxon: ^3.2.1 + checksum: 3cf248fc5cae6c19ec7124962b1cd84b76f02b9bc4f58976b3bd07624db3ef10aaf1548efcc2d2dcdab0dad4f12029d640a55ecce05ea5e1596af9db585502cf + languageName: node + linkType: hard + "cron@npm:^3.0.0": version: 3.1.7 resolution: "cron@npm:3.1.7" @@ -23787,39 +24498,6 @@ __metadata: languageName: node linkType: hard -"csv-generate@npm:^3.4.3": - version: 3.4.3 - resolution: "csv-generate@npm:3.4.3" - checksum: 868dc630e8bcabf42d3d1ef22c09fb783de72d7e5929854aad0323f44059b1747edf8a2724e32fdc5008396e2ea38d5c45df0b0e3a1b506e3ab34f76f3e2fb3a - languageName: node - linkType: hard - -"csv-parse@npm:^4.16.3": - version: 4.16.3 - resolution: "csv-parse@npm:4.16.3" - checksum: 5ad7790fc31c32ca1623bad1a54906134ba44fa109e8dd2dfda440bf7e9fd93610d9076a78f45c872701bfafdf7f93c9b75500c09d7efd6611d863f1d45ec69f - languageName: node - linkType: hard - -"csv-stringify@npm:^5.6.5": - version: 5.6.5 - resolution: "csv-stringify@npm:5.6.5" - checksum: f93e1444857416081de3d86765b62e4c4f7c110974ad6bbcb0031d7db39b6624847ac9ee5705726e7011346f32f3696f27299b74b23a6c2b083adff0dd2755fe - languageName: node - linkType: hard - -"csv@npm:^5.5.0": - version: 5.5.3 - resolution: "csv@npm:5.5.3" - dependencies: - csv-generate: ^3.4.3 - csv-parse: ^4.16.3 - csv-stringify: ^5.6.5 - stream-transform: ^2.1.3 - checksum: 0decc2d0d7a0abf127f4556d6f3cef5a54015b78d348608b5e8f42256c2bd0a021f34f1efc9723b2cd162680917de4c0b3967bfb65a07305eca0827654ca727e - languageName: node - linkType: hard - "ctrlc-windows@npm:^2.1.0": version: 2.1.0 resolution: "ctrlc-windows@npm:2.1.0" @@ -24039,6 +24717,13 @@ __metadata: languageName: node linkType: hard +"date-format@npm:^4.0.14": + version: 4.0.14 + resolution: "date-format@npm:4.0.14" + checksum: dfe5139df6af5759b9dd3c007b899b3f60d45a9240ffeee6314ab74e6ab52e9b519a44ccf285888bdd6b626c66ee9b4c8a523075fa1140617b5beb1cbb9b18d1 + languageName: node + linkType: hard + "dateformat@npm:^3.0.3": version: 3.0.3 resolution: "dateformat@npm:3.0.3" @@ -24062,6 +24747,13 @@ __metadata: languageName: node linkType: hard +"debounce-promise@npm:^3.1.2": + version: 3.1.2 + resolution: "debounce-promise@npm:3.1.2" + checksum: 29bac4524c423cc852319d7455363909ea3d933a3b9e3eb1149d963cffc34c475fe37219d0bafc61af566500b5d663cba579bbad7ee4023bef06f8394ed900ad + languageName: node + linkType: hard + "debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -24099,6 +24791,15 @@ __metadata: languageName: node linkType: hard +"debug@npm:~3.1.0": + version: 3.1.0 + resolution: "debug@npm:3.1.0" + dependencies: + ms: 2.0.0 + checksum: 0b52718ab957254a5b3ca07fc34543bc778f358620c206a08452251eb7fc193c3ea3505072acbf4350219c14e2d71ceb7bdaa0d3370aa630b50da790458d08b3 + languageName: node + linkType: hard + "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -24106,23 +24807,6 @@ __metadata: languageName: node linkType: hard -"decamelize-keys@npm:^1.1.0": - version: 1.1.0 - resolution: "decamelize-keys@npm:1.1.0" - dependencies: - decamelize: ^1.1.0 - map-obj: ^1.0.0 - checksum: 8bc5d32e035a072f5dffc1f1f3d26ca7ab1fb44a9cade34c97ab6cd1e62c81a87e718101e96de07d78cecda20a3fdb955df958e46671ccad01bb8dcf0de2e298 - languageName: node - linkType: hard - -"decamelize@npm:^1.1.0, decamelize@npm:^1.2.0": - version: 1.2.0 - resolution: "decamelize@npm:1.2.0" - checksum: ad8c51a7e7e0720c70ec2eeb1163b66da03e7616d7b98c9ef43cce2416395e84c1e9548dd94f5f6ffecfee9f8b94251fc57121a8b021f2ff2469b2bae247b8aa - languageName: node - linkType: hard - "decimal.js@npm:^10.2.1, decimal.js@npm:^10.3.1": version: 10.4.1 resolution: "decimal.js@npm:10.4.1" @@ -24185,6 +24869,13 @@ __metadata: languageName: node linkType: hard +"deep-equal@npm:~1.0.1": + version: 1.0.1 + resolution: "deep-equal@npm:1.0.1" + checksum: 5af8cbfcebf190491878a498caccc7dc9592f8ebd1685b976eacc3825619d222b5e929923163b92c4f414494e2b884f7ebf00c022e8198e8292deb70dd9785f4 + languageName: node + linkType: hard + "deep-extend@npm:0.6.0, deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -24391,7 +25082,7 @@ __metadata: languageName: node linkType: hard -"destroy@npm:1.2.0": +"destroy@npm:1.2.0, destroy@npm:^1.0.4": version: 1.2.0 resolution: "destroy@npm:1.2.0" checksum: 0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 @@ -25057,7 +25748,7 @@ __metadata: languageName: node linkType: hard -"encodeurl@npm:~1.0.2": +"encodeurl@npm:^1.0.2, encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" checksum: e50e3d508cdd9c4565ba72d2012e65038e5d71bdc9198cb125beb6237b5b1ade6c0d343998da9e170fb2eae52c1bed37d4d6d98a46ea423a0cddbed5ac3f780c @@ -25102,13 +25793,6 @@ __metadata: languageName: node linkType: hard -"ent@npm:^2.2.0": - version: 2.2.0 - resolution: "ent@npm:2.2.0" - checksum: f588b5707d6fef36011ea10d530645912a69530a1eb0831f8708c498ac028363a7009f45cfadd28ceb4dafd9ac17ec15213f88d09ce239cd033cfe1328dd7d7d - languageName: node - linkType: hard - "entities@npm:^2.0.0": version: 2.2.0 resolution: "entities@npm:2.2.0" @@ -25153,6 +25837,13 @@ __metadata: languageName: node linkType: hard +"error-inject@npm:^1.0.0": + version: 1.0.0 + resolution: "error-inject@npm:1.0.0" + checksum: 258cb26c7c7e04d9b730d074926ff5e18755b6945781540fdd124cafc5015610d97e4b971eb3226469f407fd34fa899a60fbcf9ade8923ab42fa2a3c61e246cf + languageName: node + linkType: hard + "error-stack-parser@npm:^2.0.6": version: 2.0.6 resolution: "error-stack-parser@npm:2.0.6" @@ -25247,7 +25938,7 @@ __metadata: languageName: node linkType: hard -"es-errors@npm:^1.1.0, es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": +"es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" checksum: ec1414527a0ccacd7f15f4a3bc66e215f04f595ba23ca75cdae0927af099b5ec865f9f4d33e9d7e86f512f252876ac77d4281a7871531a50678132429b1271b5 @@ -25553,6 +26244,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.0": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": 0.21.5 + "@esbuild/android-arm": 0.21.5 + "@esbuild/android-arm64": 0.21.5 + "@esbuild/android-x64": 0.21.5 + "@esbuild/darwin-arm64": 0.21.5 + "@esbuild/darwin-x64": 0.21.5 + "@esbuild/freebsd-arm64": 0.21.5 + "@esbuild/freebsd-x64": 0.21.5 + "@esbuild/linux-arm": 0.21.5 + "@esbuild/linux-arm64": 0.21.5 + "@esbuild/linux-ia32": 0.21.5 + "@esbuild/linux-loong64": 0.21.5 + "@esbuild/linux-mips64el": 0.21.5 + "@esbuild/linux-ppc64": 0.21.5 + "@esbuild/linux-riscv64": 0.21.5 + "@esbuild/linux-s390x": 0.21.5 + "@esbuild/linux-x64": 0.21.5 + "@esbuild/netbsd-x64": 0.21.5 + "@esbuild/openbsd-x64": 0.21.5 + "@esbuild/sunos-x64": 0.21.5 + "@esbuild/win32-arm64": 0.21.5 + "@esbuild/win32-ia32": 0.21.5 + "@esbuild/win32-x64": 0.21.5 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 2911c7b50b23a9df59a7d6d4cdd3a4f85855787f374dce751148dbb13305e0ce7e880dde1608c2ab7a927fc6cec3587b80995f7fc87a64b455f8b70b55fd8ec1 + languageName: node + linkType: hard + "esbuild@npm:esbuild-wasm@^0.15.15": version: 0.15.18 resolution: "esbuild-wasm@npm:0.15.18" @@ -25803,14 +26574,14 @@ __metadata: linkType: hard "eslint-plugin-react@npm:^7.28.0": - version: 7.34.2 - resolution: "eslint-plugin-react@npm:7.34.2" + version: 7.34.3 + resolution: "eslint-plugin-react@npm:7.34.3" dependencies: array-includes: ^3.1.8 array.prototype.findlast: ^1.2.5 array.prototype.flatmap: ^1.3.2 array.prototype.toreversed: ^1.1.2 - array.prototype.tosorted: ^1.1.3 + array.prototype.tosorted: ^1.1.4 doctrine: ^2.1.0 es-iterator-helpers: ^1.0.19 estraverse: ^5.3.0 @@ -25826,7 +26597,7 @@ __metadata: string.prototype.matchall: ^4.0.11 peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - checksum: aed331239f3a64fcd884380534ece4b8716f1eca4899c8636d04306879e6b4e7339e28e427bdd571d372b78b713025e0767e5f5b5486a8d19bff82616ebe8959 + checksum: 1a519b9792ab9392a5157f2543ce98ab1218c62f4a31c4c3ceb5dd3e7997def4aa07ab39f7276af0fe116ef002db29d97216a15b7aa3b200e55b641cf77d6292 languageName: node linkType: hard @@ -26310,7 +27081,7 @@ __metadata: "@types/express-serve-static-core": ^4.17.5 "@types/luxon": ^3.0.0 azure-devops-node-api: ^12.0.0 - better-sqlite3: ^9.0.0 + better-sqlite3: ^11.0.0 dockerode: ^4.0.0 example-app: "link:../app" express: ^4.17.1 @@ -26444,6 +27215,15 @@ __metadata: languageName: node linkType: hard +"expand-tilde@npm:^2.0.0, expand-tilde@npm:^2.0.2": + version: 2.0.2 + resolution: "expand-tilde@npm:2.0.2" + dependencies: + homedir-polyfill: ^1.0.1 + checksum: 2efe6ed407d229981b1b6ceb552438fbc9e5c7d6a6751ad6ced3e0aa5cf12f0b299da695e90d6c2ac79191b5c53c613e508f7149e4573abfbb540698ddb7301a + languageName: node + linkType: hard + "expect@npm:>28.1.3, expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" @@ -26757,25 +27537,14 @@ __metadata: languageName: node linkType: hard -"fast-xml-parser@npm:4.2.5": - version: 4.2.5 - resolution: "fast-xml-parser@npm:4.2.5" +"fast-xml-parser@npm:4.4.1, fast-xml-parser@npm:^4.3.0, fast-xml-parser@npm:^4.3.2": + version: 4.4.1 + resolution: "fast-xml-parser@npm:4.4.1" dependencies: strnum: ^1.0.5 bin: fxparser: src/cli/cli.js - checksum: d32b22005504eeb207249bf40dc82d0994b5bb9ca9dcc731d335a1f425e47fe085b3cace3cf9d32172dd1a5544193c49e8615ca95b4bf95a4a4920a226b06d80 - languageName: node - linkType: hard - -"fast-xml-parser@npm:^4.3.0": - version: 4.3.2 - resolution: "fast-xml-parser@npm:4.3.2" - dependencies: - strnum: ^1.0.5 - bin: - fxparser: src/cli/cli.js - checksum: d507ce2efa5fd13d0a5ba28bd76dd68f2fc30ad8748357c37b70f360d19417866d79e35a688af067d5bceaaa796033fa985206aef9692f7a421e1326b6e73309 + checksum: f440c01cd141b98789ae777503bcb6727393296094cc82924ae9f88a5b971baa4eec7e65306c7e07746534caa661fc83694ff437d9012dc84dee39dfbfaab947 languageName: node linkType: hard @@ -26983,6 +27752,15 @@ __metadata: languageName: node linkType: hard +"find-file-up@npm:^2.0.1": + version: 2.0.1 + resolution: "find-file-up@npm:2.0.1" + dependencies: + resolve-dir: ^1.0.1 + checksum: dfe820bfb80e75bed5dd5080057858c0ad2393e1438c48a3bb682663e9ecdcfbe3224ed4768bfedd00776800b4ae76bc8953d250d15ae3feabf381d2c6d04268 + languageName: node + linkType: hard + "find-my-way@npm:^4.3.3": version: 4.5.1 resolution: "find-my-way@npm:4.5.1" @@ -26995,6 +27773,15 @@ __metadata: languageName: node linkType: hard +"find-pkg@npm:2.0.0": + version: 2.0.0 + resolution: "find-pkg@npm:2.0.0" + dependencies: + find-file-up: ^2.0.1 + checksum: 44785204c8bbbdfeaece6b834ba81a35163421c30e20f531281d26e6b5890663d7ac884b82a9aebf6ce23e479336cd6f70ea5597da35495c16abdeba2fd4f845 + languageName: node + linkType: hard + "find-process@npm:^1.4.5": version: 1.4.7 resolution: "find-process@npm:1.4.7" @@ -27080,7 +27867,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:3.3.1, flatted@npm:^3.1.0": +"flatted@npm:3.3.1, flatted@npm:^3.1.0, flatted@npm:^3.2.7": version: 3.3.1 resolution: "flatted@npm:3.3.1" checksum: 85ae7181650bb728c221e7644cbc9f4bf28bc556f2fc89bb21266962bdf0ce1029cc7acc44bb646cd469d9baac7c317f64e841c4c4c00516afa97320cdac7f94 @@ -27310,7 +28097,7 @@ __metadata: languageName: node linkType: hard -"fresh@npm:0.5.2, fresh@npm:^0.5.2": +"fresh@npm:0.5.2, fresh@npm:^0.5.2, fresh@npm:~0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" checksum: 13ea8b08f91e669a64e3ba3a20eb79d7ca5379a81f1ff7f4310d54e2320645503cc0c78daedc93dfb6191287295f6479544a649c64d8e41a1c0fb0c221552346 @@ -27352,6 +28139,18 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:9.1.0, fs-extra@npm:^9.0.0": + version: 9.1.0 + resolution: "fs-extra@npm:9.1.0" + dependencies: + at-least-node: ^1.0.0 + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 + languageName: node + linkType: hard + "fs-extra@npm:^11.0.0, fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" @@ -27385,18 +28184,6 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^9.0.0": - version: 9.1.0 - resolution: "fs-extra@npm:9.1.0" - dependencies: - at-least-node: ^1.0.0 - graceful-fs: ^4.2.0 - jsonfile: ^6.0.1 - universalify: ^2.0.0 - checksum: ba71ba32e0faa74ab931b7a0031d1523c66a73e225de7426e275e238e312d07313d2da2d33e34a52aa406c8763ade5712eb3ec9ba4d9edce652bcacdc29e6b20 - languageName: node - linkType: hard - "fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" @@ -27556,16 +28343,6 @@ __metadata: languageName: node linkType: hard -"gendocu-public-apis@npm:^1.0.0": - version: 1.0.0 - resolution: "gendocu-public-apis@npm:1.0.0" - dependencies: - "@improbable-eng/grpc-web": ^0.14.0 - google-protobuf: ^3.15.8 - checksum: ee1548d1285f9d2dc7ef04c959e620516322314c8aa2f2bfedd25345a6211abf51cdca4d54cca18de8f31a90f2b86ab171ae41f7c2f39dfdd0ac7163f875f719 - languageName: node - linkType: hard - "generate-function@npm:^2.3.1": version: 2.3.1 resolution: "generate-function@npm:2.3.1" @@ -27591,7 +28368,7 @@ __metadata: languageName: node linkType: hard -"get-caller-file@npm:^2.0.1, get-caller-file@npm:^2.0.5": +"get-caller-file@npm:^2.0.5": version: 2.0.5 resolution: "get-caller-file@npm:2.0.5" checksum: b9769a836d2a98c3ee734a88ba712e62703f1df31b94b784762c433c27a386dd6029ff55c2a920c392e33657d80191edbf18c61487e198844844516f843496b9 @@ -27858,6 +28635,17 @@ __metadata: languageName: node linkType: hard +"global-modules@npm:^1.0.0": + version: 1.0.0 + resolution: "global-modules@npm:1.0.0" + dependencies: + global-prefix: ^1.0.1 + is-windows: ^1.0.1 + resolve-dir: ^1.0.0 + checksum: 10be68796c1e1abc1e2ba87ec4ea507f5629873b119ab0cd29c07284ef2b930f1402d10df01beccb7391dedd9cd479611dd6a24311c71be58937beaf18edf85e + languageName: node + linkType: hard + "global-modules@npm:^2.0.0": version: 2.0.0 resolution: "global-modules@npm:2.0.0" @@ -27867,6 +28655,19 @@ __metadata: languageName: node linkType: hard +"global-prefix@npm:^1.0.1": + version: 1.0.2 + resolution: "global-prefix@npm:1.0.2" + dependencies: + expand-tilde: ^2.0.2 + homedir-polyfill: ^1.0.1 + ini: ^1.3.4 + is-windows: ^1.0.1 + which: ^1.2.14 + checksum: 061b43470fe498271bcd514e7746e8a8535032b17ab9570517014ae27d700ff0dca749f76bbde13ba384d185be4310d8ba5712cb0e74f7d54d59390db63dd9a0 + languageName: node + linkType: hard + "global-prefix@npm:^3.0.0": version: 3.0.0 resolution: "global-prefix@npm:3.0.0" @@ -27953,9 +28754,9 @@ __metadata: languageName: node linkType: hard -"google-auth-library@npm:^9.0.0, google-auth-library@npm:^9.6.3": - version: 9.8.0 - resolution: "google-auth-library@npm:9.8.0" +"google-auth-library@npm:^9.0.0, google-auth-library@npm:^9.3.0, google-auth-library@npm:^9.6.3": + version: 9.11.0 + resolution: "google-auth-library@npm:9.11.0" dependencies: base64-js: ^1.3.0 ecdsa-sig-formatter: ^1.0.11 @@ -27963,33 +28764,34 @@ __metadata: gcp-metadata: ^6.1.0 gtoken: ^7.0.0 jws: ^4.0.0 - checksum: dd67972861e16d47de494ac333bd76c57a5132e300793ae2dc8866fb76b3e57099846ce84b036ab2564bc4e85e0dc4d4cf245f4ca95aedebf53a9a2c03f35b38 + checksum: 984d344b5e0a21ea1e097d06e27173035619c0e8f89a363e538b445adb1414b79e938b56b4432aa36fda074c5922fa6a34f9b64734765c01dff73c45c8568554 languageName: node linkType: hard -"google-gax@npm:^4.0.3, google-gax@npm:^4.0.4": - version: 4.0.5 - resolution: "google-gax@npm:4.0.5" +"google-gax@npm:^4.0.3, google-gax@npm:^4.3.3": + version: 4.3.7 + resolution: "google-gax@npm:4.3.7" dependencies: - "@grpc/grpc-js": ~1.9.6 - "@grpc/proto-loader": ^0.7.0 + "@grpc/grpc-js": ^1.10.9 + "@grpc/proto-loader": ^0.7.13 "@types/long": ^4.0.0 abort-controller: ^3.0.0 duplexify: ^4.0.0 - google-auth-library: ^9.0.0 + google-auth-library: ^9.3.0 node-fetch: ^2.6.1 object-hash: ^3.0.0 - proto3-json-serializer: ^2.0.0 - protobufjs: 7.2.5 + proto3-json-serializer: ^2.0.2 + protobufjs: ^7.3.2 retry-request: ^7.0.0 - checksum: d4b5f2fc4a902b29d23552ab0fa498785ce88516ff07f7e7f08cac0f6572a78113a7f468f47b17e98d235d4049eeda8d77cfcad6291f6d14bda39b9eb19f935d + uuid: ^9.0.1 + checksum: f03327e42b5d0166c6655347c9ae56787a1cf0f2d32147530164f18deb808688e857e0017703d5b070c8b4ab9a003eda9598891b1cb5cdf007026be7a1c752d7 languageName: node linkType: hard -"google-protobuf@npm:^3.15.8, google-protobuf@npm:^3.19.1": - version: 3.20.1 - resolution: "google-protobuf@npm:3.20.1" - checksum: a440fbc329bb441b253bc421b01540918f4b66921c47a3bc57d154d2677dc917cb1a29126ff35ddc4d8872d1b5843ef1ece60b6dff506ba64a9f5807962d3f61 +"google-protobuf@npm:^3.15.8, google-protobuf@npm:^3.21.2": + version: 3.21.2 + resolution: "google-protobuf@npm:3.21.2" + checksum: 3caa2e1e2654714cc1a201ac5e5faabcaa48f5fba3d8ff9b64ca66fe19e4e0506b94053f32eddc77bf3a7a47ac1660315c6744185c1e2bb27654fd76fe91b988 languageName: node linkType: hard @@ -28120,17 +28922,18 @@ __metadata: languageName: node linkType: hard -"graphql-language-service@npm:5.2.0, graphql-language-service@npm:^5.2.0": - version: 5.2.0 - resolution: "graphql-language-service@npm:5.2.0" +"graphql-language-service@npm:5.2.2, graphql-language-service@npm:^5.2.0, graphql-language-service@npm:^5.2.2": + version: 5.2.2 + resolution: "graphql-language-service@npm:5.2.2" dependencies: + debounce-promise: ^3.1.2 nullthrows: ^1.0.0 vscode-languageserver-types: ^3.17.1 peerDependencies: graphql: ^15.5.0 || ^16.0.0 bin: graphql: dist/temp-bin.js - checksum: b053c6b7158d0ee7a3e55391bfd8be956fc5380211ca586b3a252007845e119540fb40efcc438975eaebc5ef25f46973f7ff4d9543c66e14ebd992957e0299b7 + checksum: 3f112ba67ae10096ac4a415e946cddad019ce9bd5467e29730fdfe08b4470f5b4126412edd6bfcdf6cfbd816c65e295da21134ae501e577540b319f521bc032d languageName: node linkType: hard @@ -28187,13 +28990,13 @@ __metadata: linkType: hard "grpc-docs@npm:^1.0.6, grpc-docs@npm:^1.1.2": - version: 1.1.3 - resolution: "grpc-docs@npm:1.1.3" + version: 1.1.4 + resolution: "grpc-docs@npm:1.1.4" dependencies: "@improbable-eng/grpc-web": ^0.15.0 "@types/minimatch": ^3.0.5 - gendocu-public-apis: ^1.0.0 - google-protobuf: ^3.19.1 + GendocuPublicApis: "https://git.gendocu.com/gendocu/GendocuPublicApis.git#master" + google-protobuf: ^3.21.2 grpc-docs: ^1.0.6 js-yaml: ^4.1.0 minimatch: ^3.0.4 @@ -28206,7 +29009,7 @@ __metadata: styled-components: ^5.3.3 peerDependencies: react: ^16.0.0 - checksum: 1bcd3179ce276e8fd34229acc6d3f1024908b7e6734e36ddcc4852c6db0100b900093355652577ab476c9927167cff92b05e560713995803154d4dbd4459f973 + checksum: 55b088011205fe82ced6af763136bb2494eb059445c5776bf29922439b814fa8983774a68910c47898cef5be82bea60801c2f96305e76170cff1df3f890b8d18 languageName: node linkType: hard @@ -28271,13 +29074,6 @@ __metadata: languageName: node linkType: hard -"hard-rejection@npm:^2.1.0": - version: 2.1.0 - resolution: "hard-rejection@npm:2.1.0" - checksum: 7baaf80a0c7fff4ca79687b4060113f1529589852152fa935e6787a2bc96211e784ad4588fb3048136ff8ffc9dfcf3ae385314a5b24db32de20bea0d1597f9dc - languageName: node - linkType: hard - "harmony-reflect@npm:^1.4.6": version: 1.6.1 resolution: "harmony-reflect@npm:1.6.1" @@ -28495,6 +29291,15 @@ __metadata: languageName: node linkType: hard +"homedir-polyfill@npm:^1.0.1": + version: 1.0.3 + resolution: "homedir-polyfill@npm:1.0.3" + dependencies: + parse-passwd: ^1.0.0 + checksum: 18dd4db87052c6a2179d1813adea0c4bfcfa4f9996f0e226fefb29eb3d548e564350fa28ec46b0bf1fbc0a1d2d6922ceceb80093115ea45ff8842a4990139250 + languageName: node + linkType: hard + "hoopy@npm:^0.1.4": version: 0.1.4 resolution: "hoopy@npm:0.1.4" @@ -28502,13 +29307,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^2.1.4": - version: 2.8.9 - resolution: "hosted-git-info@npm:2.8.9" - checksum: c955394bdab888a1e9bb10eb33029e0f7ce5a2ac7b3f158099dc8c486c99e73809dca609f5694b223920ca2174db33d32b12f9a2a47141dc59607c29da5a62dd - languageName: node - linkType: hard - "hosted-git-info@npm:^4.0.1": version: 4.1.0 resolution: "hosted-git-info@npm:4.1.0" @@ -28562,10 +29360,10 @@ __metadata: languageName: node linkType: hard -"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0": - version: 2.4.0 - resolution: "html-entities@npm:2.4.0" - checksum: 25bea32642ce9ebd0eedc4d24381883ecb0335ccb8ac26379a0958b9b16652fdbaa725d70207ce54a51db24103436a698a8e454397d3ba8ad81460224751f1dc +"html-entities@npm:^2.1.0, html-entities@npm:^2.4.0, html-entities@npm:^2.5.2": + version: 2.5.2 + resolution: "html-entities@npm:2.5.2" + checksum: b23f4a07d33d49ade1994069af4e13d31650e3fb62621e92ae10ecdf01d1a98065c78fd20fdc92b4c7881612210b37c275f2c9fba9777650ab0d6f2ceb3b99b6 languageName: node linkType: hard @@ -28626,6 +29424,16 @@ __metadata: languageName: node linkType: hard +"http-assert@npm:^1.3.0": + version: 1.5.0 + resolution: "http-assert@npm:1.5.0" + dependencies: + deep-equal: ~1.0.1 + http-errors: ~1.8.0 + checksum: 69c9b3c14cf8b2822916360a365089ce936c883c49068f91c365eccba5c141a9964d19fdda589150a480013bf503bf37d8936c732e9635819339e730ab0e7527 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -28664,6 +29472,19 @@ __metadata: languageName: node linkType: hard +"http-errors@npm:^1.6.3, http-errors@npm:~1.8.0": + version: 1.8.1 + resolution: "http-errors@npm:1.8.1" + dependencies: + depd: ~1.1.2 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: ">= 1.5.0 < 2" + toidentifier: 1.0.1 + checksum: d3c7e7e776fd51c0a812baff570bdf06fe49a5dc448b700ab6171b1250e4cf7db8b8f4c0b133e4bfe2451022a5790c1ca6c2cae4094dedd6ac8304a1267f91d2 + languageName: node + linkType: hard + "http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -28803,7 +29624,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:7.0.4, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2": +"https-proxy-agent@npm:7.0.4, https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.2": version: 7.0.4 resolution: "https-proxy-agent@npm:7.0.4" dependencies: @@ -29127,7 +29948,7 @@ __metadata: languageName: node linkType: hard -"ini@npm:^1.3.5, ini@npm:~1.3.0": +"ini@npm:^1.3.4, ini@npm:^1.3.5, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" checksum: dfd98b0ca3a4fc1e323e38a6c8eb8936e31a97a918d3b377649ea15bdb15d481207a0dda1021efbd86b464cae29a0d33c1d7dcaf6c5672bee17fa849bc50a1b3 @@ -29774,13 +30595,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^1.1.0": - version: 1.1.0 - resolution: "is-plain-obj@npm:1.1.0" - checksum: 0ee04807797aad50859652a7467481816cbb57e5cc97d813a7dcd8915da8195dc68c436010bf39d195226cde6a2d352f4b815f16f26b7bf486a5754290629931 - languageName: node - linkType: hard - "is-plain-obj@npm:^3.0.0": version: 3.0.0 resolution: "is-plain-obj@npm:3.0.0" @@ -30031,7 +30845,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.0, is-windows@npm:^1.0.2": +"is-windows@npm:^1.0.0, is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -31603,6 +32417,15 @@ __metadata: languageName: node linkType: hard +"keygrip@npm:~1.1.0": + version: 1.1.0 + resolution: "keygrip@npm:1.1.0" + dependencies: + tsscmp: 1.0.6 + checksum: 078cd16a463d187121f0a27c1c9c95c52ad392b620f823431689f345a0501132cee60f6e96914b07d570105af470b96960402accd6c48a0b1f3cd8fac4fa2cae + languageName: node + linkType: hard + "keyv@npm:*, keyv@npm:^4.0.0, keyv@npm:^4.5.2": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -31621,7 +32444,7 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": +"kind-of@npm:^6.0.2": version: 6.0.3 resolution: "kind-of@npm:6.0.3" checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b @@ -31635,7 +32458,7 @@ __metadata: languageName: node linkType: hard -"kleur@npm:^4.0.3, kleur@npm:^4.1.4": +"kleur@npm:^4.0.3": version: 4.1.5 resolution: "kleur@npm:4.1.5" checksum: 1dc476e32741acf0b1b5b0627ffd0d722e342c1b0da14de3e8ae97821327ca08f9fb944542fb3c126d90ac5f27f9d804edbe7c585bf7d12ef495d115e0f22c12 @@ -31682,8 +32505,8 @@ __metadata: linkType: hard "knip@npm:^5.0.0": - version: 5.22.2 - resolution: "knip@npm:5.22.2" + version: 5.23.2 + resolution: "knip@npm:5.23.2" dependencies: "@ericcornelissen/bash-parser": 0.5.3 "@nodelib/fs.walk": 2.0.0 @@ -31709,7 +32532,65 @@ __metadata: bin: knip: bin/knip.js knip-bun: bin/knip-bun.js - checksum: 995924973618391af39ba6bfb324f1f4dac427879a944eeecbcf48b61ec165f10f18196f27a7d079076f351223ed067bb917bc863e0424153c3fae2dd65582d6 + checksum: efcfa5bea3848bb6e054cb80a840dbcc718a1c91d8f6c508a686239de5e1fe7775afc05d03fd624f07fb545767b4a08a5c59b7a29a009cbad001cb68a1e92859 + languageName: node + linkType: hard + +"koa-compose@npm:^3.0.0": + version: 3.2.1 + resolution: "koa-compose@npm:3.2.1" + dependencies: + any-promise: ^1.1.0 + checksum: ff8e5fc0348455acf751179c6c613eb030a5fac6406d3b49ae9e00460b7ee8770db3ef62633fd3db0306cd4a6d2a0b5152399ebd5bb5e684418f9eeeb251c2de + languageName: node + linkType: hard + +"koa-compose@npm:^4.1.0": + version: 4.1.0 + resolution: "koa-compose@npm:4.1.0" + checksum: 46cb16792d96425e977c2ae4e5cb04930280740e907242ec9c25e3fb8b4a1d7b54451d7432bc24f40ec62255edea71894d2ceeb8238501842b4e48014f2e83db + languageName: node + linkType: hard + +"koa-convert@npm:^1.2.0": + version: 1.2.0 + resolution: "koa-convert@npm:1.2.0" + dependencies: + co: ^4.6.0 + koa-compose: ^3.0.0 + checksum: a33944dbda4ed87565985f5b37ba1122a012db872724b216b6fd8f9176d4bba42c4a9bf3c129330e45f6474d28f50ca0ed28d41b9bccd2ab5d36d6436cf0d676 + languageName: node + linkType: hard + +"koa@npm:2.11.0": + version: 2.11.0 + resolution: "koa@npm:2.11.0" + dependencies: + accepts: ^1.3.5 + cache-content-type: ^1.0.0 + content-disposition: ~0.5.2 + content-type: ^1.0.4 + cookies: ~0.8.0 + debug: ~3.1.0 + delegates: ^1.0.0 + depd: ^1.1.2 + destroy: ^1.0.4 + encodeurl: ^1.0.2 + error-inject: ^1.0.0 + escape-html: ^1.0.3 + fresh: ~0.5.2 + http-assert: ^1.3.0 + http-errors: ^1.6.3 + is-generator-function: ^1.0.7 + koa-compose: ^4.1.0 + koa-convert: ^1.2.0 + on-finished: ^2.3.0 + only: ~0.0.2 + parseurl: ^1.3.2 + statuses: ^1.5.0 + type-is: ^1.6.16 + vary: ^1.1.2 + checksum: b08e1aea03e70fe4ff6e35dee9f9e979e8608461ee1002f6e8dd72f45fc49404873888ea9a3aab2904e24bf43522df7c601033522f4151189e4055e87f94a979 languageName: node linkType: hard @@ -31897,6 +32778,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^5.0.0": + version: 5.0.0 + resolution: "linkify-it@npm:5.0.0" + dependencies: + uc.micro: ^2.0.0 + checksum: b0b86cadaf816b64c947a83994ceaad1c15f9fe7e079776ab88699fb71afd7b8fc3fd3d0ae5ebec8c92c1d347be9ba257b8aef338c0ebf81b0d27dcf429a765a + languageName: node + linkType: hard + "linkify-react@npm:4.1.3": version: 4.1.3 resolution: "linkify-react@npm:4.1.3" @@ -32059,6 +32949,13 @@ __metadata: languageName: node linkType: hard +"lodash.clonedeepwith@npm:4.5.0": + version: 4.5.0 + resolution: "lodash.clonedeepwith@npm:4.5.0" + checksum: 9fbf4ebfa04b381df226a2298eba680327bea3d0d5d19c5118de7ae218fd219186e30e9fd0d33b13729f34ffbc83c1cf09cb27aff265ba94cb602b8a2b1e71c9 + languageName: node + linkType: hard + "lodash.curry@npm:^4.0.1, lodash.curry@npm:^4.1.1": version: 4.1.1 resolution: "lodash.curry@npm:4.1.1" @@ -32303,6 +33200,19 @@ __metadata: languageName: node linkType: hard +"log4js@npm:6.9.1": + version: 6.9.1 + resolution: "log4js@npm:6.9.1" + dependencies: + date-format: ^4.0.14 + debug: ^4.3.4 + flatted: ^3.2.7 + rfdc: ^1.3.0 + streamroller: ^3.1.5 + checksum: 59d98c37d4163138dab5d9b06ae26965d1353106fece143973d57b1003b3a482791aa21374fd2cca81a953b8837b2f9756ac225404e60cbfa4dd3ab59f082e2e + languageName: node + linkType: hard + "log@npm:^6.3.1": version: 6.3.1 resolution: "log@npm:6.3.1" @@ -32339,6 +33249,13 @@ __metadata: languageName: node linkType: hard +"long-timeout@npm:0.1.1": + version: 0.1.1 + resolution: "long-timeout@npm:0.1.1" + checksum: 48668e5362cb74c4b77a6b833d59f149b9bb9e99c5a5097609807e2597cd0920613b2a42b89bd0870848298be3691064d95599a04ae010023d07dba39932afa7 + languageName: node + linkType: hard + "long@npm:^5.0.0, long@npm:^5.2.1": version: 5.2.3 resolution: "long@npm:5.2.3" @@ -32476,7 +33393,7 @@ __metadata: languageName: node linkType: hard -"luxon@npm:^3.0.0, luxon@npm:^3.4.3, luxon@npm:~3.4.0": +"luxon@npm:^3.0.0, luxon@npm:^3.2.1, luxon@npm:^3.4.3, luxon@npm:~3.4.0": version: 3.4.4 resolution: "luxon@npm:3.4.4" checksum: 36c1f99c4796ee4bfddf7dc94fa87815add43ebc44c8934c924946260a58512f0fd2743a629302885df7f35ccbd2d13f178c15df046d0e3b6eb71db178f1c60c @@ -32630,13 +33547,6 @@ __metadata: languageName: node linkType: hard -"map-obj@npm:^1.0.0": - version: 1.0.1 - resolution: "map-obj@npm:1.0.1" - checksum: 9949e7baec2a336e63b8d4dc71018c117c3ce6e39d2451ccbfd3b8350c547c4f6af331a4cbe1c83193d7c6b786082b6256bde843db90cb7da2a21e8fcc28afed - languageName: node - linkType: hard - "map-obj@npm:^2.0.0": version: 2.0.0 resolution: "map-obj@npm:2.0.0" @@ -32644,13 +33554,6 @@ __metadata: languageName: node linkType: hard -"map-obj@npm:^4.0.0": - version: 4.3.0 - resolution: "map-obj@npm:4.3.0" - checksum: fbc554934d1a27a1910e842bc87b177b1a556609dd803747c85ece420692380827c6ae94a95cce4407c054fa0964be3bf8226f7f2cb2e9eeee432c7c1985684e - languageName: node - linkType: hard - "markdown-it@npm:^12.2.0": version: 12.3.2 resolution: "markdown-it@npm:12.3.2" @@ -32666,6 +33569,22 @@ __metadata: languageName: node linkType: hard +"markdown-it@npm:^14.1.0": + version: 14.1.0 + resolution: "markdown-it@npm:14.1.0" + dependencies: + argparse: ^2.0.1 + entities: ^4.4.0 + linkify-it: ^5.0.0 + mdurl: ^2.0.0 + punycode.js: ^2.3.1 + uc.micro: ^2.1.0 + bin: + markdown-it: bin/markdown-it.mjs + checksum: 07296b45ebd0b13a55611a24d1b1ad002c6729ec54f558f597846994b0b7b1de79d13cd99ff3e7b6e9e027f36b63125cdcf69174da294ecabdd4e6b9fff39e5d + languageName: node + linkType: hard + "markdown-table@npm:^3.0.0": version: 3.0.1 resolution: "markdown-table@npm:3.0.1" @@ -32910,6 +33829,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^2.0.0": + version: 2.0.0 + resolution: "mdurl@npm:2.0.0" + checksum: 880bc289ef668df0bb34c5b2b5aaa7b6ea755052108cdaf4a5e5968ad01cf27e74927334acc9ebcc50a8628b65272ae6b1fd51fae1330c130e261c0466e1a3b2 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -33007,25 +33933,6 @@ __metadata: languageName: node linkType: hard -"meow@npm:^6.0.0": - version: 6.1.1 - resolution: "meow@npm:6.1.1" - dependencies: - "@types/minimist": ^1.2.0 - camelcase-keys: ^6.2.2 - decamelize-keys: ^1.1.0 - hard-rejection: ^2.1.0 - minimist-options: ^4.0.2 - normalize-package-data: ^2.5.0 - read-pkg-up: ^7.0.1 - redent: ^3.0.0 - trim-newlines: ^3.0.0 - type-fest: ^0.13.1 - yargs-parser: ^18.1.3 - checksum: 77b569781145ad030be77130623d9f74d6eef0af5e0a349419d3df39bcf6d88cc25be046a7757062162a88160fb5d8604e540b5177b371d2bbc2aaf73ec01479 - languageName: node - linkType: hard - "merge-descriptors@npm:1.0.1": version: 1.0.1 resolution: "merge-descriptors@npm:1.0.1" @@ -33455,7 +34362,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -33621,22 +34528,11 @@ __metadata: linkType: hard "minimatch@npm:^9.0.0, minimatch@npm:^9.0.1, minimatch@npm:^9.0.3": - version: 9.0.4 - resolution: "minimatch@npm:9.0.4" + version: 9.0.5 + resolution: "minimatch@npm:9.0.5" dependencies: brace-expansion: ^2.0.1 - checksum: cf717f597ec3eed7dabc33153482a2e8d49f4fd3c26e58fd9c71a94c5029a0838728841b93f46bf1263b65a8010e2ee800d0dc9b004ab8ba8b6d1ec07cc115b5 - languageName: node - linkType: hard - -"minimist-options@npm:^4.0.2": - version: 4.1.0 - resolution: "minimist-options@npm:4.1.0" - dependencies: - arrify: ^1.0.1 - is-plain-obj: ^1.1.0 - kind-of: ^6.0.3 - checksum: 8c040b3068811e79de1140ca2b708d3e203c8003eb9a414c1ab3cd467fc5f17c9ca02a5aef23bedc51a7f8bfbe77f87e9a7e31ec81fba304cda675b019496f4e + checksum: 2c035575eda1e50623c731ec6c14f65a85296268f749b9337005210bb2b34e2705f8ef1a358b188f69892286ab99dc42c8fb98a57bde55c8d81b3023c19cea28 languageName: node linkType: hard @@ -33789,13 +34685,6 @@ __metadata: languageName: node linkType: hard -"mixme@npm:^0.5.1": - version: 0.5.4 - resolution: "mixme@npm:0.5.4" - checksum: cec5f6127c92455bc86d592f0a0628e188c7ccf3909bd106703a1d939f0e2f451ddaac6da0d77c5f14d53dd2d58f64f5b2f8ff55a68ec68be95a73d290e4d430 - languageName: node - linkType: hard - "mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": version: 0.5.3 resolution: "mkdirp-classic@npm:0.5.3" @@ -33933,7 +34822,7 @@ __metadata: languageName: node linkType: hard -"mri@npm:^1.1.0": +"mri@npm:^1.1.0, mri@npm:^1.2.0": version: 1.2.0 resolution: "mri@npm:1.2.0" checksum: 83f515abbcff60150873e424894a2f65d68037e5a7fcde8a9e2b285ee9c13ac581b63cfc1e6826c4732de3aeb84902f7c1e16b7aff46cd3f897a0f757a894e85 @@ -34485,8 +35374,8 @@ __metadata: linkType: hard "node-mocks-http@npm:^1.0.0": - version: 1.14.1 - resolution: "node-mocks-http@npm:1.14.1" + version: 1.15.0 + resolution: "node-mocks-http@npm:1.15.0" dependencies: "@types/express": ^4.17.21 "@types/node": ^20.10.6 @@ -34500,7 +35389,7 @@ __metadata: parseurl: ^1.3.3 range-parser: ^1.2.0 type-is: ^1.6.18 - checksum: cc37618fb5f44a6049f8fcfc73373c4b42943e35e4b0f7463939d3f219663fe647e00e6eb9c3b8aedd48e88e10e1542993ac1a7ff8442ee2c3f6b97f23ced0e0 + checksum: 9be3ef4eec1b283800bbae50d7615fb633c7d28a0bedcf13e2ddfaa7ed4aa865d00c1d26e929ee8d617b3734958d8c19345f615760004f1940a8268c9bbacf07 languageName: node linkType: hard @@ -34521,6 +35410,17 @@ __metadata: languageName: node linkType: hard +"node-schedule@npm:2.1.1": + version: 2.1.1 + resolution: "node-schedule@npm:2.1.1" + dependencies: + cron-parser: ^4.2.0 + long-timeout: 0.1.1 + sorted-array-functions: ^1.3.0 + checksum: 6a8822b16fb024277c42efe710bdb35b6f1f6ab3a2f826283640511247d693f34ebd5ddf2863cd91609e7f323574e36c81cd2084dc204fa521f931380f0f963f + languageName: node + linkType: hard + "node-stdlib-browser@npm:^1.2.0": version: 1.2.0 resolution: "node-stdlib-browser@npm:1.2.0" @@ -34564,15 +35464,15 @@ __metadata: linkType: hard "nodemailer@npm:^6.9.13": - version: 6.9.13 - resolution: "nodemailer@npm:6.9.13" - checksum: 1b591ef480be2ff69480127cbff819e6593b1ef263b6f920e1a4e83e40280582daf7a14a809ef92f9828e2a70bdb3ce22b11924e209f2afe4975f9ff37e08e9d + version: 6.9.14 + resolution: "nodemailer@npm:6.9.14" + checksum: 4037a45e24dbca47c0ea9a962d2c316deef7afe6ebf73c7da3a84942a8d501d85aae7f36dc1c5dac58dec7275f7cb1c92fd4f9c7c19f037225019ad2f402a9dc languageName: node linkType: hard "nodemon@npm:^3.0.1": - version: 3.1.3 - resolution: "nodemon@npm:3.1.3" + version: 3.1.4 + resolution: "nodemon@npm:3.1.4" dependencies: chokidar: ^3.5.2 debug: ^4 @@ -34586,7 +35486,7 @@ __metadata: undefsafe: ^2.0.5 bin: nodemon: bin/nodemon.js - checksum: ac2fa8865ab292b7ddf66731487acca4b4282b2728361e0de633c0c74cd705d6a0852b52f785c09469d959241d038ba824f50375622e687a2a318be747d9cd9d + checksum: 3f003fc2c7bdaba559108320f188b7cb063220455e5da218ff3bf4f7468ad7059852da6e35a52b8c690cc27f6e36a433a9ad1f1bdb8096ec1ee3d930629cbeca languageName: node linkType: hard @@ -34623,18 +35523,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^2.5.0": - version: 2.5.0 - resolution: "normalize-package-data@npm:2.5.0" - dependencies: - hosted-git-info: ^2.1.4 - resolve: ^1.10.0 - semver: 2 || 3 || 4 || 5 - validate-npm-package-license: ^3.0.1 - checksum: 7999112efc35a6259bc22db460540cae06564aa65d0271e3bdfa86876d08b0e578b7b5b0028ee61b23f1cae9fc0e7847e4edc0948d3068a39a2a82853efc8499 - languageName: node - linkType: hard - "normalize-path@npm:^2.1.1": version: 2.1.1 resolution: "normalize-path@npm:2.1.1" @@ -35042,20 +35930,20 @@ __metadata: linkType: hard "octokit@npm:^3.0.0": - version: 3.2.0 - resolution: "octokit@npm:3.2.0" + version: 3.2.1 + resolution: "octokit@npm:3.2.1" dependencies: "@octokit/app": ^14.0.2 "@octokit/core": ^5.0.0 "@octokit/oauth-app": ^6.0.0 "@octokit/plugin-paginate-graphql": ^4.0.0 - "@octokit/plugin-paginate-rest": ^9.0.0 - "@octokit/plugin-rest-endpoint-methods": ^10.0.0 + "@octokit/plugin-paginate-rest": 11.3.1 + "@octokit/plugin-rest-endpoint-methods": 13.2.2 "@octokit/plugin-retry": ^6.0.0 "@octokit/plugin-throttling": ^8.0.0 "@octokit/request-error": ^5.0.0 - "@octokit/types": ^12.0.0 - checksum: 3ec8efe02144aa6210a5c846947245cbe58d31144fa8e6a1726782001d77fcd044b7e1498a56c5e47aba44c1f5726a37c261f474cb46b816579549f7a09db1f6 + "@octokit/types": ^13.0.0 + checksum: e5608f848fb11808d0c977d946c24bffa622179daf8566c4941f6050551d368aa750b362a95aea14647facd0919e4af392c67b2409f8642dc9f27c7b6c0f964e languageName: node linkType: hard @@ -35127,6 +36015,13 @@ __metadata: languageName: node linkType: hard +"only@npm:~0.0.2": + version: 0.0.2 + resolution: "only@npm:0.0.2" + checksum: d399710db867a1ef436dd3ce74499c87ece794aa81ab0370b5d153968766ee4aed2f98d3f92fc87c963e45b7a74d400d6f463ef651a5e7cfb861b15e88e9efe6 + languageName: node + linkType: hard + "ono@npm:^7.1.3": version: 7.1.3 resolution: "ono@npm:7.1.3" @@ -35648,6 +36543,13 @@ __metadata: languageName: node linkType: hard +"parse-passwd@npm:^1.0.0": + version: 1.0.0 + resolution: "parse-passwd@npm:1.0.0" + checksum: 4e55e0231d58f828a41d0f1da2bf2ff7bcef8f4cb6146e69d16ce499190de58b06199e6bd9b17fbf0d4d8aef9052099cdf8c4f13a6294b1a522e8e958073066e + languageName: node + linkType: hard + "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -35682,7 +36584,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.2, parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 407cee8e0a3a4c5cd472559bca8b6a45b82c124e9a4703302326e9ab60fc1081442ada4e02628efef1eb16197ddc7f8822f5a91fd7d7c86b51f530aedb17dfa2 @@ -36327,27 +37229,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.44.1": - version: 1.44.1 - resolution: "playwright-core@npm:1.44.1" +"playwright-core@npm:1.45.3": + version: 1.45.3 + resolution: "playwright-core@npm:1.45.3" bin: playwright-core: cli.js - checksum: ebc6fa0ff77792fe52648fda06cc4474d4e9746db6dc5750d262b7fe2caf9f9e2327a71f1fb365e862213403a9daf95361c5040a9b0fd462928d7eb4fdc760e1 + checksum: cecb58877b2c643403d7a72c24a7aa0fdd087a3c7f9a5ea5403851336ea831d8e304b1f159aacbbabd12e5c47eaac054333746c9e5431ec07b13d64dbf3b50ec languageName: node linkType: hard -"playwright@npm:1.44.1": - version: 1.44.1 - resolution: "playwright@npm:1.44.1" +"playwright@npm:1.45.3": + version: 1.45.3 + resolution: "playwright@npm:1.45.3" dependencies: fsevents: 2.3.2 - playwright-core: 1.44.1 + playwright-core: 1.45.3 dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: c2e8ff0a855e4a9e665ae5ffa0ff90423289546157be55e83f78916d17075c5910a7244773739c2a973da884a4ff0b3cf9b6e6b543de112e43b12169a9208e86 + checksum: d9d23b155ccd001553214f710561b01e48eb409676102f8ab94c0b4aa5ac5f398becc1a96528b0554944e07e34189503d891913e0e0a4aa58ad36b9c08747983 languageName: node linkType: hard @@ -37084,12 +37986,12 @@ __metadata: linkType: hard "prom-client@npm:^15.0.0": - version: 15.1.2 - resolution: "prom-client@npm:15.1.2" + version: 15.1.3 + resolution: "prom-client@npm:15.1.3" dependencies: "@opentelemetry/api": ^1.4.0 tdigest: ^0.1.1 - checksum: b9b2f439588a462c0aec840e8aa857bb0a77284174d6587ca042eb13ea6ac36ba13277f45ae6ed3696b3007a1020c5ee2c5ee46b23be033a7bb45207a5365c21 + checksum: 9a57f3c16f39aa9a03da021883a4231c0bb56fc9d02f6ef9c28f913379f275640a5a33b98d9946ebf53c71011a29b580e9d2d6e3806cb1c229a3f59c65993968 languageName: node linkType: hard @@ -37204,18 +38106,18 @@ __metadata: languageName: node linkType: hard -"proto3-json-serializer@npm:^2.0.0": - version: 2.0.0 - resolution: "proto3-json-serializer@npm:2.0.0" +"proto3-json-serializer@npm:^2.0.2": + version: 2.0.2 + resolution: "proto3-json-serializer@npm:2.0.2" dependencies: - protobufjs: ^7.0.0 - checksum: aea3433d5e0204625b97bda53001f71fc060eb83709d58d001c5bf75728f14b5ebfca39b54d81e7c90e4de363f68aecf1d203a410f33ebda9590a812f8f30b13 + protobufjs: ^7.2.5 + checksum: 21b8aa65be6dac2bb24920e5bdabef48b249bdf65b1498ae7e69ac4e70722275b083cd60a21d2b4be3ead9d768de2f6f5fb6b188bd177d51c824a539b5ba55cc languageName: node linkType: hard -"protobufjs@npm:7.2.5": - version: 7.2.5 - resolution: "protobufjs@npm:7.2.5" +"protobufjs@npm:^7.2.3, protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6, protobufjs@npm:^7.3.2": + version: 7.3.2 + resolution: "protobufjs@npm:7.3.2" dependencies: "@protobufjs/aspromise": ^1.1.2 "@protobufjs/base64": ^1.1.2 @@ -37229,27 +38131,7 @@ __metadata: "@protobufjs/utf8": ^1.1.0 "@types/node": ">=13.7.0" long: ^5.0.0 - checksum: 3770a072114061faebbb17cfd135bc4e187b66bc6f40cd8bac624368b0270871ec0cfb43a02b9fb4f029c8335808a840f1afba3c2e7ede7063b98ae6b98a703f - languageName: node - linkType: hard - -"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.3, protobufjs@npm:^7.2.5, protobufjs@npm:^7.2.6": - version: 7.2.6 - resolution: "protobufjs@npm:7.2.6" - dependencies: - "@protobufjs/aspromise": ^1.1.2 - "@protobufjs/base64": ^1.1.2 - "@protobufjs/codegen": ^2.0.4 - "@protobufjs/eventemitter": ^1.1.0 - "@protobufjs/fetch": ^1.1.0 - "@protobufjs/float": ^1.0.2 - "@protobufjs/inquire": ^1.1.0 - "@protobufjs/path": ^1.1.2 - "@protobufjs/pool": ^1.1.0 - "@protobufjs/utf8": ^1.1.0 - "@types/node": ">=13.7.0" - long: ^5.0.0 - checksum: 3c62e48f7d50017ac3b0dcd2a58e617cf858f9fba56a488bd48b9aa3482893a75540052dbcb3c12dfbaab42b1d04964611175faf06bdadcd33a4ebac982a511e + checksum: cfb2a744787f26ee7c82f3e7c4b72cfc000e9bb4c07828ed78eb414db0ea97a340c0cc3264d0e88606592f847b12c0351411f10e9af255b7ba864eec44d7705f languageName: node linkType: hard @@ -37322,6 +38204,13 @@ __metadata: languageName: node linkType: hard +"punycode.js@npm:^2.3.1": + version: 2.3.1 + resolution: "punycode.js@npm:2.3.1" + checksum: 13466d7ed5e8dacdab8c4cc03837e7dd14218a59a40eb14a837f1f53ca396e18ef2c4ee6d7766b8ed2fc391d6a3ac489eebf2de83b3596f5a54e86df4a251b72 + languageName: node + linkType: hard + "punycode@npm:1.3.2": version: 1.3.2 resolution: "punycode@npm:1.3.2" @@ -37445,13 +38334,6 @@ __metadata: languageName: node linkType: hard -"quick-lru@npm:^4.0.1": - version: 4.0.1 - resolution: "quick-lru@npm:4.0.1" - checksum: bea46e1abfaa07023e047d3cf1716a06172c4947886c053ede5c50321893711577cb6119360f810cc3ffcd70c4d7db4069c3cee876b358ceff8596e062bd1154 - languageName: node - linkType: hard - "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -37486,6 +38368,13 @@ __metadata: languageName: node linkType: hard +"rambda@npm:^9.1.0": + version: 9.2.1 + resolution: "rambda@npm:9.2.1" + checksum: d877b61f2c1e8a1878ca1e40a181702b360f6c3978bf0e55e62315a1aa215863b7ecb8f00077e2bc0f1ca0d455387fafd7a84b5c8dd004eb3cf071b15827cff7 + languageName: node + linkType: hard + "ramda-adjunct@npm:^5.0.0": version: 5.0.1 resolution: "ramda-adjunct@npm:5.0.1" @@ -38055,7 +38944,7 @@ __metadata: languageName: node linkType: hard -"react-resizable@npm:^3.0.4": +"react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5": version: 3.0.5 resolution: "react-resizable@npm:3.0.5" dependencies: @@ -38293,7 +39182,7 @@ __metadata: languageName: node linkType: hard -"react-window@npm:^1.8.6": +"react-window@npm:^1.8.10, react-window@npm:^1.8.6": version: 1.8.10 resolution: "react-window@npm:1.8.10" dependencies: @@ -38343,29 +39232,6 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:^7.0.1": - version: 7.0.1 - resolution: "read-pkg-up@npm:7.0.1" - dependencies: - find-up: ^4.1.0 - read-pkg: ^5.2.0 - type-fest: ^0.8.1 - checksum: e4e93ce70e5905b490ca8f883eb9e48b5d3cebc6cd4527c25a0d8f3ae2903bd4121c5ab9c5a3e217ada0141098eeb661313c86fa008524b089b8ed0b7f165e44 - languageName: node - linkType: hard - -"read-pkg@npm:^5.2.0": - version: 5.2.0 - resolution: "read-pkg@npm:5.2.0" - dependencies: - "@types/normalize-package-data": ^2.4.0 - normalize-package-data: ^2.5.0 - parse-json: ^5.0.0 - type-fest: ^0.6.0 - checksum: eb696e60528b29aebe10e499ba93f44991908c57d70f2d26f369e46b8b9afc208ef11b4ba64f67630f31df8b6872129e0a8933c8c53b7b4daf0eace536901222 - languageName: node - linkType: hard - "read-tls-client-hello@npm:^1.0.0": version: 1.0.1 resolution: "read-tls-client-hello@npm:1.0.1" @@ -38869,13 +39735,6 @@ __metadata: languageName: node linkType: hard -"require-main-filename@npm:^2.0.0": - version: 2.0.0 - resolution: "require-main-filename@npm:2.0.0" - checksum: e9e294695fea08b076457e9ddff854e81bffbe248ed34c1eec348b7abbd22a0d02e8d75506559e2265e96978f3c4720bd77a6dad84755de8162b357eb6c778c7 - languageName: node - linkType: hard - "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -38913,6 +39772,16 @@ __metadata: languageName: node linkType: hard +"resolve-dir@npm:^1.0.0, resolve-dir@npm:^1.0.1": + version: 1.0.1 + resolution: "resolve-dir@npm:1.0.1" + dependencies: + expand-tilde: ^2.0.0 + global-modules: ^1.0.0 + checksum: ef736b8ed60d6645c3b573da17d329bfb50ec4e1d6c5ffd6df49e3497acef9226f9810ea6823b8ece1560e01dcb13f77a9f6180d4f242d00cc9a8f4de909c65c + languageName: node + linkType: hard + "resolve-from@npm:5.0.0, resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0" @@ -38941,7 +39810,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1": +"resolve@npm:1.22.8, resolve@npm:^1.1.6, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.22.4, resolve@npm:^1.22.8, resolve@npm:~1.22.1": version: 1.22.8 resolution: "resolve@npm:1.22.8" dependencies: @@ -38977,7 +39846,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin, resolve@patch:resolve@~1.22.1#~builtin": +"resolve@patch:resolve@1.22.8#~builtin, resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.22.4#~builtin, resolve@patch:resolve@^1.22.8#~builtin, resolve@patch:resolve@~1.22.1#~builtin": version: 1.22.8 resolution: "resolve@patch:resolve@npm%3A1.22.8#~builtin::version=1.22.8&hash=c3c19d" dependencies: @@ -39112,10 +39981,10 @@ __metadata: languageName: node linkType: hard -"rfdc@npm:^1.3.1": - version: 1.3.1 - resolution: "rfdc@npm:1.3.1" - checksum: d5d1e930aeac7e0e0a485f97db1356e388bdbeff34906d206fe524dd5ada76e95f186944d2e68307183fdc39a54928d4426bbb6734851692cfe9195efba58b79 +"rfdc@npm:^1.3.0, rfdc@npm:^1.3.1": + version: 1.4.1 + resolution: "rfdc@npm:1.4.1" + checksum: 3b05bd55062c1d78aaabfcea43840cdf7e12099968f368e9a4c3936beb744adb41cbdb315eac6d4d8c6623005d6f87fdf16d8a10e1ff3722e84afea7281c8d13 languageName: node linkType: hard @@ -39624,7 +40493,7 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^3.0.0, schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": +"schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": version: 3.3.0 resolution: "schema-utils@npm:3.3.0" dependencies: @@ -39717,15 +40586,6 @@ __metadata: languageName: node linkType: hard -"semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0, semver@npm:^5.6.0": - version: 5.7.2 - resolution: "semver@npm:5.7.2" - bin: - semver: bin/semver - checksum: fb4ab5e0dd1c22ce0c937ea390b4a822147a9c53dbd2a9a0132f12fe382902beef4fbf12cf51bb955248d8d15874ce8cd89532569756384f994309825f10b686 - languageName: node - linkType: hard - "semver@npm:7.5.4, semver@npm:~7.5.4": version: 7.5.4 resolution: "semver@npm:7.5.4" @@ -39737,6 +40597,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^5.5.0, semver@npm:^5.6.0": + version: 5.7.2 + resolution: "semver@npm:5.7.2" + bin: + semver: bin/semver + checksum: fb4ab5e0dd1c22ce0c937ea390b4a822147a9c53dbd2a9a0132f12fe382902beef4fbf12cf51bb955248d8d15874ce8cd89532569756384f994309825f10b686 + languageName: node + linkType: hard + "semver@npm:^6.0.0, semver@npm:^6.2.0, semver@npm:^6.3.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -40227,22 +41096,6 @@ __metadata: languageName: node linkType: hard -"smartwrap@npm:^2.0.2": - version: 2.0.2 - resolution: "smartwrap@npm:2.0.2" - dependencies: - array.prototype.flat: ^1.2.3 - breakword: ^1.0.5 - grapheme-splitter: ^1.0.4 - strip-ansi: ^6.0.0 - wcwidth: ^1.0.1 - yargs: ^15.1.0 - bin: - smartwrap: src/terminal-adapter.js - checksum: 1a6833eb1c3d8488b036df66dcab37dcdda5270bb9629c471155785c09ee1b591177a9774c588c43f8fa28833204500019265da2ffed28ac7bbf4589b943d2fa - languageName: node - linkType: hard - "smol-toml@npm:^1.1.4": version: 1.2.1 resolution: "smol-toml@npm:1.2.1" @@ -40348,6 +41201,13 @@ __metadata: languageName: node linkType: hard +"sorted-array-functions@npm:^1.3.0": + version: 1.3.0 + resolution: "sorted-array-functions@npm:1.3.0" + checksum: 673fd39ca3b6c92644d4483eac1700bb7d7555713a536822a7522a35af559bef3e72f10d89356b75042dc394cd7c2e2ab6f40024385218ec3c85bb7335032857 + languageName: node + linkType: hard + "source-list-map@npm:^2.0.0": version: 2.0.1 resolution: "source-list-map@npm:2.0.1" @@ -40448,40 +41308,6 @@ __metadata: languageName: node linkType: hard -"spdx-correct@npm:^3.0.0": - version: 3.1.1 - resolution: "spdx-correct@npm:3.1.1" - dependencies: - spdx-expression-parse: ^3.0.0 - spdx-license-ids: ^3.0.0 - checksum: 77ce438344a34f9930feffa61be0eddcda5b55fc592906ef75621d4b52c07400a97084d8701557b13f7d2aae0cb64f808431f469e566ef3fe0a3a131dcb775a6 - languageName: node - linkType: hard - -"spdx-exceptions@npm:^2.1.0": - version: 2.3.0 - resolution: "spdx-exceptions@npm:2.3.0" - checksum: cb69a26fa3b46305637123cd37c85f75610e8c477b6476fa7354eb67c08128d159f1d36715f19be6f9daf4b680337deb8c65acdcae7f2608ba51931540687ac0 - languageName: node - linkType: hard - -"spdx-expression-parse@npm:^3.0.0": - version: 3.0.1 - resolution: "spdx-expression-parse@npm:3.0.1" - dependencies: - spdx-exceptions: ^2.1.0 - spdx-license-ids: ^3.0.0 - checksum: a1c6e104a2cbada7a593eaa9f430bd5e148ef5290d4c0409899855ce8b1c39652bcc88a725259491a82601159d6dc790bedefc9016c7472f7de8de7361f8ccde - languageName: node - linkType: hard - -"spdx-license-ids@npm:^3.0.0": - version: 3.0.12 - resolution: "spdx-license-ids@npm:3.0.12" - checksum: 92a4dddce62ce1db6fe54a7a839cf85e06abc308fc83b776a55b44e4f1906f02e7ebd506120847039e976bbbad359ea8bdfafb7925eae5cd7e73255f02e0b7d6 - languageName: node - linkType: hard - "spdy-transport@npm:^3.0.0": version: 3.0.0 resolution: "spdy-transport@npm:3.0.0" @@ -40738,7 +41564,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:>= 1.4.0 < 2, statuses@npm:~1.5.0": +"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0, statuses@npm:~1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" checksum: c469b9519de16a4bb19600205cffb39ee471a5f17b82589757ca7bd40a8d92ebb6ed9f98b5a540c5d302ccbc78f15dc03cc0280dd6e00df1335568a5d5758a5c @@ -40773,9 +41599,9 @@ __metadata: linkType: hard "stream-buffers@npm:^3.0.2": - version: 3.0.2 - resolution: "stream-buffers@npm:3.0.2" - checksum: b09fdeea606e3113ebd0e07010ed0cf038608fa396130add9e45deaff5cc3ba845dc25c31ad24f8341f85907846344cb7c85f75ea52c6572e2ac646e9b6072d0 + version: 3.0.3 + resolution: "stream-buffers@npm:3.0.3" + checksum: 3f0bdc4b1fd3ff370cef5a2103dd930b8981d42d97741eeb087a660771e27f0fc35fa8a351bb36e15bbbbce0eea00fefed60d6cdff4c6c3f527580529f183807 languageName: node linkType: hard @@ -40836,12 +41662,14 @@ __metadata: languageName: node linkType: hard -"stream-transform@npm:^2.1.3": - version: 2.1.3 - resolution: "stream-transform@npm:2.1.3" +"streamroller@npm:^3.1.5": + version: 3.1.5 + resolution: "streamroller@npm:3.1.5" dependencies: - mixme: ^0.5.1 - checksum: 26ce872a6812d5c784fa1f042bfd403644bc1c019f64627b5012c4544830a5570bef98b47225b38120c5878b326f3d1a213cd999a2285c98b536e5e202ca5bdf + date-format: ^4.0.14 + debug: ^4.3.4 + fs-extra: ^8.1.0 + checksum: c1df5612b785ffa4b6bbf16460590b62994c57265bc55a5166eebeeb0daf648e84bc52dc6d57e0cd4e5c7609bda93076753c63ff54589febd1e0b95590f0e443 languageName: node linkType: hard @@ -42142,13 +42970,6 @@ __metadata: languageName: node linkType: hard -"trim-newlines@npm:^3.0.0": - version: 3.0.1 - resolution: "trim-newlines@npm:3.0.1" - checksum: b530f3fadf78e570cf3c761fb74fef655beff6b0f84b29209bac6c9622db75ad1417f4a7b5d54c96605dcd72734ad44526fef9f396807b90839449eb543c6206 - languageName: node - linkType: hard - "triple-beam@npm:^1.3.0, triple-beam@npm:^1.4.1": version: 1.4.1 resolution: "triple-beam@npm:1.4.1" @@ -42322,6 +43143,13 @@ __metadata: languageName: node linkType: hard +"tsscmp@npm:1.0.6": + version: 1.0.6 + resolution: "tsscmp@npm:1.0.6" + checksum: 1512384def36bccc9125cabbd4c3b0e68608d7ee08127ceaa0b84a71797263f1a01c7f82fa69be8a3bd3c1396e2965d2f7b52d581d3a5eeaf3967fbc52e3b3bf + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -42347,23 +43175,6 @@ __metadata: languageName: node linkType: hard -"tty-table@npm:^4.1.5": - version: 4.1.6 - resolution: "tty-table@npm:4.1.6" - dependencies: - chalk: ^4.1.2 - csv: ^5.5.0 - kleur: ^4.1.4 - smartwrap: ^2.0.2 - strip-ansi: ^6.0.0 - wcwidth: ^1.0.1 - yargs: ^17.1.1 - bin: - tty-table: adapters/terminal-adapter.js - checksum: 0f689b7d79ad6b9e608299e667a493309901fe802f1c4d66627a90cacb6fe11e0521e1a2dc5a75f793750ecdd849e98292d4874e5e6e988edd928b67045eb847 - languageName: node - linkType: hard - "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -42454,20 +43265,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.6.0": - version: 0.6.0 - resolution: "type-fest@npm:0.6.0" - checksum: b2188e6e4b21557f6e92960ec496d28a51d68658018cba8b597bd3ef757721d1db309f120ae987abeeda874511d14b776157ff809f23c6d1ce8f83b9b2b7d60f - languageName: node - linkType: hard - -"type-fest@npm:^0.8.1": - version: 0.8.1 - resolution: "type-fest@npm:0.8.1" - checksum: d61c4b2eba24009033ae4500d7d818a94fd6d1b481a8111612ee141400d5f1db46f199c014766b9fa9b31a6a7374d96fc748c6d688a78a3ce5a33123839becb7 - languageName: node - linkType: hard - "type-fest@npm:^2.19.0": version: 2.19.0 resolution: "type-fest@npm:2.19.0" @@ -42489,7 +43286,7 @@ __metadata: languageName: node linkType: hard -"type-is@npm:^1.6.18, type-is@npm:^1.6.4, type-is@npm:~1.6.18": +"type-is@npm:^1.6.16, type-is@npm:^1.6.18, type-is@npm:^1.6.4, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" dependencies: @@ -42700,6 +43497,13 @@ __metadata: languageName: node linkType: hard +"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0": + version: 2.1.0 + resolution: "uc.micro@npm:2.1.0" + checksum: 37197358242eb9afe367502d4638ac8c5838b78792ab218eafe48287b0ed28aaca268ec0392cc5729f6c90266744de32c06ae938549aee041fc93b0f9672d6b2 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4": version: 3.17.0 resolution: "uglify-js@npm:3.17.0" @@ -43039,7 +43843,7 @@ __metadata: languageName: node linkType: hard -"upath@npm:^2.0.1": +"upath@npm:2.0.1, upath@npm:^2.0.1": version: 2.0.1 resolution: "upath@npm:2.0.1" checksum: 2db04f24a03ef72204c7b969d6991abec9e2cb06fb4c13a1fd1c59bc33b46526b16c3325e55930a11ff86a77a8cbbcda8f6399bf914087028c5beae21ecdb33c @@ -43388,16 +44192,6 @@ __metadata: languageName: node linkType: hard -"validate-npm-package-license@npm:^3.0.1": - version: 3.0.4 - resolution: "validate-npm-package-license@npm:3.0.4" - dependencies: - spdx-correct: ^3.0.0 - spdx-expression-parse: ^3.0.0 - checksum: 35703ac889d419cf2aceef63daeadbe4e77227c39ab6287eeb6c1b36a746b364f50ba22e88591f5d017bc54685d8137bc2d328d0a896e4d3fd22093c0f32a9ad - languageName: node - linkType: hard - "validate-npm-package-name@npm:^3.0.0": version: 3.0.0 resolution: "validate-npm-package-name@npm:3.0.0" @@ -43468,7 +44262,7 @@ __metadata: languageName: node linkType: hard -"vary@npm:^1, vary@npm:~1.1.2": +"vary@npm:^1, vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" checksum: ae0123222c6df65b437669d63dfa8c36cee20a504101b2fcd97b8bf76f91259c17f9f2b4d70a1e3c6bbcee7f51b28392833adb6b2770b23b01abec84e369660b @@ -44032,13 +44826,6 @@ __metadata: languageName: node linkType: hard -"which-module@npm:^2.0.0": - version: 2.0.0 - resolution: "which-module@npm:2.0.0" - checksum: 809f7fd3dfcb2cdbe0180b60d68100c88785084f8f9492b0998c051d7a8efe56784492609d3f09ac161635b78ea29219eb1418a98c15ce87d085bce905705c9c - languageName: node - linkType: hard - "which-pm@npm:2.0.0": version: 2.0.0 resolution: "which-pm@npm:2.0.0" @@ -44062,7 +44849,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.9, which@npm:^1.3.1": +"which@npm:^1.2.14, which@npm:^1.2.9, which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -44242,8 +45029,8 @@ __metadata: linkType: hard "ws@npm:*, ws@npm:^8.11.0, ws@npm:^8.12.0, ws@npm:^8.13.0, ws@npm:^8.14.2, ws@npm:^8.16.0, ws@npm:^8.17.0, ws@npm:^8.8.0": - version: 8.17.1 - resolution: "ws@npm:8.17.1" + version: 8.18.0 + resolution: "ws@npm:8.18.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -44252,7 +45039,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf + checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975 languageName: node linkType: hard @@ -44271,6 +45058,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:8.17.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 147ef9eab0251364e1d2c55338ad0efb15e6913923ccbfdf20f7a8a6cb8f88432bcd7f4d8f66977135bfad35575644f9983201c1a361019594a4e53977bf6d4e + languageName: node + linkType: hard + "ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.5": version: 7.5.10 resolution: "ws@npm:7.5.10" @@ -44428,13 +45230,6 @@ __metadata: languageName: node linkType: hard -"y18n@npm:^4.0.0": - version: 4.0.3 - resolution: "y18n@npm:4.0.3" - checksum: 014dfcd9b5f4105c3bb397c1c8c6429a9df004aa560964fb36732bfb999bfe83d45ae40aeda5b55d21b1ee53d8291580a32a756a443e064317953f08025b1aa4 - languageName: node - linkType: hard - "y18n@npm:^5.0.5": version: 5.0.8 resolution: "y18n@npm:5.0.8" @@ -44501,16 +45296,6 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^18.1.2, yargs-parser@npm:^18.1.3": - version: 18.1.3 - resolution: "yargs-parser@npm:18.1.3" - dependencies: - camelcase: ^5.0.0 - decamelize: ^1.2.0 - checksum: 60e8c7d1b85814594d3719300ecad4e6ae3796748b0926137bfec1f3042581b8646d67e83c6fc80a692ef08b8390f21ddcacb9464476c39bbdf52e34961dd4d9 - languageName: node - linkType: hard - "yargs-parser@npm:^20.2.2": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -44525,25 +45310,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^15.1.0": - version: 15.4.1 - resolution: "yargs@npm:15.4.1" - dependencies: - cliui: ^6.0.0 - decamelize: ^1.2.0 - find-up: ^4.1.0 - get-caller-file: ^2.0.1 - require-directory: ^2.1.1 - require-main-filename: ^2.0.0 - set-blocking: ^2.0.0 - string-width: ^4.2.0 - which-module: ^2.0.0 - y18n: ^4.0.0 - yargs-parser: ^18.1.2 - checksum: 40b974f508d8aed28598087720e086ecd32a5fd3e945e95ea4457da04ee9bdb8bdd17fd91acff36dc5b7f0595a735929c514c40c402416bbb87c03f6fb782373 - languageName: node - linkType: hard - "yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" @@ -44585,6 +45351,8 @@ __metadata: "@yarnpkg/builder": ^4.0.0 "@yarnpkg/core": ^4.0.3 "@yarnpkg/fslib": ^3.0.2 + chalk: ^4.0.0 + lodash: ^4.17.21 nodemon: ^3.0.1 semver: ^7.6.0 languageName: unknown @@ -44647,6 +45415,13 @@ __metadata: languageName: node linkType: hard +"ylru@npm:^1.2.0": + version: 1.4.0 + resolution: "ylru@npm:1.4.0" + checksum: e0bf797476487e3d57a6e8790cbb749cff2089e2afc87e46bc84ce7605c329d578ff422c8e8c2ddf167681ddd218af0f58e099733ae1044cba9e9472ebedc01d + languageName: node + linkType: hard + "yml-loader@npm:^2.1.0": version: 2.1.0 resolution: "yml-loader@npm:2.1.0"